适用读者:Demo 里 Agent 跑得很好、一上真实任务就转圈或烧钱的人。
前置:S21-J01(Agent 循环与工具调用)、S22-J04(ReAct 变体与 DAG)、S20-J05(上下文工程)。
覆盖旧文:4619(Agent 实战全指南:教科书 ReAct 到生产环境——循环、卡死防护、流式体验与成本)→ 1 篇 / 3,380 字符。
扩展点:E-S22-021(教科书 3 步 vs 生产 11 步:多出来的 8 步分别防什么)、E-S22-022(四种卡死模式与三类防护策略)、E-S22-023(流式体验与成本的耦合关系)。
下一篇:S22-X02 · MCP 服务器构建与项目登记。
1. 一句话结论
生产 Agent 的工程量不在"想得更聪明",在"停得下来":教科书 3 步(想→做→观察)跑一百次就会出现四种卡死,而每一轮卡死都在真实烧钱。防护策略必须是代码层的硬约束,不能写在提示词里。
出处:
A-4619。
2. 教科书 3 步 vs 生产 11 步(E-S22-021)
教科书:Thought → Action → Observation(循环直到给出答案)
生产(单轮):
① 意图识别/路由(是否需要工具)
② 上下文裁剪(预算内选材,见 S20-J05)
③ 工具清单筛选(只给相关工具,避免选择困难)
④ 参数校验(schema 校验 + 业务前置校验)
⑤ 权限与审批(危险操作走人工,见 S24-A01)
⑥ 执行(超时、重试、幂等)
⑦ 结果裁剪(大输出摘要,避免撑爆上下文)
⑧ 状态记录(写 checkpoint,支持断点续跑)
⑨ 终止判定(max_steps / 预算 / 无进展)
⑩ 输出(结构化 + 引用来源)
⑪ 观测(trace、token、耗时、失败分类)
多出来的 8 步都在防同一类问题:不可控。判据:任何"只靠模型自觉"的约束,在 100 轮里一定会被违反一次。
| 缺哪步 | 真实后果 |
|---|---|
| 缺 ③ | 工具 30 个以上时,选错工具率显著上升 |
| 缺 ④ | 参数类型错/必填缺失 → 工具 500,Agent 重试同一错误 |
| 缺 ⑦ | 一次 SQL 查询返回 5 MB → 上下文爆,费用涨十倍 |
| 缺 ⑧ | 中途失败只能从头跑,成本翻倍 |
| 缺 ⑨ | 无限循环(见 §3) |
3. 四种卡死模式与三类防护(E-S22-022)
| 卡死模式 | 机制 | 观测信号 |
|---|---|---|
| 重复调用 | 同一工具+同一参数反复调用,结果没变化 | 连续 N 步参数哈希相同 |
| 乒乓循环 | 两个工具互相触发(A 失败→B 修复→A 再失败) | 步骤序列出现周期 |
| 等待幻觉 | 模型声称"已提交,等待结果",实际无状态变化 | 无新 tool 调用,只有文本 |
| 长尾工具 | 某工具耗时极长但无超时,整体挂住 | 单步耗时 > 阈值 |
三类防护策略(按成本从低到高):
① 硬上限(必须有)
max_steps(如 15)、max_tokens、max_wall_clock、max_cost
任一触顶 → 强制收尾:输出「已完成部分 + 未完成原因 + 建议下一步」
② 无进展检测(防重复与乒乓)
记录每步 (tool, args_hash, result_hash)
连续 3 步 tool+args 相同,或 result_hash 连续不变 → 判定无进展
→ 改变策略:换工具 / 缩小任务 / 直接返回现状
③ 结构化终止(防等待幻觉)
每轮要求模型输出 {"status": "continue|done|blocked", "next": …}
只有 status=done 才允许给最终答案;blocked 必须有具体阻塞原因
判据:"防护命中率"要能被观测——每次触发防护都打一条带类型的日志(guard=dup_call|no_progress|budget),否则你永远不知道 Agent 是在正常工作还是在打转。
代码骨架(语言无关,伪代码):
for step in range(MAX_STEPS):
if cost.used > MAX_COST or clock.elapsed() > MAX_WALL:
return finish(reason="budget", partial=state.summary())
action = model.next(state) # 强制结构化
if action.status == "done":
return finish(reason="done", answer=action.answer)
if action.status == "blocked":
return finish(reason="blocked", detail=action.blocked_reason)
key = (action.tool, hash_args(action.args))
if key in recent_keys[-2:]: # 重复调用
state.hint = "上一步与两步前相同,请换策略或结束"
log_guard("dup_call"); continue
result = tools.call(action.tool, action.args, timeout=TOOL_TIMEOUT)
state.observe(hash(result)) # 结果哈希用于无进展检测
log_step(step, action, result.size)
4. 流式体验与成本的耦合(E-S22-023)
| 事实 | 影响 |
|---|---|
| 流式(SSE)不减少总 token | 只改善感知延迟(首 token 时间) |
| 流式 + 工具调用 = 两段输出 | 先流式文本,再暂停等工具,再流式续写 → 前端必须处理"分段" |
| 中途取消要能真正停 | 客户端断开 → 服务端要中断生成(否则继续烧 token) |
| 长输出流式更贵 | 用户可能只读前 20%,但全部 token 都已计费 |
判据:"先让它能停,再让它好看"——取消与超时是成本控制;流式是体验优化。顺序反了,你会得到一个很流畅但很贵的循环。
5. 反例:八种"上线就烧钱"
| 做法 | 后果 |
|---|---|
只有 max_steps,没有预算 |
每步工具很贵时,15 步也能烧穿 |
| 把"不要重复调用"写进提示词 | 100 轮里必违反一次 |
| 不裁剪工具输出 | 上下文爆、费用涨十倍 |
| 无 checkpoint | 失败重跑,成本翻倍 |
| 工具无超时 | 单步挂住整个请求 |
| 取消不中断生成 | 用户走了,token 还在烧 |
| 无 guard 日志 | 无法区分"高效"与"打转" |
| 失败后自动重试整轮 | 重复执行副作用(下单、发信) |
副作用防护:写操作必须带幂等键(业务 ID + 操作类型),重试只重试幂等操作——这条与 S17-J03(可靠投递)的幂等设计同源。
6. 自测
- 生产 11 步比教科书多出的步骤各防什么?
- 四种卡死模式与三类防护的对应关系?
- 为什么防护必须是代码层约束?
- 流式与成本的关系?取消为什么要中断生成?
- 写操作重试的安全前提?
答案
- 路由/裁剪/工具筛选/参数校验/审批/超时重试/结果裁剪/状态/终止/结构化输出/观测——分别防错选、爆上下文、挂起、重复、不可复盘。
- 重复调用与乒乓→无进展检测;等待幻觉→结构化终止;长尾→硬上限+超时。
- 模型对否定指令的遵循不可靠,100 轮内会出现违反。
- 流式不减 token,只降感知延迟;不中断则继续计费。
- 幂等键(业务 ID + 操作类型),且只重试幂等操作。
7. 下一篇
S22-X02 · MCP 服务器构建与项目登记 —— 5 篇 MCP 教程(Python/TypeScript/自家 API/推荐清单/概念对比)的统一七步骨架与三语言对照,以及 DevSupport AI 与 AI 模拟面试官两个项目的登记。
本篇属于系列《S22 · AI 工程落地实战》。旧文来源:A-4619;扩展点 E-S22-021/022/023。MCP 规范与 SDK 版本迭代较快,具体 API 名以官方文档为准(待核)。