译文:我从开发 Shortcut 中学到了什么:如何打造好用的垂直 Agent
让我们看看 Peter Wang 的经验:从 Shortcut 的电子表格 Agent 出发,拆解上下文分层、能力封装与按需加载如何决定 Agent 的可靠性。
作者:Peter Wang(@BrainsAndTennis)
原文:Building a Good Vertical Agent
如何开发出在一个领域真正好用的 Agent 呢?好用到用户会选择付费的那种。
在过去一年里,Agent 的形式越来越像预制菜,无非就是把模型丢到一个 While 循环里,不断调用 Tool,直到把任务跑完,而大多数任务都能通过暴露 File System 以及 Shell 来完成。
这种 Agent 其实一下午就能开发出来,而且很多人现在就是这么干的,可以说现在人人都是 Agent 工程师。然而,真正拉开一个好 Agent 和玩具 Demo 差距的,从来不是什么花哨的奇技淫巧,而是你对具体的业务场景有多了解,以及你愿不愿意在那几个真正重要的地方,耐下性子,把那些琐碎、枯燥但必须做细的工作一点点打扎实。
我花了将近一年时间打造 Shortcut Agent,圈内普遍认为,它是目前最准确的电子表格 Agent;Shortcut 已经被四家最大的对冲基金中的三家所使用,而在这些公司的场景里面,出错的代价很高,也不会有人给你同情分。我们没有微软或者 Anthropic 的渠道能力,我们手头的牌只有一张:这个 Agent 更容易答对。而在这一行,这才是客户掏钱的最硬道理。因此,我每天都在思考如何提升 Agent 的能力。
最大的问题恰恰在这里:教你「怎么开发 Agent」的文章很多,但讨论「怎样才能做出一个真正优秀的 Agent」的文章却少得多。仅从 Tool 数量这个最基础的设计问题,就能看出这个领域还远没有形成共识。Codex 和 Claude Code 各自配备了约 30 个 Tool,而 Pi 只有 7 个。连主流 Agent 在如此基本的问题上都能相差四倍,说明很多所谓的最佳实践,其实还只是各自的探索,并不存在一套公认的原则。因此,我想分享自己过去一年开发 Agent 时总结出来的一些经验,希望能帮助正在构建 Agent 的人看清其中真正重要的问题,也少一些对这个领域的神秘化想象。
答案就在这里:一个好的 Agent,要把真实任务的频率和重要性,浓缩进它 Prompt 设计里。本文接下来的部分会解释这句话的原理,以及它如何知道我们去设计具体的 Agent 系统。
把上下文当作分层缓存
如果我们不能掌控运行时,也不是模型提供商,那么我们真正有话语权的只有三个场景——System Prompt、Tool,以及 Skill、精心整理的文档和参考资料,从本质上说,它们都是同一种东西:Agent 的上下文。
因此,我们可以很简单地进行概括:当模型固定之后,Agent 的准确率,很大程度上取决于上下文的质量。上下文过于臃肿,真正有用的信号就会被淹没;上下文有所缺失,模型就只能依靠猜测。无论是哪一种,最终都会降低 Agent 的准确率。而准确率,在本文开头我们已经提过了,恰恰对应了 Agent 真正的商业价值,而且它与商业价值之间并不是线性关系:一个准确率达到 99% 的任务,可能比准确率只有 95% 的任务值钱十倍。
问题在于,用户交给你的任务难度并不是均匀分布的,而是呈长尾分布:
频率
|
| ████
| ████
| ████
| ████
| ████
| ████
| ████
| ████
| ████
| ████ ▓▓▓▓
| ████ ▓▓▓▓ ░░░░ ░░░░ ░░░░ ░░░░ ░░░░ ░░░░ ░░░░
+----------------------------------------------------> 任务种类
████ 核心任务:每个 session 的绝大部分内容
▓▓▓▓ 关键但低频的任务:一个 session 可能出现几次
░░░░ 长尾任务:单个任务很少出现,但种类繁多,而且每一种都要能正常处理
Agent 必须覆盖所有类型的任务,但不能把所有信息都塞进上下文,那只会让 Prompt 越来越臃肿。所以,目标不是「随时加载所有信息」,而是:根据任务的实际分布,尽可能减少每次任务需要加载的上下文。
有没有发现这和 CPU 面对的问题很像?程序运行时可能要访问数 GB 的数据,但处理器能调用的最快的存储空间却是非常有限的,因此,计算机采用了分层存储的架构:最小、最快的 L1 缓存,容量更大但稍慢的 L2 和 L3 缓存,再往下则是内存和磁盘。这种架构之所以行之有效,是因为 CPU 的数据访问同样呈长尾分布:最常用的数据留在最快的一层;只有遇到低频需求时,才去更慢的层级中查找。所谓 Cache Miss,就是当前缓存中命中不了需要的数据,系统只能花费额外时间,从下一层 的缓存把数据取回来。而对于最常见的 Agent 任务,我们要避免的正是这种成本开销。
Agent 的上下文也应该采用同样的结构:L1、L2、L3 分层的上下文:
几乎每一项优化,都绕不开同一个取舍:是把信息预先压缩进上下文,还是等到需要时再花时间把它找出来。
把一个能力放在 L1 层,使用时最快,但无论当前任务是否用得上,它都会在每次请求中占用 Prompt token。把它下沉到 L3 层,平时几乎没有成本,可一旦需要,就要经过多次 Tool 调用才能找到。我们的 Prompt 调优工作,是根据任务的实际能力分布,把每项能力放在总体成本最低的层级,其实 Agent 设计的奥妙就是这样。下面,我用自己最熟悉的场景举具体的例子。
先聊聊题外话:使用一个 Tool,而不是三十个不同的 Tool
在我们讨论 Agent 的上下文分层之前,还是先聊聊它的底层实现吧。
接下来我会介绍很多电子表格的能力,包括读取、写入,以及查询资料。虽然这些能力各不相同,但我并没有为它们分别设计不同的 Tool,所有的操作都通过同一个 Tool 完成,由它执行相应的逻辑代码。
async function execute() {
const data = await sheet.getCellRange("Sheet1!A1:D200");
// ...读取、计算、写入...
}
Agent 负责写代码,代码调用我们在运行时暴露的函数,再由这些函数完成对表格的读写和修改。因此,这套系统里没有单独的 read_range、write_range 或者 make_chart Tool。对于大模型来说,始终只有一个 Tool:execute_code,真正的 API 调用被封装在函数的执行中。
为什么要这样设计?
因为 Tool 越多,模型的准确率往往越低。我们自己的实验反复验证了这一点。每增加一个 Tool,就意味着要在 Prompt 中加入更多 schema,也意味着模型需要面对更多选择。职责相近的 Tool 越多,模型就越容易混淆,也越容易选错。统一使用 execute_code,可以把这个复杂的选择过程简化成一个动作:写代码。模型不再需要把一连串固定而僵硬的 Tool 调用拼接起来,而是可以借助编程语言或 DSL 的表达能力,自由组合各种功能。关于这一点,我会在之后的文章中继续展开。
这种设计模式也让前面的分层体系变得很简单:不管能力放在 L1、L2 还是 L3 。层级的区别只是大模型到底掌握了多少函数信息,例如在 L1,模型已经知道该调用什么;到了 L2,它需要先查阅整理好的说明;而在 L3,它还要深入原始文档,通过多次检索才能找到合适的函数。
L1 层:最基础,也最重要的能力:读写单元格
这一层覆盖了约 80% 的日常任务,如果一个表格 Agent 连单元格区域的读写都做不好,其他能力再丰富其实也没有意义,所以我们在这里投入了异乎寻常的努力。让我们看看一次 getCellRange 调用背后究竟做了哪些事情。
读取一个区域,本质上是在压缩信息
比如,读取一张包含 200 行数据的营收表:
重复出现的公式会用 F1、F2 等别名表示。
A2:North | B2:1200 | C2:9.99 | D2:11988(F1)
A3:South | B3:840 | C3:9.99 | D3:8391.6(F1)
A4:West | B4:1500 | C4:9.99 | D4:14985(F1)
...(还有 196 行,每行一条)...
=F1 -> =RC[-2]*RC[-1]
--- 样式模式 ---
D2:D201: 200 cells (numbers)
→ numberFormat:#,##0.00, font.color:#1A7F37
A2:A201: 200 cells (text)
→ font.bold:true
--- 补充的表头信息 ---
A1:Region | B1:Units | C1:Price | D1:Revenue
我们在这里做了三件事。
第一,合并重复公式。假设一整列有 500 个公式,依次是 =A2*B2、=A3*B3……它们引用的单元格不同,但计算逻辑是完全相同的。我们会先把这些公式统一转换成 R1C1 形式。这样,=A2*B2 和 =A3*B3 都会表示为 =RC[-2]*RC[-1]。接着统计这些公式模式;凡是重复超过十次的,就用 F1 这样的短名称代替。这样,大模型就不再需要读取 500 条近乎相同的公式,只需要看到每行的 F1,再配上一行对 F1 的说明,信息量没有减少,消耗的 token 却大幅下降。
第二,自动补全行列含义。只读取 C5:E20 时,模型看到的可能只是一片表格的数字内容,很难判断每一行和每一列分别代表什么。
因此,我们会继续向左查找对应的行名称,并向上寻找表头。寻找表头时,会比较附近各行中的文本单元格数量,将最像标题的一行识别为表头。因此,我们会继续向左查找对应的行名称,并向上寻找表头。寻找表头时,会比较附近各行中的文本单元格数量,将最像标题的一行识别为表头。这样,模型在读取数据的同时,还会得到 Region | Q1 | Q2 和 North America | ... 之类的辅助信息,而不是凭空去猜测这些数字的含义。
第三,压缩样式信息。表格的格式本身也包含信息,一个红色加粗、显示为 0.00% 的单元格,往往有着表格制作者赋予的特殊含义,但倘若逐一输出每个单元格的完整样式,格式信息很快就会淹没真正的重要的数据。因此,我们会把样式相同的单元格归为一组,再将相邻的单元格合并成区域;最终,每组只输出一行:覆盖范围、单元格数量,以及简要的样式描述。
就这样,六百条公式被压缩成一行说明,四百个单元格的样式被压缩成两行,而模型没有主动请求的表格表头也被一并补充了进来。整张表所包含的信息几乎完整保留,所需的 token 却只有原始数据逐项输出时的一小部分。把最常用的信息提前压缩好,让模型不必再花额外成本去寻找和理解,这就是在高频场景中最好的状态。
写入单元格:既要看清改动,也要发现异常
写入操作并不像看上去那么简单。一次 execute_code 调用可能改动数百个单元格,但 Agent 在不重读整张表的前提下(译者注:需要考虑到读取整张表的开销),必须准确获知执行结果。
因此,代码运行结束后,我们会返回一份结构化的 Diff,记录所有发生变化的单元格。但仅仅列出改动还不够:我们还会对这些结果进行压缩和筛选,让模型快速掌握表格的主要变化,并发现潜在的问题。
代码:
async function execute() {
const rows = await sheet.getCellRange("Sheet1!A2:C201");
for (let i = 0; i < rows.length; i++) {
const r = i + 2;
await sheet.setCell(`D${r}`, `=B${r}*C${r}`);
}
}
返回的 Diff:
--- CELL DIFF SUMMARY ---
(显示格式化后的展示值。∅ = undefined/empty。)
Changed without issues: 199 total cells
Sheet1!Row 2 (D): 1 cells
→ D2: ∅ -> 11,988 [=B2*C2]
Sheet1!Row 3 (D): 1 cells
→ D3: ∅ -> 8,391.6 [=B3*C3]
...(采样行)...
Sheet1!Row 201 (D): 1 cells
→ D201: ∅ -> 4,995 [=B201*C201]
... and 189 more rows
Cells that need review:
MUST FIX: INVALID_FORMULA: 1 total cells
Sheet1!Row 57 (D): 1 cells
→ D57: ∅ -> #REF! [=B57*C57]
例子中使用了两种压缩方式。
第一,对 Diff 进行归组和抽样,而不是去逐项输出所有的 Diff,发生变化的单元格会先按工作表和行归组。每一行结果只需要说明涉及哪些行列、共有多少个单元格,例如:Row 2 (D): 1 cells。系统不会把所有改动完整铺开,而是按照固定规则,从每一组 Diff 中选取少量样本展示,其余部分则用「另有 N 行」这样的缩略语进行概括。这样,200 次写入最终只占用寥寥几行内容,但 Agent 仍然可以知道一共改动了多少单元格。
第二,把正常 Diff 和可能导致问题的结果分开处理。没有发现问题的写入会被归入「没问题的修改」。任何看起来不对的结果,则会被单独归类进「需要检查的单元格」,例如:
#REF!之类的无效公式- 没有说明来源的硬编码数字;
- 公式中夹带的硬编码常量;
- 明显不合理的超大百分比。
而其中最严重的问题还会被标记为「必须修复」,例如第 57 行的 #REF! 如果混在 200 条正常 Diff 里,就很容易被 Agent 所忽略,而现在,它会被单独提取出来,并明确标出所属的问题类型。这套反馈机制告诉 Agent 的不只是「你改了什么」,而是「你改了这些内容,其中这一处很可能改错了」,它就像一个内置的 linter,帮助 Agent 审查自己刚刚完成的修改。
L1 层用一句话概括就是:为最常用操作提供专用接口。这样可以把 token 消耗降到最低,向 Agent 清晰反馈执行结果和潜在问题。这一层的开发成本固然很高,但仍然值得投入,因为它们几乎会出现在 Agent 处理的每一个任务中。
L2 层:按需加载的 Spec 文档
我们不能把所有东西都放进 L1 层,像是条件格式、数据透视表、图表、数据验证,这里的每一个能力都很重要,每一个 session 都有概率调用到这些能力,但它们的内容也很庞杂,如果把它们的文档都放进 System Prompt,会让不使用这些能力的任务的上下文变得非常臃肿。这就是典型的要放到 L2 层的内容。
于是我们编写了精心整理的英文 Spec 文档,就像 Skill 的 Markdown 文档一样,模型会在代码内部进行按需加载:
console.log(general.getConditionalFormattingInfo());
console.log(general.getPivotTableInfo());
console.log(general.getChartInfo());
console.log(general.getDataValidationInfo());
console.log(general.getAPIInfo("addSpanAt")); // 按名称查询函数的定义
这些 Spec 文档不是类型签名的掉书袋,而是一篇大概几百行的手写说明,描述完成任务的规范方法,包含那些原始 API 永远不会告诉你的知识。以数据透视表规范为例,它不只是列方法,而是按正确顺序传授整套操作步骤:
const pt = sheet.originalSheet.pivotTables.add("SalesPivot", "SalesData", 0, 0, ...);
pt.suspendLayout();
pt.add("Region", "Region", rowField);
pt.add("Quarter", "Quarter", columnField);
pt.add("Amount", "Sum of Amount", valueField, 8); // 8 = sum
pt.resumeLayout();
这部分内容还会记载那些我们通过反复失败才能学到的痛苦教训:比如批量修改前必须先调用 suspendLayout() 和 resumeLayout(),否则每次调用都会触发整表重建;再比如 value field 的聚合方式必须传一个原始整数(8 表示 sum),因为那个友好的 enum 在运行时根本不存在。这些都不是什么奇怪的坑,只是操作透视表的正确方式罢了。
L2 层的关键特点是,在一次任务实际需要这一层的内容之前,这一层是不消耗任何 token 的,例如一次任务并不涉及数据透视表,那么就不用为 L2 层的数据透视表 Spec 文档消耗 token。而需要使用的时候,一次 console.log 就是全部的查找成本,即便是碰到了 Cache Miss 的情况(需要去 L3 层查找),也能快速返回结果。
同样的想法,用于具体的 Tool
L2 层的思路不只适用于文档,我们把同样的模式应用到了延迟加载的 Tool 上,例如 web_search、web_crawl、create_website 等等。它们的 schema 不在 Prompt 里,取而代之的是一组元 Tool:
get_tool_info("web_search") → 返回 Tool 的 schema,然后标记为「fetched」
execute_tool("web_search", …) → 若未事先获取该 Tool 的 schema,则会拒绝执行
这组已 fetch 的工具,字面意义上就是一个 session 作用域的缓存。模型只需要加载一次 Tool 的 schema,从那以后它就是常驻的。这里的问题仍然是 Prompt 压缩与能力查找之间的权衡,解决思路也没有变化:尽量保持 Prompt 的精简,只有真正需要某项能力时才按需加载。这和 Claude 上的 deferred Tool 机制是同一种思路,但我们并没有依赖某一家模型厂商特有的 Tool 加载能力,也能实现相同的效果。
L3 层:用完整的 API 说明给长尾需求兜底
最后还有一类很难提前覆盖的长尾需求:某项能力非常冷门,我们既没有把它封装成 Tool,也没有为它写过专门的 Spec 文档。这类需求对于 Agent 的开发者来说本来就不可能全部预见,但这些任务真的出现时,我们需要保证 Agent 仍然有办法找到对应能力,不然一旦超出现有 Tool 和 Spec 文档的覆盖范围,任务就会卡死。
例如:
「给每一行添加一个 sparkline 来总结它的趋势」
Sparkline 是真实存在的 API,只是平时很少有人使用,因此通常不会被封装成常用 Tool。
「把图表的第二坐标轴改为对数刻度,并且只修改第三个数据系列的颜色」
这种配置藏在 Excel 图表面板很深的属性层级里,很难指望一份人工整理的 Spec 文档覆盖到这种程度。
「给这个单元格添加一个指向某个命名区域的超链接,再把这些 shape 组合起来」
Drawing、shape 和 hyperlink 相关 API 中有大量这样的冷门功能。往往只有用户真正发起请求的时候,我们才会知道原来这些冷门需求也需要被支持。
因此,L3 保存的是一份完整、未经缩写的底层 API 说明,对于 Excel plugin,它是完整的 Office.js API 文档;对于 Shortcut Web,则是完整的 SpreadJS API 文档。这份 API 文档足有 7 万行,胜在足够完整,再冷门的能力也大多能从中找到。但它实在太长,不可能整个放进 Prompt,让模型每次都从头读一遍,所以这时真正要解决的问题是 Agent 碰到冷门需求的时候,怎样从这 7 万行内容里迅速找到相关 API 的说明。所以 L3 层不仅是一份完整文档的巨著,还有一个配套的 Skill,教 Agent 去哪里查、该用什么关键词搜索,以及如何从中筛出当前任务真正需要的部分。
# advanced-api SKILL.md 中推荐的查询方式
grep -n '"charts.add"' api-reference.json -A 5 # 查找某个方法
grep -n '"pivots\.' api-reference.json | head # 查看某个 namespace 下有哪些内容
grep -n '"ChartConfig"' api-reference.json -A 10 # 查找某个类型的定义
grep -n '"isEnum": true' api-reference.json -B2 -A10 # 查看枚举及其可选值
这份 Skill 只有大约 100 行。它并不罗列具体 API,而是告诉 Agent:参考文档采用怎样的结构、方法和类型分别如何表示、以及遇到不同问题时应该怎样使用 grep 去查找。有了这套指引,原本「足有几万行、根本无从读起」的文档,就变成了 3 到 6 次有针对性的检索,Agent 不必通读全文,只需逐步找出当前任务所需的方法签名、参数和类型定义。
访问 L3 层的成本,确实比直接调用现成的 Tool 的消耗更大,但步骤和所需成本都是可控的,而且只有少数真正需要用到这一层能力的任务才需要付出这部分额外成本。
System Prompt 还会明确说明这条兜底路径,让模型知道应该在什么时候使用:
API HIERARCHY — There are 2 levels of API capability. Wrapped API: convenience functions; some listed directly, others via getAPIInfo(...). NEVER guess — read the docs in FULL. Raw API: use when the wrapped API doesn't cover your need… If the wrapped API can't do it, use the raw API — don't compromise.
API 层级——系统提供两层 API 能力。 Wrapped API:封装好的函数。一部分会直接列出,其余可以通过 getAPIInfo(...) 查询。绝对不要凭空猜测,必须完整阅读相关文档。 Raw API:当 Wrapped API 无法满足需求时使用,如果 Wrapped API 做不到,就转向 Raw API,不要为了迁就现有能力而降低要求。
最后一句正是 L3 层存在的意义:Agent 不应该因为上层能力没有覆盖,就卡在那里,更不应该用一个勉强可行的替代方案敷衍过去。它可以先在 L1 层寻找现成的 Tool;找不到,就进入 L2 层,读取人工整理的 Spec 文档;如果连 Spec 文档也没有覆盖,还可以继续深入查找完整的 Raw API 文档。这样一来,即使面对非常冷门的需求,Agent 仍然能在合理的调用次数内找到所需信息,并完成任务。
Prompt 预算究竟花在了哪里
不妨看看 token 最后都花在了什么地方,我们上述三层能力的分工,也直接体现在 System Prompt 的篇幅分配上。
占比最大的是 L1,大约有几百行。这里包括最核心的读写操作、execute_code 的调用约定、关键类型、Agent 几乎每个任务都会用到的少数 method,以及执行规范和安全要求。由于这些内容每次调用都会随 Prompt 一起加载,所以也是我们最需要反复压缩、精简的部分。
L2 只占很薄的一层,大约 50 行。这里放的并不是 Spec 正文,而是一份经过筛选的推荐 method 清单,以及一些简短提示:告诉 Agent 有哪些 getXInfo(...) Spec 文档可以查询,又应该在什么情况下使用。至于 Spec 的具体内容,平时不会进入 Prompt,只有 Agent 通过 console.log 主动获取时才会被加载。
L3 在 System Prompt 中几乎只占 5 行:一个 skill.md 的名称和简介,再加上散落在其他位置的少量引用。那份长达 7 万行的原始 API 参考文档始终保存在文件中,从不直接进入 Prompt。常驻的只有一份很短的 Skill,以及 API 层级说明中指向它的那一行。
因此,Prompt 预算的分配与需求频率几乎完全一致:大部分篇幅留给最常见的 80% 场景;用少量内容为另外 15% 的需求指路;至于极少出现的长尾需求,几乎不占常驻 Prompt。
这正是我们的分层缓存架构精心设计的结果。
把这套方法套用到你需要的领域
电子表格只是一个例子,这套分层方法适用于任何领域。
System Prompt 和精选的 Spec 文档中的信息压缩,本质上是在梳理一件事:你面对的用户通常会组织什么任务,以及这些任务的出现频率。而在你自己擅长的领域里,没有人比你更了解这种任务分布的现状。我们真正需要回答的只有三个问题:
- 哪些能力应该放进 L1?
把最常见、最基础的操作放进 L1 层,也就是频率曲线上最陡峭的那一段。这些能力的场景需要他们一定要快,而且必须尽可能节省 token,还必须在执行后清楚地告诉模型:刚才做了什么,产生了什么结果,又是否存在异常。
你应该把最多的精力投入这一层,哪怕研发的精力显得很不成比例,但也是非常值得的,因为 Agent 几乎会在每个任务中使用这些能力。
- 哪些能力应该留在 L2?
L2 层用来承载那些很重要、但不会频繁出现的能力。
把它们整理成清晰、准确,并且覆盖常见问题的 Spec 文档,让 Agent 只用一次查找就能定位并加载所需的内容。
这些 Spec 文档不能只罗列函数签名,它们还应该告诉模型,标准做法是什么、有哪些限制,以及哪些地方最容易出错。
换句话说,L2 要编码标准做法和约束,而不只是列出函数签名。
- L3 层如何为 Agent 兜底?
L3 保存最原始、最完整的底层资料,并配套一个 Skill,教 Agent 如何从中查找信息。
这一层不必简洁,也不必方便阅读,但必须满足三个条件:资料完整、能够访问,并且 Agent 可以在有限的步骤内定位到答案。
换句话说,即使前两层没有提供现成答案,Agent 最终也应该有能力从 L3 中把正确的信息找出来。
只要把这三类能力放到合适的层级,你就能得到一个结构完善的 Agent:
它处理高频任务时足够快,面对偶尔出现的重要任务时依然可靠,遇到罕见的长尾问题时也不会彻底束手无策。与此同时,它的常驻上下文仍然足够精简,不会因为信息过载而损害模型的判断力。
分层架构不会消失,只是分层的边界会不断移动
最后还有一个值得注意的现象:哪些上下文内容应该放在 L1 层,并不是一成不变的。随着模型能力增强,三个层级之间的边界也会不断移动。在早期的时候,大模型的能力比较弱,需要大量职责单一的小 Tool,每一个步骤也必须写得耳提面命。而如今的模型已经可以一次读懂更完整的 L2 层说明,也能直接处理更多未经仔细整理的 L3 层原始资料,并不会被庞杂的信息量压垮。因此,随着模型的不断进步,今天的 L3 层内容可能会变成明天的 L2 层内容,今天的 L2 层也可能进一步被压缩整合进 L1 层,越来越多的工作可以直接交给模型,原本位于更深层的信息也会逐渐向上移动。
但分层架构本身永远不会消失。
因为相对于我们可以预见的要放入上下文的信息,上下文窗口永远是稀缺的,而无关信息也始终会干扰大模型,降低输出的准确率。无论模型有多强,「在正确的时间,把正确的信息放到它面前」这件工程,永远都不会失去必要性。
更大的上下文窗口,很容易让人产生一种冲动:既然放得下,不如全部塞进去拉倒了,但实际上更好的思路其实早已被 CPU 工业使用了几十年:摘要常驻高速缓存,细节按需加载,只有在前两层缓存都无法解决问题时,才回到最原始的底层资料;像设计内存层级一样设计 Agent 的上下文,准确率自然会随之提升。