好 Agent 的核心定义
Peter Wang 是 Shortcut AI 联合创始人兼首席科学官,过去一年在打造"业内最准确的电子表格 Agent"。它被部署在四大多策略对冲基金中的三家——在那里,"错了"的代价是昂贵的,没人给你打折扣曲线。
他给出一个简洁的核心原则:
"A good agent is a faithful compression of its task distribution."
好的 Agent 是其任务分布的忠实压缩。这句话的剩下部分就是它意味着什么,以及它迫使你构建什么。
上下文作为分层缓存
假设你不拥有运行环境,也不训练模型。那么三件事是你要设计的——系统提示、工具和制品(skills、curated docs、references)——而它们本质上是同一件事:Agent 的上下文。
游戏规则简单陈述:模型固定时,准确率是上下文质量的函数。臃肿的上下文埋没信号,缺失的上下文逼迫猜测,两者都让你损失准确率。准确率就是你在卖的东西——而且这个关系不是线性的,99% 的任务比 95% 的任务价值高 10 倍。
但你的用户不带来均匀分布的问题。他们带来一个长尾:
how often
|
| ████
| ████
| ████
| ████
| ████
| ████
| ████
| ████
| ████
| ████ ▓▓▓▓
| ████ ▓▓▓▓ ░░░░ ░░░░ ░░░░ ░░░░ ░░░░ ░░░░ ░░░░
+----------------------------------------------------> task variety
████ 日常主菜 每个会话的主体
▓▓▓▓ 关键但偶发 每会话几次
░░░░ 长尾 每个都罕见——但很多
每个仍必须 work
Agent 必须处理全部。但它不能一次把所有的并集塞进上下文——那就是"臃肿 prompt 失败模式"。所以真正的目标比"让一切可用"更尖锐:最小化每个任务消耗的上下文,按任务分布做平均。
这正是 CPU 面临的问题。一个程序可能触及 GB 级数据,但处理器旁边的存储极小——所以计算机把内存堆叠成层级:小的、瞬时的缓存(L1);更大更慢的(L2、L3);然后是主存和磁盘。它能 work 是因为访问也是长尾的:在快速层保留热集,只为罕见的东西下到慢层。"缓存未命中"就是当你要的东西不在快速层、你付费从更慢层取——恰恰是你在公共路径上要避免的开销。
Agent 应该用同样的结构。把你的上下文建成 L1 / L2 / L3。
+---------------------------------------------+
L1 | 常驻 - 极小、瞬时 |
| 80%。活在 system prompt 里。 |
+---------------------------------------------+
| miss -> 一次便宜调用
v
+---------------------------------------------+
L2 | 按需 - 策划的英文规范 |
| 下个 15%。一次发现步骤就加载。 |
+---------------------------------------------+
| miss -> 读 skill,然后搜索
v
+---------------------------------------------+
L3 | 逃生通道 - 原始 API 大全 |
| 长尾。3-6 次 grep 调用挖掘。 |
+---------------------------------------------+
几乎每个优化都在用信息压缩换取发现速度。放进 L1 立即可用,但每次任务都消耗 prompt token 不管用没用。推到 L3 不用时零成本——但用时需要几次工具调用才能找到。你的工作是把每项能力放到能最小化分布上总成本的层级。这就是整个手艺。
单一工具,不是 30 个
在讲层级之前,先讲基底。我下面要描述的每个电子表格能力——每次读、每次写、每次策划的查询——都是在单个工具下执行的代码。
async function execute() {
const data = await sheet.getCellRange("Sheet1!A1:D200");
// ...读、计算、写...
}
Agent 写代码;代码调用我们的函数;函数操作 sheet。没有 read_range 工具,没有 write_range 工具,没有 make_chart 工具。只有一个工具,API 活在代码里。
为什么?因为模型准确率随工具增加而下降。这在我们自己的实验里是一致的。每个新增工具都是 prompt 中多出来的 schema、多出来的混淆 surface、多出来的选错方式,尤其是工具职责重叠时。一个 execute_code 工具把所有这些折叠成一个决策——写代码——并让模型用编程语言或 DSL 的全部表达能力去组合能力,而不是拼接僵硬的工具调用。
这对层级很重要,因为它意味着三个缓存层都能从同一个地方访问:模型总在写代码,L1/L2/L3 只是它知道能调哪些函数,以及为找到它们付出了多少工作。
L1——日常主菜:读和写单元格
这是 80%。如果读写单元格范围不够优秀,其他都不重要。所以这是我们花了不成比例的、超额的功夫的地方。看一个 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 more rows, each one line) ...
=F1 -> =RC[-2]*RC[-1]
--- Style patterns ---
D2:D201: 200 cells (numbers)
→ numberFormat:#,##0.00, font.color:#1A7F37
A2:A201: 200 cells (text)
→ font.bold:true
--- Context from cells above ---
A1:Region | B1:Units | C1:Price | D1:Revenue
三件事在发生。
第一,公式 aliasing。 一列 500 行的 =A2*B2, =A3*B3, … 是 500 个几乎相同的公式。我们把每个公式归一化到 R1C1 形式——所以 =A2*B2 和 =A3*B3 都变成 =RC[-2]*RC[-1]——统计模式,出现超过 10 次的任何模式折叠成 F1 这样的短别名。模型看到 F1 重复加一行图例,而不是 500 个公式。大量 token 节省,零信息损失。
第二,自由行列上下文。 当你读 C5:E20,那些裸数字意味着什么?我们向左扫描找行标签,向上扫描找表头行(通过对哪个附近行有最多文本单元格投票选表头),并附加上它们,所以模型白拿 Region | Q1 | Q2 和 North America | …,从不去猜一个数字网格代表什么。
第三,样式压缩。 格式也是信息——一个加粗的红色单元格带 0.00% 数字格式在告诉你什么——但列出每个单元格的完整样式会淹没值。所以我们把单元格按相同样式分组,每个组折叠成它的连接范围,每组打印一行:范围、单元格数、紧凑描述。600 个公式变成一行图例,400 个有样式的单元格变成两行。模型从未显式要求的表头行就在底部。完整表格,无损,原生 dump 一小部分 token。
写单元格:告诉模型它实际改了什么,以及什么看起来不对。
写比看起来难,因为单次 execute_code 调用可以改几百个单元格,Agent 需要知道发生了什么而不重读整个 sheet。所以代码运行后,我们交回一个结构化的 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 ---
(Formatted display values shown. ∅ = 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]
... (sampled rows) ...
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 被分组和采样,不是 dump。 改动的单元格按 sheet 和行分组,每行显示为带计数的列范围(Row 2 (D): 1 cells),每行和每节只打印一个确定性采样的单元格,其余用 ... and N more 计数。200 次写入变成几行,Agent 仍然知道总数。
第二,diff 被分类。 干净写入落到"无问题改动"下。任何看起来可疑的——无效公式如 #REF!、未标记的硬编码数字、埋在公式里的硬编码数字、不合理的大百分比——被拉进"需要审查的单元格"小节,最严重的被标记 MUST FIX。第 57 行的那个 #REF! 在 200 个绿 diff 墙里几乎不可能被忽略;这里它在顶部被带标签浮现出来。反馈循环不是"这是改了什么",是"这是改了什么,以及这部分你可能搞错了"——Agent 自己编辑的内置 linter。
L1 一句话:陡峭部分曲线上的操作被精心设计了 token 压缩、后果报告的封装,永远活在 prompt 里。它们建起来很贵,但你还是建了,因为 Agent 在每个任务上付出这个代价。
L2——策划的英文,按需
你不能把所有东西都放进 L1。条件格式、数据透视表、图表、数据验证、复制/移动语义——每个都重要,每个每会话出现几次,每个都有足够大的 surface,把它写在 system prompt 里会臃肿每个不用的任务。经典的 L2。
所以我们写了策划的能力规范,用英文,按需获取,就像 skill mds。模型从它的代码里调用:
console.log(general.getConditionalFormattingInfo());
console.log(general.getPivotTableInfo());
console.log(general.getChartInfo());
console.log(general.getDataValidationInfo());
console.log(general.getAPIInfo("addSpanAt")); // 任意单个函数,按名
这些不是类型签名的 dump。它们是手写的散文——每个几百行——描述完成任务的标准方式,包括原始 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() 包住一批更改,否则表每次调用都重建;值字段的聚合必须作为原始整数(sum 是 8)传入,因为友好的枚举在运行时不存在。这些都不是古怪的陷阱——它就是正确做透视的实际形状,由已经付过代价的人写下来一次。
关键属性:这在任务需要它之前零 token 成本。一个从不碰透视的任务永远不为透视文档付费。一个 console.log 是完整的发现成本——单次缓存未命中,响应快。
同样的思路,给可执行工具。
L2 不只是文档。我们对延迟工具应用相同模式——web_search、web_crawl、create_website 等。它们的 schema 不坐在 prompt 里。取而代之的是一面元工具墙:
get_tool_info("web_search") → 返回 schema,标记 "fetched"
execute_tool("web_search", …) → 拒绝除非你之前 fetched
已 fetched 工具的集合,字面意义上是一个 session 范围内的缓存。模型加载一次 schema,从此它就常驻。同样的压缩-发现权衡,同样的解:保持 prompt 小,当真正需要能力时付一次未命中代价。这和 Claude 的延迟工具是同一个想法,但我们没被绑在一个供应商的工具加载特性上来得到这个行为。
L3——原始大典,以及映射它的 skill
然后是长尾:我们没包装过、没写过规范的一个冷门东西。你没法预料到它——按定义。但 Agent 必须能到达那里,否则它撞墙、任务失败。具体来说,这些请求在这里结束:
- "给每行加一个 sparkline 总结它的趋势"——sparklines 是真实但少触的 API surface
- "把图表的副轴设为 log 标度,只重染第三系列的颜色"——三层深的图表属性,没有策划规范覆盖
- "从这个单元格到那个命名范围插入超链接,把这些形状分组"——画图/形状/超链接的角落,没人问直到现在
所以 L3 是完整的原始 API——整个 Office.js surface(Excel 插件)或整个 SpreadJS surface(Shortcut web),dump 到磁盘。它是机器生成的参考,7 万行长。它包含一切。它作为 prompt 上下文也完全不可用——你永远不想把它粘进去。
诀窍:你给它一个 skill——一个短映射,教它怎么用 bash 挖掘大典:
# from the advanced-api SKILL.md — the recommended workflow
grep -n '"charts.add"' api-reference.json -A 5 # 找方法
grep -n '"pivots\.' api-reference.json | head # 列命名空间
grep -n '"ChartConfig"' api-reference.json -A 10 # 解析类型
grep -n '"isEnum": true' api-reference.json -B2 -A10 # 枚举 enums
skill 大约 100 行。它说:这是结构,这是每个方法和类型条目的形状,这是每类问题的 grep 配方。有了它,Agent 从"几万行我读不了"变成"3-6 次 grep 正好浮现我需要的签名"。这就是 L3 访问成本——真实,但有界,且只由到达这个深度的罕见任务付费。
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.
最后这条子句就是 L3 的全部要点。Agent 永远不该卡住。它能在 L1 未命中,下降到 L2,如果连策划的规范都沉默,下到原始大典,仍然以合理数量的调用出来,带着答案。
Prompt 预算实际怎么分
值得看 token 去了哪里,因为层级直接体现在 system prompt 的形状上。
prompt 主体是 L1——几百行量级。核心读/写操作、execute_code 合约、关键类型和 Agent 在本质上每个任务都用的几个方法,加上执行和安全指南。这是每次调用都常驻的部分,所以也是我们最努力保持紧凑的部分。
L2 是顶上一个薄切片——大约 50 行。它不是规范本身;它是策划的"受祝福"方法的允许列表和告诉 Agent getXInfo(...) 规范存在以及何时伸手的指针。规范的实际内容留在 prompt 外,直到一个 console.log 把它拉进来。
L3 本质上是 5 行,skill.md 的名字和描述,以及其他散落的引用。原始参考——7 万行——完全活在磁盘上,从不接触 prompt。常驻的全部是短 skill 文件和 API 层级节里指向它的一行。
所以预算镜像频率曲线:prompt 大部分花在 80% 案例上,少量花在标志 15% 上,长尾上几乎为零——这正是缓存层级框架预测的分配。
配方,移植到你的领域
电子表格只是我的例子。结构转移到任何领域。那些 system prompt 和策划规范里的压缩实际上是对你用户和他们所做任务分布的编码——而在你的领域,你比任何人都更了解那个分布。所以你的工作是三个问题:
1. 你把什么包进 L1? 频率曲线陡峭部分的日常主菜操作。让它们残暴地 token 高效和快速,让它们报告后果。在这里花不成比例的功夫——Agent 在每个任务上付这个代价。
2. 你把什么延迟到 L2? 重要但偶发的能力。写成策划的、英文的、陷阱感知的规范,一次发现步骤可达。编码标准配方和约束,不只是签名。
3. 你的逃生通道(L3)是什么? 原始的、完整的基底——加上一个教 Agent 怎么挖掘它的 skill。它不一定 ergonomic。它必须可达、完整、有限步骤内可找到。Agent 必须能——而且会——最终找到正确信息。
把三个放对位置,你就建了一个 Agent:公共案例上快,偶发的上能干,罕见的不真正卡住——同时保持上下文小到让模型保持锐利。
层级不消失——它移动
最后一个观察。什么算 L1 不是固定的;它随模型强度漂移。
早期、弱的模型需要小的、单一用途的工具,所有东西都说明白。今天的模型能一次吸收更大的 L2 规范,能在更多原始 L3 细节上推理而不卡。所以随模型改进,昨天的 L3 变成明天的 L2,昨天的 L2 折叠进 L1。Agent 的责任向外扩展;层级向下一级滑。
但层级本身从不消失——因为相对你能放进去的一切,上下文永远是稀缺的,噪声永远消耗你的准确率。没有模型大到"在正确时间把正确东西放在它面前"停止重要。
更大的上下文窗口诱惑人粘进更多。更好的本能是 CPU 几十年前解决的那个:摘要进缓存,细节按需,原始基底作为最后手段。把你的 Agent 上下文建成内存层级,准确率随之而来。