Agent Engineering 课程阅读
首页/课程九 · Agent 生产可靠性/全景与基线:Agent 生产化的独特风险

在 GitHub 查看原文

本页目录

Lesson 00 — 全景与基线:Agent 生产化的独特风险

本课目标:建立「护请求 vs 护轨迹」的边界认知,交付六类故障注入的混沌任务集,跑出贯穿全课程的裸基线——把 research-assistant 的五种失控形态(死循环 / 成本超支 / 故障扩散 / 危险副作用 / 中途崩溃)现状的爆炸半径量成数字。

学完你能回答面试官那句:「你的 Agent 上线了,它跑飞了你怎么办?」——答案是:你有一张风险地图、一套可复现的故障注入、一份诚实记录的裸基线,之后每个治理机制的收益都对着这份基线量。


0. 这门课的定位

这是课程九,直接追加在 gui-agent-lessons(课程八)之后,不需要全仓重编号。理由有二:

  1. 时间线顺序:它治理的对象是经课程七(frontier)升级后的 research-assistant「Deep Research Agent v2」完整形态——记忆 / 账本 / 浏览器 / CodeAct 都是被治理的能力。先有能力、再谈治理,顺序上必须排在七、八之后。
  2. 主题弧线:收敛知识 1–6 → 前沿双课 7–8 → 生产收官 9(把前沿能力关进生产的笼子)。

课程口吻对齐 ops-lessons(讲「标准做法 + 取舍」的收敛工程知识),每课必有「## 方案对比」小节——Agent 可靠性的每个环节(限位、熔断、幂等、审批、恢复)业界都有几种成熟做法和真实取舍。

两条贯穿全程的主线(每课 README 结尾用一句话点位)

  1. 爆炸半径主线:Agent 的每种失控形态(死循环 / 成本超支 / 故障扩散 / 危险副作用 / 中途崩溃)现状的爆炸半径都是无界的,每课把一种失控的半径压到有界(循环→步数有界、成本→预算有界、故障→降级有界、副作用→审批+幂等有界、崩溃→重做量有界)。
  2. 自主-控制主线:每个保护机制都是拿 Agent 的自主性 / 延迟 / 人力换安全——闸太紧 Agent 废掉(什么都要人批),闸太松等于裸奔。每课给出「这道闸什么时候该紧、什么时候可以松」的判断依据。

1. 护请求 vs 护轨迹:本课 vs ops vs frontier 的分工边界

🎯 核心认知:ops-lessons 保护的是「一次 LLM 调用」(被观测对象是请求);本课保护的是「一个会自己转很多圈、自己决定下一步的循环」(被观测对象是轨迹)。kb-qa 是线性链,用不上本课机制——这个不对称本身就是边界的证据

1.1 为什么 kb-qa 用不上这些机制

kb-qa 的链路是线性的:问题 → 检索 → 拼 prompt → LLM → 答案。一次请求秒回,不会「自己转很多圈」,不会「自己决定下一步」,不会「跑到一半崩了要从中间接着跑」。所以 ops-lessons 给它做的保护全是请求级的:鉴权防别人打爆你、限流防流量洪峰、守护栏洗输入输出、语义缓存省重复调用。

research-assistant 完全不同:它是一个会自己拆子题、并行检索、写报告、审稿打回、反思补研的循环体。它可能转 3 圈也可能转 30 圈,每圈花多少钱事前不知道,工具挂了会把垃圾混进材料,写到一半进程崩了要决定从哪接着跑。这些风险 kb-qa 一个都没有——因为 kb-qa 不循环、不做副作用、不长时间运行

1.2 分工表(写进 L00,全课程不许越界)

维度 ops-lessons(护请求,落地 kb-qa) 本课(护轨迹,落地 research-assistant)
入口 / 过程 鉴权、限流(防别人打爆你) 步数上限、防死循环、超时降级(防自己转不出来)
成本 静态选型(选 glm-4 还是 flash)、语义缓存 轨迹级预算:运行时累计 token、超支中途刹车
内容 / 动作 注入防御、守护栏(洗单次输入输出) 副作用治理:幂等、重放安全、危险动作审批(HITL)
恢复 ——(请求失败客户端重试即可) 断点续跑:任务崩在第 7 步,从第 7 步接着跑
评估 单条答案好不好(在线评估) 整条轨迹稳不稳(故障注入下的 SLO)

1.3 与 frontier-lessons 的三处贴边(复用其产物,不重复造轮子)

frontier 课程已经做了三件和本课「贴边」的事,必须写清分工:

  • L01 vs frontier-L08(轨迹评估有循环检测指标):frontier 是事后评估——跑完看轨迹发现绕路;本课是运行时强制——转到第 N 圈当场刹车。检测离线,执法在线。本课的循环检测可复用 frontier-L08 的动作签名思路。
  • L06 vs frontier-L10(TaskLedger 任务账本):账本解决「跨多次运行的语义增量」(第三次运行接着第二次的结论做,不重复研究);本课 durable execution 解决「单次运行的执行恢复」(进程崩在 writer 节点,重启后从 checkpoint 续跑,已完成的检索不重做、已执行的副作用不重放)。账本是工作层,durable 是执行层,互补不重叠。
  • L08 vs frontier-L09(Eval Harness 机制开关矩阵):frontier 量的是干净跑下的能力收益(加记忆好多少);本课量的是故障注入下的生存能力(断网/慢工具/崩溃下还能不能出结果)。矩阵跑批的基建直接复用 eval_agent/run_harness.py 的思路。

💡 一句话记忆:ops 护请求,frontier 拓能力,本课护轨迹。三者正交,各自的对象不同。


2. Agent 生产风险地图:六个故障注入点

research-assistant 的一次运行轨迹长这样(现状拓扑):

                ┌──────────────── research-assistant 一次运行 ────────────────┐
                │                                                              │
 START → split(1) ──Send×N──→ researcher×3 ──→ summarize(1)                    │
  (拆子题)        ①慢 ②坏      (并行检索)         (汇总)                          │
                  ⑤炸弹         │                                                 │
                                ▼                                                 │
                          research_team ──→ writer ──→ reviewer ─(条件)─→ END    │
                           (子图回流)     ④崩溃     (审稿)                         │
                                          ⑥副作用    │                            │
                                              ┌─────┴─────┐                      │
                                              ▼           ▼                      │
                                           rework    re_research                 │
                                          (重写)     (补研) ③循环诱导             │
                                              │           │                      │
                                          writer    research_team                 │
                                              └─────┬─────┘                      │
                                                    ▼                            │
                                              (打回到上限)                      │
                                                    │                            │
                                                   END                           │
                └──────────────────────────────────────────────────────────────┘

六个故障注入点(标在图上),对应任务书的六类混沌故障:

注入点 故障 现状会发生什么
researcher 调 web_search 慢工具:搜索挂起 30s+ 被 search_timeout 兜住,但返回的「超时」字符串混进材料
researcher 调 web_search 坏工具:搜索抛错 / 吐垃圾 异常被 catch 返回失败串,垃圾内容混进材料
reviewer 打回循环 循环诱导:永远暗示信息不足 局部 max_rewrites 兜住,但 3×2×3 叠乘后步数仍很大
writer 节点 进程崩溃:跑到这里被杀 无 checkpoint 续跑,从头全部重跑
researcher 材料 预算炸弹:返回超长文本 无成本刹车,token 烧穿不停
publish 动作 危险副作用:发布被重复执行 无幂等键、无审批门,打回重写导致重复发布

🎯 核心认知:这六个点不是「可能出问题的地方」,而是「现状必然出问题的地方」——区别只在于故障什么时候被触发。我们的目标是让每个点的爆炸半径从「无界」变成「有界」。


3. 现状「局部限位」盘点(诚实:不是没有限位)

动手治理前,必须诚实承认现状已经有哪些兜底——不许为了效果把已有兜底说成没有:

已有限位 位置 兜住了什么 没兜住什么
max_rewrites=3 config.py reviewer→writer 打回循环最多 3 次 子图并行 + 补研会叠乘,总步数仍无界
max_re_research=2 config.py 事实冲突补研最多 2 次 同上,和 max_rewrites 叠加
browser_max_steps=12 config.py 浏览器 agent 单次最多 12 步 不约束父图总步数
search_timeout=15s tools.py 单次搜索不会无限挂死 超时返回的字符串会污染材料
Semaphore(5) tools.py 搜索并发不超 5 不约束总调用数 / 总 token
recursion_limit=25 langgraph 默认 最后保险丝,防彻底死循环 崩溃式(抛 GraphRecursionError),非诚实收尾

五个真实缺口(本课程要补的)

诚实承认已有兜底之后,缺口的轮廓就清楚了:

  1. 限位分散且各自为政:每个局部都在限内,一次运行的总步数 / 总 token 仍然无界(例:3 子题 × 重查 2 × 打回 3 全部叠加 = 18 轮检索,recursion_limit 还没到)。
  2. 没有成本刹车:烧到多少钱都不会停——enable_* 开了越多,吞金兽越多。
  3. 降级不诚实web_search 超时返回的是一段「搜索超时」字符串,混进材料被 LLM 当成内容,下游毫无感知。这是最隐蔽的故障——报告里写着「搜索 'X' 超时」被当成事实。
  4. 副作用零治理:没有幂等键、没有审批门。现状 research-assistant 全是只读工具,所以「没出过事」是因为「没做过危险的事」——一旦加发布动作就是裸奔。
  5. 崩溃 = 全部重跑:checkpointer 只被动存了状态,服务层没有任务注册与续跑语义——崩溃后没人知道「哪些任务没跑完、该从哪继续」。
  6. 没有可靠性数字:不知道故障下成功率 / 卡死率是多少。

⚠️ 诚实标注:缺口 1–3 是「结构性」的(现状代码确实没有),缺口 4 是「预防性」的(现状还没加危险工具,但马上要加)。本课程 L04 就会引入第一个副作用工具 publish_report,所以缺口 4 必须先治理。


4. 方案对比:怎么给一个循环体做生产化?

方案 做法 取舍
裸奔上线 啥也不加,出事了再 hotfix ✅ 最快、零开发成本;🚫 出事就是生产事故,背锅;Agent 跑飞了用户先发现
全人工审批 每一步都要人点确认才继续 ✅ 最安全;🚫 Agent 废掉——退化成「人肉执行器」,延迟从秒级变小时级,完全失去自动化的意义
分层治理(本课程主路线) 运行时限位(步数/成本)+ 危险动作门控(幂等/审批)+ 崩溃恢复(checkpoint 续跑)+ 混沌验证(故障注入回归) ✅ 自主性损失最小(只拦不可逆动作)、爆炸半径全部压到有界、有 SLO 数字背书;🚫 开发量大(10 课),每层都要判断「紧还是松」

选 ③ 的理由:方案 ① 是把用户当测试员,方案 ② 是把 Agent 当草稿机。分层治理的精髓是「闸只拦该拦的」——循环用步数预算拦、成本用 token 预算拦、危险动作用幂等+审批拦、崩溃用 checkpoint 拦,其余放行。这正是「自主-控制主线」的判断依据:每道闸「什么时候该紧、什么时候可以松」,后续每课都给。


5. 硬任务:混沌任务集(本课的命根子)

「混沌任务集」:对 research-assistant 的一次标准研究任务,注入六类故障,全部实现为可组合、可独立开关的注入器(包装 tools/LLM 客户端的 wrapper,全离线可复现,放 eval_agent/chaos.py)。

六类故障(已落地,见 code.pyeval_agent/chaos.py):

  1. 慢工具:web_search 挂起 30s+(超过 search_timeout)
  2. 坏工具:搜索随机抛错 / 返回垃圾内容
  3. 循环诱导:搜索结果永远暗示「信息不足需再查」+ reviewer 永远打回(把所有局部限位叠加拉满)
  4. 进程崩溃:跑到 writer 节点时进程被杀(演示用 subprocess 注入)
  5. 预算炸弹:某子题返回超长文本,token 消耗爆炸
  6. 危险副作用:报告发布动作在无门控下被重复执行 / 未经批准执行

L00 用现状 research-assistant 跑六类故障的裸基线,存档 baseline_chaos.json(每类故障的结局:卡死/污染/超支/全部重跑/重复执行,以及局部限位兜住了哪些——诚实记录)。之后每课修一类,重跑对应故障,L08 出「故障 × 防护」收益矩阵定稿。

为什么自己写注入器而不引 chaos-toolkit

故障注入器本质就是「把真实工具/LLM 包一层 wrapper,按剧本演故障」——几十行的事,写出来才懂每种故障的形态。引 tenacity/pybreaker/chaostoolkit 反而把机制藏进黑盒,违背「写出来才懂」的课程原则。本课程的硬约束是零新增重依赖:熔断器/重试/故障注入/幂等注册表全部手写。


6. 跑裸基线:现状到底有多脆

code.py 做两件事

  1. 对「现状 research-assistant 的一个简化轨迹模型」注入六类故障,诚实记录每类故障的裸奔结局(存档 baseline_chaos.json)。
  2. 跑一次纯净跑(无故障),证明注入器本身不干扰正常流程。

⚠️ 诚实标注code.py 用的是「轨迹模型」而不是直接跑真实 research-assistant——因为真实图依赖 ChatZhipuAI(要 API key)和 DuckDuckGo(要联网),无法满足「全离线可复现」的硬约束。简化模型忠实复刻了现状图的关键拓扑与缺口(局部限位、降级污染、无预算刹车、副作用无门控、崩溃全重跑),结构性结论与真实 API 一致。mock 下的 token 数为字符数/4 估算(非真实 usage_metadata)。

预期结果:裸基线的病

场景             结局          步数   token(估)    污染    发布次
pure           ✅ caught      7        142     否      0     ← 注入器不干扰
slow           ☠️ polluted     7        124     是      0     ← 超时串混进材料
flaky          ☠️ polluted     7        162     是      0     ← 垃圾混进材料
loop           ✅ caught     11        220     否      0     ← 局部兜住但步数叠乘
crash          🔄 full_rerun  8        115     否      0     ← 重做量无界
bomb           💸 overspent   7      70714     否      0     ← 烧穿无刹车
sideeffect     ⚠️ duplicate   9        168     否      2     ← 重复发布

逐条解读:

  • ① 慢工具:被 search_timeout=15s 兜住没卡死,超时返回的字符串混进了材料 → 污染。现状降级是不诚实的(L03 修)。
  • ② 坏工具:抛错被 catch,但垃圾内容(「本页面正在建设中」「404」)混进材料 → 污染(更隐蔽,因为看起来像正常结果)。
  • ③ 循环诱导:reviewer 打回到 max_rewrites=3 才停,局部限位兜住了。但注意步数从 7 涨到 11——如果叠加补研(×2)和更多子题(×N),总步数仍无界(L01 修)。
  • ④ 进程崩溃:现状无 checkpoint 续跑语义,从头全部重跑,重做成本 ≈ ×2(L06 修)。
  • ⑤ 预算炸弹:一个子题返回 40000 字符(≈10000 token),3 个子题就 30000 token,无成本刹车一路烧穿(L02 修)。
  • ⑥ 危险副作用:reviewer 打回一次 → writer 重写 → publish 被走到 2 次,重复发布(L04 加幂等、L05 加审批修)。

🎯 核心认知:五种失控里,只有循环诱导被局部限位部分兜住(步数有上限但很大),其余四种全是无界的。这就是「爆炸半径主线」的起点——每课把一种半径从无界压到有界。


7. 落地清单

文件 说明
eval_agent/chaos.py(research-assistant 内) 新增:六类故障注入器(可组合 wrapper)+ 结局分类枚举
agent-ops-lessons/00_overview/code.py 跑裸基线,存档 baseline_chaos.json
agent-ops-lessons/00_overview/baseline_chaos.json 裸基线档案(后续课程对照)
README.md(本文件) 风险地图 + 分工边界 + 现状盘点 + 方案对比

注意:L00 不动 research-assistant 主链路——只新增 eval_agent/chaos.py 和基线档案。主链路的治理从 L01 开始。

验收

# 从仓库根跑(零外部依赖,全离线)
cd portfolio-projects/research-assistant
python ../../agent-ops-lessons/00_overview/code.py

预期: - 六类故障各有明确记录的裸奔结局(stuck / polluted / overspent / full_rerun / duplicate / caught)。 - 纯净跑(无故障)结果正常(✅ caught),证明注入器本身不干扰。 - 生成 baseline_chaos.json,含现状局部限位清单和诚实标注。

# 现状 123 个测试不受影响(L00 没动主链路)
python -m pytest -q
# 预期:123 passed

8. 本课在两条主线上的位置

  • 爆炸半径主线:起点——画出五种失控的无界半径,量成 baseline_chaos.json 里的数字。之后每课把一种半径压到有界。
  • 自主-控制主线:起跑线——确立「分层治理」的基调:闸只拦该拦的(循环/成本/危险动作/崩溃),其余放行。之后每课给「这道闸紧还是松」的判断依据。

🎯 面试话术

「我的 Agent 上线前,我先把它的生产风险画成地图:循环、成本、故障扩散、副作用、崩溃五类失控,现状的爆炸半径全是无界的。我不靠拍脑袋判断风险——我做了一套可复现的故障注入基线,六类故障注入器包装在 eval_agent/chaos.py,全离线可跑,裸基线诚实记录了现状兜住了什么、没兜住什么。之后每个治理机制的收益都对着这份基线量。

这套思路和 ops-lessons 的区别在于:ops 护的是一次请求(鉴权限流守护栏),我护的是一条轨迹——Agent 会自己转很多圈、自己决定下一步,所以风险形态完全不同。kb-qa 是线性链用不上这些机制,research-assistant 是循环体非用不可,这个不对称本身就是边界的证据。」