第七章 Agent:让模型在受控边界内观察、决策与行动¶
Agent 不是“加一个提示让模型自己想”。它是一个运行时系统:模型接收目标和当前状态,选择工具或给出答案,程序执行动作并返回观察,循环直到完成、失败或需要人类决策。真正困难的是工具契约、状态管理、停止条件、权限、安全和评估。

图 7-1 模型负责提出下一步,运行时负责执行、校验、记录与终止。
7.1 Workflow 与 Agent¶
先建立直觉。 Workflow 由程序预先规定路径,Agent 让模型在运行时选择下一步。任务路径稳定、风险高时优先 Workflow;开放探索且可验证时才需要更多自主性。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 判断步骤是否可预先枚举。
- 判断工具结果是否可验证。
- 评估失败成本。
- 设置最大自主范围。
最小例子。 发票审批流程适合确定性工作流;跨来源研究问题可能需要动态决定下一次搜索。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 把一次模型调用称为 Agent、把所有流程都交给模型、没有人工接管点,会同时增加成本和风险。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:把一个业务过程画成 Workflow 版和 Agent 版,比较可测试性与失败面。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
若步骤固定、规则明确,优先使用工作流:分类后走不同分支、并行调用多个模型、由评审器打分等。Agent 适合路径难以预先写死、需要基于中间结果动态决策的任务。自治程度越高,成本和风险越高。
ReAct 可概括为“推理—行动—观察”的交替。工程实现不应依赖模型输出冗长私密思维;只需结构化地返回动作、参数和简短理由,详细 trace 由运行时记录。
7.2 一个最小工具循环¶
从问题出发。 最小 Agent 循环只有观察、决策、行动和停止。状态必须由程序保存,模型输出只是候选决策;每一步都要有预算与 trace。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 把目标和当前状态交给模型。
- 解析 action 或 final。
- 验证并执行工具。
- 把结构化观察写回状态。
用小数据走一遍。 搜索工具返回 title、url、snippet 与错误码;模型看到的是受控结果,不应直接获得浏览器或系统任意权限。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 工具异常后无限循环、观察文本没有长度限制、停止完全靠模型自觉、状态原地混乱修改,都会失控。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:实现 max_steps=5 的工具循环,用假工具测试成功、未知工具、超时和达到步数上限。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
from dataclasses import dataclass
from typing import Any, Callable
@dataclass
class Tool:
name: str
description: str
fn: Callable[..., Any]
class AgentRuntime:
def __init__(self, model, tools: list[Tool], max_steps=8):
self.model = model
self.tools = {t.name: t for t in tools}
self.max_steps = max_steps
def run(self, goal: str):
state = {"goal": goal, "observations": []}
for step in range(self.max_steps):
decision = self.model.decide(state, list(self.tools.values()))
if decision["type"] == "final":
return {"status": "completed", "answer": decision["answer"],
"steps": step + 1}
if decision["type"] != "tool":
return {"status": "failed", "reason": "非法动作"}
name = decision["name"]
if name not in self.tools:
state["observations"].append({"error": f"未知工具 {name}"})
continue
try:
result = self.tools[name].fn(**decision["arguments"])
state["observations"].append({"tool": name, "result": result})
except Exception as exc:
state["observations"].append({"tool": name, "error": str(exc)})
return {"status": "stopped", "reason": "达到最大步数"}
代码解读:运行时而非模型控制最大步数;未知工具和异常被转成观察;每次工具调用都应在 fn 内再做参数、权限与副作用检查。真实系统还要限制总 token、金额、网络域名和墙钟时间。
7.3 写好工具契约¶

图 07-2 模型负责提议,Schema、权限、确认和审计共同决定动作能否执行。 先看它解决什么。 工具契约既是模型说明书,也是执行器的安全边界。名称、用途、参数、必填项、枚举、返回值、错误和副作用必须明确。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 用 JSON Schema 定义参数。
- 让执行器做二次校验。
- 区分读操作和写操作。
- 错误返回稳定机器码。
一个可以手算的例子。 get_weather 的 city 应是字符串,但还要限制长度和允许字符;delete_file 则必须限制根目录并要求确认。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 描述含糊、参数可接受任意 SQL/命令、异常直接返回密钥或堆栈、工具名相似,会诱发错误调用。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:为三个工具写契约并设计十个非法参数测试。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
工具名要稳定,描述要说明“何时用、何时不用”,参数 Schema 要少而明确。一个“万能搜索”工具往往不如拆成内部知识库、公开网页、数据库查询等有清楚边界的工具。返回值使用结构化字段,避免把整页 HTML 塞回上下文。
写操作应分类:只读可自动执行;可逆写操作可要求一次确认;不可逆或高影响操作必须明确目标并人工批准。凭据由运行时保管,绝不放进提示词。工具输出属于不可信输入,要防提示注入。
7.4 规划、反思与停止¶
抓住这一节的主线。 规划把目标拆成可执行子任务,反思检查证据缺口,停止条件防止无穷探索。三者都应由外部预算和验证器约束。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 生成带依赖的任务列表。
- 完成一步后更新状态。
- 检查目标是否已被证据满足。
- 触发成功、失败或预算停止。
先做最小实验。 研究报告可要求每个结论至少一条来源;若缺来源则继续检索,但达到最大查询数后必须明确不确定。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 反思只让同一模型重复说一遍、计划一次生成后从不更新、没有硬停止和成本上限,会产生冗长轨迹。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:为旅行规划定义成功条件、三个失败条件和 token/工具/时间预算。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
复杂目标可先产生短计划,但计划不是承诺;观察改变时要更新。任务可分解为有依赖关系的子目标,用状态机或 DAG 比一段自由文本更可控。
Reflection 适合在失败后总结“证据缺什么、下一步验证什么”,不适合无限自我批评。停止条件包括:目标已满足、关键证据不足、需要用户偏好、工具连续失败、预算耗尽、风险升级。可靠 Agent 知道什么时候停。
7.5 记忆与上下文工程¶
先把概念落到可观察对象上。 记忆是可持久化信息,上下文是本轮实际提供给模型的信息。长期记忆必须经过写入筛选、来源记录、检索和遗忘,而不是保存全部对话。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- 短期状态保存当前任务。
- 长期记忆只写稳定有用信息。
- 按当前问题检索。
- 对过期与敏感信息删除。
把它缩小到能逐项检查。 用户明确偏好素食可存为带来源和时间的偏好;一次临时说‘今天想吃辣’不应自动成为永久身份属性。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 把模型猜测写成用户事实、跨用户混用记忆、没有删除入口、把检索结果当系统指令,都会伤害隐私与正确性。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:设计 memory record schema,包含主体、内容、来源、置信、时间、权限和过期策略。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
短期记忆是当前任务状态和最近观察;长期记忆应是经过筛选的结构化事实或可检索记录。不要把所有对话永久保存并全部塞回模型。建议把记忆分为:用户明确偏好、任务事实、可复用程序经验和原始审计日志。
写入长期记忆前检查来源、置信度、时效、隐私和删除机制。摘要记忆会漂移,重要事实应链接到原始证据。RAG 可做长期检索,但“相似”不等于“仍然正确”。
7.6 Deep Research 的工程结构¶
先建立直觉。 Deep Research 是受控的信息获取流水线:拆解问题、多轮搜索、页面阅读、证据去重、矛盾处理和带引用写作。难点在证据质量,而不是生成篇幅。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 把开放问题拆成可检索子问。
- 记录每个来源与摘录。
- 识别重复和冲突。
- 按结论—证据映射生成报告。
最小例子。 统计数据应优先原始发布机构,新闻可用于发现线索但不替代官方数据;不同时间口径必须在报告中说明。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 只搜索一次、引用搜索摘要而未打开原文、生成不存在的 URL、把发布时间当事件时间,都会造成虚假研究。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:完成一个五来源研究报告,为每条事实保存 claim、source、quote span 和访问日期。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
研究型 Agent 常执行:澄清问题、制定检索子题、搜索多源资料、读取原文、提取证据、交叉验证、生成带引用报告。关键不是搜索次数,而是证据质量与覆盖。
可把每条证据存成 {claim, source_url, excerpt, date, confidence},生成结论时只引用可回溯来源。对时间敏感主题检查发布日期与事件发生日期;对技术主题优先论文和官方文档;对冲突来源明确说明差异。
7.7 MCP:标准化模型与外部能力的连接¶
从问题出发。 MCP 规定 client、host、server 之间怎样发现并调用工具、资源和提示,使能力连接标准化。它解决互操作,不自动解决权限、可信度和业务安全。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- Host 管理会话与用户界面。
- Client 与单个 Server 连接。
- Server 暴露工具或资源。
- 传输层交换协议消息。
用小数据走一遍。 文件系统 MCP server 即使遵循协议,也必须限制允许目录;标准化的调用格式不意味着可访问整块磁盘。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 把 MCP 当 Agent 框架、让同一 server 同时拥有过宽读写权限、忽略 server 返回内容不可信,都会扩大攻击面。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:画出 Host—Client—Server 架构,并为一个只读知识库 server 写最小权限表。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
Model Context Protocol 使用 client-host-server 架构,让服务器暴露工具、资源和提示等能力。MCP 解决“怎样描述和连接能力”,不自动解决权限与可信问题。安装任何 MCP server 前,应审查来源、它能访问的数据、可执行动作、凭据范围和日志策略。
工具调用与 MCP 的关系可以理解为:模型仍生成调用意图,MCP 提供标准化发现与传输,宿主负责授权、用户交互和安全边界。不要把“协议标准化”误解为“所有工具天然安全”。
7.8 为什么 Agent 会失败¶
先看它解决什么。 Agent 失败通常来自目标不清、工具契约错误、状态丢失、上下文污染、权限过宽、重试不当和缺少评估。模型只是系统中的一个组件。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 保存完整 trace。
- 找到首次偏离目标的位置。
- 把失败归因到具体组件。
- 加入最小修复和回归。
一个可以手算的例子。 重复调用同一工具可能是观察字段不清,也可能是停止条件未表达;仅更换更大模型无法证明根因。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 只看最终答案、事后人工补轨迹、失败样本不版本化、把所有问题归因于 hallucination,都会阻碍改进。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:为十条失败轨迹标注第一错误步骤和责任组件,统计最大失败类别。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
- 工具描述含糊,模型选错工具或填错参数。
- 上下文堆积导致关键约束被淹没。
- 计划依赖错误事实,后续步骤不断放大偏差。
- 缺少幂等、重试和状态恢复,工具部分成功后重复执行。
- 没有明确完成标准,循环不停或过早结束。
- 评估只看最终文本,不看实际副作用和轨迹。
改进顺序通常是:缩小任务范围,简化工具,增强 Schema 与验证,加入可观测性,最后才考虑更复杂框架或多 Agent。
7.9 多 Agent 不是默认答案¶
抓住这一节的主线。 多 Agent 适合可独立并行、角色边界清晰、结果可合并验证的任务。否则通信、冲突和重复工作可能大于收益。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 按可交付物拆分角色。
- 定义共享状态与消息契约。
- 限制每个角色工具权限。
- 由确定性规则或审阅者合并。
先做最小实验。 三个 Agent 分别检索不同数据源可以并行;让三个 Agent 都自由规划同一任务通常只产生重复。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 用角色提示代替权限隔离、共享无限对话、没有冲突解决和总预算,都会使系统更脆弱。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:估算一个多 Agent 方案的额外调用数、最长依赖链和失败传播路径,再与单 Agent 比较。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
多个 Agent 可按研究、执行、评审等角色分工,但会增加通信、冲突、重复工作和故障定位难度。只有当子任务能清楚分割、并行收益明显、输出契约可验证时才值得使用。多数业务先用单 Agent + 明确工具 + 工作流路由更可靠。
7.10 评估与安全¶
先把概念落到可观察对象上。 Agent 评估要覆盖任务完成、步骤效率、工具正确性、恢复能力、成本、延迟和安全。最终答案正确也不能掩盖越权或不可重复的过程。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- 建立带初始状态的任务集。
- 记录期望工具与允许动作。
- 模拟超时和错误。
- 审计副作用与权限。
把它缩小到能逐项检查。 预订测试中可使用沙箱日历,检查是否选对时间、是否重复创建、是否在写入前获得确认。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 只用自然语言 judge、测试工具永不失败、没有 adversarial prompt、线上真实写操作直接评测,都会高估可靠性。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:设计 20 个 Agent 测试,至少含注入、工具超时、空结果、重复调用和权限拒绝。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
Agent 评估要同时看任务成功率、步骤数、工具错误、成本、延迟、恢复能力和安全违规。测试集应包含工具超时、空结果、恶意网页、权限不足、重复请求、歧义目标与需要人工确认的场景。
防提示注入采用分层策略:不可信内容与系统指令隔离;工具最小权限;危险动作确认;限制网络与文件范围;输出验证;异常检测;完整审计。没有单一提示能彻底解决注入。
练习:实现天气与计算器两个只读工具;加入每工具超时、总预算和 trace;构造含“忽略之前指令”的恶意工具结果,验证运行时不会扩大权限。
延伸资料:Anthropic《Building Effective Agents》、MCP 架构、Agent 评估实践。
本章配套代码¶
下面的脚本与正文使用相同符号。建议先在代码中打印形状和中间量,再运行断言;若依赖尚未安装,至少先阅读入口函数、输入输出和测试部分。
examples/07_safe_agent_loop.py:安全边界明确的工具循环。
本章端到端实验:把知识变成可复现证据¶
本实验不是把本章代码重新抄一遍,而是把概念、实现、测试和解释串成一个小型工程。请新建独立目录,保存 README.md、环境文件、源代码、测试、运行日志和结果图。README 至少说明任务、输入输出、运行命令、预期现象、已知限制和复现条件。
实验步骤¶
- 把一个业务过程画成 Workflow 版和 Agent 版,比较可测试性与失败面。
- 实现 max_steps=5 的工具循环,用假工具测试成功、未知工具、超时和达到步数上限。
- 为三个工具写契约并设计十个非法参数测试。
- 为旅行规划定义成功条件、三个失败条件和 token/工具/时间预算。
- 设计 memory record schema,包含主体、内容、来源、置信、时间、权限和过期策略。
- 完成一个五来源研究报告,为每条事实保存 claim、source、quote span 和访问日期。
每完成一步,先写下预期,再运行代码。若结果与预期不一致,不要覆盖旧日志;建立 failures.md,记录现象、假设、证据、修复和回归测试。这样得到的不是一次性 Demo,而是一份能证明你真正理解本章内容的实验档案。
验收标准¶
- 全新环境能够按照 README 从头运行,依赖和随机种子已记录。
- 关键函数至少有正常、边界和错误输入三类测试;涉及数值计算时检查有限值与合理误差。
- 结果包含一个基线和至少一个受控改动,能够说明变化来自哪里。
- 日志保留输入规模、耗时、内存或显存、软件版本和失败样本。
- 结论区分“实验直接证明的事实”“根据事实做出的推断”和“仍未验证的猜想”。
本章自测¶
- 不看正文,用自己的话解释“Workflow 由程序预先规定路径,Agent 让模型在运行时选择下一步”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“最小 Agent 循环只有观察、决策、行动和停止”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“工具契约既是模型说明书,也是执行器的安全边界”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“规划把目标拆成可执行子任务,反思检查证据缺口,停止条件防止无穷探索”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“记忆是可持久化信息,上下文是本轮实际提供给模型的信息”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“Deep Research 是受控的信息获取流水线:拆解问题、多轮搜索、页面阅读、证据去重、矛盾处理和带引用写作”,并给出一个可以证伪的测试。
回答时先画图或写形状,再给结论。若只能说出术语而不能给出最小例子、边界条件和验证方法,说明这一节仍需要回到代码中重做。