跳转至

第七章 Agent:让模型在受控边界内观察、决策与行动

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

Agent 运行闭环

图 7-1 模型负责提出下一步,运行时负责执行、校验、记录与终止。

7.1 Workflow 与 Agent

先建立直觉。 Workflow 由程序预先规定路径,Agent 让模型在运行时选择下一步。任务路径稳定、风险高时优先 Workflow;开放探索且可验证时才需要更多自主性。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。

把过程拆开看:

  1. 判断步骤是否可预先枚举。
  2. 判断工具结果是否可验证。
  3. 评估失败成本。
  4. 设置最大自主范围。

最小例子。 发票审批流程适合确定性工作流;跨来源研究问题可能需要动态决定下一次搜索。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。

容易踩坑。 把一次模型调用称为 Agent、把所有流程都交给模型、没有人工接管点,会同时增加成本和风险。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。

动手任务:把一个业务过程画成 Workflow 版和 Agent 版,比较可测试性与失败面。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。

若步骤固定、规则明确,优先使用工作流:分类后走不同分支、并行调用多个模型、由评审器打分等。Agent 适合路径难以预先写死、需要基于中间结果动态决策的任务。自治程度越高,成本和风险越高。

ReAct 可概括为“推理—行动—观察”的交替。工程实现不应依赖模型输出冗长私密思维;只需结构化地返回动作、参数和简短理由,详细 trace 由运行时记录。

7.2 一个最小工具循环

从问题出发。 最小 Agent 循环只有观察、决策、行动和停止。状态必须由程序保存,模型输出只是候选决策;每一步都要有预算与 trace。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。

沿数据流逐步检查:

  1. 把目标和当前状态交给模型。
  2. 解析 action 或 final。
  3. 验证并执行工具。
  4. 把结构化观察写回状态。

用小数据走一遍。 搜索工具返回 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、权限、确认和审计共同决定动作能否执行。 先看它解决什么。 工具契约既是模型说明书,也是执行器的安全边界。名称、用途、参数、必填项、枚举、返回值、错误和副作用必须明确。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。

可以把实现分成以下环节:

  1. 用 JSON Schema 定义参数。
  2. 让执行器做二次校验。
  3. 区分读操作和写操作。
  4. 错误返回稳定机器码。

一个可以手算的例子。 get_weather 的 city 应是字符串,但还要限制长度和允许字符;delete_file 则必须限制根目录并要求确认。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。

排错时先看这些地方。 描述含糊、参数可接受任意 SQL/命令、异常直接返回密钥或堆栈、工具名相似,会诱发错误调用。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。

现在动手:为三个工具写契约并设计十个非法参数测试。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。

工具名要稳定,描述要说明“何时用、何时不用”,参数 Schema 要少而明确。一个“万能搜索”工具往往不如拆成内部知识库、公开网页、数据库查询等有清楚边界的工具。返回值使用结构化字段,避免把整页 HTML 塞回上下文。

写操作应分类:只读可自动执行;可逆写操作可要求一次确认;不可逆或高影响操作必须明确目标并人工批准。凭据由运行时保管,绝不放进提示词。工具输出属于不可信输入,要防提示注入。

7.4 规划、反思与停止

抓住这一节的主线。 规划把目标拆成可执行子任务,反思检查证据缺口,停止条件防止无穷探索。三者都应由外部预算和验证器约束。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。

真正动手时按这个顺序走:

  1. 生成带依赖的任务列表。
  2. 完成一步后更新状态。
  3. 检查目标是否已被证据满足。
  4. 触发成功、失败或预算停止。

先做最小实验。 研究报告可要求每个结论至少一条来源;若缺来源则继续检索,但达到最大查询数后必须明确不确定。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。

别被表面现象带偏。 反思只让同一模型重复说一遍、计划一次生成后从不更新、没有硬停止和成本上限,会产生冗长轨迹。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。

验证任务:为旅行规划定义成功条件、三个失败条件和 token/工具/时间预算。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。

复杂目标可先产生短计划,但计划不是承诺;观察改变时要更新。任务可分解为有依赖关系的子目标,用状态机或 DAG 比一段自由文本更可控。

Reflection 适合在失败后总结“证据缺什么、下一步验证什么”,不适合无限自我批评。停止条件包括:目标已满足、关键证据不足、需要用户偏好、工具连续失败、预算耗尽、风险升级。可靠 Agent 知道什么时候停。

7.5 记忆与上下文工程

先把概念落到可观察对象上。 记忆是可持久化信息,上下文是本轮实际提供给模型的信息。长期记忆必须经过写入筛选、来源记录、检索和遗忘,而不是保存全部对话。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。

把抽象概念还原成操作:

  1. 短期状态保存当前任务。
  2. 长期记忆只写稳定有用信息。
  3. 按当前问题检索。
  4. 对过期与敏感信息删除。

把它缩小到能逐项检查。 用户明确偏好素食可存为带来源和时间的偏好;一次临时说‘今天想吃辣’不应自动成为永久身份属性。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。

需要特别守住的边界。 把模型猜测写成用户事实、跨用户混用记忆、没有删除入口、把检索结果当系统指令,都会伤害隐私与正确性。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。

本节练习:设计 memory record schema,包含主体、内容、来源、置信、时间、权限和过期策略。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。

短期记忆是当前任务状态和最近观察;长期记忆应是经过筛选的结构化事实或可检索记录。不要把所有对话永久保存并全部塞回模型。建议把记忆分为:用户明确偏好、任务事实、可复用程序经验和原始审计日志。

写入长期记忆前检查来源、置信度、时效、隐私和删除机制。摘要记忆会漂移,重要事实应链接到原始证据。RAG 可做长期检索,但“相似”不等于“仍然正确”。

7.6 Deep Research 的工程结构

先建立直觉。 Deep Research 是受控的信息获取流水线:拆解问题、多轮搜索、页面阅读、证据去重、矛盾处理和带引用写作。难点在证据质量,而不是生成篇幅。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。

把过程拆开看:

  1. 把开放问题拆成可检索子问。
  2. 记录每个来源与摘录。
  3. 识别重复和冲突。
  4. 按结论—证据映射生成报告。

最小例子。 统计数据应优先原始发布机构,新闻可用于发现线索但不替代官方数据;不同时间口径必须在报告中说明。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。

容易踩坑。 只搜索一次、引用搜索摘要而未打开原文、生成不存在的 URL、把发布时间当事件时间,都会造成虚假研究。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。

动手任务:完成一个五来源研究报告,为每条事实保存 claim、source、quote span 和访问日期。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。

研究型 Agent 常执行:澄清问题、制定检索子题、搜索多源资料、读取原文、提取证据、交叉验证、生成带引用报告。关键不是搜索次数,而是证据质量与覆盖。

可把每条证据存成 {claim, source_url, excerpt, date, confidence},生成结论时只引用可回溯来源。对时间敏感主题检查发布日期与事件发生日期;对技术主题优先论文和官方文档;对冲突来源明确说明差异。

7.7 MCP:标准化模型与外部能力的连接

从问题出发。 MCP 规定 client、host、server 之间怎样发现并调用工具、资源和提示,使能力连接标准化。它解决互操作,不自动解决权限、可信度和业务安全。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。

沿数据流逐步检查:

  1. Host 管理会话与用户界面。
  2. Client 与单个 Server 连接。
  3. Server 暴露工具或资源。
  4. 传输层交换协议消息。

用小数据走一遍。 文件系统 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 失败通常来自目标不清、工具契约错误、状态丢失、上下文污染、权限过宽、重试不当和缺少评估。模型只是系统中的一个组件。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。

可以把实现分成以下环节:

  1. 保存完整 trace。
  2. 找到首次偏离目标的位置。
  3. 把失败归因到具体组件。
  4. 加入最小修复和回归。

一个可以手算的例子。 重复调用同一工具可能是观察字段不清,也可能是停止条件未表达;仅更换更大模型无法证明根因。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。

排错时先看这些地方。 只看最终答案、事后人工补轨迹、失败样本不版本化、把所有问题归因于 hallucination,都会阻碍改进。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。

现在动手:为十条失败轨迹标注第一错误步骤和责任组件,统计最大失败类别。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。

  • 工具描述含糊,模型选错工具或填错参数。
  • 上下文堆积导致关键约束被淹没。
  • 计划依赖错误事实,后续步骤不断放大偏差。
  • 缺少幂等、重试和状态恢复,工具部分成功后重复执行。
  • 没有明确完成标准,循环不停或过早结束。
  • 评估只看最终文本,不看实际副作用和轨迹。

改进顺序通常是:缩小任务范围,简化工具,增强 Schema 与验证,加入可观测性,最后才考虑更复杂框架或多 Agent。

7.9 多 Agent 不是默认答案

抓住这一节的主线。 多 Agent 适合可独立并行、角色边界清晰、结果可合并验证的任务。否则通信、冲突和重复工作可能大于收益。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。

真正动手时按这个顺序走:

  1. 按可交付物拆分角色。
  2. 定义共享状态与消息契约。
  3. 限制每个角色工具权限。
  4. 由确定性规则或审阅者合并。

先做最小实验。 三个 Agent 分别检索不同数据源可以并行;让三个 Agent 都自由规划同一任务通常只产生重复。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。

别被表面现象带偏。 用角色提示代替权限隔离、共享无限对话、没有冲突解决和总预算,都会使系统更脆弱。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。

验证任务:估算一个多 Agent 方案的额外调用数、最长依赖链和失败传播路径,再与单 Agent 比较。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。

多个 Agent 可按研究、执行、评审等角色分工,但会增加通信、冲突、重复工作和故障定位难度。只有当子任务能清楚分割、并行收益明显、输出契约可验证时才值得使用。多数业务先用单 Agent + 明确工具 + 工作流路由更可靠。

7.10 评估与安全

先把概念落到可观察对象上。 Agent 评估要覆盖任务完成、步骤效率、工具正确性、恢复能力、成本、延迟和安全。最终答案正确也不能掩盖越权或不可重复的过程。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。

把抽象概念还原成操作:

  1. 建立带初始状态的任务集。
  2. 记录期望工具与允许动作。
  3. 模拟超时和错误。
  4. 审计副作用与权限。

把它缩小到能逐项检查。 预订测试中可使用沙箱日历,检查是否选对时间、是否重复创建、是否在写入前获得确认。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。

需要特别守住的边界。 只用自然语言 judge、测试工具永不失败、没有 adversarial prompt、线上真实写操作直接评测,都会高估可靠性。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。

本节练习:设计 20 个 Agent 测试,至少含注入、工具超时、空结果、重复调用和权限拒绝。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。

Agent 评估要同时看任务成功率、步骤数、工具错误、成本、延迟、恢复能力和安全违规。测试集应包含工具超时、空结果、恶意网页、权限不足、重复请求、歧义目标与需要人工确认的场景。

防提示注入采用分层策略:不可信内容与系统指令隔离;工具最小权限;危险动作确认;限制网络与文件范围;输出验证;异常检测;完整审计。没有单一提示能彻底解决注入。

练习:实现天气与计算器两个只读工具;加入每工具超时、总预算和 trace;构造含“忽略之前指令”的恶意工具结果,验证运行时不会扩大权限。

延伸资料:Anthropic《Building Effective Agents》MCP 架构Agent 评估实践

本章配套代码

下面的脚本与正文使用相同符号。建议先在代码中打印形状和中间量,再运行断言;若依赖尚未安装,至少先阅读入口函数、输入输出和测试部分。

本章端到端实验:把知识变成可复现证据

本实验不是把本章代码重新抄一遍,而是把概念、实现、测试和解释串成一个小型工程。请新建独立目录,保存 README.md、环境文件、源代码、测试、运行日志和结果图。README 至少说明任务、输入输出、运行命令、预期现象、已知限制和复现条件。

实验步骤

  1. 把一个业务过程画成 Workflow 版和 Agent 版,比较可测试性与失败面。
  2. 实现 max_steps=5 的工具循环,用假工具测试成功、未知工具、超时和达到步数上限。
  3. 为三个工具写契约并设计十个非法参数测试。
  4. 为旅行规划定义成功条件、三个失败条件和 token/工具/时间预算。
  5. 设计 memory record schema,包含主体、内容、来源、置信、时间、权限和过期策略。
  6. 完成一个五来源研究报告,为每条事实保存 claim、source、quote span 和访问日期。

每完成一步,先写下预期,再运行代码。若结果与预期不一致,不要覆盖旧日志;建立 failures.md,记录现象、假设、证据、修复和回归测试。这样得到的不是一次性 Demo,而是一份能证明你真正理解本章内容的实验档案。

验收标准

  • 全新环境能够按照 README 从头运行,依赖和随机种子已记录。
  • 关键函数至少有正常、边界和错误输入三类测试;涉及数值计算时检查有限值与合理误差。
  • 结果包含一个基线和至少一个受控改动,能够说明变化来自哪里。
  • 日志保留输入规模、耗时、内存或显存、软件版本和失败样本。
  • 结论区分“实验直接证明的事实”“根据事实做出的推断”和“仍未验证的猜想”。

本章自测

  1. 不看正文,用自己的话解释“Workflow 由程序预先规定路径,Agent 让模型在运行时选择下一步”,并给出一个可以证伪的测试。
  2. 不看正文,用自己的话解释“最小 Agent 循环只有观察、决策、行动和停止”,并给出一个可以证伪的测试。
  3. 不看正文,用自己的话解释“工具契约既是模型说明书,也是执行器的安全边界”,并给出一个可以证伪的测试。
  4. 不看正文,用自己的话解释“规划把目标拆成可执行子任务,反思检查证据缺口,停止条件防止无穷探索”,并给出一个可以证伪的测试。
  5. 不看正文,用自己的话解释“记忆是可持久化信息,上下文是本轮实际提供给模型的信息”,并给出一个可以证伪的测试。
  6. 不看正文,用自己的话解释“Deep Research 是受控的信息获取流水线:拆解问题、多轮搜索、页面阅读、证据去重、矛盾处理和带引用写作”,并给出一个可以证伪的测试。

回答时先画图或写形状,再给结论。若只能说出术语而不能给出最小例子、边界条件和验证方法,说明这一节仍需要回到代码中重做。