目标读者:负责将工具调用类 Agent(ReAct / Claude Code 类多轮 tool-use 循环)落地到生产环境的工程师、SRE、平台架构师。 覆盖后端:Claude API、OpenAI / 兼容 API、自部署开源模型(vLLM / SGLang / TRT-LLM)、本地与边缘(llama.cpp / Ollama)。 关注维度:延迟、吞吐、成本、稳定性。
0. 为什么 Agent 推理特别难加速
通用 LLM 推理优化的经验在 Agent 场景下会失效,原因来自 Agent 自身的负载特征:
| 特征 | 通用 Chat | 工具调用 Agent |
|---|---|---|
| 单次请求轮数 | 1 | 10–100+ |
| 上下文增长 | 平缓 | 单调累积(每轮追加工具结果) |
| 输出长度 | 中长 | 多数轮短输出(只为决定下一个工具) |
| 前缀重复率 | 低 | 极高(system + history 几乎不变) |
| 失败放大 | 1 次重试 | 1 次失败 → 整条 trace 重跑 |
| 阻塞点 | 模型本身 | 经常是 工具 I/O,不是模型 |
工程上要先认清一个事实:Agent 端到端延迟里,模型推理常常只占 40–60%,剩下是工具 I/O、序列化、调度、网络。盲目压模型 latency 而不优化外围,常常做的是负功。先 profile,再动手。
1. 端到端延迟拆解(先 profile,再优化)
任何加速工作第一步都是把一次完整 Agent run 拆成下面六段,分别打点:
[1] 请求进入 → 第一次模型调用开始 // 入口排队、鉴权、限流
[2] 模型 prefill(TTFT) // 长上下文的主要成本
[3] 模型 decode(输出 token) // 工具调用决策通常很短
[4] 工具执行 (HTTP / DB / shell) // 经常是真正的瓶颈
[5] 结果回填 + 下一轮 prefill // **prefix cache 命中与否决定一切**
[6] 终止判定与最终响应组装 // 流式 flush
最小可用埋点(OpenTelemetry 语义):
with tracer.start_as_current_span("agent.turn", attributes={
"agent.turn_index": i,
"agent.input_tokens": usage.input_tokens,
"agent.cached_input_tokens": usage.cache_read_input_tokens, # 关键
"agent.output_tokens": usage.output_tokens,
"agent.ttft_ms": ttft_ms,
"agent.tool_name": tool_name,
"agent.tool_latency_ms": tool_ms,
}):
...
必须采集的四个指标,缺一不可:
- TTFT(每轮):体感延迟来源。
- 缓存命中率 =
cache_read_input_tokens / input_tokens。生产 Agent 应稳定在 >85%,低于 70% 说明缓存策略有问题。 - 每轮工具耗时分布:找出 P99 工具,常常是某个慢 SQL 或外部 API。
- 轮数分布:P50 / P95 / P99 轮数,超长 trace 是成本和超时的主要来源。
2. 前缀缓存(Prefix Cache)—— 单点收益最大的优化
这是 Agent 加速里 ROI 最高 的一项,没有之一。原理:每一轮 Agent 调用,前 N-1 轮的内容是完全一样的,只在尾部追加新工具结果。复用前缀的 KV,能把第 N 轮的 prefill 成本从 O(全部上下文) 降到 O(新增部分)。
2.1 Claude API 上的 prompt caching
Anthropic 的 prompt caching 通过 cache_control 显式标记缓存断点(最多 4 个),命中后 cached 部分按 10% 价格计费,写入按 125%(5 分钟 TTL)或 200%(1 小时 TTL)计费。
Agent 场景的正确放置顺序(从前到后,必须保持稳定):
messages = [
{"role": "system", "content": [
{"type": "text", "text": STABLE_SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"}}, # 断点 1: system
]},
# tools 定义紧随 system —— tools schema 变了会击穿所有缓存
]
response = client.messages.create(
model="claude-sonnet-4-6",
system=[{"type": "text", "text": SYSTEM, "cache_control": {"type": "ephemeral"}}],
tools=[
*TOOLS_STABLE,
{**TOOLS_STABLE[-1], "cache_control": {"type": "ephemeral"}}, # 断点 2: tools 末尾
],
messages=[
*history[:-1],
{**history[-1], "cache_control": {"type": "ephemeral"}}, # 断点 3: 历史末尾
],
)
Agent 缓存的五条铁律:
- 稳定前缀绝不动态拼接时间戳、随机 ID、用户名。把这些放到 system 末尾或单独的 user turn 里。
- 工具 schema 一旦上线就冻结。新增工具追加到列表末尾,不要插入中间,不要重排序。
- 历史只追加,不修改。任何对历史的"清理""压缩"都会击穿缓存——要做也只在 trace 结束后做。
- 断点放在长且稳定的内容末尾,不要放在易变内容前。
- TTL 选择:交互式 Agent(用户在线)用 5 分钟 ephemeral;长尾批处理或人工审核 Agent 用 1 小时。
健康指标:cache_read_input_tokens / input_tokens > 0.9,成本下降通常 70–85%,TTFT 下降 50–80%。
2.2 vLLM / SGLang 上的自动前缀缓存
自部署后端通常默认开启 automatic prefix caching (APC),按 block(默认 16 token)做哈希匹配,无需手动标记。
vLLM 启动参数(生产推荐):
vllm serve <model> \
--enable-prefix-caching \
--block-size 16 \
--max-num-seqs 256 \
--gpu-memory-utilization 0.90 \
--max-model-len 131072 \
--enable-chunked-prefill # 长上下文必开
SGLang 的优势在 RadixAttention——多分支 Agent(并行子任务)共享前缀的能力比 vLLM 强。多 Agent 编排选 SGLang,单分支线性 Agent 选 vLLM,性能差距通常在 10% 以内。
自部署场景独有的坑:
- APC 与 LoRA 多租户混用:不同 LoRA 适配器的前缀不能共享,按租户分池。
- Tokenizer 漂移:升级模型版本时如果 tokenizer 变了,缓存全部失效,要在变更窗口预热。
--enforce-eager会关掉 CUDA Graph:调试可用,生产必须关掉这个开关。
2.3 OpenAI 兼容 API
OpenAI 自 2024 年起对 gpt-4o 系列做 自动 prompt caching(>1024 token 自动生效,无需 API 改动),命中部分 50% 价格。Agent 工程上要做的只有一件事:保持前缀字节级稳定。常见无意击穿:
tools数组顺序在不同进程间不一致(用 OrderedDict 或显式排序)。- system 里嵌入了
datetime.now()或 trace_id。 - JSON 序列化 key 顺序不稳定(用
sort_keys=True或固定 schema)。
3. 上下文管理:让前缀稳定的工程手段
前缀缓存的前提是历史"只增不改",但 Agent trace 会无限增长。生产上要在 不破坏前缀的前提下 控制上下文长度,否则会同时撞上模型上限、成本和延迟三堵墙。
3.1 滑窗 + 摘要混合策略(推荐)
[system + tools] ← 永久缓存层,绝不动
[历史摘要 block] ← 缓存层,超过阈值时整体替换(接受一次性击穿)
[最近 K 轮原文] ← 滚动窗口
触发条件:当 input_tokens > 阈值(如 80K / 模型上限的 60%)时,把最早的 M 轮压缩成一段摘要,整体替换前缀的"历史摘要 block"。这一次会击穿缓存,但之后又能稳定命中,比每轮微调划算得多。
反模式:每轮都让 LLM 决定"要不要压缩历史" —— 这会让前缀永远不稳定,缓存命中率会跌到个位数。
3.2 工具结果裁剪
工具输出是上下文膨胀的主要来源。在结果回填给模型前做一次裁剪:
| 工具类型 | 裁剪策略 |
|---|---|
| 文件读取 | 只保留请求的行范围;大文件用 offset/limit 强制分页 |
| SQL 查询 | 默认 LIMIT 100;超过时返回行数 + 前 50 行,让模型显式翻页 |
| HTTP / 网页抓取 | 服务端 HTML→Markdown 后再返回;剥广告、导航、脚本 |
| Shell | stdout 截断到 N KB,超过部分写文件让模型按需读 |
裁剪在 工具侧 做,不要在 prompt 里让模型"忽略 X"——前者节省 token,后者只增加噪声。
3.3 工具集合精简
一个常见错误是把 50 个工具一次性塞给模型。后果:
- tools schema 占用 5–15K token,每轮都付钱。
- 工具越多,模型选择正确工具的准确率越低(实测掉 5–15%)。
- schema 任何改动都会击穿缓存。
生产做法:分阶段暴露工具。第一层用"meta-tool"(如 search_tools(intent))让模型动态发现工具;或按 Agent 角色固定一个 5–10 个工具的子集。
4. 模型层加速
在前缀缓存和上下文管理之后,模型本身的加速才有边际收益。
4.1 模型分级路由
并非每一轮都需要最强模型。典型 Agent 的轮分布是:
- 意图理解 + 规划(开头 1–2 轮):需要最强模型。
- 机械工具调用(中间 N 轮):弱模型够用,甚至更稳定。
- 最终总结输出(结尾 1 轮):中等模型。
落地:
def pick_model(state):
if state.turn_index == 0 or state.needs_planning:
return "claude-opus-4-7"
if state.last_tool_was_deterministic and state.expects_tool_call:
return "claude-haiku-4-5-20251001"
return "claude-sonnet-4-6"
注意:切换模型会击穿前缀缓存,因为不同模型缓存命名空间隔离。所以要么整条 trace 用同一个模型,要么把切换点做在 trace 自然分段处(如完成一个子任务后)。一种妥协方案:主循环固定一个模型,把"简单分类"型子任务外包给小模型异步调用,结果作为工具返回。
4.2 Claude 特有特性
- Streaming + 早停:开启
stream=True,当解析到完整tool_useblock 且后续不可能再有 block 时,可以提前发起工具调用(流式 partial tool call)。在工具 I/O 大于 500ms 时收益明显。 - Extended thinking:只对规划轮开启。每轮都开 thinking 是浪费——工具调用决策的 reasoning budget 设到 1024–2048 token 已经足够。
- Batch API:离线评测、回放、数据生成场景必用,50% 折扣,24h SLA。不要用于在线 Agent。
- 并行 tool use:在 system 中明确允许
parallel_tool_use,模型会在单轮内返回多个独立工具调用,客户端并发执行后一次性回填。Agent 总轮数能降 20–40%。
4.3 自部署:投机解码与量化
| 技术 | 收益 | 代价 | Agent 场景适配 |
|---|---|---|---|
| Speculative decoding (EAGLE / Medusa) | decode 提速 1.5–3× | 显存 +10%,draft 模型训练 | 输出长时收益高;工具调用决策很短,收益有限 |
| INT8 / FP8 量化 | 显存减半,吞吐 +30–50% | 工具调用准确率掉 1–3% | 必须用工具调用基准重新评测,不要只看 MMLU |
| INT4 (AWQ/GPTQ) | 显存 ¼,吞吐 +50% | 工具调用准确率掉 5–10% | 一般 不推荐 用于 Agent 生产 |
| Chunked prefill | 长上下文 TTFT 降 30%+ | 无 | 强烈推荐,Agent 必开 |
| CUDA Graph | decode 延迟 -10–20% | 启动慢 | 默认开 |
量化的关键警告:Agent 对工具调用 schema 遵守率极敏感,量化造成的小数点误差会让模型偶尔吐出非法 JSON,重试代价远超量化收益。上线前必须跑 ToolBench / 自有工具调用集回归。
4.4 llama.cpp / Ollama 边缘场景
- 用
n_keep+n_batch配合 KV cache shift,让前缀真正复用,否则每轮都重算。 - Q4_K_M 是质量/速度甜点,Q5_K_M 用于工具调用更稳。
- Ollama 的
OLLAMA_KEEP_ALIVE必须设到分钟级,否则模型反复 load。 - 边缘 Agent 把"工具选择"留给本地,把"复杂推理"通过远程 API 兜底,是常见混合架构。
5. 并发与编排
5.1 工具并行执行
模型一轮返回多个 tool_use 时,客户端必须并发执行:
async def execute_tools(tool_calls):
results = await asyncio.gather(
*[run_tool(tc) for tc in tool_calls],
return_exceptions=True,
)
return [normalize(r, tc) for r, tc in zip(results, tool_calls)]
并发时要注意:
- 每个工具单独超时,不要用整体超时(一个慢工具会拖死所有快工具的结果回填)。
- 失败的工具返回 error tool_result 给模型,不要 raise 整条 trace。
- 共享资源(DB 连接池、外部 API 配额)要做限流,否则并发反而打挂下游。
5.2 推测性工具预取
对高确定性场景,可以在模型 streaming 出 tool_use 名称的瞬间就乐观启动工具,等参数补全后判断是否复用结果。仅适用于幂等只读工具(search、read_file),不适用副作用工具。
5.3 多 Agent 拓扑选择
| 拓扑 | 适用 | 加速点 |
|---|---|---|
| 单 Agent + 多工具 | 任务边界不清晰 | prefix cache 最大化 |
| Orchestrator + Workers | 子任务独立、可并行 | worker 并发 + 子 trace 独立缓存 |
| Pipeline (DAG) | 任务可静态分解 | 各阶段独立模型分级 |
多 Agent 系统每个子 Agent 用独立的稳定 system,子 Agent 之间 不要共享对话历史——会互相击穿缓存,并污染上下文。父子之间只传结构化结果。
6. 稳定性:生产级护栏
加速不能以牺牲稳定为代价。下面这些护栏在生产 Agent 是 必须项。
6.1 超时分层
工具级超时 < 单轮模型超时 < 单轮总超时 < 整条 trace 超时
(5–30s) (60s) (90s) (5–10min)
- 工具级超时必须比模型超时小,否则模型已经等不及。
- 整条 trace 必须有硬上限和最大轮数(典型 20–50),防止死循环。
6.2 限流与排队
- 客户端 token bucket 限流,避免触发服务端 429。
- 不同优先级用 不同的密钥/项目,互相隔离配额。
- 失败重试用指数退避 + jitter,永远不要无脑 while True retry。
6.3 熔断与降级
| 故障 | 检测 | 降级动作 |
|---|---|---|
| 主模型 P99 飙升 | 30 秒窗口 P99 > 阈值 | 切到备用模型(同家族同尺寸) |
| 单个工具连续失败 | 滑窗失败率 > 50% | 给模型返回"工具暂不可用",让其换路径 |
| 缓存命中率骤降 | 5 分钟均值 < 50% | 告警 + 强制检查 system/tools 是否被无意改动 |
| 上下文超限 | 入参 token > 上限 95% | 强制摘要压缩或主动结束 trace |
6.4 幂等性
带副作用的工具(写 DB、发邮件、转账)必须设计幂等键,由 Agent 框架在工具调用层传入。Agent 重试是常态,幂等是底线。
6.5 可观测性必备字段
每条 trace 至少落库:
- trace_id、turn_index、model、input/output/cached token、TTFT、总耗时
- 每个工具调用:name、参数 hash、耗时、状态、错误
- 终止原因:
stop(自然结束)/max_turns/timeout/error/human_intervention
终止原因分布是 Agent 健康度的最直观指标。max_turns 占比 > 5% 说明任务边界没设计好;timeout 占比 > 1% 说明工具或模型有性能问题。
7. 成本控制
成本和延迟在 Agent 场景下高度相关,因为两者主因都是 token 量。下面给出按优先级排序的成本下降清单:
- prompt caching 命中率拉到 90%+:单项节省 70%+,几乎零代码改动。
- 模型分级:简单轮换 Haiku/小模型,整体成本再降 30–50%。
- 工具结果裁剪:单条 trace token 量降 30–60%。
- 轮数压缩:开启并行 tool use + 良好 system prompt,平均轮数降 20–40%。
- batch API(离线场景):再降 50%。
- 流式 + 早停:模型自我重复时及时停(设 stop sequence 或检测 tool_use block 完成)。
- 失败 trace 不计费 retry 策略:4xx 立即停,不要把错误内容回填给模型继续推。
成本看板必须按 trace 维度 而非 请求维度 看:一条 trace 平均成本、P99 成本、超过 X 元的 trace 占比,这才能反映真实业务成本。
8. 上线 Checklist
正式接入生产前,过一遍:
前缀稳定性
- system / tools schema 已冻结版本,变更走灰度
- system 内无
datetime.now()等动态注入 - tools 数组顺序在所有进程稳定(显式排序)
- JSON 序列化使用
sort_keys=True或固定 schema - 历史只追加,不做 in-place 修改
缓存
- 缓存命中率监控告警阈值(< 80% 告警)
- Claude:4 个 cache_control 断点放置正确
- 自部署:
--enable-prefix-caching已开
上下文
- 滑窗 + 摘要混合策略已实现
- 工具结果服务端裁剪
- 单 trace 最大轮数硬上限
模型
- 模型分级路由策略(如适用)
- streaming 已开
- 并行 tool use 已启用且客户端并发执行
- 量化模型已用工具调用基准回归
稳定性
- 工具/模型/trace 三层超时
- 限流 + 指数退避
- 熔断降级策略覆盖主模型/工具/缓存
- 副作用工具幂等键
可观测
- 六段延迟分别打点
- cache hit rate、轮数分布、终止原因看板
- trace 维度成本看板
演练
- 主模型不可用切备演练
- 工具雪崩演练
- 长 trace(接近上下文上限)回放
9. 常见反模式速查
| 反模式 | 后果 | 正解 |
|---|---|---|
| system 里嵌入时间戳 | 缓存 0 命中 | 时间放第一个 user turn |
| 每轮让模型决定是否压缩历史 | 前缀永不稳定 | 阈值触发 + 整体替换 |
| 50 个工具一次塞给模型 | schema 占满 + 准确率下降 | 分阶段暴露 / meta-tool |
| 并发但整体超时 | 慢工具拖死所有 | 每个工具独立超时 |
| 工具失败直接 raise | trace 全部重跑 | error tool_result 回填给模型 |
| 量化后只跑 MMLU 上线 | 工具 schema 偶发违例 | 用工具调用基准回归 |
| while True retry | 把下游打挂 | 指数退避 + 最大次数 + jitter |
| 多 Agent 共享对话历史 | 互相击穿缓存 + 上下文污染 | 只传结构化结果 |
| 主模型 / 备模型缓存共用 | 切换瞬间命中率归零 | 缓存按模型隔离,切换在自然分段 |
| 端到端只看模型延迟 | 优化方向错 | 六段拆解 |
10. 各后端速查表
| 维度 | Claude API | OpenAI 兼容 | vLLM | SGLang | llama.cpp |
|---|---|---|---|---|---|
| Prefix cache | 显式 cache_control | 自动(gpt-4o+) | 自动 (APC) | 自动 (RadixAttn) | n_keep 手动 |
| 缓存价格优惠 | 10% read / 125% write | 50% read | 免费(自托管) | 免费 | 免费 |
| 并行 tool use | ✅ 原生 | ✅ 原生 | 依赖模型 | 依赖模型 | 一般不支持 |
| Streaming tool use | ✅ partial | ✅ | ✅ | ✅ | 有限 |
| Batch API | ✅ 50% off | ✅ 50% off | N/A | N/A | N/A |
| 投机解码 | 平台内置 | 平台内置 | ✅ 可配 | ✅ 可配 | ✅ |
| 多分支共享前缀 | 单分支 | 单分支 | 一般 | 强 | 弱 |
| 推荐场景 | 在线工具 Agent / 长上下文 | 通用 | 单 Agent 高吞吐 | 多 Agent / 树搜索 | 边缘 / 隐私 |
附:一次性优化决策树
TTFT 高?
├─ 缓存命中率 < 80%? → 修前缀稳定性(第 2、3 节)
├─ 输入 token 巨大? → 上下文裁剪 + 摘要(第 3 节)
└─ 自部署? → 开 chunked prefill(第 4.3 节)
总延迟高但 TTFT 正常?
├─ 工具 P99 高? → 单工具优化 / 并发 / 推测预取(第 5 节)
├─ 轮数 P99 高? → 并行 tool use + system 改进(第 4.2、5.1 节)
└─ decode 长? → streaming 早停 / 投机解码(第 4.2、4.3 节)
成本高?
├─ 缓存命中率? → 第 2 节
├─ 简单轮太多? → 模型分级(第 4.1 节)
└─ 工具结果太长? → 服务端裁剪(第 3.2 节)
不稳定?
├─ 偶发非法 JSON? → 检查量化 / 改用更强模型
├─ 长尾超时? → 三层超时 + 熔断(第 6 节)
└─ 重试雪崩? → 退避 + 限流 + 幂等(第 6 节)
最后一句:Agent 加速 80% 的收益来自三件事 —— 前缀稳定 + 缓存命中、并行工具调用、合理超时与熔断。把这三项做透,再去碰投机解码和量化。
11. 多 Agent 架构深入
单 Agent 的优化做到极致后,下一个量级的收益来自架构层面:把一个长 trace 拆成多个可并行、可独立缓存、可独立失败的子 Agent。但多 Agent 引入的复杂度也是非线性的——下面把生产中真正用得到的拓扑、缓存策略、通信协议、失败模型一次说清。
11.1 何时该上多 Agent
不是所有任务都该多 Agent。先用单 Agent 跑通,出现以下任一信号才考虑拆分:
- 平均轮数 > 30,且明显能切成阶段(调研 → 规划 → 执行 → 总结)。
- 上下文逼近模型上限,且无法靠摘要稳定压缩。
- 存在可并行的独立子任务(同时查 N 个数据源、对 N 个文件做相同操作)。
- 不同子任务需要不同工具集 / 不同模型尺寸,单 Agent 时 tools schema 太胖。
- 某些子任务对失败容忍度不同(探索性 vs 副作用性),需要独立的重试边界。
反过来,下面情况 不要 拆:
- 任务边界依赖中间结果才能确定(拆分后还要回流,得不偿失)。
- 子任务之间需要频繁共享大量上下文(通信成本会吃掉并行收益)。
- 团队还没把单 Agent 的可观测性做好(多 Agent 调试难度 5–10 倍)。
11.2 三种主流拓扑
A. Orchestrator-Worker(最常用)
┌──────────────┐
│ Orchestrator │ ← 强模型,规划 & 汇总
└──────┬───────┘
┌──────┼──────┐
▼ ▼ ▼
Worker Worker Worker ← 中/弱模型,并行执行
- Orchestrator 持有任务全貌,负责拆分子任务、分发、汇总,不直接调工具。
- Worker 是无状态的,输入 = 子任务描述 + 必要上下文片段,输出 = 结构化结果。
- 适用于:调研类(多源检索)、批量处理(对 N 个对象执行相同操作)、规划-执行分离场景。
关键工程点:
- Orchestrator 派发的子任务必须 完全自包含——Worker 不应需要"问回去"。这要求 Orchestrator 的规划 prompt 设计得足够细。
- Worker 的输出 schema 必须由 Orchestrator 预先声明,否则汇总阶段会反复重试解析。
- Worker 失败时 不要默认重试到底——把失败结果(含原因)返回给 Orchestrator,让强模型决定是重试、换路径还是放弃。
B. Pipeline / DAG(静态可分解任务)
[Stage 1: 抽取] → [Stage 2: 校验] → [Stage 3: 入库] → [Stage 4: 通知]
- 阶段静态,每个阶段是一个专用 Agent,可独立部署、独立扩容、独立升级模型。
- 适用于:数据处理流水线、文档处理、结构化抽取等可枚举步骤的场景。
- 加速点:每个阶段独立的 prefix cache 命名空间,能各自做到 95%+ 命中;阶段间用消息队列解耦,慢阶段不阻塞快阶段。
C. Debate / Critic(质量优先,非加速优先)
Proposer → Critic → Proposer → ... → Finalizer
- 用第二个 Agent 审查第一个 Agent 的输出。
- 不要把它当作加速手段——它的目的是质量,会让延迟和成本翻倍。仅在 high-stakes 决策(代码改动、SQL 执行、对外发言)使用,且 Critic 用同等或更强模型。
11.3 子 Agent 的缓存隔离与共享
这是多 Agent 最容易被做错的地方。核心原则:缓存按 (model, role, tools schema) 三元组隔离。
| 共享方式 | 是否推荐 | 说明 |
|---|---|---|
| Orchestrator 与 Worker 共用 system | ❌ | 工具集不同,schema 拼接后没人能命中 |
| 所有 Worker 共用 system | ✅ | 同角色 Worker 必须共用,prefix 命中率才能上去 |
| Worker 之间共享对话历史 | ❌ | 互相污染,前缀永不稳定 |
| Worker 之间共享只读上下文片段 | ✅ | 用结构化 reference(如文档 ID + 行号),让每个 Worker 自己按需取 |
| 父子之间传递大段上下文 | ❌ | 通信开销会击穿并行收益 |
| 父子之间传递结构化 handoff | ✅ | JSON schema 化的任务规约 |
实际部署的命名空间设计:
cache_key_prefix = f"{model}:{agent_role}:{tools_version}"
model:每个 Agent 固定一个模型,切换走灰度。agent_role:orchestrator / worker.search / worker.code / critic ...tools_version:工具 schema 哈希,schema 变了就升版本,老版本自然过期。
11.4 消息总线与通信协议
子 Agent 间通信不要用"自由对话"——会变成不可调试、不可缓存的黑盒。生产上用 结构化消息:
class AgentTask(BaseModel):
task_id: str
parent_trace_id: str
role: Literal["search", "code", "summarize", ...]
input: dict # 任务专用 schema
context_refs: list[str] # 只读上下文的引用(不内联)
constraints: dict # 超时、最大轮数、允许工具集
deadline_ms: int
class AgentResult(BaseModel):
task_id: str
status: Literal["success", "partial", "failed", "timeout"]
output: dict | None
error: str | None
usage: TokenUsage # 子 trace 的 token 计费,必须回报给 orchestrator
turns: int
通信层的工程要点:
- 选 消息队列(NATS / Redis Streams / Kafka) 而不是同步 HTTP——子 Agent 可独立伸缩,失败有重投。
- Worker 数量超过 10 时,用 work stealing 模式而不是静态分配,避免 straggler。
- Orchestrator 等待 Workers 时用
asyncio.as_completed流式处理,第一个返回的就开始汇总,不要全部等齐。 - 给每个子 task 单独的
deadline_ms,到点强制返回 partial,由 Orchestrator 决定补救。
11.5 上下文传递的三种范式
| 范式 | 通信成本 | 加速效果 | 适用 |
|---|---|---|---|
| 完整传递 | 高 | 差(每个 Worker 都付完整 prefill) | 不推荐 |
| 摘要传递 | 中 | 中(信息损失,可能要回问) | 探索性任务 |
| 引用传递 | 低 | 最佳(Worker 按需取,独立缓存) | 推荐默认 |
引用传递的实现:把原始上下文存到一个共享 K/V(Redis / S3),Orchestrator 传给 Worker 的是 ID + 取数范围。Worker 用专门的 fetch_context(ref) 工具按需取。这样:
- Orchestrator 的 prefix 不被 Worker 的子任务上下文污染。
- Worker 之间互相独立,可并行可重试。
- 只读上下文可以被多个 Worker 同时缓存命中(同一 ref 内容字节相同)。
11.6 失败模型与重试边界
多 Agent 系统的稳定性核心是明确每一级的失败边界:
工具失败 → Worker 内部处理(返回 error tool_result 让模型重选)
Worker 失败 → Orchestrator 处理(换路径 / 换 Worker / 放弃此子任务)
Orchestrator 失败 → 整 trace 失败(不再上面套一层)
反模式:层层重试。Orchestrator 重试 Worker,Worker 重试工具,工具内部又重试 HTTP——一次故障被放大成 N×M×K 次请求,下游被打挂。正确做法:
- 每一级 只重试自己抽象层的失败,下层失败如实上抛。
- 整条 trace 设全局重试预算(如 5 次),由 Orchestrator 统一调度。
- 区分 可重试失败(超时、429、5xx)和 不可重试失败(鉴权、参数非法、业务拒绝),后者立即上抛。
11.7 多 Agent 特有的可观测性
单 Agent 的 trace 是线性的;多 Agent 是树/DAG。监控需要扩展:
必须采集的额外字段:
parent_trace_id:建立父子关系。agent_role:聚合时按角色看分布。dispatch_latency:从 Orchestrator 发出到 Worker 开始的时间(队列健康度)。fan_out:每次扇出的子任务数(异常扇出是 bug 信号)。critical_path_ms:所有子任务中最长那条的耗时(这才是端到端延迟下限)。
看板必看的几个指标:
| 指标 | 健康范围 | 异常意味 |
|---|---|---|
| Worker 平均利用率 | 60–80% | 太低 = 扇出粒度太大;太高 = 容量不足 |
| 子任务 P99 / P50 比 | < 5× | 过大说明有 straggler,要查具体子任务 |
| Orchestrator 等待时间占比 | < 30% | 过高说明 Worker 慢或扇出失衡 |
| 跨 Agent 缓存命中率 | 各角色独立 > 85% | 任一角色低 → 该角色 system/tools 在抖 |
| trace 级失败率 | < 1% | 多 Agent 的失败率天然比单 Agent 高,需重点关注 |
11.8 多 Agent 的成本陷阱
多 Agent 不是天然更便宜——以下场景反而更贵:
- 小任务硬拆:拆分本身的 orchestration 开销 > 加速收益。经验阈值:单子任务预期 < 3 轮模型调用就不该拆。
- 重复上下文:Worker 间没用引用传递,每个 Worker 都付一遍 prefill 钱。
- Critic 滥用:每个子任务都过 Critic,成本直接 ×2。
- Orchestrator 用最强模型 + 每轮都长上下文:是单 Agent 成本的高倍数。
省钱的多 Agent 配方:
Orchestrator: 强模型 + 极致 prefix cache + 仅做规划/汇总(短输出)
Workers: 中/弱模型 + 独立小 tools schema + 高并发
Critic: 仅在 high-stakes 路径,且用相同档位模型
11.9 子 Agent 的灰度与版本
多 Agent 系统的发布是分布式问题:
- 不要同时升级 Orchestrator 和 Worker——总有一边先发,新旧消息 schema 必须兼容。
- 通信消息 schema 走 向后兼容演进(只加可选字段,不删不改语义),破坏性变更走双写双读窗口。
- 每个 Agent 独立灰度比例,并发现回滚要按 Agent 独立回滚,而不是整条链。
- prompt / tools 变更视同代码变更走 review,每次变更打 tag,缓存命名空间含 tag。
11.10 多 Agent 上线追加 Checklist
在第 8 节单 Agent 清单基础上,多 Agent 还需要:
- 每个 Agent 角色有独立的 system / tools 版本与缓存命名空间
- 子任务 schema 已声明,Orchestrator 与 Workers 双向兼容
- 上下文用引用传递,非整段内联
- 每个子任务有独立
deadline_ms与最大轮数 - 三层失败边界明确,无跨层重复重试
- 全局重试预算
-
parent_trace_id/agent_role/critical_path_ms已上报 - 跨 Agent 缓存命中率看板按角色拆分
- Orchestrator / Worker 独立灰度与回滚通道
- 演练过:Worker 雪崩、单 Worker 慢、消息队列堆积、Orchestrator 重启 mid-trace
多 Agent 的一句话总结:它换的是 并行度和模块化,付的是 复杂度和通信开销。在单 Agent 把缓存、并行工具、护栏都做到位之前,不要急着上多 Agent;上了之后,把每个子 Agent 当独立的生产服务对待——独立的 system、独立的缓存、独立的失败边界、独立的灰度。