成本与延迟:把账压下来
同一个任务,做对缓存和模型分层能便宜一个数量级。
学完这节你能做到
- 算出单次任务成本的构成,并指出最值得优化的那一项
- 设计缓存友好的上下文布局
- 用模型分层把简单步骤交给便宜模型
同一个任务,做对缓存和模型分层能便宜一个数量级。这不是夸张 —— 缓存本身就是一折价,再叠上模型分层,十倍是常规操作。
这一节把账拆开,然后按「性价比」排个序。
成本由什么构成
一次 agent 任务的账单:
Σ(每轮) 未命中缓存的输入 × 输入价
+ 命中缓存的输入 × 缓存价(≈ 输入价 × 0.1)
+ 输出 × 输出价(通常是输入价的 3–5 倍)
+ 重试的部分 × 全价
+ 压缩的那几轮 × 全价(前缀变了)
+ 子 agent 的全部
四个直觉误区:
- 输出比输入贵得多,但 agent 的输出通常很短 —— 大头几乎总是输入。
- 重试是全价,而且重试往往发生在上下文最长的时候。
- 压缩省窗口但花钱:那一轮缓存全失效。
- 子 agent 的钱最容易漏算 —— 记得用工具返回的
usage上报。
先用 L1 的计算器把自己的形态跑一遍,再往下看。
优化的性价比排序
按「省下的钱 ÷ 花的工夫」排:
| 手段 | 典型收益 | 工夫 |
|---|---|---|
| ① 稳定前缀,让缓存命中 | 输入成本降到约 1/10 | 小 |
| ② 裁剪工具输出 | 直接减少输入量,还顺带推迟撞墙 | 小 |
| ③ 收窄工具集 | 每轮省几千 token | 小 |
| ④ 模型分层 | 简单步骤便宜 5–20 倍 | 中 |
| ⑤ 减少无谓轮次(提示词/工具改进) | 轮次少三成,钱就少三成 | 中 |
| ⑥ 换更小的模型跑全部 | 省钱但成功率下降,要评测背书 | 中 |
| ⑦ 自建/微调 | 视规模 | 大 |
前三条几乎是免费的,先把它们做完再谈别的。
① 缓存友好的布局
命中条件是前缀逐字节相同。所以顺序固定成:
[ 系统提示 ][ 项目指令 ][ 工具声明 ][ 早期历史 ][ 新增的这一轮 ]
稳定 ────────────────────────────────→ ← 只有这里是新的
会打碎缓存的常见操作,逐条检查:
| 操作 | 后果 |
|---|---|
| 系统提示里注入时间戳 / 随机 id | 每轮全废 |
| 工具声明顺序不稳定(从 Map / Set 遍历) | 每轮全废 |
| 中途插入工具、改工具描述 | 从改动处往后全废 |
| compaction | 那一轮全价(不可避免,但可以少触发) |
| 动态注入项目状态到系统提示 | 每轮全废 —— 放到最后一条用户消息里 |
把连续两轮的请求体 dump 出来,diff 一下前缀。 第一个不同的字节出现在哪,缓存就从哪断。 理想情况:只有最后的新消息不同。
② 裁剪工具输出
这条既省钱又推迟撞墙,是最划算的一项。三个具体动作:
// 1. 上限 + 说清怎么拿剩下的
if (lines.length > 50) {
text = lines.slice(0, 50).join("\n")
+ `\n\n[共 ${lines.length} 处,只显示前 50。缩小范围或指定目录。]`
}
// 2. 摘要优先:先给分布,再按需给明细
text = `命中 47 处,分布:src/api(31) src/web(12) tests(4)。需要明细请指定目录。`
// 3. 结构化压缩:去掉模型不需要的字段
const slim = raw.items.map(({ id, name, status }) => ({ id, name, status }))
第 2 条最容易被忽略:模型要的往往是「在哪、有多少」,不是 47 行原文。
③ 收窄工具集
L1 算过:40 个工具 × 320 token = 12.8k,每轮都付。 按场景分组、运行中按需注册(L3 的 MCP 那节给了代码), 常驻的就只剩基础几件。
④ 模型分层
不是所有步骤都需要最强模型。常见的分法:
| 步骤 | 模型 |
|---|---|
| 主循环:决策、写代码、判断 | 强模型 |
| 压缩摘要 | 中小模型(Pi 允许扩展换摘要模型) |
| 结构化提取、分类、格式转换 | 小模型 |
| 子 agent 的检索类任务 | 中模型 |
Pi 里有两个现成落点:session_before_compact 事件可以接管压缩、用别的模型生成摘要;
scopedModels 和 pi.setModel() 可以按阶段切主模型。
「简单步骤用小模型」听起来安全,但摘要质量下降会在十轮之后才显形 —— 表现是 agent 忘了早期约束。上线前用长任务用例测一遍(L3 的评测那节)。
⑤ 减少轮次
轮次是成本的乘数:每多一轮,就多付一次完整上下文。 减轮次的三个最有效手段其实都在前面的课里:
- 工具返回可行动的错误(L1)—— 一次纠正 vs 三次试错。
- description 写清触发条件与限制(L1)—— 少一轮「用错工具再改」。
- 允许并行只读工具 —— 三个
read一轮完成,而不是三轮。
延迟:优化的地方常常和直觉相反
先量,再优化。一次典型任务的耗时构成往往是:
模型思考与输出 ████████░░░░░░░░ 30%
工具执行 ██████████████░░ 55% ← 跑测试、装依赖、网络请求
上下文组装等 ██░░░░░░░░░░░░░░ 15%
对应的手段:
| 症状 | 手段 |
|---|---|
| 首字慢 | 流式(必须)、缩短系统提示、缓存命中 |
| 工具慢 | 并行只读工具、给命令加超时、缓存重复查询的结果 |
| 总时长长 | 减轮次;把「跑全量测试」换成「只跑相关测试」 |
| 感觉卡住 | onUpdate 报进度 —— 不减少时间,但显著改善体感 |
给自己设预算上限
再省也架不住一次失控。三道闸门都要有:
const MAX_TURNS = 40
const MAX_TOKENS = 800_000
const MAX_USD = 5
const MAX_WALL = 30 * 60 * 1000
无人值守场景再加上一条无进展检测(连续 N 轮无写操作就停)—— L2 的闯关算过账:它能把 47 美元变成 3 美元。
一个编码 agent 的账单是同行的 8 倍。查了一下:缓存读取 token 几乎为零。最该先做什么?
这一节的结论
- 大头几乎总是输入;重试和压缩按全价算;子 agent 的钱别漏。
- 优化顺序:稳定前缀 → 裁剪输出 → 收窄工具 → 模型分层 → 减轮次。
- 缓存读取占比接近零 = 前缀不稳定,是最值得先修的一项。
- 延迟的大头常常是工具执行而不是模型,先量再优化。
- 四道闸门 + 无进展检测,是无人值守的最低配置。