M Amonstercat

Article

一篇看懂 Agent 的运行机制:Harness、上下文、记忆、工具调用与多智能体

阅读说明:本文面向能读懂基本代码、希望从应用工程层理解 Agent 的读者。内容按一次任务的实际执行顺序展开,技术事实核对日期为 2026 年 11 月 16 日。具体产品界面、模型与协议版本可能继续变化。

配图与源文件:打开十张工程图的完整图集 · 下载可编辑 Draw.io 多页源文件 · 下载文章 Markdown。文章中的配图可以点击打开原尺寸版本。

我们常说“让 Agent 去查资料”“让模型改文件”“上下文快满了”“装一个 MCP”“再派一个子 Agent”。这些话在日常交流里足够方便,真正要写代码、定位故障、控制成本时,却必须回答更具体的问题:是哪一个进程做了什么,数据从哪里来,哪个组件有权限,哪一项结果证明操作已经发生。

本文不把 Agent 当作一个悬浮在云端的抽象大脑,而是沿着可观察的数据和控制流,把模型、运行框架、状态存储与工具执行连接起来。阅读时可以始终保留四个问题:谁提出动作、谁批准和执行、结果保存在哪里、下一次推理实际能看到什么。

为了避免只讲名词,全文使用三组彼此独立的教学案例:前半部分以“排查重复取消订单导致库存重复回补”解释本机编码 Agent;中间以“读取、修改并核验文档”解释调用协议和失败恢复;后半部分以“客服调查重复扣款并受控退款”解释多 Agent、确定性规则和业务验收。案例中的接口、编号和运行结果都是为说明机制构造的,不是对真实交易或某个产品内部抓包的披露。

这几个案例共享一套边界:模型生成判断和调用请求,Harness 维护执行过程,工具与业务系统落实真实动作,证据决定能否宣称完成。理解这些边界之后,Function Calling、MCP、CLI 和 Skill 就可以放回各自的位置,而不必把它们看成互相竞争的工具名称。

读者想解决的问题对应章节最关键的判断
Agent 到底在本机还是云端1—3把推理、执行、状态保存分别定位
上下文是什么、为什么会满4—5区分磁盘历史、当前输入和长期记忆
模型如何把想法变成工具操作6—7请求、校验、执行、结果回传是不同步骤
MCP、CLI、Skill 如何搭配8—10协议、程序入口、工作方法不是同一层
子 Agent 为什么仍然需要等待11—12异步可以重叠独立工作,不能消除依赖
如何把演示做成可靠的工程13—15授权、幂等、可恢复状态和成功契约

1. 从一次请求开始:谁在理解,谁在执行,谁在保存状态

假设一个订单服务已经运行了几年。客服发现,用户连续点击两次取消按钮后,订单只取消了一次,库存却回补了两次。开发者打开本机 Codex,选中代码仓库,输入:“定位重复取消订单导致库存重复回补的问题,修复它,补上能够复现问题的测试,并说明改动风险。”从界面看,这是一条自然语言消息;从工程实现看,它启动了一项跨越界面、任务运行器、远端模型、操作系统和项目文件的工作。

理解这个过程,先要把“说出一个操作”和“让这个操作发生”分开。模型可以生成“读取订单取消函数”的决定,也可以输出一次结构化的工具调用,但它不会因为写出了一个文件名,就自动看见硬盘里的内容;也不会因为生成了 pytest,测试就已经运行。只有任务运行器接收这次调用,检查参数和执行条件,再把请求交给实际工具,外部世界才会发生变化。工具把读取到的源码、退出码或错误信息带回来,模型才有了下一步判断的依据。

本文使用一组固定术语,避免把不同层面的东西统称为“AI”。“模型”指提供推理和生成能力的模型服务及其背后的参数;“Harness”指围绕模型组织任务、上下文、工具、权限和执行状态的运行系统;“Agent 实例”指在特定任务、工具和约束下持续开展工作的一次有状态任务实体;“工具”指被调用的能力接口;“环境”指命令真正运行、文件真正存在的地方;“上下文”指某次推理实际可以使用的输入与关联状态;“长期记忆”指为以后任务保留、筛选和重新检索的信息。它们能够组合,却不能相互替代。

名称在订单案例中对应什么容易混淆的另一件事
模型根据已提供的代码和测试输出判断下一步整个 Codex 应用,或某个一直常驻的私人进程
Harness组织消息、调用模型、分发工具、维护任务状态一段很长的系统提示词
Agent 实例当前这项“修复重复取消”的有状态任务一份模型权重,或一个固定的人格名字
工具文件读取、终端命令、补丁编辑、数据库查询接口模型直接拥有的硬盘和网络权限
环境本机工作目录、远程开发机或隔离容器远端模型运行所在的数据中心
上下文本次推理看到的要求、代码片段、结果和状态历史日志中存在过的全部内容
记忆跨任务保留的项目线索、习惯和经验当前对话全文,或模型参数的即时更新

当开发者按下发送,客户端首先收集任务入口:消息正文、当前项目、相关选区、附件,以及能够适用的指令。随后 Harness 形成一次推理请求。这个请求并不一定包含整个仓库,更合理的做法通常是提供足够的初始线索,让模型决定先查什么。模型可能先要求搜索取消订单入口,再读取服务实现和已有测试。Harness 执行搜索,把命中的文件与必要片段放回任务状态,接着发起下一次推理。

第一次读到代码后,模型可能提出一个假设:库存回补写在状态判断之前,重复请求虽然没再次改变订单状态,却仍然执行了库存更新。这个假设还不是事实。Agent 应当通过调用链、事务边界、并发行为和测试来验证它。如果工具返回的源码表明库存回补其实已经放在条件更新之后,原先假设就应该被放弃。闭环的价值正在这里:后续动作受真实观察约束,而不只是沿着最初猜测继续写下去。

接着,模型可以请求增加一个重复取消测试。编辑工具写入测试文件,终端工具执行测试,结果显示第二次请求再次触发库存增加。这时“问题可以复现”才有外部证据。模型再生成补丁,把状态跃迁与库存回补安排在合适的事务和幂等边界内;工具应用补丁,测试重新运行。若测试失败,Agent 继续读取错误,调整实现或纠正测试。若测试通过,它还需要检查改动是否覆盖并发取消、已发货订单、事务回滚等相关路径。这里并没有要求每个任务机械执行同一套步骤,而是要求每项完成声明都有与之对应的证据。

一个用户请求因此可能对应很多次模型调用,也可能触发数十次工具执行。反过来,一次模型调用也可能产生多个工具调用,由 Harness 根据依赖关系串行或并行处理。“一轮”这个词尤其容易误导:用户的一轮交互、一次模型推理、一次工具往返,是三个不同单位。讨论延迟、成本和故障时,应当明确自己在数什么。用户等了两分钟,并不能直接推出模型连续思考了两分钟;其中可能有等待测试、下载依赖、排队推理和整理上下文的时间。

OpenAI 对公开 Codex Harness 的介绍,确认了这类系统负责会话状态、流式执行、工具交互以及配置边界内的运行。以下链路和伪代码用于解释工程结构,不宣称还原某个桌面版本的全部内部实现。产品界面、协议版本和具体组件可以变化,模型与运行系统之间的职责分离仍然成立。官方 Harness 说明

理解一次请求时,可以沿着四个问题追踪:请求如何被整理成模型输入,模型要求了什么操作,哪个执行器真正完成了操作,什么证据被带回下一次推理。只要四条线能接起来,“Agent 在做什么”就不再只是界面上的动词,而是一段可以观察、排错和约束的执行过程。

2. Agent 在哪里:任务身份、物理部署与数据生命周期

“Agent 在我的电脑里,还是在云端?”这个问题没有一个覆盖所有组件的单一答案。在一种常见的本机编码模式下,界面和 Harness 的一部分运行在电脑上,终端执行器也在电脑上,模型推理则通过网络交给远端服务。任务的物理位置横跨多处。把整个系统都叫作“云端 AI”,会漏掉本机文件和本机权限;把它都叫作“本地 Agent”,又会漏掉被发送到远端的输入和远端推理过程。

图1:Agent 的本机与远端部署边界
图1:Agent 的本机与远端部署边界。打开原尺寸图

图将执行地点和跨边界的数据流分开呈现。本机可以保存仓库、会话记录、配置和记忆文件;远端服务接收为推理选出的输入并返回输出;外部工具服务还可能运行在第三个位置。箭头表示实际跨越边界的信息,例如代码片段、工具参数和查询结果。一个文件最初存在本机,并不意味着它的内容从未进入远端请求;一个工具的名称出现在本机界面,也不意味着它在本机执行。

从软件设计看,Agent 实例更接近一项带身份和状态的任务,而不是一台永远占用某块 GPU 的独立机器。可以把它理解为一组相互关联的东西:任务目标、当前会话、工具集合、约束、工作环境、执行进度,以及恢复工作所需的状态。它们未必集中放在一个对象里,也未必由一个进程全部持有。这里的“任务实例”是一种便于分析的工程抽象,不应反过来要求所有产品都采用相同的数据结构。

同一个模型可以先后为成千上万个 Agent 任务提供推理能力。不同任务使用相同模型,不等于共享了用户的文件、权限或会话;隔离必须由服务和 Harness 的状态管理实现。另一方面,同一个任务也可以在不同步骤调用不同模型:较轻的模型整理检索结果,另一模型分析复杂并发问题。任务身份由状态连续性维持,不由某个模型名称永久绑定。换一个模型继续任务,类似更换了参与判断的计算能力,并不自动意味着任务日志、文件或工具环境已经丢失。

可以用五个位置问题检查一项部署:界面在哪里运行,循环调度在哪里运行,推理在哪里发生,工具在哪里执行,状态在哪里持久化。它们能够自由组合。本机客户端可以连接远程开发机,文件和终端就在远程开发机上;云端 Harness 可以向自托管执行器发出命令,命令仍然发生在企业环境中;工具也可以只是调用业务 API,根本不需要自己的命令行沙箱。因此,“有一个本机窗口”和“所有执行都在本机”没有必然关系。

订单案例中,如果终端工作目录是 /Users/your-name/projects/order-service,补丁通常会落到这份工作副本中。如果任务改为在远程容器里运行,相同的相对路径 src/orders/cancel.py 可能指向容器内完全不同的一份文件。模型只看到了路径字符串,并不能自行判断这两份文件是否同步。可靠的 Harness 需要向任务明确环境身份;可靠的 Agent 也需要用当前分支、文件内容和实际命令结果校验工作位置,而不是看到熟悉的文件名就假定环境没有变化。

“本地存储”“远端处理”“服务端留存”和“用于训练”还应拆成四个问题。本地存储回答电脑上保存了什么;远端处理回答什么数据被送去计算;服务端留存回答数据在请求后是否保留、保留在哪里以及多久;用于训练回答这些数据是否进入未来模型改进流程。这四个维度彼此相关,却不能互相推出。请求在远端处理,不等于一定用于训练;没有用于训练,也不等于没有任何服务端日志或应用状态;本机存在会话文件,更不等于数据只在本机存在。

OpenAI 的 API 数据控制文档明确区分应用状态与滥用监测日志,并分别说明留存控制。阅读这类文档时,必须确认自己使用的是哪种产品、哪种账户与接口,以及适用的组织设置。API 的条件不能直接套用到所有订阅客户端;某项零留存控制也不能仅凭界面上“本地运行”的字样推定已经启用。本文不把任何一套账户政策作为所有部署的默认保证。API 数据控制说明

模型参数又是另一层。普通推理通常是在既有参数上计算,并不是每读取一次仓库,就把仓库永久写进模型权重。会话让系统“记住订单取消函数”的常见机制,是下一次请求继续提供这段信息,或者通过状态引用和检索使信息再次可用。任务结束后再打开一个完全无关的会话,能否找到这条线索,取决于应用的历史、检索或记忆设计,而不能解释为模型已经被这一段代码即时训练。

本地日志的作用也需要准确理解。日志可以支持恢复、审计和问题定位,但磁盘上保存了十万行历史,不意味着每次推理都会读取它们。日志像档案室,当前上下文像摆在桌上的材料,记忆索引像帮助查档案的目录。三个地方能够包含有关同一件事的信息,却有不同的容量、访问方式和可信度。删除其中一个地方的内容,也不应凭直觉推断其他副本同步消失;数据治理要看实际持久化位置和删除机制。

当这些位置和生命周期分清后,排错会更直接:文件没有变化,先检查工具执行环境;模型不知道某条规则,先检查是否进入当前输入;重启后进度丢失,检查任务状态是否持久化;担心外发内容,检查请求整理和工具结果返回边界。不同问题落在不同组件上,不能都归结为“模型忘了”或“Agent 不够聪明”。

3. Harness 做什么:把模型判断接成可以运行的闭环

模型给出了下一步建议,为什么不能直接执行?因为模型输出属于需要解释和约束的请求,操作系统和业务系统需要的是明确的调用。Harness 就处在两者之间:向模型说明可用工具和参数形式,接收输出,校验调用,交给执行器,接收结果,更新任务状态,再决定如何进入下一次推理。OpenAI 的公开 Harness 文章给出了这一职责边界;下面展开的是通用实现思路,而非对闭源客户端逐行推断。Codex Harness 与集成层

图2:模型、工具与 Harness 构成的执行循环
图2:模型、工具与 Harness 构成的执行循环。打开原尺寸图

图中的循环包含“准备输入、模型判断、调用分发、环境执行、结果观察、状态更新”几个阶段。“判断继续或结束”单列了停止条件:任务完成、需要用户补充、达到预算、操作被拒绝、运行失败,都可以结束或暂停循环。结果返回的路径使后续决策能够依据实际观察,停止判断则把资源控制和任务完成契约接入循环。

下面是一段教学伪代码,用来展示控制流。它刻意省略具体 SDK、鉴权和消息协议,不能直接当作生产实现:

state = load_or_create_task(goal, environment)
while not state.finished:
    context = assemble_context(state, instructions, tool_catalog)
    decision = model.generate(context)
    record_model_output(state, decision)

    if decision.requests_tool:
        call = validate_tool_call(decision.tool_call)
        policy = check_execution_policy(call, state)
        result = execute_or_pause(call, policy, environment)
        record_tool_result(state, result)
    else:
        state = assess_completion_or_need_for_input(state, decision)

    persist_checkpoint(state)

这几行看起来简单,实际困难集中在每一个动词里。assemble_context 要控制材料范围、顺序和预算;validate_tool_call 要处理不存在的工具、参数类型错误和缺失字段;check_execution_policy 要把用户授权、沙箱边界和敏感动作要求变成实际判断;record_tool_result 要保留调用与结果的对应关系,也要避免把巨大输出原样塞满上下文;persist_checkpoint 要让进程异常退出后仍然能够恢复到一个可解释的位置。所谓 Harness 工程,主要就在这些连接处。

先看工具调用的可信边界。模型输出一个删除命令,只能说明它提议删除,不能说明该操作已经获得授权。工具是否可见、文件是否可写、网络目标是否可访问、某类写入是否需要人工批准,都应由可执行的规则决定。自然语言指令可以帮助模型提前避开不合适的动作,但如果某条约束必须始终成立,仅仅把它写进提示词还不够。真正的边界要落在执行接口、权限系统或业务服务上,能在模型误判时仍然阻止动作。

再看执行结果的可靠性。终端返回“退出码为零”通常说明这个命令按自己的规则成功退出,不自动说明业务目标已经达成。订单服务的一条旧测试通过,不能证明并发幂等性正确;运行了格式化工具,也不能证明库存不会多加。Harness 可以保留原始结果和元信息,模型则需要把结果映射到目标:这条测试覆盖了哪种触发条件,还有哪条路径没有证据。任务完成应当绑定可观察的交付物和验收条件,避免把“动作已执行”误当作“问题已解决”。

Harness 还要处理等待和失败。一个测试进程可以运行很久,工具服务可以超时,模型服务可以暂时不可用。正确行为未必是重新开始整个任务。对于只读查询,重试通常较容易;对于创建订单、发送通知、执行迁移这类有副作用的动作,重试前必须弄清上一次是否已经生效。幂等键、执行标识和操作状态查询,比简单增加重试次数更有价值。Agent 的自由选择不会消除分布式系统的基本问题,它只是让这些问题出现在更灵活的控制流里。

3.1 Workflow、RAG、Prompt Registry 与 Guardrail 各管哪一层

确定性 Workflow 用预先定义的状态和转移来安排工作。例如客服退款流程可以规定:读取订单,校验退款资格,计算金额,等待审批,提交退款,记录结果。每个步骤允许调用模型,但模型不必拥有整个流程的任意跳转权。当规则已经清楚、审计要求严格、错误代价很高时,把关键顺序写进程序往往更稳。Agent 更适合在某个开放子任务里选择信息和工具,例如调查“这笔退款为什么失败”,因为事先难以穷举所有证据路径。

两者可以组合。Workflow 可以在“调查异常”节点启动一个 Agent,再检查其结构化结论是否满足证据要求;Agent 也可以调用一项已经封装好的确定性流程,让退款资格和金额计算始终走固定逻辑。把一个大业务流程全交给模型选择,并不是实现 Agent 的必要条件。工程上更有用的问题是:哪些地方确实需要动态判断,哪些地方已经有明确规则,如何让两者交接时保留可验证的输入和输出。

RAG,即检索增强生成,解决的是“本次判断需要什么外部知识,以及如何把它取回来”。在订单案例中,它可以检索取消规则、历史事故记录和接口说明,给模型提供相关片段。检索可以使用关键词、向量、结构化过滤或混合方案。它不会仅凭名字就负责执行补丁、管理审批或保证结论正确。取回一条过期文档,可能让模型更有根据地犯错;因此材料的版本、来源、适用范围和时效性必须随内容一起管理。

Prompt Registry 解决的是“使用哪一份提示模板,以及这份模板如何受控地变化”。它可以保存模板版本、变量约定、适用模型、评测结果和发布记录。客服调查任务使用模板第四版,模板新增了“区分推测与已验证事实”的要求,这是一项可追踪的配置变更。Registry 不负责把文件读出来,也不等同于记忆系统。一次任务里模型自行写出的临时计划,不应因为也是文字,就被当作需要长期发布的提示版本。

Guardrail 关注的是允许做什么、输出必须满足什么要求,以及何时阻止或转交。它可以是确定性规则,例如禁止退款金额超过计算结果;可以是结构化校验,例如输出必须包含证据链接;也可以包含用于风险分类的模型。不同防护方式有不同误差,不能把“另一个模型看过了”当作绝对安全保证。尤其在业务动作前,权限和金额上限等确定条件应由业务系统检查,不能依赖模型自行解释后放行。

组件主要回答的问题放进客服调查场景后的例子
Harness如何持续运行、调用能力、维护状态调工具、接结果、恢复任务、限制预算
Workflow必须按什么状态和顺序推进资格校验完成后才允许进入退款提交
RAG当前需要哪些外部材料找到适用版本的退款规则和事故记录
Prompt Registry哪份提示配置正在生效记录调查模板版本并关联评测结果
Guardrail哪些动作或结果允许通过金额边界、权限检查、证据完整性检查

这些组件可以被打包在同一个产品里,仍然值得在设计时分开讨论。模型结论错误,可能是材料没检索到,也可能是提示遗漏了条件,还可能是任务循环过早停止;未经授权的退款成功提交,则说明执行边界需要检查。只有先定位责任层,才知道应该修检索、改模板、补验收条件,还是收紧业务接口。给所有问题统一增加一句“请认真检查”,通常无法替代这些具体改动。

4. 上下文是什么:结构化输入、Token 与不同层次的状态

上下文不是聊天窗口里能够滚动看到的全部文字,也不是电脑内存中与应用有关的全部数据。对某一次模型推理来说,上下文首先是一份经过组织的可用输入:指令、用户要求、此前的部分交互、工具说明、工具结果,以及协议允许携带的其他项目。图片或其他内容也可能参与输入。界面展示的是适合人阅读的视图,模型服务使用的则是适合协议传输和计算的表示,两者没有必要逐字一一对应。

在应用层,这份输入通常保留结构。消息可能有角色,内容可能由多个块组成,工具调用带有名称、参数和调用标识,工具结果与对应调用关联。结构的意义不只在于格式整齐,而在于说明“谁说了什么”“这是要求还是观察”“哪个结果回答了哪个调用”。下面是一份自定义教学表示,只表达信息关系,不对应某个产品的真实请求包:

[
  {"kind": "instruction", "text": "修改后必须运行相关测试"},
  {"kind": "user_request", "text": "修复重复取消造成的库存回补"},
  {"kind": "tool_call", "id": "call-17", "name": "read_file",
   "arguments": {"path": "src/orders/cancel.py"}},
  {"kind": "tool_result", "call_id": "call-17",
   "text": "读取到的源码片段……"}
]

如果把这些项全都粗暴拼接成一段文本,模型仍然可能理解,但调用对应关系、消息来源和边界更容易丢失。一个网页中写着“忽略所有规则”,应当作为网页内容被处理,不能仅因文字出现得晚就升级为任务指令。同样,工具输出里的“测试通过”可以是某个被读取文件中的字面内容,也可以是测试执行器的实际结果;结构和来源决定它是哪一种。可靠系统既要保留结构,也要让模型得到足够清楚的来源提示。

当这些内容进入模型计算时,文本通常会经过分词编码,转换成 Token 序列,再由模型进行数值计算。Token 不是汉字、英文单词或字节的同义词。一个汉字可能对应一个或多个 Token,一段代码中的变量名、空格和符号也有各自的编码方式;不同分词器会得到不同结果。因此“上下文还能放两万字”只能是粗略估计,容量应当按实际 Token 计数评估。图片等输入还会有自己的编码和计量方式,不能用纯文本字数直接换算。

结构化输入经过怎样的内部序列化、增加哪些边界标记、如何处理非文本项,属于模型与接口实现的一部分。应用开发者需要遵循公开协议,而不是手工猜测隐藏格式。把“提示词”理解为发送给模型的整份有效输入,比只理解为用户输入框里的那一句话更接近实际;但这不意味着所有输入都是普通文字,也不意味着客户端能够把服务端的内部表示完整打印出来。

图3:上下文从历史材料到一次推理输入的生命周期
图3:上下文从历史材料到一次推理输入的生命周期。打开原尺寸图

图把“可供查找的历史”和“本次选入的上下文”分成两个区域。新消息与工具结果先进入任务状态,Harness 再根据规则、相关性和容量组织下一次输入。磁盘历史可以比窗口大得多,只有经过重新读取、检索、摘要或状态引用后,相关内容才会影响当前推理。上下文窗口是一张有限的工作台,历史文件是可以回查的外部材料。

4.1 运行时内存、磁盘记录和服务端状态不是一件事

第一层是客户端或 Harness 的运行时状态。例如进程里保存当前消息列表、等待中的工具调用、已启动进程的标识和任务是否被打断。这些通常以对象、缓冲区或队列存在。进程退出时,如果没有写入持久化存储,这部分状态就可能丢失。“应用仍然开着”有助于保留运行状态,却不能替代恢复机制。

第二层是磁盘持久化。会话日志、任务数据库、工作文件、测试输出和检查点都可以保存在这里。它们让系统有机会在重启后恢复工作,也让人能够事后核对某次操作。但磁盘记录可能有截断、脱敏、版本变化或保留期限,不能仅凭文件存在就假定它是完整真相。尤其是源码本身可能已经变化,历史日志里的片段只证明当时读取到什么,不能证明现在文件仍然相同。

第三层是服务端会话状态。公开 API 可以让调用方自己重放历史,也可以通过响应标识或持久会话对象延续状态。这解释了一个看似矛盾的现象:模型推理能力不必拥有私人永久记忆,服务仍然可以提供持续对话。持续性由上下文管理和状态服务建立,不要求每次都由客户端重新上传肉眼可见的全部历史。官方会话状态文档

此外,模型推理期间还会有计算状态,例如为了高效生成而使用的中间表示或缓存。它们属于推理执行机制,不能直接等同于用户可管理的长期记忆。开发者不应把某个技术名词当作“我的所有对话都永久住在这里”的证据,也不应把一次缓存命中当作模型已经学会了某项私人事实。讨论这些状态时,要分别问它们服务哪次计算、由谁管理、能否持久化、是否可被未来任务主动检索。

4.2 窗口容量与累计用量为何可以同时呈现不同数字

上下文窗口约束一次推理能够容纳多少相关 Token。实际可用预算还要考虑输出空间,以及特定模型对推理 Token 等项目的计算规则。累计用量则是多次调用的计量之和;订阅额度还可能采用产品自己的速率窗口和成本折算。它们解决的问题不同:窗口在问“这次能放下多少”,用量在问“到现在一共计算了多少”。上下文窗口与状态管理

做一个纯教学计算:第一次发送一万 Token 的背景,模型输出两千;第二次继续携带这些材料并增加三千新结果,输入规模就可能超过一万五;第三次又在此基础上继续增长。即使任何一次都没有超过所选模型的窗口,多次调用的累计输入也可能很快超过窗口数值。窗口像单次可装载的容量,累计计量像多次装载的总和。历史重复出现会产生重复计算或缓存处理,具体费用还要看接口和缓存规则。

由此也能解释为什么减少工具噪声有直接价值。输出五万行构建日志,不会因为内容真实就自动提高判断质量。更好的做法是把完整日志保存到可回查位置,先返回错误摘要、退出码和关键位置,再按需读取细节。这样既保留证据,又让模型的注意力集中在当前问题上。上下文工程的目标不是把材料删得越少越好,而是在有限窗口里维持足够、准确、可追溯的任务状态。

5. 长任务如何延续:上下文压缩与长期记忆的生命周期

长任务会不断产生材料:搜索结果、源码、计划、失败实验、测试输出、用户修正。若每次都继续携带完整历史,窗口迟早不够,延迟与成本也会增长。压缩要解决的是当前任务如何继续;长期记忆要解决的是以后的任务如何从已有工作获益。两者都涉及信息筛选,却有不同目的、触发条件和保存对象。把它们合并成“模型记住了”,会让很多行为变得难以解释。

5.1 压缩保存的是继续工作的依据

一种容易理解的压缩方式是生成摘要,再用摘要替代较早历史。对订单问题,一份有效交接摘要应说明:当前目标仍是防止重复回补;已经证实重复请求会触发两次库存更新;修改涉及哪个模块;哪些测试已经通过;并发情形是否验证;下一步要检查什么。相反,“已经阅读大量代码,接下来继续优化”几乎没有恢复价值。压缩质量取决于保留了哪些决定和证据,不能只用缩短了多少字衡量。

摘要也会带来风险。若它把“怀疑事务边界有问题”写成“已经确认事务边界有问题”,下一轮就可能建立在错误前提上。若它遗漏用户刚补充的“必须保持接口兼容”,Agent 可能恢复了技术进度,却偏离任务约束。因此好的压缩应区分已知事实、待验证假设、用户要求、已完成操作和下一步计划。路径、分支、测试命令、关键结果与剩余风险,往往比对思考过程的泛泛转述更值得保留。

摘要式压缩是通用实现方式,但不能把所有产品的压缩都描述为“写了一段普通文字摘要”。OpenAI 的公开 Compaction API 还提供包含不透明加密压缩项的上下文表示,用更少 Token 传递继续任务所需的状态。它并非给人阅读的摘要,应用应按协议传递;独立压缩端点的返回窗口还可能保留其他项目,不能只取其中一个字段就当作完整结果。官方 Compaction 文档

两种方式最值得区分的地方,是应用能够看到和能够修改的边界。文本摘要可以人工检查,也可能因人为改写丢失细节;不透明压缩项应作为协议对象处理,不能因为看不懂就随意删改。无论采用哪种方式,压缩都不意味着完整历史永久消失,也不意味着所有历史都继续以原文参与推理。原始日志可以仍在外部存储,活跃上下文则已经换成更紧凑的表示。

图4:当前任务压缩与跨任务记忆的两条信息路径
图4:当前任务压缩与跨任务记忆的两条信息路径。打开原尺寸图

图中一条路径把当前窗口整理后送回同一任务,用来继续执行;另一条路径从符合条件的历史中提取候选事实,再经过筛选、归并和未来检索。两条路径可以读取相同的历史来源,但终点不同。一次压缩发生,并不必然写入长期记忆;长期记忆更新,也不要求先出现窗口溢出。

压缩还必须留有提前量。如果窗口已经装满,再请求模型把整段历史压缩,压缩请求本身也可能放不下。应用通常需要依据模型容量、输出预留和增长速度决定触发时机。具体触发可能来自模型默认、Harness 配置、任务阶段或服务端机制。不能给所有版本规定“到了固定百分比必定压缩”。官方配置参考提供自动压缩的 Token 阈值等控制项;API 的服务端压缩也有自己的阈值机制,它们不应混为同一个开关。Codex 配置参考

在工程实践中,压缩不只是容量告急时的补救。一个调查阶段结束后,明确保留结论与证据、减少已放弃的猜测,也能让后续实现阶段更清晰。但主动压缩有成本,而且可能损失细节;是否值得做,应当结合任务状态判断。对只剩一次简单修改的短任务,额外制造复杂交接没有明显收益;对连续工作很久、分支和假设很多的调查,阶段性状态整理通常更有价值。

5.2 长期记忆先产生候选,再决定未来是否采用

长期记忆不是把每轮消息复制到另一个文件夹。比较实用的一条生命周期是:从历史工作中识别候选事实,判断它是否稳定、是否有证据、是否值得跨任务保留,归并重复与矛盾记录,保存来源和适用范围,然后在未来任务中按相关性选用。这是一种工程设计框架,并不要求产品实现完全相同。它说明了为什么记忆系统既需要写入策略,也需要读取策略,还需要修正与过期策略。

当前官方本地记忆文档描述的是后台处理符合条件的既往聊天,活跃或过短的会话可以被跳过,更新也可能等待会话闲置及满足资源条件。这与“每轮结束立即写一次记忆”不同,也与“上下文压缩后必定生成长期记忆”不同。是否允许某次聊天参与记忆生成、是否允许它使用已有记忆,也可以分别控制。本地 Codex 记忆文档

在订单案例里,“本次修复已经运行某条测试”大多属于任务状态,保留到当前交付和审计就足够;“这个仓库的库存事件采用事务发件箱,取消流程必须通过统一领域服务”可能是值得在未来调查时重新定位的项目线索;“用户偏好先给结论再解释风险”则可能是跨项目的沟通偏好。三者即使来自同一次聊天,合适的保存范围和有效期也不相同。

自动提取尤其需要避免把临时选择固化成永久偏好。开发者说“这次先用 SQLite 做演示”,不意味着所有项目都应该使用 SQLite;某次调试把测试数据库端口改到另一个值,也不意味着这个端口永远正确。记忆条目最好保留条件和来源:这是演示项目的临时选择,还是整个仓库的长期约定;是已经验证的事实,还是当时未完成的猜测。没有条件的信息越短,越容易在未来被错误泛化。

未来任务使用记忆,也不要求把记忆目录中的所有内容全量发送给模型。系统可以先提供精简索引或概要,再按问题检索相关条目和证据,最后只把本次需要的部分加入上下文。不同版本可能采用不同读取策略,但“磁盘有完整记忆库”与“每次推理都看到整个库”不是一回事。若某条记忆被选入远端推理输入,它仍然会经过相应的数据处理边界;本地存放只说明保存位置,不能推出使用时完全不外发。

5.3 用户、项目与任务信息应当有不同作用域

用户作用域适合稳定的个人偏好,例如解释时先说明结论、默认使用某种语言,或对交付格式的长期要求。项目作用域适合团队都需要遵循的规则,例如运行测试的方法、代码目录约定、部署流程和领域边界。任务作用域适合当前目标、临时约束、已验证结果和待办。作用域越宽,写入标准越应该严格,因为一条错误信息影响的未来任务会更多。

需要始终适用的团队规则,最好进入版本受控的项目文档或 AGENTS.md,让人能够评审、更新和追踪。记忆更适合帮助找到线索,不能成为唯一规则来源。例如“退款金额必须由计价服务计算”应写进设计文档和接口校验;记忆可以提醒调查者相关文档在哪里,却不应独自承担金额安全约束。源代码、配置和业务服务仍然是需要核对的当前事实。

冲突处理也要明确。假设旧记忆记载项目使用某种测试框架,新仓库配置已迁移到另一种框架,Agent 应以当前可验证状态为依据,必要时说明旧线索已失效。若用户在任务中明确修正了范围,当前任务应沿修正后的目标继续,而不能被过往摘要拉回旧目标。记忆提供便利,不获得高于当前有效指令和现实证据的权威。

压缩与记忆最终共同服务于连续工作,但连续性并不是“永远不忘任何字”。更可靠的连续性,是在需要时能够知道当前目标,找回关键证据,辨认事实与假设,恢复尚未完成的动作,并在环境变化时重新验证。对 Agent 工程而言,能够解释信息为什么被保留、为什么被选入、何时应被更新,往往比堆积更多历史更重要。

6. Function Calling:把模型的选择变成可执行的请求

6.1 普通函数调用早已有之,模型接口改变的是调用方

后端工程师每天都在调用函数。代码执行到 load_document(document_id),解释器或编译后的程序找到实现,传入参数,执行函数体,再把返回值交给调用位置。跨进程以后,这件事可以变成远程过程调用:调用方序列化参数,服务端找到处理器,结果通过网络返回。无论是否经过网络,传统程序都已经写明了这一次执行走哪条分支,以及什么条件下调用哪个函数。

把语言模型放到这个位置,变化首先发生在决策来源。用户说“先看看这份文档,再把退款规则写清楚”,没有提交函数名、文档标识和补丁格式。模型要结合可用能力说明,把自然语言目标转换成一项或多项结构化调用请求。宿主程序接到请求以后,仍然要用普通代码完成读取、校验、网络访问和写入。模型没有凭空获得解释器内部的函数指针,也不会因为输出了函数名,就自动进入某个业务服务的进程。

因此,理解 Function Calling 可以从一个朴素问题开始:如果模型决定下一步需要外部能力,它用什么机器能够稳定识别的格式,把这个决定交给运行时? 自然语言回答“我现在读取文档”不能承担这个接口,因为它既可能是行动计划,也可能只是陈述。结构化工具调用则把“说了什么”和“请求执行什么”区分开,宿主据此进入另一条处理分支。

这里的“函数”是一种接口抽象,不要求真实实现一定是同名的 Python 函数。它可以对应一个内存中的处理器、一个内部服务请求、一个命令行程序,或者一个调用远程工具服务器的适配器。对模型呈现稳定的业务动作,可以让后端在更换实现时保留上层语义。反过来,把内部所有方法原样暴露给模型,也不意味着它们就形成了合适的工具设计。

一个文档服务可能提供十几个存储层方法,但 Agent 只需要“读取某个版本”和“基于某个版本应用补丁”两个动作。让模型直接选择存储节点、锁类型和缓存分片,会把服务本来能够确定的事情转交给不确定的推理。好的函数工具边界,通常沿着用户可以理解、服务能够约束、结果能够验证的动作切分。

6.2 声明描述可用能力,不包含实现

以 Responses API 的函数工具为例,工具声明包含 type: "function"、name、描述和参数模式;模型输出的调用项使用 type: "function_call",其中带有 name、call_id 和序列化的 arguments;应用提交执行结果时,使用对应的 function_call_output。这些名称属于该接口的具体协议,其他模型接口可能有不同封装。OpenAI Function calling 官方文档

下面是一个原创文档工具的声明片段。它只展示一个工具定义,外层模型请求、鉴权配置和会话管理均未展开。

{
  "type": "function",
  "name": "docs_read",
  "description": "Read the current document and return its version.",
  "parameters": {
    "type": "object",
    "properties": {
      "document_id": { "type": "string" }
    },
    "required": ["document_id"],
    "additionalProperties": false
  },
  "strict": true
}

不要被这里反复出现的 type 迷惑。最外层的 type 是工具类别的判别字段,用来告诉接收方“这是函数工具”。parameters 里面的 type: "object" 则属于参数模式,用来描述业务参数的结构。这两个字段恰好同名,却服务于不同层次:前者帮助协议分派,后者帮助参数校验。调用项中的 type: "function_call" 又是在响应项的联合类型中标识“这是调用请求”。

name 负责选中一种能力,arguments 负责给这一次调用提供具体输入。声明中的 document_id 是参数名称;调用中的某个文档标识才是参数值。模式表达“必须有一个字符串字段”,不能证明这个标识真实存在,也不能证明当前用户拥有读取权限。即使字符串在形式上完全合法,业务服务仍然可能返回文档不存在、权限不足或版本已归档。

JSON Schema 可以约束对象属性、必填字段和额外属性,但模式本身不会执行业务动作。把 additionalProperties 设为 false,表达的是不接受未声明字段,不能据此推出“调用已经安全”。JSON Schema 对象模式说明

对于调用方,这份声明更像一张经过精简的接口说明。模型根据它判断何时适用、如何填参;运行时根据它判断请求是否符合接口形状;真实实现根据自己的业务规则决定是否执行。三者面对同一个接口,却没有因此合并成同一个责任主体。

6.3 请求与结果沿相反方向流动

下面把两类消息并排放置。示例中的标识和业务内容均为教学构造,省略了完整响应的其他项目。

{
  "type": "function_call",
  "name": "docs_read",
  "call_id": "call_read_01",
  "arguments": "{\"document_id\":\"refund-guide\"}"
}
{
  "type": "function_call_output",
  "call_id": "call_read_01",
  "output": "{\"version\":\"v17\",\"text\":\"Refunds arrive in 3 days.\"}"
}

第一项由模型侧产生,沿模型到宿主的方向流动,含义是请求读取。第二项由应用在获得实际执行结果后提交,沿宿主到模型的方向流动,含义是这次读取观察到了什么。call_id 把两者关联起来;它不是文档版本,也不是存储服务的事务编号。不能仅凭二者都是字符串,就在程序中混用。

arguments 在这一接口里是承载 JSON 的字符串,宿主应先解析为对象,再进行结构和业务检查;不要把字符串直接拼进命令或表达式。output 中放一个业务 JSON 字符串,是本例选择的结果表示方式。这里出现的 version、text 属于文档服务的契约,不是所有函数工具必须返回的公共字段。

图5:函数工具调用时序
图5:函数工具调用时序。打开原尺寸图

图中的关键边界在“收到调用”和“开始执行”之间。模型生成请求后,运行时先决定能否执行、由谁执行,再取得结果。结果回到模型以后,模型才可以根据观察继续安排下一步。箭头跨过的是责任边界;即使这些步骤全部发生在同一台电脑上,也不能把它们缩写成“模型直接修改文件”。

还有一个容易误判的现象:模型收到读取结果以后,不一定立即生成最终回答。它可能发现目标文档不是最新版本,继续读取索引;也可能发现问题已经解决,直接说明无需修改。Function Calling 只提供动作请求的通道,不规定整个任务一定调用几次,也不替代任务终止条件。真正完整的循环,需要把调用结果重新纳入上下文,并让宿主在每一轮继续执行同样的边界检查。

7. tool_registry:运行时的分派关系,不能从日志里长出来

7.1 日志记录发生过什么,注册表决定现在能调用什么

假设一份会话记录中出现过 docs_read,旁边还有参数和结果。它能证明当时系统记录了这样一次事件,却不能直接证明当前进程仍然注册了这个能力。服务可能已经卸载,权限可能已经收回,工具名称也可能在升级时改变。把历史 JSONL 文件中所有出现过的函数名扫描出来,当作当前可调用列表,会把时间记录误当成执行配置。

tool_registry 在这里是一个通用的工程名称,指运行时维护的工具分派关系,不是所有框架都必须公开一个同名对象。最简单的实现是一张内存映射:名称对应处理器。实际系统可能把它拆到多个组件:声明存在配置中心,执行器注册在工作进程,路由规则由网关管理,权限在策略服务判断。数据结构不同,目的相同:收到一个已识别的工具调用时,找到受控的实现路径。

下面是说明责任顺序的伪代码,不代表任何现成产品的内部实现。

async def dispatch(call, actor):
    entry = registry.lookup(call.name)
    args = parse_and_validate(call.arguments, entry.schema)
    await entry.authorize(actor, args)
    return await entry.handler(args, actor=actor)

真正的运行时还要处理取消、超时、审计和结果大小限制,但这段已经足以指出一个事实:函数名称只是一次查找的键。它可以与源代码函数名相同,也可以通过别名指向版本化的适配器。没有注册关系,模型输出一个看起来合理的新名称,也只能得到未知工具错误,不能让运行时自动发明实现。

工具声明与注册表通常应当从一致的定义生成。否则会出现模型看得到却无法分派,或者服务已经有实现却从未向模型展示的情况。前一种是可见性和可执行性漂移,后一种可能是有意隐藏,也可能是配置遗漏。排查时应分别检查本轮发给模型的声明、当时的注册状态、最终选择的执行目标,不能只看某个源码目录里有没有同名文件。

7.2 六个问题对应六道不同检查

一个调用可能通过前五项检查,仍在第六项失败。把这些问题分开,既能解释现象,也能让系统给出更有用的恢复建议。

责任层需要回答的问题文档修改例子
工具声明模型本轮知道哪些能力是否展示了读取和应用补丁工具
名称路由请求应交给哪个实现docs_apply_patch 是否绑定到文档适配器
格式校验参数结构是否满足接口补丁是否为数组、版本是否为字符串
业务授权该主体能否对该对象做该动作当前用户能否编辑这份退款指南
操作系统或环境权限执行进程能否访问实际资源沙箱能否写入指定工作目录
实际执行目标系统是否完成操作文档服务是否提交了新版本

格式校验通过,只能说明输入符合形状。业务授权通过,说明身份和动作在既定规则下匹配,却不能保证本地目录已经挂载。操作系统允许写文件,也不代表业务上允许修改所有者不同的文档。以管理员权限运行一个脚本,不能代替应用层的租户隔离;把工具描述写成“仅限内部文档”,也不能代替服务端校验。

这里尤其要区分“用户授权了任务”与“系统拥有执行能力”。用户要求修改自己的项目,能够构成该任务的行动范围;但操作系统仍可能拒绝当前进程访问目录。反过来,进程能够读到某个目录,也不能推出用户希望把其中所有内容交给远程服务。执行能力是可做的集合,任务授权是本次应做的集合,工具调用应落在二者的交集之内。

工程上可以把部分检查放在同一组件,但不要让组合实现抹掉责任含义。一个文档服务入口同时检查参数、租户和版本,是合理的实现;因此把所有失败都标成“参数错误”,则会让调用方失去恢复信息。模型可能不断重写参数,实际上需要的是刷新身份、重新读取版本,或者停止越界操作。

7.3 错误应该告诉下一步,超时必须保留未知状态

读取不存在的文档与远程网络暂时中断,都可能表现为“这次没有拿到正文”,恢复动作却不同。前者应该确认标识或搜索目录,后者可以在预算内重试。权限拒绝需要改变授权条件,不能靠换一种措辞再次请求同一操作。参数结构错误往往可以根据模式修正;目标文档版本冲突则需要重新读取业务状态。

因此,工具结果最好保留稳定的错误类别、可读原因以及是否允许重试。不要仅返回一大段调用栈,让模型从堆栈中猜下一步;也不要吞掉失败,伪造一个空正文,使后续推理把“读取失败”当成“文档确实为空”。错误本身就是任务观察的一部分,应当以明确形式进入后续决策。

对于读取操作,一次超时通常还容易恢复。对于写入,超时最危险的地方是它经常表示调用方不知道结果,而不是服务端一定没有执行。请求可能已经提交,新版本也已经生成,只是回包丢失。如果运行时把所有超时都解释为未执行,并立刻发起相同写入,可能重复追加段落,甚至重复创建记录。

解决这个问题要借助业务幂等设计。修改请求可以带一个稳定的操作标识,服务端保存该标识对应的结果;相同操作重复到达时,返回第一次执行的结论。这个标识应在重试时保留,并且绑定目标、参数摘要和主体,避免相同标识被意外用于另一项修改。模型调用的 call_id 可以帮助跟踪某一次消息交换,却不天然承担跨请求、跨进程重启的业务幂等承诺。

重试策略同样需要区别处理。网络闪断可以使用有上限的退避,版本冲突需要重新规划,格式错误需要先修正参数,确定性的权限拒绝应该停止同一路径。把“最多重试三次”应用到所有错误,只是限制了次数,没有修复语义。更合理的预算包括总时长、外部调用次数以及允许产生的副作用次数,并在预算耗尽时报告已经确认的状态与仍未知的部分。

记录审计事件时,值得保留的是工具名称、经过必要脱敏的参数摘要、主体、目标、执行结果以及关联标识。历史日志可以帮助复盘哪道检查拦住了请求,也可以用于评估工具选择质量。但它仍然是执行的证据和观察材料,不是下一次启动时无条件恢复权限、重放写入的命令清单。

8. MCP:统一工具接入的外壳,不统一业务世界

8.1 它解决的是重复集成,不是替模型思考

假设有三种 Agent 宿主,都需要访问公司文档、代码仓库和工单系统。如果每个宿主分别适配每种业务系统,就会形成多组工具发现方式、参数描述方式和调用封装。每新增一个宿主,常常需要再做一遍集成;每个业务系统也要面对不同客户端的私有约定。MCP 试图把这条接入边界标准化,让提供方通过一种协议描述能力,让支持该协议的宿主理解如何发现和调用它们。

MCP 最初由 Anthropic 于二〇二四年十一月公开,发布时定位为连接 AI 助手与外部数据系统的开放标准。这个来源说明了它的历史起点,不能据此把它理解为某一家模型专属的函数格式。Anthropic 最初的 MCP 公告

从后端角度看,MCP 很像给工具集成定义了一条公共边界,但不能因此把“公共边界”误解为“所有业务接口统一”。统一查找工具的方法,不会让各家公司都拥有同样的文档模型;统一调用消息的外壳,也不会让审批、退款和发版共享一套参数。工具真正做什么,仍然由服务提供者设计,宿主仍然需要根据任务决定要不要把它呈现给模型。

这也解释了为什么接上 MCP 以后,Agent 不会自动变成可靠的业务专家。它获得的是一条可发现、可描述、可调用的能力通道。如何理解工单优先级,如何判断某次修改是否符合规范,如何比较修改前后的行为,仍然需要任务上下文、领域知识、执行约束和验证机制。传输协议能够减少集成差异,不能消除任务本身的歧义。

8.2 Host、Client、Server 分别站在哪一侧

MCP 架构使用 Host、Client、Server 三个角色:Host 是管理交互和连接的 AI 应用,Client 是宿主内部面向某个服务器的协议组件,Server 则提供工具或其他上下文能力。官方架构还区分基于 JSON-RPC 的数据层与传输层,常用传输包括本地进程间的标准输入输出和 Streamable HTTP。MCP 架构概览

可以用一个文档工作台理解这些角色。工作台负责展示用户任务、选择模型、维护会话和呈现结果,它承担 Host 的职责;工作台中负责向文档服务器发消息、处理响应的组件承担 Client 的职责;理解文档读取和补丁操作的服务承担 Server 的职责。它们是逻辑角色,不必对应三台机器,也不必对应三个互不相识的商业产品。

如果工作台同时连接文档服务和代码仓库服务,模型并没有因此获得两个“网络插口”。宿主分别管理接入关系,再把选择后的能力说明提供给模型。不同连接可以使用不同身份,不同工具也可以有不同可见范围。宿主应该把“来自哪个服务器的能力”保留在路由关系中,避免两个服务都叫 search 时发生错误分派。

服务端同样可以很薄。一个 MCP 文档服务器接到读取工具请求后,可以调用已有的文档 SDK;也可以访问内部 HTTP 服务,或者通过公司已有的远程调用框架获取内容。某些服务选择把数据存在数据库里,处理器可能执行参数化 SQL;这只是可选的后端实现方式,不是 MCP 的定义,更不是每个 MCP 请求都会生成 SQL。标准化边界位于客户端与工具服务器之间,业务实现可以沿用原有技术栈。

图6:MCP 接入桥接
图6:MCP 接入桥接。打开原尺寸图

图中分开呈现两条边界:模型与宿主之间的动作请求,宿主客户端与工具服务器之间的协议交换。后半段采用 MCP,不要求前半段也使用同一种消息。宿主可以把模型产生的函数调用转换成 MCP 请求,也可以使用模型平台已经提供的托管 MCP 接入。无论适配器放在哪里,都要能解释能力从何处发现、请求由谁发送、结果又如何进入模型上下文。

OpenAI 的 Responses API 提供 mcp 工具接入方式,可以由平台连接远程 MCP 服务器并处理工具发现与调用。因此,“使用 MCP”并不总是意味着应用作者必须手写每一个 JSON-RPC 请求,具体分工取决于所选接口。OpenAI MCP servers 官方文档

8.3 tools/list 一致,inputSchema 可以完全不同

对于工具,MCP 定义了 tools/list 和 tools/call 等方法。服务器在工具列表中提供名称、描述与输入模式;调用请求把具体工具名称与业务参数放入约定的外层结构。这使通用客户端知道如何发现和发送请求,而不必事先为每个业务工具写一种通信协议。MCP 工具规范

以下片段是原创教学示例,只保留发现与调用相关的关键字段。这些 MCP 请求片段省略了 _meta,不能作为完整报文直接发送。 当前核对的二〇二六年七月二十八日规范要求每个请求带协议版本与客户端能力元数据,客户端身份信息则建议提供;新实现应以对应版本规范及 SDK 为准,不能把旧教程里的初始化顺序当成所有版本的唯一流程。MCP 请求元数据规范

{
  "jsonrpc": "2.0",
  "id": 21,
  "method": "tools/list",
  "params": {}
}
{
  "jsonrpc": "2.0",
  "id": 21,
  "result": {
    "resultType": "complete",
    "tools": [{
      "name": "docs_read",
      "description": "Read a document with its version.",
      "inputSchema": {
        "type": "object",
        "properties": {"document_id": {"type": "string"}},
        "required": ["document_id"],
        "additionalProperties": false
      }
    }]
  }
}
{
  "jsonrpc": "2.0",
  "id": 22,
  "method": "tools/call",
  "params": {
    "name": "docs_read",
    "arguments": {"document_id": "refund-guide"}
  }
}

注意这里的 arguments 是 JSON 对象,前面 Responses 函数调用示例中的 arguments 是 JSON 字符串。适配器要完成的是有类型的转换,不能只把两段文本原样粘贴。MCP 请求的 id 对应这一层的请求响应,模型调用的 call_id 对应模型与宿主之间的调用结果关联;一个桥接层可以记录两者的映射,但它们没有必须相同的要求。

另一个 MCP 服务器完全可以提供名为 ticket_update 的工具,要求 ticket_id、priority 和 reason。它仍然采用 tools/call,却不会接受文档工具的 document_id。再换一家文档服务,可能用数值编号,可能用空间与页面的组合标识,也可能把补丁定义为位置操作。MCP 统一的是如何公开模式以及如何承载调用,业务参数的含义与兼容性仍需要单独设计。

这与“大家都使用 HTTP”很相似:客户端知道怎样建立请求,不等于可以把任意服务的请求体发给另一家服务。工具名称相同也不是兼容证明。集成时既要检查协议版本和消息外壳,也要检查业务参数、返回结构、分页语义以及副作用约定。只验证服务器能够响应工具列表,最多证明发现链路连通,不能证明一次真实任务已经完成。

9. CLI、MCP、Skill、Plugin:分清入口、协议、方法与包装

9.1 同一种能力,可以拥有不同入口

设想公司已经有一个文档命令行工具,可以读取正文、应用补丁和查询操作状态。为了让 Agent 使用它,有几条合理路径。宿主可以提供执行命令的工具,让模型在受控环境中运行该程序;也可以包装成几个窄接口,避免让模型处理复杂的命令拼接;还可以运行一个 MCP 服务器,由服务器调用同一套文档 SDK。三条路径的底层业务能力可以相同,面向宿主的入口却不同。

CLI 主要把能力呈现为程序、子命令、选项、标准输入输出和退出状态。MCP 把工具发现与调用呈现为约定的协议消息。二者可以并存:MCP 服务器内部调用 CLI,CLI 内部访问同一个 HTTP 服务,或者两者各自调用共享库。不能因为两个入口最终访问同一份文档,就认为协议没有区别;也不能因为一个使用终端、另一个使用结构化消息,就认定它们在业务上必然拥有不同能力。

例如,宿主收到 docs_read 的函数调用后,可以选择直接调用 Python 处理器;处理器可以运行 docctl read refund-guide --json;也可以把它转换成 MCP 的 tools/call。Function Calling 回答的是模型怎样提交动作请求,CLI 和 MCP 回答的是执行路径在某条边界上怎样暴露能力。把层次区分开以后,“到底该选函数调用还是 MCP”常常就不再是二选一的问题。

具体选择应当依据运行环境。已有成熟命令行程序、本地身份配置和稳定的结构化输出时,包装 CLI 可能很直接;多个宿主希望共享工具发现和调用时,提供 MCP 边界可能更合适;进程内只需要少量确定能力时,一个普通函数适配器就足够。选择哪条入口,不会取消业务授权、输入校验或结果验证这些责任。

命令行也不天然意味着“只有文本、没有结构”。它可以输出 JSON,可以读取标准输入中的补丁,也可以通过明确的退出状态表达失败。MCP 也不意味着“任何返回都是完美结构化业务对象”,工具仍然可能返回文本内容。真正要检查的是契约质量:输出是否稳定,错误能否分类,是否区分成功与部分完成,以及能否把真实状态交给下一步处理。

9.2 Skill 提供可复用的方法与资源

官方插件概念把 Skill 描述为包含指令和资源的目录:SKILL.md 说明名称、适用条件与执行方法,目录还可以包含参考资料、脚本和模板。它可以指导模型组合 MCP 工具,也可以只使用宿主已经具有的能力,独立于 MCP 工作。OpenAI 插件 Skills 概念

把这个定义放进文档修改任务,Skill 可以规定:先读取当前版本,列出准备修改的句子,使用最小补丁,写入后重新读取,并检查关键业务条件是否保留。这里真正可复用的是完成任务的方法、判断标准和辅助资源。Skill 没有因此承担连接文档服务、验证远程身份或传递网络报文的责任。

一个目录中可以附带确定性脚本,例如检查 Markdown 链接或比较两个版本。执行这些脚本时,依然要经过宿主已有的执行工具,受到相应权限与环境约束。脚本跟 Skill 一起分发,不等于模型只要读到文件就拥有运行权限;说明书中提到某个外部服务,也不会自动建立连接。这一点与后端项目里的操作手册相似:手册能够说明正确步骤,但实际动作仍由具备能力的程序完成。

Skill 的价值也不只是把一段提示词换个文件名。它可以把触发条件、流程顺序、失败处理和输出要求放到一起,并链接只在需要时读取的领域资料。写得好的 Skill 会减少任务每次从零规划的成本;写得含糊的 Skill 也可能放大误用,例如只写“尽可能自动完成”,却没有说明什么叫修改完成、什么情况需要停下,以及验证必须观察什么。

不过,方法约定仍然不等于硬性访问控制。即使 Skill 要求“只修改当前空间的文档”,服务端也必须验证目标属于允许范围;即使 Skill 要求“先读后写”,写入接口仍然应该检查预期版本。如果这些条件只存在于可被模型解释的文字里,程序就把关键不变量寄托在了每一次推理都恰好正确之上。可靠系统会让方法指导规划,让代码与服务约束执行。

9.3 Plugin 可以把方法与连接一起交付

Plugin 是产品中的可安装包装,可以包含 Skill、MCP 服务器相关配置,或者把两者组合起来;某些插件还包含界面和其他扩展资源。具体清单与部署方式属于产品实现,不能把“装了插件”直接等同于“所有服务器都已在本机运行”。OpenAI Plugin architecture

一个文档审校插件可以提供两部分:Skill 告诉模型如何完成审校,MCP 服务器提供读取和受控写入。用户安装以后,看见的是一个整体功能,但架构上仍然有方法层与能力层。服务器升级身份验证方式,不一定需要改动审校方法;审校方法增加术语一致性检查,也不一定需要改变传输协议。包装让它们一起交付,边界让它们能够分别演进。

概念主要回答的问题在文档修改任务中的位置
Function Calling模型如何提出结构化动作请求选择读取工具并给出文档标识
运行时注册与分派请求由哪个受控实现执行把读取请求交给文档适配器
CLI程序如何通过命令行提供能力运行文档读取子命令
MCP宿主与工具服务器如何发现和调用能力发现读取工具并发送协议请求
Skill这类任务应按什么方法完成先读版本、构造补丁、写后验证
Plugin如何把相关扩展作为一个整体交付安装审校方法与服务器连接配置
图7:CLI、MCP、Skill 与插件的关系
图7:CLI、MCP、Skill 与插件的关系。打开原尺寸图

图中按职责连接这些概念:模型通过动作请求进入运行时,运行时可以接不同入口;Skill 在规划与执行过程中提供方法和资源;Plugin 把其中一部分组成一个可安装整体。它们处在不同维度,可以组合,也可以独立存在,没有共同遵循的技术替代时间线。

因此,“MCP 已经过时,Skill 会替代 MCP”缺少可以比较的共同层面。一个方法包能够教模型如何使用工具,却不能单独替代远程服务的发现、消息交换和身份边界;一条连接协议能够把工具接进来,却不会自动提供完整的领域工作法。真正需要比较的是相同目标下的工程代价,例如某个现有能力用 CLI 包装还是用 MCP 暴露,以及某段流程应保留为模型指导还是固化为确定性程序。

10. 完整案例:读取、提出修改、应用补丁,再验证结果

10.1 先约定要完成什么

下面给出一条从用户请求到验证完成的教学链路。工具名称、文档、版本、请求标识和返回内容均为原创构造,用来展示真实接口之间应当如何衔接,不代表任何当前 Codex 会话的抓包或内部实现记录。模型侧消息采用前面介绍的 Responses 函数调用形状,服务器侧采用 MCP 工具消息形状。为了便于阅读,省略完整模型请求及 MCP 请求必需的 _meta;实际程序需要由适配器或 SDK 补齐对应版本的字段。

用户提出的任务是:“读取退款指南,把到账时间改为退款审核通过后三个工作日,并补充节假日顺延;先说明准备怎么改,然后应用修改,最后确认文档已经更新。”这里包含一个明确范围:只修改到账时间说明,保留退款条件和其他规则。它也包含一个完成标准:最终要观察到保存后的文档,不能止于生成一段建议文本。

假设系统为这项任务准备了三个业务工具。读取工具返回正文与版本;应用补丁工具要求预期版本和操作标识;操作查询工具用于确认结果不明的写入。它们是文档服务自己设计的业务接口,MCP 不规定所有服务器都必须实现这些名字。

工具核心输入本例约定的结果
docs_read文档标识正文、当前版本
docs_apply_patch文档标识、预期版本、补丁、操作标识已应用的新版本或明确失败
docs_get_operation操作标识已提交、未执行、执行中或未知

宿主在这轮任务中可以只向模型展示这些必要能力,同时在执行侧把身份绑定到当前用户。租户和身份不让模型自行填写,避免出现模型猜测某个账号字段就扩大访问范围的设计。文档标识可以由用户选择或前序搜索确定;本例假设已经确定为 refund-guide,不再让模型从标题猜测存储路径。

10.2 第一次读取:两个调用标识对应两层消息

模型先提出读取请求,宿主按工具注册关系把它交给文档 MCP 客户端。

{
  "type": "function_call",
  "name": "docs_read",
  "call_id": "call_r1",
  "arguments": "{\"document_id\":\"refund-guide\"}"
}

客户端发出的 MCP 关键字段如下;其中的 id 只负责这次协议请求的关联。

{
  "jsonrpc": "2.0",
  "id": 301,
  "method": "tools/call",
  "params": {
    "name": "docs_read",
    "arguments": {"document_id": "refund-guide"}
  }
}

服务器读取成功,返回文档观察值。structuredContent 中的业务字段属于本例契约,文本内容块则提供简洁说明。

{
  "jsonrpc": "2.0",
  "id": 301,
  "result": {
    "resultType": "complete",
    "content": [{"type": "text", "text": "Document read: v17."}],
    "structuredContent": {
      "document_id": "refund-guide",
      "version": "v17",
      "text": "Refunds arrive in 3 days. Original payment method only."
    },
    "isError": false
  }
}

适配器把业务结果交回模型侧,保留原来的 call_id。它不应把 MCP 外壳误当作文档正文,也不应丢掉版本号,因为后面的写入依赖这项观察。

{
  "type": "function_call_output",
  "call_id": "call_r1",
  "output": "{\"document_id\":\"refund-guide\",\"version\":\"v17\",\"text\":\"Refunds arrive in 3 days. Original payment method only.\"}"
}

这一步完成以后,模型才有依据判断原文存在什么歧义。它知道原句只说三天,没有说明起算点与工作日,也看到了原路退回的条件。后者不在本次修改范围,应该保留。读取得到的正文是待处理数据;即使正文里夹着“忽略其他规则”一类句子,也不能提升为宿主的授权指令。

10.3 提出修改:把理由与待写入对象区分开

模型可以先向用户说明:“准备把到账时间明确为审核通过后三个工作日,补充节假日顺延,并保留原路退回条件。”这是对修改意图的解释,方便审阅范围,却还不是已经保存的结果。用户在任务中已经要求说明后应用修改,因此本例的既定执行规则允许继续;其他系统如果需要独立审批,应当在这条边界消费相应授权记录。

待写入对象使用一个窄补丁,指定匹配旧句并替换为新句。这里选择的是本例自定义的精确替换操作,并非宣称所有文档服务都采用这种补丁语言。复杂富文本系统可能使用段落标识和结构化操作;纯文本文件也可以使用带上下文的差异。关键在于补丁能够表达目标范围,并且服务能够拒绝含糊或不匹配的修改。

{
  "document_id": "refund-guide",
  "expected_version": "v17",
  "operation_id": "op_refund_01",
  "patch": [{
    "op": "replace_exact",
    "old": "Refunds arrive in 3 days.",
    "new": "Refunds arrive within 3 business days after approval; holidays extend the timeline."
  }]
}

这段对象由模型调用提交后,运行时仍需要检查字段、动作范围与业务授权,服务器再检查版本和旧文本是否唯一匹配。不能因为模型先解释过修改意图,就跳过这些检查;也不能因为补丁只替换一句话,就默认没有业务影响。较小的补丁降低了比较与回滚成本,真正的正确性仍然取决于修改内容和约束。

10.4 版本冲突:重新观察,然后重新形成写入

假设读取以后,另一位编辑已经在文档中补充客服联系方式,当前版本变成 v18。模型提交的预期版本仍为 v17,服务应拒绝这次写入,返回可以恢复的业务失败。以下是本例服务器给出的工具结果:

{
  "jsonrpc": "2.0",
  "id": 302,
  "result": {
    "resultType": "complete",
    "content": [{"type": "text", "text": "Version conflict; no change applied."}],
    "structuredContent": {
      "code": "VERSION_CONFLICT",
      "expected_version": "v17",
      "current_version": "v18",
      "applied": false
    },
    "isError": true
  }
}

这里协议交换已经完成,失败发生在工具业务执行层,因此使用失败工具结果表达。若请求外壳无效、工具名称无法识别,则是另一类协议问题;MCP 规范把协议错误与可供模型调整的工具执行错误区分开。MCP 错误处理说明

适配器应把失败状态和“未应用”事实交还模型。模型接着重新读取 v18,确认新增联系方式与到账时间修改无关,再基于新版本生成相同目标的补丁。它不能只把 expected_version 改成服务器提示的新值而不读取正文,因为那样相当于跳过冲突检查,盲目声明自己已理解别人的修改。

如果另一位编辑恰好已经修正了到账时间,重新读取后可能根本不需要写入;如果新文案与用户要求冲突,就需要解释差异或缩小补丁。这说明版本冲突不是普通网络异常,恢复需要新的业务观察。只有在确认新内容之后,模型才能判断原计划是否仍适用。

对重新生成的修改,本例使用新的操作标识 op_refund_02,因为它对应新的预期版本与参数;而同一请求在网络层重试时,应继续使用原操作标识。业务上的重新规划与传输上的重复发送要分开,才能同时保证冲突处理和幂等行为正确。

10.5 写入超时:先确认操作状态,再决定是否重试

接下来服务器完成了基于 v18 的补丁,生成 v19,但响应在网络途中丢失。宿主只观察到超时。此时不能向模型回报“修改失败,未保存”,也不能直接重复追加文本。它应把状态表述为结果尚未确认,并调用 docs_get_operation 查询 op_refund_02。

本例操作查询返回已经提交、新版本为 v19,因此无需再次执行写入。若返回仍在处理,可以在预算内继续查询;若明确没有执行,可以在相同业务输入与相同操作标识下重试;若服务无法判断,就应保留未知状态并进行额外核对。不能把“查不到记录”无条件等同于“从未执行”,因为这取决于服务承诺的记录持久性和一致性。

这条恢复路径完全可以由宿主的确定性代码承担,不需要把每一次网络抖动都交给模型重新规划。模型擅长的是结合新观察调整任务方案;运行时擅长的是根据明确错误类别执行有限重试、查询幂等状态和维持关联关系。职责划分合理时,模型只需要看见对下一步决策有用的业务事实。

10.6 验证:从“调用成功”走到“目标成立”

确认写入提交后,宿主继续读取文档,观察当前正文与版本。验证至少检查三件事:新句包含审核通过这一时间起点;时限明确为三个工作日并说明节假日顺延;原路退回条件和其他编辑新增内容仍然存在。这些条件来自本次任务目标与范围约束,不能只检查返回消息里有没有出现“成功”。

对于这个小例子,可以用确定性的句子匹配与差异检查完成验证。真实文档如果存在复杂排版,还需要检查结构是否损坏;如果修改的是代码或配置,则应该验证相关行为,而非只查文本已经替换。验证动作的选择应当跟随副作用类型,不需要为所有任务套用同一张冗长清单。

假设重新读取确认当前版本为 v19,最终回答应当说明已完成哪些变化、保存后的版本,以及验证到了什么。可以写成:“已更新退款指南:到账时间明确为审核通过后三个工作日,节假日顺延;已重新读取保存结果,确认原路退回条件和客服联系方式保留。”这样的陈述把用户目标、实际动作和观察证据连了起来。

如果验证读取失败,则应区分“服务确认已提交”与“尚未完成读取验证”;如果读取发现目标句仍是旧文本,则必须继续排查目标标识、写入状态或后续覆盖,不能依赖先前的成功回包结束任务。系统最后向用户提交的应是当前能够证明的结论,而不是模型最初打算执行的计划。

回看这条链路,Function Calling 承载模型提出的动作,运行时决定执行路径并守住边界,MCP 连接工具服务器,业务服务负责版本与幂等,Skill 可以规定读写和验证的方法,Plugin 则可以把方法与连接组合交付。每一部分都参与任务,却没有哪一部分可以单独替其他部分保证结果。把这些责任连接起来,Agent 才从“会说下一步”走到“能执行、能恢复、能证明任务完成”。

11. 子 Agent:分出去的是任务状态与执行循环

当一个主 Agent 提出“让另一个 Agent 去核对支付记录”时,最容易产生的误会,是想象系统临时造出了一个具有新知识的大脑。更准确的工程图景是:运行时创建了一份可独立推进的任务,给它任务目标、初始上下文、可用工具和资源限制,然后为它启动自己的执行循环。子 Agent 可以使用与主 Agent 相同的模型,也可以按配置使用不同模型;角色名称并不会生成新的模型权重,分派任务也不意味着系统一定分配一台新机器。模型请求可以落到同一服务,任务循环可以运行在同一进程、不同进程或托管环境,具体取决于实现。

OpenAI 的子 Agent 文档说明了模型设置继承、分派与等待、权限边界等产品行为。这里提炼出一个便于设计系统的理解:子 Agent 的独立性首先体现在任务状态与上下文的独立推进,而不是物理资源的独占。不能从“新建了一个 Agent”直接推导出新的知识、更高权限或更强能力。

11.1 一次分派真正创建了什么

假设主 Agent 正在处理一个账单投诉。它已经掌握客户身份、投诉目标和回复要求,但还需要核对交易事实。一个有用的子任务可以写成:“只读检查订单对应的支付与退款流水,判断是否存在两次成功扣款;返回交易编号、状态、证据时间和无法确认之处,不执行退款。”这段指令不仅指定专业角色,还给出了输入边界、输出契约和禁止跨越的业务动作。

运行时至少需要为此保存四类东西。第一类是任务标识及父子关系,让结果能够送回正确的请求。第二类是子任务的消息历史,包括初始材料、模型提出的调用和已经收到的工具结果。第三类是执行状态,例如排队、运行、等待工具、等待审批、完成或失败。第四类是能力和预算,例如只读工具集合、最长运行时间、最多模型轮次和累计费用上限。缺少其中任何一类,演示仍可能跑通,但一旦并发、重启或失败,系统就很难解释发生了什么。

这里的“独立上下文”也不等于只能从空白开始。有的实现传入一段任务说明,有的复制父任务的历史快照,有的筛选出几条相关消息,还可以附上可访问的文档引用。这些策略解决的是“子任务开始时看到什么”。它们没有自动解决“父任务后来知道了什么”。如果主 Agent 在分派以后获得了客户撤销申请的新消息,已经启动的子 Agent 不会凭空同步这条变化;运行时必须明确发送更新、取消任务,或者在采纳结果前重新核对前提。

一个高质量的分派通常比一句“你是账单专家”更具体。它应该让接收者知道,哪一个订单属于本次任务,哪个账户边界已经过服务端确认,哪些数据只是客户陈述,什么情况下应返回证据不足。否则,主 Agent 只是把自己的含糊复制给了另一个执行循环,协调成本增加了,问题却没有被分解。

11.2 模型提出分派,Harness 负责创建与约束

在工具驱动的设计中,主模型可能输出一个创建子任务的调用请求。真正创建任务记录、分配执行槽位、装配子上下文并启动循环的是 Harness。工具可见性、并发上限和权限校验都发生在这个执行边界。模型即使输出了结构正确的参数,也不代表它有权增加并发额度、打开未授权数据或替子任务授予退款权限。

下面的交互名称只是教学示意,不是各个平台统一采用的 API:

主模型 → start_subtask({job: "核对重复扣款", order_ref: "O-demo"})
Harness → {task_id: "task-billing-1", state: "queued"}
主模型 → 继续整理客户已经提供的信息
Harness → 子任务运行,内部可能调用只读账单工具
事件通道 → {task_id: "task-billing-1", state: "completed", result_ref: "r-1"}
主模型 → 读取结果,检查证据,再完成回复

这里的子任务启动请求与账单查询请求在结构上类似:它们都是模型提出、运行时处理的动作。区别在于,子任务内部还会发生新的模型请求与工具调用。正因为形成了嵌套执行,主任务的 trace 应当能够追踪到子任务,限制也要沿执行链落实。把子任务叫作“内部专家”不能使它绕过外层控制。

《Codex as a platform》把 Harness 描述为管理 Agent 循环的外围执行系统,并区分运行时能力与应用所拥有的业务上下文、工具及控制。由此可以理解一个设计责任:即便采用现成 Harness,业务方仍需为“谁能查哪个账户、什么退款需要谁批准”提供可执行的规则。

图8:主子Agent的分派与汇合
图8:主子Agent的分派与汇合。打开原尺寸图

11.3 同步返回与异步返回,改变的是控制关系

子 Agent 作为一个工具同步调用时,主循环通常在这一调用处等待,直到子任务给出结果或失败。子任务内部仍然可以有复杂循环,但对主调用者而言,它像一个耗时较长的函数。这个模式容易理解,也容易保证主 Agent 用到结果后再往下走,适合边界清楚且执行时间可控的任务。

另一种模式先返回任务标识,允许主任务继续做独立工作。结果通过事件、回调、消息队列或显式查询送回。此时,运行时要回答更多问题:任务是否真正接受,结果在哪里保存,怎样等待而不反复轮询,主任务已经结束后结果交给谁,重复事件是否会重复触发后续动作。异步提高了调度自由度,也把原本藏在函数调用栈里的控制状态变成了必须保存的数据。

等待机制因此不是装饰。主 Agent 可以向一个任务发送补充说明,可以等待指定任务集合,也可以等待第一个完成者;这些都需要明确语义。消息送达不一定代表子模型已经读到,发出取消请求也不一定代表外部工具已经停止。可靠实现会区分“请求已接收”“执行已停止”和“已产生的副作用已核实”。对于不需要立即汇合的任务,主 Agent 应有能力把自己标记为等待依赖,而不是不停生成“正在等待”的文字消耗资源。

最后还要区分两种看起来相似的协作。OpenAI Agents SDK 的编排说明区分了由管理者调用专业 Agent 的工具模式,以及将后续对话控制权交给专业 Agent 的 handoff。前者由主 Agent 汇总最终答复;后者切换当前负责该分支的 Agent。二者都可能使用多个 Agent,但“谁最终对用户负责”不同,不能把 handoff 一概理解为后台多开一个工作者。

12. 异步与并行:先画依赖,再决定开几个 Agent

“异步执行”描述调用者不必一直占据原来的等待位置,“并行执行”描述多项工作在时间上重叠。两者相关,却不是同一个保证。一个系统可以接受异步任务后把它们放进单工作者队列;客户端立即拿到了任务编号,实际工作仍然逐个执行。另一个系统可以同时查询多个服务,但必须等全部结果才能形成下一步判断。用户看到的完成时间,取决于队列、资源限制和依赖链,而不取决于代码中出现了多少个 async。

12.1 关键路径决定并行的上限

以一个虚构的调查任务为例:核对支付流水需要八秒,读取当前政策需要五秒,检查已存在的工单需要三秒,结合证据做判断需要两秒。假设前三项确实独立,且没有排队与协调开销,顺序执行约需十八秒,并行后约需十秒,因为判断必须等待最慢的支付查询。这里八秒的查询加两秒的判断构成关键路径;加开第四个 Agent 并不会把八秒查询变成两秒。

这个计算也暴露了一个常见误拆。若“政策查询”必须先知道支付工具返回的产品地区和结算类型,它就不再是完全独立节点。可以提前取一份通用说明,但正式判断仍需等待关键字段,再定位适用条款。把尚未确定的字段交给另一个 Agent 猜测,表面上重叠了执行,实际上可能引入重查与返工。正确的优化通常是先用普通后端请求一次取齐结构化事实,再把确实需要语义理解的部分交给模型。

为每个节点写出输入条件与产物,比先画“策划 Agent、专家 Agent、审核 Agent”的组织图更有帮助。一个节点的产物如果只是固定字段转换,用函数就够了;如果需要理解不规则材料、提出调查假设,才可能值得配置一个模型循环。并行化的是可独立完成的工作,不是职位名称。

12.2 Fork、消息与文件共享是三件事

Fork 可以理解为从某一时刻分出新的任务分支。分支拿到父任务的上下文快照后,通常形成自己的后续历史。Join 则是主任务在某个汇合点接收并检查各分支产物。即使两个分支最初看到了同一段对话,它们后来查询过什么、得到了什么,也不必自动出现在彼此的上下文中。若需要交换结论,必须经过消息、共享数据服务或主任务转发。

图9:上下文隔离与共享资源
图9:上下文隔离与共享资源。打开原尺寸图

上下文隔离尤其不能被误读为文件系统隔离。两个 Agent 可以各自持有消息历史,却编辑同一个目录。一个分支刚读取文件,另一个分支就可能将它改写;第一个分支的推理仍基于旧内容,但保存时覆盖了新版本。相反,两个 Agent 也可能看到相同文件内容,却处于完全独立的容器中,彼此的修改永远不会出现。讨论“共享”时必须指出具体对象:消息历史、业务数据库、目录、进程变量、缓存,还是长期记忆存储。

对共享写入,一个简单的教学策略是分配独占产物路径:证据分支只写自己的调查记录,回复分支只写草稿,最后由单个合并者读取并生成正式结果。若必须共同更新记录,则应采用数据库版本条件、文件锁或明确的合并流程。让每个 Agent 都在指令里“注意不要冲突”不能取代并发控制;它们可能在不知道对方当前动作的情况下,同时做出看似合理的更新。

消息也有自己的版本问题。子任务回传“没有退款”,这句话如果缺少查询时间与流水版本,主任务无法判断它是否仍然有效。更有用的返回是“截至某时刻,支付记录版本为十七,目标扣款没有成功退款,查询来源为退款服务”。主任务在执行写动作前再查询最新状态,就能发现调查后发生的变化。这样,结果既能帮助推理,也能作为有范围的证据,而不是被误当作永远有效的事实。

12.3 并行的账要把协调、失败与取消一起算进去

多 Agent 的实际成本包括子任务各自的模型输入与输出、工具请求、主任务的分派文本、结果汇总,以及失败后的补查。重复给三个 Agent 传入同一份长材料,即使墙上时间缩短,总 token 也可能明显增加。若每个子任务只做几十毫秒的格式检查,调度开销甚至会超过工作本身。适合并行的常见情况,是产物边界清楚、主要进行只读探索、彼此不依赖、耗时足以覆盖协调成本的任务。

也要为“部分成功”定义规则。三个证据源中两个成功,并不自动表示可以根据多数票退款;失败的那个来源可能恰好掌握退款状态。汇合点应区分必要依赖与可选补充:缺少必要的支付状态就停在证据不足,缺少可选的历史工单摘要则可以带着限制继续。不要把所有异常吞成一句“专家无意见”,否则主任务会把缺失证据解释成不存在风险。

取消同样需要业务语义。客户撤回投诉后,已经排队的只读调查可以取消;正在执行的退款则必须先确认外部结果,再决定是否需要后续处理。结束父任务不应留下无人认领的子任务继续写数据。对教学实现,可以要求父任务关闭时传播取消标记,子任务在每次模型请求和工具调用前检查标记;对已经提交的外部动作,则记录为待核实,而不是假定取消成功。

一个有用的并行试验,应同时测量完成时间、总调用量、有效证据率与返工率。先给同一组任务建立单 Agent 基线,再比较多 Agent 的结果。只有在成功标准相同的前提下,延迟收益才有意义。增加一个没有看过业务约束的“审核 Agent”,不一定降低错误率;它可能只是更快地为同一个错误背书。

13. 把概念放回业务:一个重复扣款客服 Agent

下面是一套虚构的客服业务与教学方案,所有订单、金额、阈值和政策都用于说明结构,不代表任何公司的真实流程。客户表示,购买一份一百九十八元的年度服务后,银行卡上出现了两笔相同扣款,要求退回多收的钱。客服系统已经通过自己的登录流程识别客户,前端传给 Agent 的是服务端绑定的工单与订单引用。Agent 不接收密码,也不凭客户在聊天里写出的账户编号取得查询权限。

这类任务的难点不在于生成一句礼貌的退款答复,而在于把客户陈述、支付事实、政策依据、处理权限和外部结果放到同一条可核验的链路中。两笔相同金额可能是两次成功入账,也可能是一笔成功扣款加一笔暂时占款,还可能对应两个不同订单。若在没有区分这些状态前就调用退款,语言上显得果断,业务上却可能制造第二个问题。

图10:重复扣款客服 Agent 的业务流程
图10:重复扣款客服 Agent 的业务流程。打开原尺寸图

13.1 让每个组件承担可以检查的责任

这套系统的 Harness 负责推进状态、调度工具、保存中间结果,并在条件未满足时停止副作用。它从会话存储装配模型上下文,但账户身份来自已经认证的服务端会话。模型负责理解投诉、判断还缺哪些证据、比较工具结果并组织解释。它可以提出调查或退款建议,具体动作仍由工具服务验证。

RAG 在这里承担知识检索:从经批准的政策库中取回重复扣款、占款释放、退款说明等条款,并返回文档标识、生效时间与适用范围。RAG 没有证明这位客户确实被扣了两次钱,也没有凭检索命中授予退款权限。支付事实必须查询交易系统,政策适用性还要与地区、产品和交易时间对应。检索到旧政策时,应标识版本并查找适用版本,不能把更容易搜到的页面当作当前规则。

Prompt Registry 管理的是模型指令模板及其版本,例如客服调查模板、证据摘要模板和答复模板。一次运行固定使用哪个模板版本,应与模型设置、工具 schema 版本一起记录。Registry 不是政策数据库;提示词里写着“退款上限五百元”,也不等于这个数字已经得到业务授权。若政策阈值调整,真正阻止不合规退款的应当是服务端规则。模板可以提醒模型如何解释规则,但不能成为唯一的执行依据。

确定性的政策组件接收结构化事实,返回允许、拒绝、需要补证或需要人审。为这套教学方案,可以规定:只有同一订单存在两笔已结算成功的捕获记录,且第二笔尚无成功退款,才可能进入重复扣款处理;跨账户、币种不一致、支付状态冲突等情况都需要进一步处理。这里哪些规则适合自动化,取决于业务真实风险与数据质量。关键是规则的输入、版本、判定结果能够单独重放,而不是只能在模型的一段解释里寻找依据。

还要注意检查的执行位置。Agents SDK 的 guardrails 文档说明,Agent 级输入检查针对链条入口,输出检查针对最终输出;要围绕每次受保护的函数工具调用验证,应使用对应的工具检查。因此,入口处的一次“请求合法”判断不能直接替代后续每次退款的执行许可。

13.2 调查与执行分开,身份与建议分开

主 Agent 首先调用只读工具读取订单、支付与退款记录。如果已有明确订单引用,身份校验通过后可以直接查询;如果订单不明确,就先向客户收集定位订单所需的最少信息。不能为了让流程自动往前走,从模糊时间描述中猜一个订单,然后在猜测上执行退款。

支付工具返回的结构化记录显示:同一订单确有两笔一百九十八元的成功结算,支付编号不同,第二笔没有退款记录。与此同时,政策检索分支找到适用于该产品和交易日期的重复扣款处理条款。此时可以让一个只读子 Agent整理证据,但并不必然需要它;如果交易字段清楚,普通函数即可完成一致性校验。子 Agent 更适合处理多份客服记录中的矛盾陈述,或者把较长政策压缩成带引用的待审说明。

模型随后提出退回第二笔扣款的建议。规则引擎验证订单归属、原始捕获金额、累计退款、适用政策和本次申请金额。这套虚构业务要求所有重复扣款退款由有权限的客服审核。因而运行时创建待审记录,供审核员看到具体支付编号、金额、证据引用与规则结果;在审核决定到来前,退款工具不执行。OpenAI 的Guardrails 与人工审核说明也明确区分自动校验与暂停等待审批,这两种控制应按各自的执行位置设计。

审核页上的“批准”要绑定一项具体动作。服务端验证审核员身份及权限,再将决定绑定到这次退款参数的指纹和待审记录版本。如果客户撤回申请、退款金额发生变化或支付已经被另一位客服处理,旧批准不能直接用于新动作。模型在 JSON 中写入 approved: true,只是生成了一个字段;真正的批准必须来自受信任的审批通道。

如果人工拒绝,Agent 应解释当前进度与后续渠道,并根据拒绝原因决定是否补充证据,不能换个子 Agent 再试一次以绕开限制。如果外部工具因账户边界拒绝查询,也不能通过更宽泛的搜索工具寻找同一客户数据。模型有灵活选择调查路径的能力,授权范围仍需在每一条路径上成立。

13.3 退款成功取决于支付结果,不取决于模型说得多确定

审批通过后,退款适配器在实际提交前重新读取支付状态,确认调查期间没有发生退款。它使用业务服务生成的稳定幂等键提交本次逻辑动作,例如把“工单、目标捕获记录、重复扣款退款类型”映射成受控键。金额、币种和目标支付必须固定;同一逻辑动作的重试复用同一个键,不能每次模型重新提出请求就生成一个全新的退款意图。

幂等键也不是贴上就生效的保险标签。接收退款请求的系统需要原子地识别重复键,并约束相同键对应的参数。不同工单也可能指向同一捕获记录,支付服务仍须按捕获记录原子校验累计退款和剩余可退额,工单幂等键不能替代这道约束。如果外部系统不支持这些保证,适配器就要提供可核对的提交记录与对账机制,不能承诺恰好执行一次。OpenAI Agents SDK 的人工介入文档特别提醒:审批允许执行,并不保证外部动作只发生一次;崩溃恢复仍需要幂等与副作用核实。

假设支付网关接受退款,却在返回响应时连接中断。此刻 Agent 不能直接宣布退款失败,也不能立即创建第二笔退款。它应把动作记为结果待核实,按原幂等键或退款请求编号查询。查询确认已受理后,才能给出“已提交退款,预计到账进度以支付渠道为准”这一类与真实状态一致的答复。只有支付系统确认完成,才能称为完成;“工具请求已发出”“受理成功”和“到账完成”是三个不同状态。

客户回复也应由证据约束。可以解释查到了哪一笔重复扣款、已经采取什么动作、当前状态及下一步,避免暴露无关账户信息。若调查发现的是占款,则说明当前证据支持占款,而不是为了满足客户预期改写为“已退款”。一次正确的客服处理,既可能以退款完成结束,也可能以有依据的解释或转人工结束。

13.4 先定义成功契约,再评价 Agent

评估前应写清这项业务的成功契约。本例可以要求:识别正确客户与订单,区分结算、占款及已有退款;每一项事实判断能回溯到工具记录;需要审批时未提前提交;本次逻辑退款最多产生一项实际副作用;最终答复准确表达处理状态。证据不足时转人工也可能满足契约,擅自退款则不能用较高的客户满意度抵消。

确定性 grader 先检查可直接判定的部分:目标支付是否属于该订单,金额是否超出可退余额,退款调用前是否存在有效审批,幂等键是否在重试中保持一致,最终文本是否把“待核实”说成“已完成”。这些检查最好消费事件和业务记录,而不是让另一个模型读完整聊天后凭感觉判断“似乎合规”。其中若有硬性违规,应直接判失败,不能与礼貌程度求平均。

LLM Judge 适合处理难以完全编码的维度,例如解释是否容易理解、是否明确表达证据限制、有没有把客户的情绪误解为新的业务授权。不同维度分别给分,并要求 Judge 指出支持评分的文本片段。业务事实与安全门槛仍由确定性检查承担。Judge 本身也要评估:让客服人员标注一批常见和边界案例,比较人机分歧,修订评分说明,检查是否偏爱更长、更自信但不准确的回答。

离线数据集应包含未扣款、占款、重复结算、已退款、跨币种、审批拒绝、工具超时及同一工单重复恢复等情况。OpenAI 的 Agent 评估指南建议利用 trace 定位流程问题,再以数据集和评估运行比较变更。应用到本例,可以先影子运行,只生成建议并与人工结果对比;达到门槛后做小流量灰度,监测错误退款、重复提交、转人工质量和处理时长。线上真实业务分布可能与离线集合不同,因此离线高分只是进入验证的条件,不能代替后续观测。

14. 最小 Harness:把一次调用写成可恢复的状态机

理解执行原理,最有效的方法是实现一个很小的运行时。这里讨论的是教学设计,使用虚构工具和本地测试数据,不是可以直接处理真实退款的完整生产系统。最小实现的目标,是让开发者看见模型请求、调用分派、等待、恢复与业务副作用之间的关系,而不是在几十行代码里宣称已经解决认证、并发、隔离和支付一致性。

14.1 先保存状态,再考虑让循环更聪明

可以从一张运行记录表和一张事件表开始。运行记录保存会话标识、运行标识、当前状态、模板版本、工具版本、截止时间与预算;事件表追加用户输入、模型输出、工具意图、审批决定、工具结果和状态转换。原始事件便于追溯,当前记录便于高效调度。业务订单和退款事实仍由各自系统保存,不应把一段聊天记录当成账务数据库。

一个可读的状态流是:READY → MODEL_RUNNING → TOOL_PENDING → TOOL_RUNNING → READY。遇到需要批准的动作进入 AWAITING_APPROVAL,遇到异步依赖进入 AWAITING_DEPENDENCY;最终进入 COMPLETED、FAILED 或 CANCELLED。这些名称只是本例约定。关键在于每个状态有明确的进入条件、可接收事件与离开条件,不能让同一个“处理中”同时表示等模型、等人工和外部结果未知。

状态转移要检查版本。例如,审核员批准了版本十二的动作,但任务已经因客户撤回而进入版本十三,旧审批回调应被拒绝或重新核实。多个工作者同时收到同一条队列消息时,只有取得有效租约、且成功完成条件更新的工作者可以推进。队列提供了事件送达,并不自动保证消费者只执行一次。

会话存储解决“下次如何取回历史”。OpenAI Agents SDK 的Sessions 文档提供了自动维护对话历史与自定义存储的接口。工程上还应区分两份数据:用于装配下一次模型输入的历史,以及恢复运行所需的执行状态。保存了所有聊天文字,也可能仍然缺少尚未执行的工具意图或待审决定;保存了暂停快照,也不一定足以生成一份面向用户的完整历史。

14.2 一个循环中,哪些步骤必须由代码决定

下面是框架无关的伪代码。store、policy、dispatcher 都是待实现的接口,不对应某家平台的完整 SDK;示例省略了具体网络协议和存储实现。

async def advance(run_id):
    run = await store.acquire_versioned_lease(run_id)
    enforce_deadline_and_budget(run)

    # 持久化的暂停点由外部事件恢复,不占着循环空转。
    if run.is_waiting:
        return

    # 恢复已有待办,避免重新让模型生成同一批动作。
    if await store.has_pending_intents(run):
        await drain_pending_intents(run)
        return

    context = await assemble_context(run, prompt_registry)
    reply = await model.generate(context, allowed_tool_schemas(run))
    await store.record_reply_and_intents(run, reply)

    if reply.is_final:
        await validate_and_finish(run, reply)
        return

    await drain_pending_intents(run)


async def drain_pending_intents(run):
    for intent in await store.pending_intents(run):
        enforce_deadline_and_budget(run)
        spec, args = tool_registry.validate(intent.name, intent.arguments)
        verdict = await policy.check_current(run.identity, intent, spec, args)
        await store.record_policy_decision(run, intent, verdict)
        if verdict.requires_review:
            await store.pause_for_review(run, intent)
            return
        if not verdict.allowed:
            await store.reject_and_stop(run, intent, verdict)
            return
        result = await dispatcher.execute_or_reconcile(intent, spec)
        await store.complete_intent_with_result(run, intent.call_id, result)

    await store.make_ready_and_enqueue(run)

这个循环刻意把重要判断留在代码里。工具注册表负责把名字映射到真实实现,校验 schema 和参数,拒绝未知工具;它与管理模板的 Prompt Registry 是两个不同组件。政策检查使用服务端身份,不能采用模型声称的角色。执行器还要落实超时、网络边界和必要的幂等机制。返回给模型的工具结果必须关联原调用标识,让模型知道哪一个结果回应哪一个请求。

存储接口也有实质契约:模型回复与由它产生的待执行意图应作为一次原子记录保存,完成意图与关联结果也应一起提交。否则,进程可能停在“保存了回复,但没创建待办”的缝隙里。外部写入无法靠这一次本地事务包住,仍需由执行器核实。租约释放、异常处理与取消检查同样必须在实际实现中补齐,不能把伪代码里省略的部分当成已具备的保证。

示例按顺序处理工具调用,便于观察状态,不能据此推导出必须顺序执行。在教学实现能够正确暂停、恢复和核实结果之后,才适合按只读性与依赖关系增加并行。若一批调用中出现人审暂停,也要保存尚未执行的调用,恢复时继续处理原待办;不能只重新询问一次模型,再假定它会生成完全相同的参数。

14.3 标识、恢复和预算决定它能否持续运行

至少要区分三类标识。trace_id 关联一次端到端处理,call_id 关联某次模型工具请求与相应结果,业务动作标识关联同一项退款意图。重试模型请求可能产生新的 call_id,但业务动作仍然是同一次退款。把调用编号直接当成退款幂等键,会在模型重新规划时失去去重效果。子任务还应保留父运行标识,使一次投诉的分派和汇合能够从同一条链路查看。

Agents SDK 的 tracing 文档说明了 trace 与 span 如何记录模型生成、工具、handoff 等事件。在本例中,最值得观测的除了耗时,还有“做了什么决定、依据哪个版本、是否真的提交、结果是否确定”。日志应只记录必要字段,并处理敏感内容;为了排查一次退款,没有必要在每个 span 中复制完整客户资料。

恢复时最重要的问题是:上一次到底停在了哪里?若停在模型响应前,可以按既定重试策略重发;若停在只读查询中,通常可以重新查询;若停在外部写入后、结果保存前,就必须先对账。教学执行器中的 execute_or_reconcile 正是这个位置的占位,它不能只是把所有异常都重试。对一个结果未知的退款调用,正确的下一步可能是查询原请求状态,或交给人工核实。

资源预算也属于执行状态。需要同时约束模型轮次、输入规模、工具次数、子任务数量、累计消耗与墙上时间。子任务预算应纳入父任务总额,避免每个子任务都认为自己还剩完整额度。等待人工的时间与模型计算时间可以分开计量,但不能无限保留过期动作。预算耗尽后的结果应说明已完成什么、缺少什么、下一步需要什么,而不是为了“成功结束”让模型编造一个结论。

15. 故障排查:从最后一个可验证事件往前找

Agent 故障常被描述为“模型不够聪明”,但同一段最终文字可能掩盖完全不同的失效层。回复说“正在退款”,可能是模型只写了自然语言,可能是调用被政策阻止,可能是工具已经执行但结果丢失,也可能只是前端没有收到完成事件。排查时应先定位最后一个已经确认的事件,再检查它与下一个预期事件之间的断点。

15.1 “卡住”要先拆成等待对象

首先看运行状态和最近一次更新时间。停在模型请求中,就查服务响应、流是否结束、重试与超时;停在工具运行中,就查工具调用编号、外部请求记录及执行租约;停在审批中,就查是否真正创建了待审项、是否已送到有权限的审核员;停在依赖中,就查对应子任务是否完成,以及完成事件是否被消费。某个 Agent 长时间没有输出,并不直接证明它在思考,也不直接证明它死锁。

一个特别隐蔽的错误,是任务已经等待人工,但工作者继续反复调用模型,希望模型“想出下一步”。模型可能产生重复申请,也可能把等待误当失败,再改用另一条路径。只要状态本身要求外部事件,正确行为就是保存并让出执行资源。是否向用户展示进度属于产品交互,不能通过不断生成内容来代替调度。

还要区分工作者存活与任务进展。心跳正常只能说明进程还在发送信号;若同一个查询被重试了三十次,业务状态仍毫无推进。可以为每类任务定义有意义的进展,例如新增有效证据、解决一个依赖、产生审批决定或确认外部结果。超过合理时间没有这些变化,就触发诊断,而不是仅延长超时。

15.2 模型“说要调用”,工具为何没有执行

按一条具体调用检查五个边界:模型是否输出了真正的调用项,工具名是否在当前注册表里,参数是否通过校验,政策是否允许,执行器是否收到意图。聊天中出现一段漂亮的 JSON,并不证明 API 返回了工具调用;调用项存在,也不证明注册表能找到实现。把两个现象混为一谈,会导致开发者不断修改提示词,却始终没有修复分派代码。

如果校验失败,记录应指出是哪个字段、什么约束、当前工具版本。日期格式错误可以把结构化错误交回模型修正;缺少授权则应进入相应业务路径,不能用“请再想一想”反复试探。若执行器收到意图却没有外部请求,查看队列消费和租约;若外部已有成功记录而本地没有结果,进入核实流程。每一层都保留同一个调用关联,定位才能从猜测变成证据对比。

调用结果也可能没有被正确送回。模型提出了两个查询,运行时却把两份结果关联到同一个调用编号,模型就可能重复另一项查询。类似地,工具返回的是分页结果,执行器却没有标记还有下一页,模型可能把部分记录当作全集。此时要修复工具结果契约:给出范围、分页、时间与错误状态,而不只是调整模型“认真阅读”的指令。

15.3 状态过期与重复写入,通常是同一个时间问题的两面

假设调查完成时没有退款,等待审批的十分钟内另一位客服已经处理。审批后直接使用旧证据,就会再次提出退款。仅增加一个五分钟缓存期限也不充分:即使证据只过期一秒,关键事实仍可能变化。应在副作用边界重新读取必要状态,并使用业务系统支持的条件更新或原子校验,让“检查仍可退”和“提交退款”之间尽可能少留不受控制的间隙。

重复写入还可能来自重复事件、工作者崩溃、前端连点、模型重复规划或子任务同时行动。排查时不要只搜索“同一个函数调用了几次”,要比较业务动作标识与外部结果:五次请求可能只产生一次退款,也可能两个不同调用各自生成一笔退款。真正的成功标准,是符合契约的副作用次数与状态,而不是日志里看起来没有重复。

对于结果未知的外部动作,系统需要一个明确去处。如果所有未知状态最终都被压成“失败”,运营人员会以为没有发生,手动再次处理。保留“已提交待核实”这种中间状态,并在界面上提供原请求引用,可以防止技术错误被人工操作放大。这个状态应有后续查询、超时升级与责任归属,不能永久悬空。

15.4 错误压缩会把不确定性变成错误授权

长任务通常需要压缩上下文。危险往往不在少记了一段客套话,而在把事实的限定条件压掉。例如原记录是“退款服务超时,尚未确认是否已有退款,不可提交第二次”,摘要却变成“尚未退款,可以处理”。几个字的变化,同时把未知改成否定、把限制改成授权,后续模型即使忠实遵守摘要也会做错。

压缩时应分别保存已证实事实、证据引用、未决问题、动作状态与授权范围。尤其不能把“客户希望退款”压缩为“客户已授权本工具执行任意退款”,也不能把“审核员同意这笔一百九十八元退款”扩展为“后续退款均已批准”。摘要是给模型阅读的派生材料,关键审批与副作用状态仍要从受控存储装配,不能仅依赖摘要记忆。

验证压缩是否可靠,可以构造一组带否定、例外和未知状态的记录,比较压缩前后系统是否允许相同的业务动作。若文本更短,但可执行动作集合被扩大,就是一次有实质影响的错误。对重要结论保留原始事件引用,主任务在遇到冲突时能够回到证据,而不是在多层摘要之间反复改写。

15.5 用可以观察的产物检验理解

读者可以用几个问题检查自己的设计。子 Agent 使用相同模型是否仍是独立任务?看它是否有独立的状态、上下文与执行循环,而不是看模型名是否不同。父子 Agent 的对话分开,是否就不会互相覆盖文件?要看它们的实际写入位置与并发控制。退款工具返回超时,是否应该重试?先区分请求尚未提交与结果未知,再看外部系统的去重和查询能力。RAG 找到允许退款的条款,是否足以执行?还需要事实匹配、身份授权、规则校验及该业务要求的审批。

实践可以分四步推进。先做一个只读调查器,使用固定测试数据,要求每个结论返回证据编号,并保留一份运行事件。然后加入一个有明确副作用的模拟工具,通过超时和崩溃注入验证重试不会重复产生业务结果。接着加入人工暂停与恢复,测试重复审批、旧版本审批和拒绝以后是否仍会执行。最后再引入一个边界明确的子任务,对比单任务基线的耗时、消耗与失败恢复质量。

每一步都要交付可以检查的东西:输入样例、预期业务状态、实际事件链以及失败原因。一次运行如果生成了正确答复,却没有正确的外部状态,不能判为成功;一次运行如果选择了转人工,完整保留证据且没有越权,反而可能是最合适的结果。做到能解释一次真实执行为什么继续、为什么暂停、为什么拒绝,再考虑增加更多角色和更复杂的自主规划。

附录:用一条真实执行链检查概念是否已经接通

拿到一个新的 Agent 产品或代码仓库时,不必先寻找它使用了多少个模型。先选择一次明确任务,记录任务标识、工作环境、模型输入中的工具定义、模型产生的调用、执行方收到的参数,以及最终可观察的业务结果。如果这一条链能完整追踪,许多看似抽象的问题都会落到具体接口和状态上。

例如,界面出现了“正在修改文档”,先区分这是模型的进度文字、已经提交的修改请求,还是文档服务已经返回的成功结果。出现一段 JSON,也先区分它是给用户展示的结构化答案,还是协议里需要调度的工具调用。磁盘上存在一份记忆文件,继续追问这一次调用是否读取了它、选取了哪一段、是否进入模型实际输入。看到三个子 Agent 运行,追问三个任务是否真的独立、结果在哪里汇合、共享文件会不会冲突。这些问题比把所有组件都称为“智能”更有工程解释力。

对学习者而言,一个合适的实践顺序是:先实现单次模型请求,再实现一项无副作用的工具,接着让工具结果进入下一次请求;之后加入会话存储、明确的停止条件和错误路径,再考虑上下文选择、压缩和跨会话记忆。只有当单 Agent 的行为已经能观察和验证时,多 Agent 的分派、消息和等待才会成为可控的扩展。否则,增加执行实例往往只是把同一个不可解释的问题复制了多份。

对业务落地而言,真正值得稳定下来的东西是任务契约和执行边界:什么输入可以被接受,什么动作需要什么权限,哪些事实必须重新读取,哪种结果足以证明目标达成,失败后从哪里恢复。模型可以持续升级,工具接入可以从 CLI 换成 MCP,部署也可以从本机搬到云端;这些契约仍然是判断系统是否可靠的依据。

配套图集和可编辑材料

Draw.io 文件可以用 diagrams.net 或兼容编辑器打开,每一页对应一张文章配图。SVG 与 HTML 图使用浏览器字体回退机制;离线编辑器可能替换字体,语义和节点关系不会因此改变。所有图都是用于解释边界的工程示意,不代表某个商业客户端完整的内部部署拓扑。

文章目录