本页目录
Lesson 00 — 全景与基线:Agent 生产化的独特风险
本课目标:建立「护请求 vs 护轨迹」的边界认知,交付六类故障注入的混沌任务集,跑出贯穿全课程的裸基线——把 research-assistant 的五种失控形态(死循环 / 成本超支 / 故障扩散 / 危险副作用 / 中途崩溃)现状的爆炸半径量成数字。
学完你能回答面试官那句:「你的 Agent 上线了,它跑飞了你怎么办?」——答案是:你有一张风险地图、一套可复现的故障注入、一份诚实记录的裸基线,之后每个治理机制的收益都对着这份基线量。
0. 这门课的定位
这是课程九,直接追加在 gui-agent-lessons(课程八)之后,不需要全仓重编号。理由有二:
- 时间线顺序:它治理的对象是经课程七(frontier)升级后的 research-assistant「Deep Research Agent v2」完整形态——记忆 / 账本 / 浏览器 / CodeAct 都是被治理的能力。先有能力、再谈治理,顺序上必须排在七、八之后。
- 主题弧线:收敛知识 1–6 → 前沿双课 7–8 → 生产收官 9(把前沿能力关进生产的笼子)。
课程口吻对齐 ops-lessons(讲「标准做法 + 取舍」的收敛工程知识),每课必有「## 方案对比」小节——Agent 可靠性的每个环节(限位、熔断、幂等、审批、恢复)业界都有几种成熟做法和真实取舍。
两条贯穿全程的主线(每课 README 结尾用一句话点位)
- 爆炸半径主线:Agent 的每种失控形态(死循环 / 成本超支 / 故障扩散 / 危险副作用 / 中途崩溃)现状的爆炸半径都是无界的,每课把一种失控的半径压到有界(循环→步数有界、成本→预算有界、故障→降级有界、副作用→审批+幂等有界、崩溃→重做量有界)。
- 自主-控制主线:每个保护机制都是拿 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),非诚实收尾 |
五个真实缺口(本课程要补的)
诚实承认已有兜底之后,缺口的轮廓就清楚了:
- 限位分散且各自为政:每个局部都在限内,一次运行的总步数 / 总 token 仍然无界(例:3 子题 × 重查 2 × 打回 3 全部叠加 = 18 轮检索,recursion_limit 还没到)。
- 没有成本刹车:烧到多少钱都不会停——
enable_*开了越多,吞金兽越多。 - 降级不诚实:
web_search超时返回的是一段「搜索超时」字符串,混进材料被 LLM 当成内容,下游毫无感知。这是最隐蔽的故障——报告里写着「搜索 'X' 超时」被当成事实。 - 副作用零治理:没有幂等键、没有审批门。现状 research-assistant 全是只读工具,所以「没出过事」是因为「没做过危险的事」——一旦加发布动作就是裸奔。
- 崩溃 = 全部重跑:checkpointer 只被动存了状态,服务层没有任务注册与续跑语义——崩溃后没人知道「哪些任务没跑完、该从哪继续」。
- 没有可靠性数字:不知道故障下成功率 / 卡死率是多少。
⚠️ 诚实标注:缺口 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.py 和 eval_agent/chaos.py):
- 慢工具:web_search 挂起 30s+(超过 search_timeout)
- 坏工具:搜索随机抛错 / 返回垃圾内容
- 循环诱导:搜索结果永远暗示「信息不足需再查」+ reviewer 永远打回(把所有局部限位叠加拉满)
- 进程崩溃:跑到 writer 节点时进程被杀(演示用 subprocess 注入)
- 预算炸弹:某子题返回超长文本,token 消耗爆炸
- 危险副作用:报告发布动作在无门控下被重复执行 / 未经批准执行
L00 用现状 research-assistant 跑六类故障的裸基线,存档 baseline_chaos.json(每类故障的结局:卡死/污染/超支/全部重跑/重复执行,以及局部限位兜住了哪些——诚实记录)。之后每课修一类,重跑对应故障,L08 出「故障 × 防护」收益矩阵定稿。
为什么自己写注入器而不引 chaos-toolkit
故障注入器本质就是「把真实工具/LLM 包一层 wrapper,按剧本演故障」——几十行的事,写出来才懂每种故障的形态。引 tenacity/pybreaker/chaostoolkit 反而把机制藏进黑盒,违背「写出来才懂」的课程原则。本课程的硬约束是零新增重依赖:熔断器/重试/故障注入/幂等注册表全部手写。
6. 跑裸基线:现状到底有多脆
code.py 做两件事
- 对「现状 research-assistant 的一个简化轨迹模型」注入六类故障,诚实记录每类故障的裸奔结局(存档
baseline_chaos.json)。 - 跑一次纯净跑(无故障),证明注入器本身不干扰正常流程。
⚠️ 诚实标注:
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 是循环体非用不可,这个不对称本身就是边界的证据。」