先讲一个我自己踩过的坑。去年年中,我们团队在做一个面向企业客户的AI客服Agent,早期架构很“天真”:用户聊天记录、长期记忆、知识分块,全都往S3桶里扔,觉得存储就是“容量的事”。结果用户量跑到几万之后,问题开始一个接一个冒出来:恢复一个用户最近20轮上下文,得先到数据库里查消息Key列表,再逐个把对象读回来,最后还要自己拼装、排序、召回,高峰期LLM调用还没成为瓶颈,存储拼接层先崩了。后来我们花了两周时间梳理方案,逐步切到一个叫Agent Bucket的新存储模型,才真正把这块理顺。
写这篇文章前,我自己完整走了一遍从S3迁移到Agent Bucket的流程。标题里说“30分钟构建百万级用户应用”,指的是接入主链路的速度,不是包含所有历史数据清洗的排期。这篇文章的价值在于:第一,讲清楚为什么AI Agent应用不能继续沿用传统对象存储的思路;第二,把Agent Bucket的存储模型拆开看,给一份可以直接参考的落地指南;第三,分享一些百万级用户规模下、我在生产环境里真正踩过的性能与账单陷阱。适合正在做AI Agent应用选型、准备把Demo推上生产、或者被存储架构折磨过的开发者阅读。
1. AI Agent场景里,传统S3到底卡在哪
1.1 把文件扔进桶里的思路,从来不是为了“记忆”而设计的
S3这类对象存储的设计目标非常明确:提供一个海量、持久、便宜的文件仓库。你上传一张图片、一份PDF、一段日志,给它一个唯一的Key,存进去,以后需要了再取出来。这套模型在传统Web应用里无可挑剔,它解决的问题是“把静态的字节可靠地存住”。
但AI Agent消费数据的方式完全不是这样。Agent处理的是会话、上下文、用户偏好、知识片段,这些数据的核心特征是流动性和关联性。一段对话不是写完就不动的文件,它会和用户历史行为关联,会被新信息覆盖,会过期作废,还会在某个时刻因为语义相似而被召回。你把一条消息存成一个对象,就像把河流里的水一瓢一瓢装进密封罐:物理上能存,逻辑上却失去了河流本来的流动关系。
S3引以为傲的几个特性,在Agent场景下反而成了负担:
- Key是无结构的字符串,没人在Key里维护“这是哪个用户第几个会话的第几条消息”这种语义。
- 没有原生的数据过期机制,对象存进去之后要自己写定时任务清理。
- 不支持“按相关性排序”的查询,只能做前缀匹配和列表扫描。
- 权限是基于桶或前缀的,做不到“这个Agent能读属于自己的记忆块,看不到别人的”。
这些不是S3的缺陷,而是它的适用边界。对象存储适合做备份、归档、文件源,但它天然不适合成为Agent记忆的主力存储。很多团队绕来绕去,最后做出一个“S3+关系型数据库+向量数据库+定时任务”的缝合方案,代码越来越厚,脏数据越来越多,这就是没有意识到S3定位局限的代价。
1.2 Agent会生成四种数据,普通对象存储一种都伺候不好
我在生产环境里给AI Agent应用做了数据盘点,最后归成四类,每一类和S3的“文件思维”都有冲突。
第一类是会话流水。 一个用户在一个session内会产生很多条消息,这些消息按时间顺序追加,同时需要支持“拉取最近N条”“按主题找某一段聊天”“找到某个用户上周说过的话”等操作。把每条消息作为一个对象存S3,最大的麻烦是会话恢复时要批量读取,每一轮对话都产生几十次GET请求,而且这种读和写是高频的,不像文件归档一年可能只读一两次。会话流的本质是一条动态增长的队列,对象存储没有原生的“游标分页”能力,你得在外部维护索引,这不叫省事,叫把简单问题复杂化。
第二类是长期记忆和知识块。 这类数据可能是用户画像、产品知识、FAQ片段,也可能是Agent从一次对话里提炼出来的偏好。比如一个用户说“以后预订酒店尽量选高层”,这句话如果只是存成日志,下一次对话根本不会被想起来。它需要被切片、向量化、按语义召回,还要处理“用户改主意了”的更新和“这条记忆过时了”的遗忘。S3对这样的数据完全无感,它只会安静地帮你保存,至于这个片段和另一个片段之间的语义关系,它既不理解也不关心。
第三类是任务状态和工具调用结果。 Agent在执行任务时,会产生中间状态:一个查询是否成功、外部API返回了什么、哪个步骤重试过。这些数据生命周期短,更新频繁,需要版本控制,有时还需要在链路中断后恢复。如果存放不当,很容易堆积成海量垃圾对象。
第四类是带格式的源文件。 PDF、表格、图片、录音,这些文件本身需要被Agent阅读、解析、切片。你不能只保存原始二进制,还需要生成对应的文本块、embedding向量、引用关系。纯对象存储能放文件,但文件和它的语义索引之间缺少关联能力。
可以看出,这四类数据的共性是:它们需要一套理解“会话”“记忆”“租户”“关系”的存储能力。传统对象存储给不了,所以我们必须换一种存储组织方式。
1.3 根本矛盾不是容量,而是百万级用户下的关系面压力
很多人问我:“百万级用户一天产生多少数据?S3扛不住吗?”实际上,如果只算容量,S3完全可以扛。假设100万日活用户,每人每天产生20条消息,平均一条文本2KB,一天新增也就是40GB,对象存储的容量和带宽都毫无压力。
真正的问题出在每次请求需要跨越多少层才能把数据拼回来。我拆过一条典型的Agent对话恢复链路,原来长这样:
- Agent收到一个用户消息,需要恢复该用户最近20轮上下文。
- 程序先查PostgreSQL里的消息索引表,找到最近20轮的消息Key,顺便按user_id过滤。
- 拿到Key后,逐个调用S3的GET,把每条消息内容拉回来。
- 如果要加入长期记忆,还要再查向量数据库,把这几天沉淀的记忆碎片召回。
- 最后合并排序,截断到上下文窗口,再调用LLM。
这个链路的故障点太多了:PostgreSQL在百万级消息量下,按session分页查询需要正确的联合索引;S3的GET延迟在高并发时会抖动;向量库和PG之间还会出现数据不一致。用户可能感觉不到存储容量有问题,但他们会觉得Agent变笨了、反应变慢了、偶尔还会“失忆”。
Agent类应用对存储的诉求,从来不是“能不能放下”,而是“一次对话开始前,能不能快速找到与这个话题相关的所有信息”。当场景从几万用户放大到百万级,这个“找到”的动作会无限放大存储模型的缺陷。S3提供了文件级访问能力,Agent需要的却是基于时间、用户、会话、语义的关联检索能力。想通这一点,后面接受Agent Bucket的模型就顺理成章了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Agent Bucket的存储模型:把桶升级成“语义加工厂”
2.1 它不是又一个数据库,而是带语义层的存储服务
Agent Bucket这个命名其实很讲究。它保留了S3里“Bucket”的概念,让你在做权限、配额、监控时依然有清晰的边界,但它内部不再只存扁平对象,而是存带有语义信息的“数据块”。
我个人理解,Agent Bucket的定位是:对象存储的能力底座,加上一层AI场景的数据抽象。 底层仍然需要对象存储来保证持久化,但对外暴露的API不再是put/get/list,而是面向会话、记忆、知识数据的高级操作。比如你可以把一个知识片段写入某个Collection,自动做向量化,设定它在哪个时间范围内有效,并和某个租户、某个Agent绑定;查询时不再用Key前缀去猜,而是直接问“和这句话语义最接近的5个记忆节点是什么”。
这个设计的高明之处在于,它把过去需要团队自己拼装的零件整合成了一个整体。过去你为了做一个对话记忆系统,要自己部署Redis做短期会话、MySQL存长期记忆、向量库做召回、定时任务做清理——Agent Bucket把这几样合并到了一层:数据写入时天然知道它是哪个会话的,数据检索时天然支持时间范围和向量相似度,数据过期时按照Collection配置自动淘汰。对开发团队来说,少维护一套系统,就意味着少踩一堆坑。
2.2 核心抽象只有四层,别想复杂
一开始我也担心,这类系统会不会引入一堆新概念,让团队学习成本陡增。实际走下来,Agent Bucket的核心抽象非常克制,只有四层:Bucket、Collection、Chunk、Attachment。
- Bucket:最外层命名空间,通常一个应用或一条业务线一个Bucket。它承担配额管理、日志审计、IAM策略绑定。这里我不建议按用户拆Bucket,后文专门讲。
- Collection:相当于“数据类型”的分区。建议把会话消息、用户记忆、知识文档、工具调用记录拆成不同Collection,因为它们的生命周期和访问模式完全不同。
- Chunk:最小读写单元。一个Chunk可以是一条聊天消息、一个记忆片段、一段切好的知识文本。除了正文,它还携带routing_key(通常由user_id:session_id组成)、content_type、metadata、expire_at、version等属性。
- Attachment:用于表达Chunk与其他实体的关联关系。比如一条用户记忆可能来自某次会话,需要挂到某个用户实体上;一个知识块可能引用了一个PDF源文件,需要留一条指针指向S3。
我用一张表把各层核心字段和用途整理过:
| 层级 | 核心字段 | 作用 |
|---|---|---|
| Bucket | name, region, policy, quota | 命名空间与权限边界 |
| Collection | routing_key_prefix, retention, indexing_mode | 区分消息、记忆、日志,配置生命周期 |
| Chunk | id, routing_key, payload, metadata, version, ttl | 最小概念单元,支持读写与版本 |
| Attachment | from_id, to_type, to_id, relation | 建立记忆、会话、用户、文件的关系网 |
这套模型跑下来,最明显的感受是:以前存下来的只是一堆字节,现在存下来的数据天生“带着关系”。比如一个用户改了收货地址,系统只需要对该用户的记忆Collection做一次upsert,新写入的Chunk会替换旧的地址记忆,同时保留历史版本用于回溯,Agent下一次回答时天然用到新地址,这就是语义层带来的价值。
2.3 从PutObject到UpsertChunk的API对照
真正开发时,你很快就会体会到这里面的差别。我们以异步写入一条消息、恢复最近对话、按语义召回记忆三步为例,对比原来的S3版写法和Agent Bucket版写法。
以前你用S3,可能要写成这样:
python复制import boto3
s3 = boto3.client("s3")
# 写入一条消息
message_id = "msg_10492"
key = f"users/{user_id}/sessions/{session_id}/{message_id}"
s3.put_object(Bucket=app_bucket, Key=key, Body=text)
# 查询时:先从数据库查出key列表,再逐个读
keys = index_db.query_message_keys(user_id, session_id, limit=20)
for k in keys:
body = s3.get_object(Bucket=app_bucket, Key=k)["Body"].read().decode()
messages.append(body)
换成Agent Bucket,写入和查询可以这样:
python复制from agent_bucket import AgentBucketClient
agent_bucket = AgentBucketClient("app-customer-agent", region="...")
# 写入消息,自动按 routing_key 归类
agent_bucket.append_message(
routing_key=f"{user_id}:{session_id}",
chunk_id=message_id,
role="user",
text=text,
created_at=timestamp,
)
# 恢复最近20轮消息
recent = agent_bucket.scan_session(
routing_key=f"{user_id}:{session_id}",
limit=20,
order="desc",
)
# 如果这条消息需要沉淀为用户长期记忆,再单独写入记忆Collection
agent_bucket.upsert_chunk(
collection="memory",
routing_key=user_id,
content="用户希望预订酒店时优先选择高层房间",
embedding=embedding_model.encode("用户希望预订酒店时优先选择高层房间"),
expire_at=None, # 长期记忆不自动过期
)
原来的代码里,你花了大量精力维护“先查索引再取文件”的逻辑;新写法里,存储服务自己知道“同一个routing_key下的数据属于同一个会话”,扫描会话和召回记忆都是原生能力。代码量减少多少不重要,重要的是少了一个跨系统一致性的维护点。
2.4 权限边界:每个Agent都有一道不该越过的墙
多Agent协同的场景越来越多:一个售前Agent管产品咨询,一个售后Agent管退换货,还有一个内部Agent做数据分析。每个Agent背后都挂着不同的数据权限。过去用S3做隔离,通常依靠“每个Agent一个桶”或“不同前缀+不同IAM策略”,但这套逻辑管不住同一桶里动态创建的记忆数据。
Agent Bucket把权限推进到了Chunk和Attachment级别。每个Chunk都带routing_key和owner信息,检索接口强制要求传入身份上下文,如果你用一个售后Agent的凭证去检索另一个用户私有记忆,哪怕存储桶本身能访问,底层策略引擎也会把它过滤掉。也就是说,物理上能连通,不代表逻辑上能越权。
这里我想多说一句:如果你的Agent应用涉及大量C端用户私人数据,权限逻辑不能只依赖“前端不传别人的id”这种君子协定。Agent的工具调用是模型自己决定的,模型偶尔会构造出奇怪的参数,一旦它拿到了过宽的存储凭证,就可能把别人的session数据拼进自己的上下文。把权限藏在存储策略层,是对数据安全最基本的尊重。
3. 30分钟迁移实战:从存量S3切到Agent Bucket
3.1 开始前先盘点三类数据,决定哪些迁、哪些留
别一上来就写代码。第一步是把现有S3桶里的数据做一个分类,我的经验是分成三批处理。
第一批:不需要迁移的数据。 用户上传的头像、附件、原始PDF、录音文件、离线日志,这些本来就该留在S3里继续当“文件仓库”。Agent Bucket需要的是语义化数据,它甚至可以把源文件作为Attachment指针指向这些S3对象,不需要全部搬进来。
第二批:必须迁移的数据。 会话消息正文、长期记忆数据、知识库切块,这些是Agent推理时真正要消费的数据,是这次迁移的核心对象。
第三批:可以暂时不动、只做新老并行写入的数据。 比如工具调用产生的中间状态,如果历史数据价值不高,没必要清洗,直接让新链路从切换时间点开始接管即可。
盘点时做一个简单的存量统计表,至少记录这些维度:对象总数、平均大小、是否有partition前缀、是否已有索引库记录Key、写入/读取频率。我的经验是,如果存量数据已经有数据库记录Key,迁移会轻松很多;如果全靠S3的前缀组织,那就要先想好“对象的Key能解析出哪些业务字段”。
3.2 初始化一个Agent Bucket,几条命令就能搞定
Agent Bucket的部署方式并不神秘。如果你的公司已经上了云,通常控制台直接创建就可以;如果想先在本地验证,社区版也提供了容器方案。我这里以本地容器为例:
bash复制docker run -d --name agent-bucket \
-p 9000:9000 \
-e AGENT_BUCKET_REGION=cn-east-1 \
-e AGENT_BUCKET_INDEX_DIR=/data/index \
-v /data/agent-bucket:/data \
agentbucket/community:0.11
启动后,创建一个业务Bucket,并建立两个基础Collection:
bash复制# 创建消息流水 collection,保留30天
agent-bucket create-collection \
--name message \
--retention-days 30 \
--index-mode time
# 创建长期记忆 collection,不过期,开启向量索引
agent-bucket create-collection \
--name memory \
--retention-days 0 \
--index-mode vector
这里有个容易忽略的点:每个Collection的索引模式要提前规划。 会话消息流水建议用时间索引,不需要每条都做embedding;长期记忆和知识块才开向量索引。如果所有数据一股脑全走向量化,成本会直线上升,后面我会专门算这笔账。
3.3 数据访问层改造,集中在这三处代码
30分钟能完成接入的前提是,你的代码里对存储的调用不能散落得到处都是。理想情况下,应该有一个数据访问层或Repository层。如果没有,改造前先花一点时间把调用点收敛,这比盲目替换要紧得多。
代码改造重点有三个地方:
第一处:写入消息的代码。 把原来的“写PostgreSQL”“写S3”“写Redis”合并成一次Agent Bucket写入调用。这里要注意,如果你还在测试阶段,建议开双写模式:新旧链路同时写,读走新链路,发现异常立刻切回。
python复制async def write_message(user_id: str, session_id: str, message: Message):
# 旧链路,便于回滚
legacy_storage.put_message(user_id, session_id, message)
# 新链路
agent_bucket.append_message(
routing_key=f"{user_id}:{session_id}",
chunk_id=message.id,
role=message.role,
text=message.text,
created_at=message.timestamp,
)
第二处:恢复上下文的代码。 把原来“查数据库Key列表,再逐个GET”的逻辑,替换成一次scan_session调用。这里你会立刻感受到延迟的变化,因为从N+1次请求变成了一次范围读取。
第三处:长期记忆的读取。 原来可能要从向量数据库单独查询后再合并,现在直接调用query_memory,把结果和时间线数据一起返回。
python复制async def build_context(user_id: str, session_id: str, query_text: str):
recent_messages = agent_bucket.scan_session(
routing_key=f"{user_id}:{session_id}",
limit=20,
order="desc",
)
memory_hits = agent_bucket.query_memory(
routing_key=user_id,
query_embedding=embedding_model.encode(query_text),
top_k=5,
)
return merge_context(recent_messages, memory_hits)
这段代码特别适合先跑通一个小流量灰度:选一批测试用户把读写切到Agent Bucket,其他用户继续走旧链路,对比准确率和延迟,没问题再放量。
3.4 存量数据迁移与双写回放,注意幂等
如果你的历史数据都在S3,写一个迁移脚本是免不了的。一个轻量方案是把旧的S3对象扫描一遍,把Key解析成user_id、session_id、message_id,再写入Agent Bucket。为了演示,我贴一段核心逻辑:
python复制import boto3
from agent_bucket import AgentBucketClient
s3 = boto3.client("s3")
client = AgentBucketClient("app-customer-agent", region="...")
paginator = s3.get_paginator("list_objects_v2")
for page in paginator.paginate(Bucket="legacy-bucket", Prefix="messages/"):
for obj in page.get("Contents", []):
raw = s3.get_object(Bucket="legacy-bucket", Key=obj["Key"]).read()
# 假设 key 形如 messages/{user_id}/{session_id}/{message_id}.json
user_id, session_id, message_id = parse_key(obj["Key"])
client.append_message(
routing_key=f"{user_id}:{session_id}",
chunk_id=message_id,
text=raw.decode("utf-8"),
created_at=parse_timestamp(raw),
)
这段脚本要加两样东西:分批限流和幂等判断。分批限流是为了别把旧S3的读带宽打满;幂等判断是要避免迁移中断重启后同一批数据重复写入。我的做法是给每个Chunk带上原始对象Key作为dedupe_key,处理逻辑里遇到重复Key直接跳过。
存量消息的历史部分我建议只做时间索引,不统一做向量化。对几个月前的消息重新计算embedding,既花钱又收益低。正确的做法是:先把消息流水完整迁移过去,让时间线恢复功能可用;然后按业务优先级,只对近一个月的高价值消息异步做向量化,让语义召回具备“近期记忆”能力就行了。
3.5 验证清单和回滚开关,别裸奔上生产
迁移完成不是终点,上线前我会跑一遍这样的验证清单:
- 会话恢复验证:随机抽10个老用户,把最近20轮消息完整恢复出来,和S3老链路的结果做diff。
- 长短期记忆验证:给测试专用用户写入几条偏好记忆,再模拟一次相关提问,看召回结果是否包含这些记忆。
- 权限隔离验证:用两个不同租户的测试身份交叉访问,确认A租户无法查到B租户的routing_key数据。
- 延迟指标验证:对比切换前后的P99延迟,重点看scan_session在长会话下的表现。
- 数据量核对:按用户维度统计消息条数,迁移前后总量误差应在万分之一以内。
同时要留好回滚开关。我当时用一个环境变量控制读写走向:AGENT_BUCKET_ENABLED=true/false,一旦新链路出现写入失败率高或召回结果异常,直接切回旧链路。因为有双写机制兜底,切回去不会有数据断档。存量迁移是copy不是move,旧S3对象保留至少7天,确认稳定后再做清理。
4. 百万级用户规模下的性能、成本与避坑
4.1 别再用list_objects扫描“所有聊天记录”了
做S3开发出身的人,遇到“查一下这个用户所有聊天记录”的需求,第一反应是构造prefix,调用list_objects。这种做法在数据量小的时候没什么,数据量一大就是灾难。S3的List接口是按字典序返回的,返回1000个对象要翻页,跨多个前缀更是痛苦。而会话恢复需要的是按时间倒序取最近N条,对象存储的列表顺序和这个诉求完全相反。
换到Agent Bucket之后,直接使用scan_session接口按时间游标读取,底层索引结构是专为会话流设计的,不再依赖暴力扫描。这个变化对延迟影响非常大。我曾经模拟过一个用户有5000条历史消息的场景,老办法恢复最近20轮消息需要先把整个前缀下的Key拉一遍做排序,耗时随消息总量线性增长;新方案只读取这个session最近的20条索引记录,耗时基本恒定。可以这样说:如果你在S3上做的操作是“列表+排序”,那大概率用错了工具,这类操作应该交给带索引的存储层。
4.2 “一个用户一个Bucket”是我见过最失控的隔离方案
不止一次有同行跟我分享他们的设计:“为了租户隔离,我给每个用户单独建一个S3 Bucket,这样权限就不会串了。”听起来很合理,实际在百万用户规模下,这套方案会把你拖垮。
第一个问题是运维侧爆炸。一百万个桶,每个桶都要有生命周期策略、日志配置、跨区域复制规则,控制台根本没法看。第二个问题是查询侧无解。如果你需要做全量用户的内容分析、给黑名单用户批量打标、给某类知识统一更新,就要遍历百万个桶,这比遍历百万个对象痛苦得多。第三个问题是冷启动问题,新用户注册时动态创建一个Bucket,表面看很快,但在高并发注册场景下,底层元数据服务会频繁扩缩容,埋下稳定性隐患。
Agent Bucket的思路完全不同:Bucket代表业务线,Collection代表数据类型,routing_key和metadata代表租户归属。 一个客服应用只需要一个Bucket,里面放message和memory两个Collection,每个Chunk通过routing_key和owner字段天然隔离。权限由策略引擎在做查询时动态过滤,不需要用户维度建物理桶。租户隔离是数据模型的一部分,而不是用基础设施资源数量堆出来的。
4.3 热记忆缓存与检索热点是两件不同的事
百万级用户规模下,存储层的压力往往非常不均匀。大部分用户可能一天只聊几句,但热门知识库的不同问题会反复命中同一批知识块,这就是检索热点。很多团队在热点出现后的第一反应是加缓存,但缓存解决不了“分布不均”的根本问题。
我自己的经验是分两层处理。第一层,把公共知识库里的高频知识块按内容Hash做多副本,让同一块知识落到不同分片上,避免所有请求都打到同一个索引节点;第二层,对用户级记忆做短时间缓存,但缓存时间不宜过长,因为用户可能刚说完“我不喜欢这个方案”,紧接着就会提问“按我刚才说的调整一下”。如果缓存了旧记忆,Agent会非常尴尬地用原来的偏好去回答。
对于会话流水,Agent Bucket支持同一个routing_key的数据簇在写入时保持亲和性,这样单个会话的连续读写都落在同一组节点上,不需要跨区聚合。这是个很容易被忽略但非常重要的小设计。会话是强时间局部性的数据,Append之后通常马上要被读取,如果写到一个节点,读却在另一个节点,每次都要走分布式事务或跨节点同步,性能不可能好。
4.4 账单拆解:真正的成本大头不在存储容量
我们做一个粗略的模型来帮助理解成本结构:假设100万日活用户,平均每人每天产生20条消息,一天就是2000万条消息,每条消息文本2KB,纯存储日增约40GB,这个规模在对象存储里微不足道。但如果你的每条消息都做embedding,再送到向量索引里,那才是成本失控的开始。
来看几种数据在使用链路中的差异:
| 数据类型 | 建议索引方式 | 成本控制 |
|---|---|---|
| 会话原文 | 时间索引 | 低 |
| 用户长期记忆 | 向量索引 | 中,控制在每个用户每天几条 |
| 知识库分块 | 向量索引 | 高,更新不频繁,适合常驻 |
| 工具调用日志 | 时间索引,或直接丢弃 | 低 |
我在接入早期犯过一个错误:把对话消息全部做了向量化。结果语义召回的效果提升不明显,账单倒是涨得飞快。后来调整策略,只对用户主动表达“我喜欢”“我不喜欢”“以后要怎样”这类关键节点做向量化,其余消息保持原文时间索引。这样既保留了语义召回能力,又不会让索引成本和无用向量垃圾膨胀。
关于账单,真正的核心观察是:成本的大头来自“多套存储体系叠加”。 原来你为了支持Agent,同时维护了S3消息体、MySQL索引、Redis短期缓存、向量库记忆,每套系统都有独立的计算和存储成本,还要加上数据同步的运维人力。切到Agent Bucket后,很多重复建设被合并了。相比之下,它产生的索引费用甚至低于原来维护一套独立向量数据库的费用。不要被“新增一个存储”吓到,算总账往往更节省。
5. 上线一段时间后,你大概率会遇到的几个问题
5.1 Worker直接持有对象存储密钥是最常见的隐患
很多团队把Agent服务部署成常驻Worker,环境变量里直接写着存储桶的AccessKey和SecretKey。短期看很方便,但Agent的应用场景和传统服务不同——模型会调用工具,工具函数的参数可能来自用户输入。一旦某个工具把存储凭证暴露到日志,或者模型被诱导生成了一个访问其他前缀的请求,你的数据边界就被撕开了一个口子。
更稳妥的方案是走短期凭证或最小权限令牌。Agent启动时从凭证服务换取一个有效期短、只允许访问当前租户指定Collection的令牌,过一段时间自动失效。这样即使环境变量被泄露,攻击者也拿不到永久密钥。另外一个细节是:Agent在对话中不应该直接具备“删除记忆”的权限,记忆的修改应该由Agent先提出动作,再由服务端业务审批逻辑确认后执行,否则模型一旦误判就可能把用户的重要记忆清掉。
5.2 TTL千万别一刀切,否则Agent会“失忆”
Collection的生命周期策略如果不分类设置,事故概率非常高。我们曾经把message Collection的保留期统一设成了30天,结果一并把存用户长期偏好的memory Collection也覆盖了默认过期时间,上线两周后发现不少用户在一个月后不再记得自己的偏好。这类问题的隐蔽性在于:它不会立刻报错,而是表现为Agent的个性化能力逐渐退化。
现在我的策略是明确分层:
- 会话细节类消息:保留30~90天即可,过期后可以安全清理。
- 用户长期记忆:不自动过期,只做审计和手动清理。
- 会话过程中产生的用户摘要:定期续期,比如用户在最近7天内有活跃,就把该用户摘要的过期时间往后延长。
Agent会“失忆”的原因往往不是模型能力不行,而是存储层把不该丢的数据丢了。设置生命周期策略之前,先问一句:如果这条数据明天消失,用户会感受到吗?如果会,就不要简单套用过期规则。
5.3 覆盖更新不等于删除,版本是个好东西
我这里说的版本,不是简单保留历史快照,而是指Agent业务层需要具备“撤销刚才那次改写”的能力。比如用户说“我以后订酒店都选高层”,系统把记忆改成了偏好高层;过了半小时用户又说“其实我之前说反了,低层更方便”,Agent需要更新这条记忆。这个更新动作如果不保留前一个版本,回溯时就没有依据。
有些场景甚至需要把记忆回滚到三天前。比如运营人员发现某次数据清洗脚本写错了,把一批正常用户的偏好全部重置成了默认值,如果没有版本机制,这些数据只能靠备份恢复,运维事故就变成了数据事故。所以写入记忆类Chunk时,尽量保留前一个version,至少保留最近几个版本用于回滚。
5.4 源文件与语义块分开存,才是最舒服的组合
最后一条经验是:不要试图把源文件原样塞进Agent Bucket的语义模型里。Agent Bucket适合存切好的文本块、消息、记忆、元数据和向量,而用户上传的PDF、图片、录音原始文件,放在S3里仍然是最理想的选择。怎么做衔接呢?在Agent Bucket的Chunk里留一个attachment字段,指向那个S3对象的URI,就可以把“语义引用”和“原始证据”自然地串起来。
这样分工之后的好处很多。Agent在回答问题时,如果需要引用某个PDF里的具体内容,可以通过Attachment快速拿到源文件;合规审计时也能追踪到某段回答是依据哪个源文件生成的。你不需要为一个语义块存储方案放弃S3的优势,它们本来就是配合关系,而不是替代关系。
我自己的体会是,存储层的改造没有那么多玄学,关键是找对边界:文件找S3,会话与记忆找Agent Bucket,源文件和语义块各自放在自己最擅长的位置上。把这条边界理清楚,AI Agent从Demo走向百万级用户时才不会在存储上栽跟头,这也是我最想提醒你的地方。
