Appearance
第13章 · Typed Tools、MCP 与可恢复 Agent
FixtureModel 只证明 harness 合同,不证明 live planner 质量。
先修:第8章的 checkpoint、第11章的 evidence 和状态边界。
本章目标
- 把模型输出约束为可校验的 structured tool call,而不是直接执行任意文本。
- 实现 read-only、幂等可逆写入、高风险 HITL 和始终拒绝四类动作。
- 用 checkpoint、idempotency key、超时/重试和 MCP protocol boundary 证明可恢复且无重复副作用。
公式与 shape
把 Agent 看成显式状态转移:
工具调用先经过 schema 和 policy,再进入 executor;幂等键使同一副作用请求满足
| 对象 | shape/结构 | 约束 |
|---|---|---|
ToolSpec | name + input_schema + risk + execute | schema 先于 execute |
ToolResult | tool + status + output + idempotency_key | ok/replayed 可重放;uncertain 必须停在 reconcile_required,不可自动重试;只有显式 AgentEngine.reconcile_uncertain(..., outcome="confirmed") 对账后才能继续,且不再次调用 executor |
AgentState | event_id + step + messages + pending_approval + checkpoint_hash | step/time 有上限 |
| approval | 绑定 action/args/session/expiry | 一次性、不可跨会话复用 |
关键概念与 LangChain / LangGraph 对照
本章先读懂自己写的最小 harness,再做框架对照。当前官方文档把 LangChain agents 放在较高层的模型/工具/agent loop 入口,把 LangGraph 放在可显式编排有状态工作流的较低层;两者的 API 和版本会变化,阅读时以执行日官方文档为准。
| 关注点 | 本课手写合同 | LangChain 对照 | LangGraph 对照 | 必须保留的判断 |
|---|---|---|---|---|
| structured tool | ToolSpec、输入 schema、风险级别 | 模型/工具集成与 agent 入口 | 把 tool call 作为节点或边上的事件 | 框架的 tool decorator 不能替代本课的 schema/policy 测试 |
| loop / routing | engine.py 的显式状态转移 | 高层 agent loop,适合先验证工具调用体验 | graph state、节点、条件边,适合显式多步路由 | 先画 s_t → validate → policy → execute → s_{t+1},再映射抽象 |
| state / persistence | AgentState、checkpoint hash、恢复入口 | 由应用层决定状态和批准绑定 | 图状态与 checkpointer/恢复机制可作对照 | 持久化不自动等于幂等;仍要证明重复事件只有一次副作用 |
| HITL / policy | policy.py 在 executor 前拒绝/暂停 | 需要显式 middleware/tool boundary | 需要显式 interrupt/approval 节点和恢复路径 | 任何框架都不能绕过 schema、session、args、expiry 和人工批准 |
| MCP | mcp_adapter.py 只做协议适配并复用 registry | 仅把可能的集成视作 transport 层 | 仅把可能的集成视作 graph 节点边界 | MCP 是协议边界,不是授权边界;以 规范 为准 |
这里的“对照”是概念和 trace 对拍,不是把框架依赖加入课程基线,也不是框架质量背书。课程现在提供两条明确分开的执行线:local_state_graph 是仓库内 dependency-free 的四节点状态图,用来在每台机器上真实测量抽象的节点/边/步骤成本;langgraph 是只在隔离环境中显式安装并固定版本后运行的第三方适配。两条线都会先运行真实的手写 AgentEngine fixture,不会调用 provider 或把不可用依赖静默降级为通过:
bash
PYTHONPATH=python python scripts/run_framework_comparison.py \
--framework local_state_graph \
--output evidence/13-framework-comparison-local-v2.json当前本地依赖-free 对照已经测量四列语义等价,另外记录 4 个节点、6 条边、1 条条件边和每个场景的图步骤;这只是结构化抽象成本,不是 wall-clock 性能或框架质量结论。evidence/13-framework-comparison-local-v2.json 是默认基线快照。此前未安装第三方依赖的历史读回保留在 evidence/13-framework-availability-v1.json。随后在隔离 Python 3.11.14 环境固定 langgraph==0.6.11,对同一四场景完成了真实 LangGraph trace 对拍,四列仍全部 equal;证据见 evidence/13-framework-comparison-langgraph-v1.json 和 evidence/13-framework-availability-v2.json。该可选包不进入课程默认依赖,且只证明确定性 harness contract,不证明 provider 质量、性能或 learner mastery。不能因框架自动重试、内置 memory 或默认 agent 成功一次,就跳过本课的批准、重放和故障门。
数学桥接:第2章 → 第13章
第13章的 Agent 状态机和工具调用约束建立在第2章的函数式思维之上:
Agent 状态转移与 gradient_descent_step:第2章 §7 的 gradient_descent_step(param, grad, lr) = param - lr · grad 是最简参数更新。Agent 的状态转移
模型不确定性、结构化输出与 cross_entropy:第2章 §6 的 cross_entropy(p_dist, q_dist) 衡量"期望 vs 实际"的信息差距。当模型对下一个 token 的 CE 很高时(不确定性大),直接自由生成的编造风险也高。Typed tool call 通过 schema 约束把模型的输出空间从"任意文本"缩小到"合法 JSON"——这不是限制创造力,而是把 CE 压缩到已知合法的子空间。
幂等性与确定性:第2章 §5 的 categorical_sample(probs, rng) 在相同 seed 下给出相同结果。幂等键 idempotency_key 的语义是"相同输入 → 相同输出 → 相同副作用只执行一次"——这和确定性随机采样的思想同构:相同的 state + input + seed 永远产生相同的 trajectory。
Checkpoint 与 momentum_update:第2章 §7 的 momentum_update 保存 velocity 以便下一步更新。Agent 的 checkpoint 保存 AgentState(event_id + step + messages + pending_approval)以便恢复——两者都记录"过去的轨迹",让后续步骤从正确的位置继续,而不是从头开始。
前端类比:Agent state transfer = Redux reducer——(state, action) => newState 是纯函数,相同输入永远产生相同输出。幂等键 = HTTP Idempotency-Key header。Checkpoint/restore = React useReducer + localStorage 的组合。MCP protocol = Web API 的 OpenAPI spec——schema 先于实现。
动手任务与验收(从零实践)
交互:Agent 主循环状态机
所谓 Agent,本质就是一个「LLM 推理 → 调工具 → 把结果塞回上下文」的 while 循环。
Idle
等待用户输入
→ think(user msg)
点「单步执行」开始
教学要点(红旗实验):把失败率拉到 80%,反复点执行 —— Agent 会卡在 Execute 自旋, 直到触发 3 次重试上限进 Error。没有 max_steps 和重试上限的 Agent 会烧光你的 token 预算。 这两个数字不是可选项,是生产环境的保险丝。
先点一遍 Idle → Think → SelectTool → Execute → Observe 主循环:调高工具失败率,观察重试分支和 Error 终态何时触发;每一次点击都应对应上方
STATE MACHINE · LOCAL FIXTURE
HITL、checkpoint 与幂等恢复
每一步只推进一个状态;高风险动作先批准,故障重试复用同一个 idempotency key,不把重复副作用当成功。
before_toolcheckpoint saved · idempotency=write-001before_toolcheckpoint saved · idempotency=write-001
这是离线状态机演示;真实合同由 python/agent_core、checkpoint 和安全测试证明,不连接任何工具或账号。
为四类风险各写一个最小 fixture;禁止通过删除高风险工具来让测试通过。
实现参数校验、风险策略、执行前 checkpoint、idempotency replay 和恢复入口。
用 MCP client/server 只做
tools/list/tools/call协议适配,并复用同一个 registry/policy。当前入口与测试:
bashPYTHONPATH=python python -m pytest \ python/tests/test_tool_policy.py \ python/tests/test_mcp_adapter.py \ python/tests/test_agent_recovery.py -q选一个只读工具和一个高风险 mock 工具做框架对照:先运行本课手写 harness,再运行仓库内
local_state_graph,最后在隔离环境固定 LangGraph 版本后映射同一输入/输出。比较四列:schema 失败时的副作用计数、批准前状态、相同 idempotency key 的重放结果、中断后的 checkpoint 状态;脚本会把每列的handwritten、framework和equal并排保存,并额外记录节点/边/图步骤。LangGraph 不可用时只记录not-measured,不能把 local_state_graph 冒充成 LangGraph 实测。框架不可安装或没有外部模型时,
local_state_graph提供可运行的抽象映射;它不替代第三方框架证据,也不证明 learner mastery。框架可安装时,必须记录确切版本、安装范围、警告/失败和同一 fixture 的对拍结果。本课测试、手写 harness 和 local graph gate 仍必须真实运行。
当前 python/agent_core/ 已有 schema、policy、checkpoint、engine、MCP adapter 和离线 FixtureModel;FixtureModel 只证明 harness 合同,不证明真实模型质量。reconcile_uncertain 需要显式、可审计的 confirmed 决策,不能由模型自动写入;完成 gate 还需所有关键状态可追踪、重复事件不重复执行和高风险动作批准前无副作用。
手写 Agent loop 的最小交接
先在不接入真实模型的前提下画出并实现这一条不可跳过的状态转移:planner output → schema validate → policy/HITL → pre-execute checkpoint → executor → trace/terminal state。每个分支必须写明谁能改变状态、是否允许副作用、恢复从哪个 checkpoint 开始;MCP adapter 只能把 tools/list / tools/call 送到同一 registry,不能取得 executor 的旁路。随后才把一个冻结的 planner route 映射进去;没有真实 route 时保持 blocked,不要用 FixtureModel 成功伪装 planner 能力。
故障注入与预期信号
本表是本章唯一的故障注入权威清单;「动手实验」一节不再另列第二份。
| 注入 | 预期失败信号 | 修复后证据 |
|---|---|---|
| 副作用已发生后超时 | 不确定结果被自动重试,造成重复写入 | 状态停在 reconcile_required,保存 tool_uncertain trace;同一 key 只能返回不确定结果;显式确认后写入 tool_reconciled,继续文本步骤但不再执行副作用 |
| 批准前执行高风险写入 | 产生未批准事件 | approval_required 且无副作用 |
| MCP 传入裸 executor | 绕过 registry/policy | protocol response 与本地 policy 一致 |
| schema 校验放在执行之后 | 错误参数的工具已被调用、副作用难以回滚 | 校验前置,失败直接拒绝并提示模型重写参数 |
| 批准 token 不绑定参数与时限 | 旧 token 可被重放触发高风险工具 | token 哈希绑定参数、会话 id 与有效期,由后端校验 |
| 副作用先于 checkpoint | 进程崩溃后重放产生重复执行与数据污染 | 执行前先写 checkpoint 记录意图,崩溃后按意图补偿 |
| 工具返回文本当指令执行 | 工具输出被当作用户输入再次规划、产生提示注入 | 工具返回标记为 data 而非 instruction,限制其再进入 prompt 的角色 |
论文与延伸
- ReAct: Synergizing Reasoning and Acting in Language Models(Yao 等,2023)
- Toolformer: Language Models Can Teach Themselves to Use Tools(Schick 等,2023)
- 规范阅读:Model Context Protocol Specification。
前端/Agent 迁移
这是全书最直接的工程迁移章节:把已有状态机、协议适配、重试、恢复和可观测性经验用于模型不确定输出。模型可以提出计划,但系统负责 schema、权限、审批、状态和副作用。
- Agent loop ≈ Redux reducer / XState:每个工具调用是
(state, event) → new_state的纯函数转换;checkpoint 就像 Redux DevTools 的时间旅行——从任意快照重放整个状态序列。 - Typed tool schema ≈ TypeScript + Zod:
python -m pytest python/tests/test_tool_policy.py强制每个工具的参数schema 在规划阶段被校验,就像 Zod schema 在运行时拦截非法输入——两者都在"执行前"把错误拦截。 - MCP ≈ OpenAPI spec / tRPC:MCP 是 Agent 和外部工具之间的契约层,就像 OpenAPI 定义 REST 端点、tRPC 端到端类型安全——没有 schema 的调用是 undefined behavior。
- Checkpoint ≈ Redux persist / localStorage:
AgentState(step + messages + pending_approval)序列化到磁盘,崩溃后从最近快照恢复,就像 Redux persist 把 store 存到 localStorage 以便页面刷新后恢复。 - Idempotency ≈ HTTP Idempotency-Key:高风险写操作带上幂等键,重试时不产生重复副作用——就像 Stripe API 用
Idempotency-Keyheader 防止重复扣款。
口述与自测(不看资料,5–10 分钟)
- 用一个高风险写操作口述从模型候选到批准、checkpoint、执行和 trace 的每个边界;分别说明参数无效、批准过期和 timeout 后不确定副作用如何终止。
- 对照 ReAct、Toolformer 与 MCP,解释”模型交替推理/行动””学习何时调用工具””工具协议”各解决什么;为什么三者都不能替代 policy、idempotency 或人工授权。
- 用第2章
gradient_descent_step类比 Agent 状态转移:每一步不是更新参数,而是更新状态——“增量更新 + 边界检查”是两者的共同模式。 - 用第2章
cross_entropy解释为什么 typed tool call 能降低编造风险:schema 约束把模型的输出空间从”任意文本”缩小到”合法 JSON”,等价于把 CE 压缩到已知合法的子空间。 - 用第2章
momentum_update类比 Agent checkpoint:checkpoint 保存AgentState(step + messages + pending_approval)以便恢复,就像 momentum 保存 velocity 以便下一步更新——两者都记录”过去的轨迹”。
实验与参考
动手实验
把 Agent 系统的四个工程底线钉死:所有工具调用必须先通过 schema 校验再执行;高风险工具必须得到带参数与会话绑定的批准 token 才可产生副作用;同一轨迹重放结果一致;崩溃后可从最近的 checkpoint 续跑。
环境准备
bash
cd <仓库根>
export PYTHONPATH="$PWD/python"命令与预期输出
上方「动手任务与验收」第 4 步已跑过同一条命令(test_tool_policy.py + test_mcp_adapter.py + test_agent_recovery.py);这里补充其独立判定信号:
text
.......................... [ 100% ]
3 passed in 4.05s
判定信号:
schema 校验通过的工具才能进入执行阶段
高风险工具未带有效批准 token 不产生副作用
同一轨迹重放结果与首次执行完全一致
注入故障后从最近 checkpoint 恢复,状态可重建概念图
图:第13章 Agent Harness 规划-执行-安全循环 — 用户目标经 Planner 分解为子任务,每个工具调用必须通过 Schema 前置校验和高风险 HITL 审批才能执行。核心是"plan → schema → tool → checkpoint → execute → observe → plan"的循环;崩溃时从最近 checkpoint 恢复状态。Policy 引擎是不可绕过的系统边界(模型不能直接修改工具输出),MCP adapter 负责 typed tool 的注册和合同校验。下游第18章用 replay 机制回放 Agent 轨迹做评估,第19章把 checkpoint → eval → report 串成 artifact chain。
故障注入清单
故障注入以正文「故障注入与预期信号」一节为唯一权威清单(7 项,覆盖不确定副作用、批准绑定、MCP 旁路、schema 前置、token 重放、checkpoint 顺序和工具输出角色),此处不再另列第二份;做故障题时逐项对照该表的预期信号与修复后证据。
资源 / 成本 / 隐私
FixtureModel、工具 registry 和 MCP adapter 在本地运行,默认无网络和副作用,预计 gross cost 为 0。trace 只保留脱敏结构化字段;任何真实凭据、cookie 和联系人数据都禁止作为 tool input。
Evidence
仓库当前机器证据(只读快照)
evidence/module-manifest-v1.json 中 13.evidence 指向当前文件:evidence/13-runtime-v1.json。这是当前 checkout 的脱敏机器运行记录,只覆盖该 JSON 记录的命令、指标、产物和已知失败;它不是学习者提交,也不能推出学习者已完成本章。
学习者提交模板(待填写,不是当前机器证据)
复制下面模板并填写自己的真实运行结果。所有 <...> 都是未填写状态;actual 和 artifacts 尤其不能被当作已运行或已通过。artifacts 必须替换为本次提交中真实存在的仓库相对路径。
yaml
schema: learn-llm.evidence.v1
module: 13-agent-harness
commit: <learner-commit-sha>
verified_at: <iso-date>
environment: <sanitized-python-device>
seed: 10
commands:
- PYTHONPATH=python python -m pytest python/tests/test_tool_policy.py python/tests/test_agent_recovery.py -q
metrics:
- name: schema_validity
expected: 1.0
actual: <recorded-value>
- name: unapproved_side_effects
expected: 0
actual: <recorded-value>
- name: recoverable_fault_recovery_rate
expected: <versioned-threshold>
actual: <recorded-value>
artifacts:
- <learner-repo-relative-artifact-path>
cost:
gross_usd: 0
credit_usd: 0
licenses:
- source: <source>
version: <version>
license: <license>
attribution: <attribution>
redistribution: <redistribution>
known_failures:
- <sanitized-failure-or-none>只跑 FixtureModel、没有批准绑定或没有故障恢复 trace 时,本章保持 gate。