如何构建一个在你的领域中真正表现出色——并且让客户因为其卓越而选择你的——智能代理?
过去一年,基础架构已经标准化:一个智能代理本质上是一个围绕模型的循环,它调用工具直到任务完成。给它一个文件系统,给它一个命令行,让它通过命令行完成大部分工作。你一个下午就能写出来,多数人也确实这么做了。每个人都能构建一个智能代理——这其实并不难,而且,我稍后会说明,这也不算深奥。区分一个好代理和一个小玩具的,不是聪明才智,而是对你所在领域的真正理解,以及在少数关键地方放慢节奏、耐心细致完成工作的能力。
我花了将近一年的时间构建 Shortcut 智能代理,它被广泛认为是最准确的电子表格代理——它已部署在三家最大的多策略对冲基金中的两家内部,在这些地方,犯错代价高昂,且没有人按曲线评分。我们没有微软或 Anthropic 的渠道优势。我们所拥有的是,这个代理更经常地给出正确答案,并且在这个领域,这已成为客户选择我们的最有力理由。因此,代理的性能是我整天都在思考的问题。
而我不断遇到的一个差距是:关于构建智能代理的文章很多,但关于构建好代理的却很少。看看这个领域在像工具数量这样基础的问题上有多大的分歧——Codex 和 Claude Code 各自提供了约 30 个工具;而 Pi 只提供了 7 个。当主流代理在最基本的设计问题上相差 4 倍时,这暗示着:没有公认的原则。所以,我将分享我基于一年构建经验的原则,为任何编写自己代理的人揭开这个过程的神秘面纱。
我的原则是:一个好的智能代理是其任务分布的忠实压缩。剩下的内容只是在阐述这意味着什么,以及它迫使你构建什么。
上下文作为分层缓存
假设你不拥有环境,也没有训练模型。那么有三样东西是由你设计的——系统提示、工具和制品(技能、精选文档、参考资料)——它们本质上是同一回事:智能代理的上下文。
所以,目标说起来很简单。在模型固定的情况下,准确性是上下文质量的函数:臃肿的上下文会埋没信号,缺失的上下文会迫使其猜测,而这两者都会损害准确性。而准确性正是你所销售的——这种关系不是线性的,一个得分为 99% 的任务其价值可能是一个得分为 95% 的任务的 10 倍。
但是你的用户带来的问题并非均匀分布。他们带来的是长尾分布:
1 出现频率2 |3 | ████4 | ████5 | ████6 | ████7 | ████8 | ████9 | ████10 | ████11 | ████12 | ████ ▓▓▓▓13 | ████ ▓▓▓▓ ░░░░ ░░░░ ░░░░ ░░░░ ░░░░ ░░░░ ░░░░14 +----------------------------------------------------> 任务多样性1516 ████ 核心任务 每次会话的主体17 ▓▓▓▓ 重要但偶发 每次会话出现几次18 ░░░░ 长尾 每个都很少见——但数量众多,19 而且每个都必须能正常工作
智能代理必须处理所有这些情况。但它无法在上下文中同时容纳所有信息的联合体——这就是臃肿提示的失败模式。所以,真正的目标比“拥有所有可用信息”更精确:最小化每个任务花费的上下文,按任务分布进行平均。
这正是 CPU 面临的问题。一个程序可能触及千兆字节的数据,但紧邻处理器的存储空间却很小——所以计算机将内存分层堆叠:一个微小、即时的缓存(L1),下面更大更慢的缓存(L2, L3),然后是主内存和磁盘。它之所以有效,是因为访问也是长尾分布:将热数据保存在快速层级中,只在需要稀有数据时才向下访问慢速层级。“缓存未命中”意味着你需要的数据不在快速层级中,需要付出代价从较慢的层级中获取——这正是你在通用路径上试图避免的成本。
智能代理应该具有相同的结构。将你的上下文构建为 L1 / L2 / L3。
1 +---------------------------------------------+2 L1 | 常驻内存 - 微小,即时。 |3 | 占 80%。位于系统提示中。 |4 +---------------------------------------------+5 | 未命中 -> 一次廉价调用6 v7 +---------------------------------------------+8 L2 | 按需加载 - 精选的英文规格说明。 |9 | 约占下一个 15%。一步发现即可加载。 |10 +---------------------------------------------+11 | 未命中 -> 读取技能,然后搜索12 v13 +---------------------------------------------+14 L3 | 逃生舱口 - 原始 API 巨著。 |15 | 长尾部分。需 3-6 次 grep 调用来挖掘。 |16 +---------------------------------------------+
几乎每一个优化都是在信息的压缩与发现速度之间进行权衡。把某样东西放入 L1,它就即时可用,但无论是否使用,它都会在每次任务中消耗提示令牌。将其推送到 L3,则在需要之前不消耗任何成本——但随后需要多次工具调用来查找它。你的工作是将每一项能力放置在能够最小化总成本的层级上。这就是全部的艺术。让我用我最熟悉的领域来具体说明。
附带说明:一个工具,而非三十个
在说层级结构之前,先说说基础。我将要描述的每一个电子表格功能——每一次读取、每一次写入、每一次精选查找——都是在单个工具下执行的代码。
1async function execute() {2 const data = await sheet.getCellRange("Sheet1!A1:D200");3 // ...读取、计算、写入...4}
智能代理编写代码;代码调用我们的函数;函数操作电子表格。没有 read_range 工具,没有 write_range 工具,没有 make_chart 工具。只有一个工具,API 存在于代码内部。
为什么?因为随着你添加工具,模型的准确性会下降。这在我们自己的实验中是一致的。每增加一个工具,就意味着提示中增加更多模式,增加更多混淆点,增加更多选错工具的可能性,尤其是当这些工具职责重叠时。一个单一的 execute_code 工具将所有问题归结为一个决策——编写代码——并让模型利用编程语言或 DSL 的全部表达能力来组合功能,而不是拼接僵硬的工具调用(更多内容将在未来文章中阐述)。
这对层级结构很重要,因为它意味着所有三个缓存层级都可以从同一个地方访问:模型始终在编写代码,而 L1/L2/L3 仅仅是它知道可以调用的函数,以及为了找到它们所需的工作量。
L1 — 核心任务:读写单元格
这占 80%。如果读写单元格范围做不好,其他一切都无关紧要。所以这就是我们投入了不成比例巨量努力的地方。看看单个 getCellRange 实际上做了什么。
读取范围是一种压缩行为
读取一个有 200 行的收入表格:
1常见公式缩写为 F1, F2 等。23A2:North | B2:1200 | C2:9.99 | D2:11988(F1)4A3:South | B3:840 | C3:9.99 | D3:8391.6(F1)5A4:West | B4:1500 | C4:9.99 | D4:14985(F1)6...(还有 196 行,每行一条)...78=F1 -> =RC[-2]*RC[-1]910--- 样式模式 ---11D2:D201: 200 个单元格(数字)12 → numberFormat:#,##0.00, font.color:#1A7F3713A2:A201: 200 个单元格(文本)14 → font.bold:true1516--- 来自上方单元格的上下文 ---17A1:Region | B1:Units | C1:Price | D1:Revenue
这里发生了三件事。
第一,公式别名化。一个 500 行的 =A2B2, =A3B3, ... 列包含 500 个几乎相同的公式。我们将每个公式标准化为 R1C1 形式 —— 这样 =A2B2 和 =A3B3 都变成 =RC[-2]*RC[-1] —— 统计模式数量,任何出现超过十次的模式都折叠成一个简短的别名,如 F1。模型看到的是重复的 F1 加上一行说明,而不是 500 个公式。节省了巨大的令牌,信息零损失。
第三,样式压缩。格式也是信息 —— 一个加粗的红色单元格带有 0.00% 的数字格式正在告诉你某些信息 —— 但列出每个单元格的完整样式会淹没数值信息。所以我们将样式完全相同的单元格分组,将每个组折叠为其连接范围,并打印每组一行信息:范围、单元格计数和简洁描述。
六百个公式变成了一行说明。四百个样式化单元格变成了两行。而模型从未明确请求过的标题行就在底部。这就是整张表,无损地,以原始转储所需令牌的一小部分呈现。每一个都是压缩与发现之间的权衡,在常见情况下取得了决定性的胜利。
写入单元格:告诉模型它实际改变了什么,以及看起来哪里有问题
写入比看起来要难,因为一次 execute_code 调用可以改变数百个单元格,而代理需要知道发生了什么,而无需重新读取整个工作表。所以,在代码执行后,我们会返回每个发生变化的单元格的结构化差异——而且同样重要的是,我们对其进行压缩和分类处理。
代码:
1async function execute() {2 const rows = await sheet.getCellRange("Sheet1!A2:C201");3 for (let i = 0; i < rows.length; i++) {4 const r = i + 2;5 await sheet.setCell(`D${r}`, `=B${r}*C${r}`);6 }7}
返回的差异报告:
1--- 单元格差异摘要 ---2(显示格式化后的显示值。∅ = 未定义/空。)34 无问题变更:共 199 个单元格5 Sheet1!Row 2 (D): 1 个单元格6 → D2: ∅ -> 11,988 [=B2*C2]7 Sheet1!Row 3 (D): 1 个单元格8 → D3: ∅ -> 8,391.6 [=B3*C3]9 ...(抽样行) ...10 Sheet1!Row 201 (D): 1 个单元格11 → D201: ∅ -> 4,995 [=B201*C201]12 ... 以及另外 189 行1314 需要审查的单元格:15 必须修复:INVALID_FORMULA:共 1 个单元格16 Sheet1!Row 57 (D): 1 个单元格17 → D57: ∅ -> #REF! [=B57*C57]
这里有两种压缩方式在起作用。
首先,差异是分组和抽样的,而不是转储。发生变化的单元格按工作表和行分组,每行显示为列范围并附有计数(Row 2 (D): 1 cells),并且每行和每个部分只打印一个确定性的单元格样本,其余的用“... 以及另外 N 个”来统计。两百次写入变成了寥寥数行,而代理仍然知道总数。
其次,差异被分类了。干净的写入放在“无问题变更”下。任何看起来可疑的内容——像 #REF! 这样的无效公式、未标记的硬编码数字、隐藏在公式中的硬编码数字、不合理的大百分比——都会被拉到“需要审查的单元格”部分,最严重的问题会被标记为“必须修复”。那个位于第 57 行的 #REF! 在两百个绿色差异中很容易被忽略;在这里,它被标记并显示在顶部。反馈循环不再是“这是变更的内容”,而是“这是变更的内容,以及这可能是你出错的部分”——一个针对代理自身编辑的内置检查器。
L1 用一句话概括:在曲线最陡峭部分的操作,被封装成具有特征工程、令牌压缩和后果报告功能的包装器,它们永久存在于提示中。构建它们代价高昂,但你仍然要构建,因为代理在每个任务上都要承担这个成本。
L2 — 精选英文文档,按需加载
你不能把所有东西都放在 L1 中。条件格式、数据透视表、图表、数据验证、复制/移动语义——每一项都很重要,每一项在每次会话中都会出现几次,并且每一项都有足够多的细节,以至于在系统提示中记录它们会使所有不涉及它们的任务变得臃肿。经典的 L2 场景。
所以,我们用英文编写了精选的能力规格说明,按需获取,就像技能文件一样。代理从其代码内部调用:
1console.log(general.getConditionalFormattingInfo());2console.log(general.getPivotTableInfo());3console.log(general.getChartInfo());4console.log(general.getDataValidationInfo());5console.log(general.getAPIInfo("addSpanAt")); // 任何单个函数,按名称
这些不是类型签名的转储。它们是手写的散文——每个几百行——描述了完成任务的规范方法,包括了原始 API 永远不会告诉你的知识。以数据透视表规格为例。它不仅列出了方法;它还按照正确的顺序教授了整个操作步骤:
1const pt = sheet.originalSheet.pivotTables.add("SalesPivot", "SalesData", 0, 0, ...);2pt.suspendLayout();3pt.add("Region", "Region", rowField);4pt.add("Quarter", "Quarter", columnField);5pt.add("Amount", "Sum of Amount", valueField, 8); // 8 = 求和6pt.resumeLayout();
并且它融入了那些你需要反复失败才能学到的东西:你必须围绕一批变更调用 suspendLayout()/resumeLayout(),否则表格会在每次调用时重建;值字段的聚合必须作为原始整数传递(8 表示求和),因为在运行时友好的枚举并不存在。这些都不是奇怪的陷阱——这正是正确执行数据透视表的实际方式,由已经付出代价的人一次性记录下来。
关键特性:直到任务需要它时,它才消耗令牌。一个从不接触数据透视表的任务永远不会为数据透视表文档付出代价。一次 console.log 是全部的发现成本——一次缓存未命中,快速服务。
同样的想法,用于可执行工具
L2 不仅用于文档。我们将完全相同的模式应用于延迟工具——web_search, web_crawl, create_website 等。它们的模式不在提示中。取而代之的是一个元工具墙:
1get_tool_info("web_search") → 返回模式,标记为“已获取”2execute_tool("web_search", …) → 除非你先获取了它,否则拒绝
已获取工具集,字面上,是一个会话作用域的缓存。模型加载一次模式,之后它就常驻内存。相同的压缩与发现权衡,相同的解决方案:保持提示小巧,当你真正需要该能力时,支付一步未命中的代价。这与 Claude 上的延迟工具理念相同,但我们不必局限于某一家供应商的工具加载特性来获得这种行为。
L3 — 原始巨著,以及将其映射的技能
然后是长尾部分:我们从未包装过、也从未编写过规格说明的那个晦涩功能。你无法预见它——这是必然的。但代理仍然必须能够到达那里,否则它会撞墙并导致任务失败。具体来说,像这样的请求最终会落在这里:
- “为每一行添加一个迷你图来总结其趋势”——迷你图是一个真实存在但很少接触的 API 表面。
- “将图表的次要轴设置为对数刻度,并仅重新着色第三个系列”——一个深度达三层的图表属性,没有精选规格说明费心覆盖。
- “从此单元格插入一个超链接到那个命名区域,并对这些形状进行分组”——绘图/形状/超链接的角落功能,之前没有人问到过。
所以 L3 是完整的原始 API——全部 Office.js 表面(Excel 插件)或全部 SpreadJS 表面(Shortcut 网页版),转储到磁盘。它是一个机器生成的参考资料,长达 7 万行。它包含一切。但它作为提示上下文完全不可用——你永远不会把它粘贴进去。
诀窍在于:你给它一个技能——一个简短的地图,教它如何使用 bash 挖掘巨著:
1# 来自 advanced-api SKILL.md — 推荐的工作流程2grep -n '"charts.add"' api-reference.json -A 5 # 查找一个方法3grep -n '"pivots\.' api-reference.json | head # 列出一个命名空间4grep -n '"ChartConfig"' api-reference.json -A 10 # 解析一个类型5grep -n '"isEnum": true' api-reference.json -B2 -A10 # 枚举枚举值
这个技能大约 100 行。它说明:这里是结构,这里是每个方法和类型条目的形状,这里是为每种问题准备的 grep 配方。有了它,代理从“我无法阅读的数万行代码”转变为“只需 3-6 次 grep 就能精确找到我需要的签名”。这就是 L3 的访问成本——真实存在,但有上限,并且只由达到这个深度的罕见任务承担。
系统提示使这个逃生舱口明确化,使模型知道路径的存在以及何时使用它:
API 层级 — 有 2 级 API 能力。包装 API:便利函数;有些直接列出,其他的通过 getAPIInfo(...) 获取。绝不猜测——全面阅读文档。原始 API:当包装 API 无法满足你的需求时使用……如果包装 API 做不到,就使用原始 API——不要妥协。
最后一句是 L3 的全部意义所在。代理永远不应该卡住。它可以在 L1 未命中,下降到 L2,即使精选规格说明也保持沉默,就深入原始巨著,并仍然能在合理的调用次数内找到答案。
提示预算实际上是如何分配的
看看令牌的去向是值得的,因为层级结构直接体现在系统提示的形态中。
提示的主体是 L1——大约几百行。核心的读/写操作、execute_code 契约、关键类型以及代理在几乎所有任务上都会用到的那几个方法,加上执行和安全指南。这是每次调用都常驻的部分,所以这也是我们最努力保持精简的部分。
L2 是顶上一层薄薄的切片——大约 50 行。它不是规格说明本身;它是一个精选的“受祝福”方法允许列表,以及告诉代理 getXInfo(...) 规格说明存在以及何时使用它们的指针。规格说明的实际内容在 console.log 将其拉入之前,一直保持在提示之外。
L3 基本上只有 5 行,即 skill.md 的名称和描述,以及其他分散在各处的参考资料。原始参考资料——7 万行——完全存放在磁盘上,从不触及提示。常驻的只有那个简短的技能文件和 API 层级部分指向它的一行内容。
所以,预算反映了频率曲线:大部分提示用于 80% 的情况,一点点用于 15% 情况的指路牌,几乎没有任何部分用于长尾——这正是缓存层级框架所预测的分配方式。
将配方移植到你的领域
电子表格只是我的例子。这种结构可以移植到任何领域。那些系统提示和精选规格说明中的压缩,实际上是对你的用户和他们所执行任务的分布的一种编码——而你,在你的领域中,比任何人都更了解这种分布。所以,你的工作是回答三个问题:
- 你把什么包装到 L1 中?频率曲线上最陡峭部分的核心操作。让它们变得极其令牌高效和快速,并让它们报告后果。在这里投入不成比例的努力——代理在每个任务上都承担这个成本。
- 你把什么延迟到 L2?那些重要但偶发的能力。将它们编写成精选的、英文的、包含常见陷阱的规格说明,只需一次发现步骤即可到达。不仅要编写签名,还要对规范的操作步骤和约束进行编码。
- 你的逃生舱口(L3)是什么?原始、完整的底层平台——加上一个教代理如何挖掘它的技能。它不必符合人体工程学。但它必须是可到达的、完整的,并且可以在有界的步骤数内被找到。代理必须能够——也将会——最终找到正确的信息。
把这三种放置方式搞对了,你就构建了一个在常见情况下快速、在偶发情况下有能力、在罕见情况下永不真正卡住的智能代理——同时保持上下文足够小,使模型保持敏锐。
层级结构不会消失——它会移动
一个最后的观察。什么算是 L1 并不是固定的;它会随着模型能力的变化而变化。
早期,弱小的模型需要微小的、单一用途的工具,以及把事情都解释清楚。今天的模型可以一次性吸收更大的 L2 规格说明,并且能够在不卡住的情况下推理更多原始的 L3 细节。所以,随着模型的改进,昨天的 L3 变成了明天的 L2,而昨天的 L2 则折叠进了 L1。代理的职责范围向外扩展;层级向下滑动一层。
但是层级结构本身永远不会消失——因为相对于你可以放入其中的所有内容,上下文将永远是稀缺的,而噪声将永远损害你的准确性。没有哪个模型大到“在正确的时间将正确的东西放在它面前”不再重要。
更大的上下文窗口诱使人们粘贴更多的东西。更好的直觉是 CPU 几十年前就确定下来的:缓存中的摘要,按需的详细信息,原始的底层平台作为最后的手段。像构建内存层次结构一样构建你的智能代理的上下文,准确性自然会随之而来。


![成田悠辅的天才 AI 使用技巧 [珍藏版]](https://youmind.club/__ym/cms-assets/media/1784137658627_u4bwry_HNMS89bbsAAUPJI.jpg)


