第 7 章 / 共 11 章
选:需要时才取回来
7.1 你把 12 个文件一次性喂了进去
你想让 agent 彻底搞懂 shopfront 的结算流程,于是做了看起来最负责的一件事:把 src/checkout/ 下你能想到的文件一次性 @ 了进去,一共 12 个,又把整份 docs/api-migration.md 原文贴在后面。你的想法很朴素——信息越全,判断越准。
它读完了,给出的迁移方案条理清晰,然后在第三步引用了一个字段:order.legacyTotal。这个字段确实在迁移文档里出现过,就在文档中段,紧挨着一句「v1 中的 legacyTotal 已在 v2 中废弃,改用 amounts.total」。它把废弃项当成了现行接口。
你没法说它「瞎编」。那个字段真的在你给的材料里。问题出在你给材料的方式上:你把一份包含大量历史包袱的文档整块推进了窗口,而废弃说明和字段名之间只隔着半句话。对模型来说,legacyTotal 和 amounts.total 是两个高度语义相似的候选项,一个在文档中段,一个在文档中段偏后。它挑错了。
第 6 章解决的是「怎么把状态留下来」,这一章解决相反方向的问题:怎么决定这一轮把什么放进去。
7.2 「全都给它,让它自己挑」为什么不管用
这套旧做法背后有个隐含假设:模型的筛选能力是免费的,只要材料在窗口里,它就能找到对的那条。三件事同时否定了这个假设。
第一,语义相似的干扰项会拉低准确率。 这不是「无关信息被忽略」那么温和。legacyTotal 和 amounts.total 都长得像「订单总额」,都出现在同一份文档里。你多塞进去的每一个近义候选,都在把正确答案的相对信号压低一点。上下文腐化(context rot)的受控实验里,语义相似干扰项正是主要机制之一。
第二,中间的内容最容易被忽略。 「中间迷失」是可复现的现象:模型对输入的开头和结尾注意得好,对中段明显更差。你贴的那份迁移文档,废弃说明恰好落在中段。你不是没给它,你是把它放到了注意力最薄的位置。
第三,你付了全额预算。 12 个文件加一份长文档,可能就是几万 token。这些 token 每一轮都跟着走,占掉的是本该留给后续几十轮工具调用的空间。你为一次性的「求全」,透支了整个长任务的预算。
更根本的一点:预先塞满是在你还不知道需要什么的时候做的决定。任务开始时你并不知道结算流程的关键在哪个文件,所以你只能全塞。而如果推迟这个决定——先看结构,命中了再深入——你会在信息更充分的时候做出更准的取舍。
7.3 选:把「取回」变成任务中的一个动作
「选(Select)」的核心是把检索从「任务开始前的一次性装载」改成「任务过程中随时可发生的动作」。四动作里,写管的是把东西挪出窗口,选管的是再把对的那一小块挪回来。
落地有三条路径,通常混着用。
路径一:检索。 embedding 语义检索、关键词、grep,组合起来用。在代码仓库里,grep -rn "legacyTotal" src/ 的信噪比常常高过语义检索——代码里的标识符是精确的,精确匹配就够了。语义检索适合找「跟结算相关的文档段落」这类模糊需求。别在两者之间二选一。
路径二:把文件路径和目录结构本身当成检索线索。 这是代码 agent 最常用也最被低估的一条。src/checkout/ 这个路径名本身就携带信息;src/api/client.ts 告诉你 API 调用都收在这里。有经验的做法是逐层收窄:先看目录 → 再看文件头(import、导出的函数签名)→ 最后才读全文。三步之后你可能只读了 2 个文件的完整内容,而不是 12 个。每一层都是一次「值不值得往下读」的判断,而这个判断是拿着上一层的结果做的,比一开始就做要准。
路径三:渐进式披露(progressive disclosure)。 这是把「选」做成一种常驻结构的办法。它把一份知识拆成三层:只有元数据常驻上下文,命中了才加载核心内容,真正用到才读附属文件。Agent Skills 就是这个结构的一个实现,但它不是某个产品的独有特性——它是「选」的通用落地形态,你在任何 agent 系统里都可以照着搭。

图 7.1:看漏斗的三道口径——1200 个文件的仓库、目录与元数据层、最终真正进入这一轮推理的两三份内容。注意每一层的收窄都发生在你已经掌握上一层信息之后,而不是在任务开始时一次性决定。
7.4 三层结构:什么常驻,什么按需
把 shopfront 的知识按这三层摆一遍,界线就清楚了。
| 层 | 常驻还是按需 | 典型体积 | 放什么 | shopfront 里的例子 |
|---|---|---|---|---|
| 元数据层 | 常驻 | 每项几十 token | 只有 name + description。让模型知道「有这么个东西、什么时候该找它」 | checkout-v2-migration:「把结算相关代码从内部 v1 API 迁到 v2 时使用,覆盖字段映射、错误码语义和回滚步骤」 |
| 核心层 | 命中才加载 | 几百到几千 token | 主体内容:流程、规则、判据 | 该技能的 SKILL.md:v1→v2 字段对照表、409 的处理约定、迁移顺序 |
| 补充层 | 用到才读 | 不限,可以很大 | 附属文件、长表、脚本、样例 | docs/api-migration.md 全文、tests/checkout/ 下的既有用例、一个批量改写字段名的脚本 |
关键在第一列和第二列的组合。元数据层为了常驻,必须极小——它是你为「让模型知道这东西存在」付的固定月租,仓库里技能一多,这笔月租就是实打实的预算。核心层为了能被完整读懂,可以铺开写。补充层则彻底放弃常驻,只在核心层明确指路时才被打开。
回到开头那个 bug:如果 docs/api-migration.md 待在补充层,agent 是在「已经知道自己在做 v2 迁移、已经读过字段对照表」的状态下去查它的。这时候它去查 legacyTotal,拿到的是一个带着上下文的答案,而不是一段孤零零的中段文字。
7.5 工具描述也要走一遍「选」
容易被忽略的是:工具定义本身就是上下文。shopfront 接了一个内部订单系统的 MCP 服务器,它可能带进来十几个工具,每个工具的名字、描述、参数 schema 都常驻在每一轮请求里。你没主动贴过一个字,但这部分预算已经花掉了。
所以「选」同样适用于工具:把工具描述本身当作可检索对象,只把这一轮可能相关的那几个放进去。做法是先对工具描述建一层索引,按当前任务检索出候选集,只展开候选集的完整定义。改结算代码的那几轮,订单系统的退款工具、库存工具大概率一个都用不上。
这里有一条来自生产经验的重要修正:如果你要临时禁用某个工具,掩码 logits,不要把工具从定义里删掉。删掉会改变提示前缀,让 KV-cache 整段失效——你省下的那点工具描述 token,代价是缓存命中率崩掉,而缓存命中与否的输入单价可以差出接近一个数量级(具体倍数以你所用模型的当期定价为准)。「选」是在装载阶段决定放什么,不是在对话中途反复增删已经稳定下来的前缀。
7.6 怎么写出「能被选中」的东西
三层结构成立的前提是:元数据层足够准,命中该命中的,不命中不该命中的。这意味着 description 就是这份知识的 API。模型看不到核心层内容,它只能拿 description 做判断。
写 description 的原则是:写触发场景,不要罗列功能。功能罗列回答的是「这是什么」,触发场景回答的是「我现在这个处境该不该调它」——后者才是模型要做的判断。三组对照:
| 模糊写法 | 精准写法 |
|---|---|
| 「结算模块相关知识」 | 「修改 src/checkout/ 下的下单、支付回调、订单状态流转代码时使用;包含 v1/v2 API 的字段对照与错误码语义」 |
| 「API 迁移助手」 | 「把内部 API 调用从 v1 迁到 v2 时使用,包括改写请求体字段、处理 409 幂等响应、确认哪些端点还没有 v2 版本;不覆盖第三方支付网关的接口」 |
| 「测试相关」 | 「为 tests/checkout/ 补充或修复用例时使用;说明本仓库的 mock 约定、如何构造测试用订单夹具、以及为什么禁止在结算测试里打真实网络」 |
右列多做了三件事:给了具体路径(模型可以拿路径和当前工作对照)、给了具体触发动作(「改写请求体字段」而不是「迁移」)、必要时明确了边界(「不覆盖第三方支付网关」)。第三条尤其值钱——说清不适用范围,能挡掉一半误命中。当你有两个技能职责相邻时,边界句就是它们之间的分界线。
这条判据和工具设计是同一条:如果人类工程师读完描述都说不清该用哪个,不能指望 agent 做得更好。写完一组 description,把它们并排放着读一遍,问自己能不能一眼分开。分不开就改,或者合并。
7.7 一次检索就撒手不管,是这一章最大的坑
这在长任务里必然出问题,因为需求会漂移。shopfront 的迁移任务开始时,你需要的是 src/checkout/ 的结构和字段对照表。跑到第 25 轮,你发现 v2 的 409 语义和你想的不一样,这时你真正需要的是错误码那一节和 src/api/client.ts 里的重试逻辑——而你开头检索的那批内容,一大半已经变成了纯粹的占位符,还在每一轮消耗预算,还在给中间位置制造干扰项。
所以要允许中途重新选:检索是一个可以随时再次触发的动作,不是初始化步骤。具体做法是在任务里显式留出重选的时机——一个子任务做完、一次方向调整、一次报错暴露出你之前理解错了某个前提,都是重新问一句「现在这一轮真正需要的是哪几块」的时机。配合第 6 章的笔记文件,你甚至可以把「当前需要哪些材料」写成笔记里的一个字段,每次更新笔记时顺手校一遍。
还有一个反向的坑:过度收窄。三层结构不是让你把什么都推到补充层。如果一条约束每一轮都要用到(比如「本仓库禁止在结算测试里打真实网络」),它就该常驻在 CLAUDE.md 里,而不是藏在某个需要命中才加载的技能中。判断标准是使用频率,不是内容长度。
7.8 动手:给 shopfront 搭一层元数据
- 打开一个你真实在跑的 agent 会话,列出这一轮上下文里所有预先装载的材料:
@进去的文件、贴的文档、MCP 带进来的工具定义。估算各自的 token 量。 - 对每一项问一句:这一轮推理真的会用到它吗? 把答案分成三堆——每轮都用、偶尔才用、这次根本没用到。
- 把「偶尔才用」的那堆改造成三层结构:为每一项写一条 30 到 60 字的
description,只写触发场景和边界,正文挪到独立文件里。 - 把「这次根本没用到」的那堆整个移出常驻上下文,只留一条元数据。
- 重新跑一次同样的任务开头,对比两次的初始 token 数,以及 agent 第一次工具调用选得对不对。