返回 FEED
AGENT2026-06-12

如何构建真正好用的垂直 Agent:内存分层架构的设计哲学

好 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 | Q2North 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_searchweb_crawlcreate_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 上下文建成内存层级,准确率随之而来。