给AI Agent装文件柜:Agent Bucket持久化存储与工程避坑
上个月陪一个团队复盘他们的 AI Agent 项目,演示环节顺得很,问答、检索、生成报告一气呵成。散会前有人随口问了一句:用户上传的那批合同 PDF 现在存在哪?负责后端的同学愣了两秒,说在容器里的/tmp目录,重启就没了。会议室安静了大概五秒钟。这种场面我见过太多次——大家在提示词工程、工具调用、多轮编排上砸了大量精力,偏偏“AI Agent 的东西放哪”这件事,一直拿临时目录凑合。所谓给 Agent 装一个专属文件柜,说的就是 Agent Bucket 这类能力:一个持久的、能按前缀隔离、能被工具直接读写的存储空间,独立于 Agent 进程的生命周期存在。这篇把从建桶到跑稳的全过程捋一遍,包括文档里通常不写、只有真跑起来才会撞上的那些细节。适合正在做 Agent 落地的后端、算法同学,也适合刚接触对象存储、想搞清楚“桶”和“目录”区别的新手。
1. 上下文窗口治不了的病:Agent 为什么非要一个独立存储层
很多人第一反应是:上下文窗口不是已经到几十万 token 了吗,还搞什么外部存储?这个想法在演示阶段成立,进了生产环境立刻破功。存储层解决的不是“装不装得下”的问题,而是“东西归谁、能活多久、谁能看见”的问题。这两件事压根不在一个维度上。
1.1 把状态塞进 prompt 的三个隐性代价
最直观的代价是钱。一段对话如果每轮都带着前面所有产物,token 消耗是随轮次线性增长的,长会话跑到后面,光是把历史内容重新发一遍就吃掉大半预算。我实测过一个文档处理场景,同一个任务在“全量塞上下文”和“只塞一份文件清单”两种策略下,单次会话成本差了将近七倍。
第二个代价更隐蔽,是注意力被稀释。上下文里堆了几十份中间产物,模型在真正需要引用某一段时反而容易抓错位置。这不是模型笨,是有效信号被噪音淹没了。我习惯的做法是:上下文里只留“有什么文件、每个文件一句话说明”,需要正文时再按需读取。这样一来,模型的决策路径短了,出错率明显下降。
第三个代价是审计。所有状态都在对话流里,出了问题你想还原“当时到底喂进去的是什么版本的文件”,基本靠翻聊天记录。一旦状态落到对象存储上,每次写入都有 key、有版本、有时间戳,排障时直接按 key 拉出来看就行。这个差别在事故复盘会上体现得特别明显。
1.2 临时目录和业务存储之间那道缝
容器里的/tmp有个致命问题:它不是共享的。Agent 服务一旦横向扩到三个副本,用户第一次请求打到副本 A,上传的文件落在 A 的本地盘,第二次请求被负载均衡扔到副本 B,B 一脸茫然地说没这个文件。你可能会说加个会话粘滞就行,但粘滞解决不了重启和滚动发布,一次发版所有临时文件清零。
也有人把中间产物直接写进业务数据库的 blob 字段。小文件还行,超过几兆之后,数据库的连接会被大字段读写占满,备份体积也跟着膨胀。更麻烦的是这类数据往往带着业务语义,混在业务表里既不好做生命周期清理,也不好单独授权给某个 Agent 读。
还有一种是拿业务的对象存储桶直接给 Agent 用,前面挂个固定前缀。能跑,但很快就会乱:Agent 的临时文件和生产素材躺在一个桶里,权限是一条策略通吃,清理规则不敢配,怕误删生产数据。这几种做法我都试过,最后都卡在同一个地方——Agent 的数据没有明确的“归属边界”。
1.3 Agent Bucket 补的是“归属”,不是“容量”
理解这类产品的关键,是别把它当成一个更大的硬盘。它真正提供的是一个边界:一个 Agent 实例、或者一个租户、或者一次任务,对应一个独立的命名空间。边界一旦清晰,后面所有事都好办了——权限可以只授到前缀级别,生命周期规则可以只对这个边界生效,成本可以按边界拆分核算,出问题可以按边界整体回滚。
我在设计的时候习惯把这三层拆开看:物理上是同一个桶,逻辑上用前缀切出三级,租户/Agent标识/会话标识。这样既不用为每个租户建一个桶(桶数量多了管理成本很高),又能在策略里用通配符精确控制到某一层。这个结构后面第 4 章还会展开讲,先记住一点:存储本身不难,难的是让 Agent 在几千个 key 里准确找到它要的那一个。
2. 能力边界:它不是网盘,也不是本地磁盘
刚上手最容易犯的错,是拿本地文件系统的心智模型去理解它。os.path那一套用惯了,总觉得“建个目录”“改个文件名”是理所当然的操作。真跑起来就会发现,很多在本地一秒完成的事,在这上面要么代价很高,要么根本做不到。
2.1 对象语义带来的四条硬约束
第一,没有真正的目录。你看到的a/b/c.txt里,a/b/只是 key 的一部分,不是真实存在的容器。所谓“创建目录”在上传接口里是靠传一个以斜杠结尾的空对象实现的,很多控制台为了体验友好会这么干,但底层并不存在这个实体。删掉a/b/这个占位对象,底下的文件一个都不会少。
第二,重命名等于复制加删除。本地mv是改个 inode,这边得把整个对象读出来再写到新 key,然后删掉旧的。一个 2GB 的文件改名,意味着 2GB 的读写流量。我见过有人在 Agent 的工具集里放了一个rename_file工具,模型一高兴连着改了十几次名,账单直接失控。后来我把这个工具从工具列表里摘了,改成“一开始就想好名字”。
第三,写入是整体覆盖,不是追加。你在本地用open(path, "a")追加一行日志很简单,这边做不到。想追加只能读全量、拼字符串、再整体写回。这个特性决定了不要拿它存需要频繁追加的日志,日志该走日志服务。
第四,列举有分页上限。一次列举请求通常最多返回一千条,超出的部分要靠继续标记翻页。这个坑我踩过,详见 5.5 小节,当时 Agent 信誓旦旦地说“目录里只有 1000 个文件”,实际上有两千多个。
2.2 挂载模式和接口模式怎么选
主流平台一般给两种接入姿势:一种是把桶挂到容器的某个路径上,Agent 里用普通的文件读写就能操作;另一种是走 SDK 或 HTTP 接口,显式调用上传下载。两者没有绝对优劣,取决于你的 Agent 怎么组织代码。
| 维度 | 挂载模式 | 接口模式 |
|---|---|---|
| 改造成本 | 极低,老代码几乎不动 | 需要替换文件操作层 |
| 随机读写 | 支持有限,大文件偏移写容易出问题 | 天然不支持,只能整体读写 |
| 延迟表现 | 元数据操作快,首次读大文件有冷启动 | 稳定,可预估 |
| 权限粒度 | 跟随挂载凭证,粒度较粗 | 可按每次请求签名,粒度细 |
| 排障难度 | 出错信息往往在系统层,不好抓 | 异常类型明确,易定位 |
| 适合场景 | 快速验证、已有脚本迁移 | 生产环境、多租户、精细授权 |
我的建议是验证阶段用挂载,生产切接口。挂载能让你在半小时内验证“这条路走不走得通”,但真上量之后,接口模式在错误处理、重试控制、权限收口上的优势会明显压过改造成本。特别是当你有多个 Agent 共享同一个桶时,挂载那套统一凭证基本没法做隔离。
2.3 哪些任务压根不该往这上面放
有三种东西我明确不建议放:一是 SQLite 或 LevelDB 这类嵌入式数据库文件,它们依赖文件锁和随机写,放在对象存储上轻则性能暴跌,重则数据损坏;二是高频小文件写入,比如每秒几十条的状态心跳,请求次数费用会非常难看,而且延迟抖动大;三是需要文件锁做互斥的协作场景,这边没有flock的等价物,硬做只能靠外部锁服务。
判断标准其实很简单:如果你的操作模式是“整份写入、整份读出”,那就合适;如果是“打开、seek、改几个字节、关闭”,那就不合适。Agent 场景里绝大多数是前者——上传的文档、生成的报告、抓下来的网页快照、跑出来的图表,全都是整体读写的形态,天然契合。
3. 接入实录:从建桶到第一个能跑的读写 Demo
理论说够了,动手。这一节按我实际的接入顺序走一遍,每一步都说明为什么这么做,而不是只给命令。
3.1 建桶时的三个决策点
区域选择排在第一位。桶的区域最好和 Agent 运行的计算资源在同一个区域,走内网端点。跨区域访问的延迟和流量费用是两个独立的问题,前者影响体验,后者影响账单,都不划算。我曾经图省事把一个测试桶建在了离自己近的区域,结果 Agent 跑在另一个区域,每次读文件的延迟稳定在 80 毫秒以上,一个任务读二十个文件就多出将近两秒。
命名规则要注意全局唯一,而且桶名通常会进到访问域名里,所以只能用小写字母、数字和短横线。我习惯用项目代号-用途-环境的格式,比如agent-workspace-dev,一眼能看出是干什么的、能不能动。
访问端点分公网和内网两个。开发机在办公室,用公网端点;服务部署在云上,必须切内网端点。这个切换经常被忘掉,代码里写死了公网地址上线,流量费悄悄涨起来还查不出原因。我的做法是把端点做成环境变量,不同环境注入不同值。
3.2 权限收口:一个 Agent 一把钥匙
生产环境千万别用主账号的长期密钥。正确姿势是给每个 Agent 分配一个独立的身份,策略里把权限限定到具体的前缀。策略大概长这样(各家语法略有差异,核心是资源路径的通配):
{ "version": "1", "statements": [ { "effect": "allow", "actions": ["bucket:putObject", "bucket:getObject", "bucket:listObjects"], "resources": ["agent-workspace-prod/tenant-a/agent-001/*"] } ] }注意resources里写的是桶名/前缀/*,这样这个身份只能操作agent-001底下的东西,碰不到别人的。这条规则看着简单,实际价值极高:Agent 的工具调用是模型驱动的,你永远无法百分百保证它不会拼出一个奇怪的路径,权限层是最后一道闸。
顺带说一句,删除权限我一般不给。真需要清理,交给生命周期规则在服务端做,比让 Agent 自己删安全得多。
3.3 一个能跑的最小 Demo
下面这段是我实际用的脚手架,兼容 S3 协议的客户端库基本都这套写法。重点是里面的Config,超时和重试参数比很多人想的要关键。
import os import boto3 from botocore.config import Config from botocore.exceptions import ClientError def make_client(): return boto3.session.Session().client( "s3", endpoint_url=os.environ["AGENT_BUCKET_ENDPOINT"], aws_access_key_id=os.environ["AGENT_BUCKET_AK"], aws_secret_access_key=os.environ["AGENT_BUCKET_SK"], config=Config( signature_version="s3v4", retries={"max_attempts": 5, "mode": "standard"}, connect_timeout=5, read_timeout=60, ), ) def build_key(*parts: str) -> str: # 关键:统一去掉首尾斜杠,避免出现 // 和 / 开头的 key cleaned = [p.strip("/") for p in parts if p] return "/".join(cleaned) def put_text(s3, bucket: str, key: str, text: str) -> None: s3.put_object( Bucket=bucket, Key=key, Body=text.encode("utf-8"), ContentType="text/plain; charset=utf-8", Metadata={"agent": "writer", "schema": "v1"}, ) def get_text(s3, bucket: str, key: str) -> str: try: resp = s3.get_object(Bucket=bucket, Key=key) return resp["Body"].read().decode("utf-8") except ClientError as e: code = e.response["Error"]["Code"] if code in ("NoSuchKey", "404"): return "" raise def list_all(s3, bucket: str, prefix: str): paginator = s3.get_paginator("list_objects_v2") for page in paginator.paginate(Bucket=bucket, Prefix=prefix): for obj in page.get("Contents", []): yield obj["Key"], obj["Size"], obj["LastModified"]build_key那个函数看着不起眼,是踩了坑之后才加的,具体原因见 5.1。list_all用分页器而不是单次列举,是 5.5 的产物。这两个函数现在是我所有项目的标配,复制过去就能用。
3.4 凭证别烧进镜像
这条单独拎出来说。容器镜像一旦构建完成,里面的文件就是永久的,你把密钥写进Dockerfile的ENV,推上去的那一刻就等于公开了。正确做法有三种,按推荐程度排序:云上运行时用实例角色自动注入临时凭证;跨环境用密钥管理服务在启动时拉取;实在不行至少用环境变量在部署时注入,别进镜像。
我做代码审查时有个固定动作:全局搜一遍AK、SK、secret、password这几个关键词,看有没有硬编码。搜出来的十次里有八次是测试脚本忘了改。测试脚本也建议统一走环境变量,别留例外。
4. 目录规划:让 Agent 在几千个文件里自己找到该找的那个
到这一步链路已经通了,能存能取。接下来是最容易做错、也最影响后续维护的部分:key 怎么设计。我见过最糟的情况是一个 Agent 跑了一周,桶里三万多条 key 全平铺在根下,名字是output_1.txt到output_30000.txt,谁也不知道哪个是哪次任务产生的。
4.1 前缀的三层结构
我的默认结构是租户ID/Agent标识/会话ID/,后面接具体的文件。这四段各自承担明确职责:
- 租户ID是第一道隔离,涉及数据边界,必须放在最前面,方便做策略和审计;
- Agent标识用来区分不同角色的 Agent,比如负责检索的和负责生成的,它们的产物不应该混在一起;
- 会话ID用时间戳加随机串,保证单次任务的所有产物聚在一起,任务结束可以整段清理;
- 文件名保留业务语义,比如
contract-2024-03-signed.pdf,别用哈希值。
有人会问,为什么不把日期放在最前面,方便批量按时间清理?如果你的清理是按任务粒度走的,日期放在会话ID里就够了,比如20240315-142301-a7f2。把租户放第一位的好处是,当你要给某个租户做数据导出或者整体删除时,一条前缀就能圈定全部。
4.2 元数据比文件名更靠得住
文件名能承载的信息有限,而且改起来麻烦。真正的业务属性应该塞进对象的元数据里。具体做法是上传时带自定义元数据,查询时用列举接口的返回字段,或者用标签功能。
我在一个文档处理项目里就是这么干的:文件名只保留人类可读的标题,真正的属性放元数据。
s3.put_object( Bucket=bucket, Key=build_key(tenant, agent_id, session_id, "report.md"), Body=content.encode("utf-8"), Metadata={ "doc-type": "analysis", "source-count": "12", "model": "planner-v3", "schema": "v1", }, )schema这个字段是我强烈建议加的。数据结构一定会变,加了版本号之后,读取端就能按版本分支处理老数据,不至于新代码上线把历史文件全读崩。source-count这类统计字段也很实用,模型拿到文件清单时能直接判断“这份报告是基于 12 篇材料生成的”,不用打开看。
注意:自定义元数据的 key 通常会被转成小写,值有长度限制,中文和特殊字符容易出问题。要存长文本或者非 ASCII 内容,老老实实放进对象主体,或者单独写一份
meta.json放到同前缀下。
4.3 清单文件还是实时列举
Agent 每次任务开始都要知道“有哪些文件可用”。两种做法:实时调列举接口,或者维护一份manifest.json。
文件数量在一百以内,实时列举完全够用,简单直接,不存在不一致问题。超过几百之后,列举的延迟和返回体大小开始变明显,尤其是每次任务都全量拉一遍,请求次数费用也会累积。这时候我倾向维护一份清单:每次写入完成后同步更新manifest.json,读取端只拉这一个文件。
代价是并发写会打架,两个进程同时更新清单可能丢更新。解决方案有两个:一是清单拆分,每个 Agent 一份,天然无竞争;二是清单只做增量追加,定期由离线任务合并成完整版。我一般选第一种,简单。真要精确一致,就退回去实时列举,别在清单上做复杂的并发控制——那属于给自己找麻烦。
还有个中间方案是上向量索引,把文件描述做嵌入,按语义检索。这个适合文件数量上千、且 Agent 需要“按内容找文件”而非“按路径找文件”的场景。代价是索引维护成本,文件一变更就得重建部分索引,我个人建议等文件量真的到了那个规模再上。
5. 实测翻车记录:六个坑和它们的排查链路
下面这六个问题是我在两个月里真真切切撞上的,每一个我都把排查过程写出来,因为比结论更有用的是排查思路。
5.1 双斜杠与前导斜杠:一个文件变成三个文件
现象是 Agent 说“我已经保存了文件”,但另一个工具去读的时候报不存在。拉出桶里的 key 列表一看,同一个逻辑文件有三条记录:/tenant-a/report.md、tenant-a//report.md、tenant-a/report.md。
排查过程:先把 Agent 每次调用工具时传的路径打日志。发现路径来源有两处,一处是模型自己生成的,它在拼接时习惯性加了个前导斜杠;另一处是代码里os.path.join(prefix, filename)在 prefix 结尾有斜杠时产生双斜杠。两处叠加,就出现了三个变体。
根因是对“路径”的理解不一致。模型理解的是文件系统路径,代码理解的是字符串拼接,服务端理解的是 key 字面量——对它来说这三个字符串就是三个完全不同的对象。
修复方案是在所有边界处做归一化,就是我前面给的build_key函数,strip("/")之后再用单个斜杠连接。同时给 Agent 的工具定义里把路径参数的描述改清楚:传相对路径,不要以斜杠开头。改完之后这类问题归零。
5.2 并发写同一个 key 的静默覆盖
这个坑更阴险,因为没有任何报错。现象是任务结束后产出的报告内容缺了几段,看起来像是模型偷懒。
排查时我先怀疑模型,把完整输出打出来发现内容是全的。再查写入调用,发现三个子任务并行跑,每个都写了同一个summary.md,谁最后写完谁赢,前面两次的内容直接没了。对象存储的覆盖写是原子的,服务端认为这就是正常行为,不会给你任何提示。
定位之后改成了每个子任务写自己的文件summary-part-1.md,最后由汇总步骤合并。另一个场景是需要强一致的,我们引入了条件写(基于 ETag 的乐观锁),写入时带上先读到的版本标识,不匹配就失败重试。这个方案能解决问题,但会带来重试开销,只在真正需要的地方用。
这里的心得是:凡是并行写入,key 必须唯一。只要看到多个执行流写同一个路径,先假设它会出问题,别抱侥幸。
5.3 大文件分片上传的超时与断点
传一个 800MB 的模型输出包,跑到 60% 断了,重试又从 0 开始,来回三次都没成功。日志里是读超时。
排查链路是这样的:先确认客户端配置,read_timeout设的 60 秒,单次 PUT 传 800MB 在带宽波动时确实可能超;改成流式分段上传后,问题变成偶发的分片失败,这时候才想到要看分片大小——默认分片比较大,单个分片超时的概率不低。
调整方案:分片大小设成 8MB 到 16MB 之间,配合并发上传,既让单次请求够快,又不至于分片数过多。同时把已完成的分片记录下来,失败重试时跳过,实现断点续传。这套改完之后,大文件上传的成功率从 70% 左右提到了接近百分之百。
提示:大文件不要用一次性 PUT,无论多大都走分片。这个习惯一旦养成,后面就不用反复调参数了。
5.4 中文文件名:上传成功但下载 404
上传一个叫季度分析报告.pdf的文件,控制台能看到,Agent 按名字去读却报不存在。查了半天发现是编码问题。
上传时我用的是requests直接拼 URL,中文没做百分号编码,被某些环节转成了别的字节序列。而 SDK 内部会做编码处理,两边生成的 key 字节不一样,自然找不到。同一个文件在控制台能看,是因为控制台自己也做了编码。
解决办法有两个层次:短期是在拼接 URL 时显式做 URI 编码;长期是业务上不要用中文做 key,用英文加日期,中文标题放元数据里。我现在默认用report-2024-Q1.pdf这种格式,展示层再映射回中文名。这个约定写进了团队规范,省了很多事。
5.5 列表只返回一千条,Agent 以为文件没了
现象特别有欺骗性:Agent 报“目录中只有 1000 个文件”,但实际有两千多条。原因是列举接口单次返回上限一千,超出的部分要靠继续标记翻页,而当时的工具实现只取了一次响应。
排查过程很快,看一眼返回体里有没有截断标志就明白了。但这件事的教训不在技术层面——Agent 会毫不犹豫地把工具的错误当成事实,然后用这个“事实”继续推理。它不知道工具可能只返回了部分结果,于是基于不完整的信息给出了看起来很确定的结论。
修复很简单,用分页器把所有页拉全。但我在工具设计上加了一条:当结果被截断时,工具返回里必须显式带上“还有更多结果”的标记和总数,让模型有机会说“数据不完整,需要进一步查询”。这个改动让后面很多类似问题都能自愈。
5.6 生命周期规则把正在用的中间产物清掉了
配了一条“三天后删除临时文件”的规则,本意是清理垃圾。结果一个跑了四天的长任务,中途的中间产物被删了,任务在最后合并阶段崩掉。
根因是规则按对象最后修改时间计算,而中间产物写完就不动了,虽然任务还在进行中。规则本身没写错,是我对“临时”的定义和规则的理解对不上。
改法是把临时产物和任务产物分开前缀:tmp/下的走三天清理,result/下的不动。另外给长任务加了个心跳文件,每次检查点更新一次,规则改成“最后修改超过三天”而不是“创建超过三天”,这样活跃任务的产物不会被误伤。
6. 跑稳之后的收口:清理、成本与安全三条线
能跑通不代表能长期跑。上线两周之后我开始做三件事:把清理规则理顺、把成本看清楚、把权限收到最紧。
6.1 给每个会话装一个保质期
我的默认策略是:会话产出保留 30 天,中间产物保留 7 天,被打标为“最终交付”的对象长期保留。实现方式是在前缀上分好类,再按前缀配规则,而不是靠给每个对象打标签——后者需要写代码维护,前者一条规则搞定。
有个细节值得说:规则生效时间通常有延迟,不是精确到秒的。如果你需要“任务结束立即清理”,得在业务流程里显式删除,别指望规则。规则适合兜底,不适合做实时控制。
另外,清理之前最好先统计再删。我习惯先跑一遍“如果按这条规则删,会删掉多少对象、多少容量”,看看数字合不合理。有一次规则写错了前缀,预演显示要删掉四万多条,占了总量的八成,一眼就知道不对。
6.2 成本其实主要不在存储容量上
很多人以为存储贵,实际账单拉出来看,容量费用往往只占一小部分,大头在请求次数和外网流量。几百 GB 的存储一个月花不了多少,但每天几十万次列举请求就很可观了。
几个省钱的做法,都是实测有效的:把列举结果在进程内缓存几十秒,避免 Agent 连续调用时重复查询;能用单次批量获取就不要循环单个拉;下载到同区域计算资源走内网,别绕公网;大文件加压缩,文本类内容压缩比通常能到三比一以上,省下的流量费远超一点 CPU 开销。
还有一条容易被忽略:失败重试也会计费。那个大文件上传反复重试的场景,一天下来多花了不少钱。把重试策略调对,本身就是在省钱。
6.3 只读挂载加写入前缀隔离
最后是安全收口。我的原则是:Agent 能读的范围和能写的范围必须分开,而且读的范围通常应该更大但只读,写范围小而精确。
具体落地是两套凭证:一套只读,覆盖整个租户前缀,用于检索类工具;一套只写,限定到当前会话前缀,用于产出类工具。Agent 的不同工具绑定不同凭证,模型再怎么能编,也跳不出这个框。
另外,对外分享文件不要用长期可访问的地址,用带过期时间的临时签名链接,有效期设短一点,几分钟到半小时。我见过有人把桶设成公开读,然后忘了,几个月后发现里面有测试数据被搜索引擎抓走了。这种事故一次就够记一辈子。
7. 我个人在几个项目里攒下的判断标准
真跑过几个项目之后,我对“要不要上这套东西”有了比较明确的判断。如果 Agent 每次任务都是独立的、不留任何产物、也不需要读外部文件,那确实不需要,直接内存处理最省事。但只要出现下面任意一条,我就建议尽早把存储层抽出来:任务需要跨多轮保留中间结果、需要多个 Agent 共享产物、需要人来查看和干预产物、需要对产物做审计或者回滚。
抽出来的时机也很关键。我见过等到业务跑起来才改造的,代价是几百个文件读写点要逐一手改,还要处理历史数据迁移。早一点做,哪怕初期只是包一层薄薄的接口,后面切到真正的桶上就是换个实现类的事。
还有个反过来的教训:不要一开始就设计一套过度复杂的存储抽象,搞七八层封装,结果每加一个功能都要动三个模块。我现在的做法是先写一个三十行的小模块,只包含put、get、list、delete四个方法和那个build_key归一化函数,跑起来之后按需长肉。这套最小实现已经陪我走过了四个项目,改动都很小。
真正拉开差距的从来不是选了哪个平台、用了哪个接口,而是那些看着琐碎的约定:key 怎么拼、路径怎么归一化、并行写怎么避冲突、列举怎么处理分页、清理规则怎么不误伤。这些东西文档里不会专门写,只能靠踩一遍记下来。踩过之后你会发现,给 Agent 配的这个文件柜,本质上定的是一套数据纪律——纪律立住了,Agent 才敢放心地把东西往里放。