从"调用大模型"到"管理有状态运行时"
在大多数入门教程与快速原型中,一个 Agent 的实现似乎只需要十几行代码:写一个 while True 循环,调用 LLM,解析返回的 JSON,执行本地函数,把输出拼回 Prompt,然后继续下一轮。
这种"玩具抽象"给很多人带来了一种错觉:Agent 框架不过是在大模型 API 之上做了一层浅薄的 Prompt 模板与工具分发封装。
但当我们将视线投向生产环境中的真实 Agent 时,会发现系统面临的根本挑战完全变了:
- 不可逆的外部世界 :Agent 不只是在生成文本,它在发邮件、改数据库、操作容器、调用转账接口。模型生成的内容可以重试,但现实世界中的副作用无法简单"撤销"。
- 不确定的时间跨度 :一个复杂的任务可能需要人类审批介入,或者等待异步任务完成。执行进程必须在任意时刻挂起,状态落库,数小时甚至数天后由另一台物理机无缝恢复。
- 脆弱的状态一致性 :网络中断、服务端限流、超时重试、并发冲突......一旦重试不当,就会导致工具被重复调用、或者历史消息在上下文窗口中出现不可逆的雪崩式重复。
生产级的 Agent 框架不是什么 LLM 客户端包装器;它本质上是一个为不确定性模型提供确定性边界的微型操作系统内核(Runtime Kernel)。
它向左接收来自 LLM 的概率性决策,向右连接会产生真实副作用的工具和外部系统;同时还要与会话存储、人类审批和运行状态打交道。它真正要解决的不是一组并列的"形式",而是四个层次不同的工程问题:
- 执行模型 :怎样把连续运行的黑盒循环拆成可检查、可中断的状态迁移;
- 一致性边界 :怎样区分可以丢弃的模型输出与必须保留的现实副作用;
- 协作协议 :怎样在人类、服务端会话、远程工具和不同进程之间交接控制权;
- 工程约束 :怎样用确定性测试和 API 契约约束一个以概率模型为核心的系统。
#mermaid-svg-Ww0M549RCngpTc7A{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Ww0M549RCngpTc7A .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Ww0M549RCngpTc7A .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Ww0M549RCngpTc7A .error-icon{fill:#552222;}#mermaid-svg-Ww0M549RCngpTc7A .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Ww0M549RCngpTc7A .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Ww0M549RCngpTc7A .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Ww0M549RCngpTc7A .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Ww0M549RCngpTc7A .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Ww0M549RCngpTc7A .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Ww0M549RCngpTc7A .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Ww0M549RCngpTc7A .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Ww0M549RCngpTc7A .marker.cross{stroke:#333333;}#mermaid-svg-Ww0M549RCngpTc7A svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Ww0M549RCngpTc7A p{margin:0;}#mermaid-svg-Ww0M549RCngpTc7A .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Ww0M549RCngpTc7A .cluster-label text{fill:#333;}#mermaid-svg-Ww0M549RCngpTc7A .cluster-label span{color:#333;}#mermaid-svg-Ww0M549RCngpTc7A .cluster-label span p{background-color:transparent;}#mermaid-svg-Ww0M549RCngpTc7A .label text,#mermaid-svg-Ww0M549RCngpTc7A span{fill:#333;color:#333;}#mermaid-svg-Ww0M549RCngpTc7A .node rect,#mermaid-svg-Ww0M549RCngpTc7A .node circle,#mermaid-svg-Ww0M549RCngpTc7A .node ellipse,#mermaid-svg-Ww0M549RCngpTc7A .node polygon,#mermaid-svg-Ww0M549RCngpTc7A .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Ww0M549RCngpTc7A .rough-node .label text,#mermaid-svg-Ww0M549RCngpTc7A .node .label text,#mermaid-svg-Ww0M549RCngpTc7A .image-shape .label,#mermaid-svg-Ww0M549RCngpTc7A .icon-shape .label{text-anchor:middle;}#mermaid-svg-Ww0M549RCngpTc7A .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Ww0M549RCngpTc7A .rough-node .label,#mermaid-svg-Ww0M549RCngpTc7A .node .label,#mermaid-svg-Ww0M549RCngpTc7A .image-shape .label,#mermaid-svg-Ww0M549RCngpTc7A .icon-shape .label{text-align:center;}#mermaid-svg-Ww0M549RCngpTc7A .node.clickable{cursor:pointer;}#mermaid-svg-Ww0M549RCngpTc7A .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Ww0M549RCngpTc7A .arrowheadPath{fill:#333333;}#mermaid-svg-Ww0M549RCngpTc7A .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Ww0M549RCngpTc7A .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Ww0M549RCngpTc7A .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ww0M549RCngpTc7A .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Ww0M549RCngpTc7A .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ww0M549RCngpTc7A .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Ww0M549RCngpTc7A .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Ww0M549RCngpTc7A .cluster text{fill:#333;}#mermaid-svg-Ww0M549RCngpTc7A .cluster span{color:#333;}#mermaid-svg-Ww0M549RCngpTc7A div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Ww0M549RCngpTc7A .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Ww0M549RCngpTc7A rect.text{fill:none;stroke-width:0;}#mermaid-svg-Ww0M549RCngpTc7A .icon-shape,#mermaid-svg-Ww0M549RCngpTc7A .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ww0M549RCngpTc7A .icon-shape p,#mermaid-svg-Ww0M549RCngpTc7A .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Ww0M549RCngpTc7A .icon-shape .label rect,#mermaid-svg-Ww0M549RCngpTc7A .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ww0M549RCngpTc7A .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Ww0M549RCngpTc7A .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Ww0M549RCngpTc7A :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户请求
Agent 运行时
LLM
产生概率性决策
工具与外部系统
产生真实副作用
运行状态与会话存储
保存可恢复现场
人工审批
批准或拒绝高风险动作
最终输出
图中的中心不是 LLM,而是运行时。LLM 只负责提出下一步;运行时决定这一步是否允许执行、执行后记录什么、何时需要暂停,以及失败后能否安全重试。
执行模型:从黑盒循环到离散状态机
初学者构建 Agent 时,最习惯的心智模型是"""黑盒控制流""":一个阻塞的线程从头跑到尾,变量存在函数栈帧里。一旦服务器重启或线程中断,整个执行现场彻底湮灭。
生产运行时的第一大抽象,是将不可控的黑盒循环,拍平成严格可检查、可中断、可持久化的离散单步状态机。
#mermaid-svg-POZTe0YyiOgL2qK7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-POZTe0YyiOgL2qK7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-POZTe0YyiOgL2qK7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-POZTe0YyiOgL2qK7 .error-icon{fill:#552222;}#mermaid-svg-POZTe0YyiOgL2qK7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-POZTe0YyiOgL2qK7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-POZTe0YyiOgL2qK7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-POZTe0YyiOgL2qK7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-POZTe0YyiOgL2qK7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-POZTe0YyiOgL2qK7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-POZTe0YyiOgL2qK7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-POZTe0YyiOgL2qK7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-POZTe0YyiOgL2qK7 .marker.cross{stroke:#333333;}#mermaid-svg-POZTe0YyiOgL2qK7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-POZTe0YyiOgL2qK7 p{margin:0;}#mermaid-svg-POZTe0YyiOgL2qK7 defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-POZTe0YyiOgL2qK7 g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-POZTe0YyiOgL2qK7 g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-POZTe0YyiOgL2qK7 g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-POZTe0YyiOgL2qK7 g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-POZTe0YyiOgL2qK7 g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-POZTe0YyiOgL2qK7 .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-POZTe0YyiOgL2qK7 .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-POZTe0YyiOgL2qK7 .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-POZTe0YyiOgL2qK7 .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-POZTe0YyiOgL2qK7 .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-POZTe0YyiOgL2qK7 .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-POZTe0YyiOgL2qK7 .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-POZTe0YyiOgL2qK7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-POZTe0YyiOgL2qK7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-POZTe0YyiOgL2qK7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-POZTe0YyiOgL2qK7 .edgeLabel .label text{fill:#333;}#mermaid-svg-POZTe0YyiOgL2qK7 .label div .edgeLabel{color:#333;}#mermaid-svg-POZTe0YyiOgL2qK7 .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-POZTe0YyiOgL2qK7 .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-POZTe0YyiOgL2qK7 .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-POZTe0YyiOgL2qK7 .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-POZTe0YyiOgL2qK7 .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-POZTe0YyiOgL2qK7 .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-POZTe0YyiOgL2qK7 .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-POZTe0YyiOgL2qK7 #statediagram-barbEnd{fill:#333333;}#mermaid-svg-POZTe0YyiOgL2qK7 .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-POZTe0YyiOgL2qK7 .cluster-label,#mermaid-svg-POZTe0YyiOgL2qK7 .nodeLabel{color:#131300;}#mermaid-svg-POZTe0YyiOgL2qK7 .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-POZTe0YyiOgL2qK7 .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-POZTe0YyiOgL2qK7 .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-POZTe0YyiOgL2qK7 .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-POZTe0YyiOgL2qK7 .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-POZTe0YyiOgL2qK7 .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-POZTe0YyiOgL2qK7 .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-POZTe0YyiOgL2qK7 .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-POZTe0YyiOgL2qK7 .note-edge{stroke-dasharray:5;}#mermaid-svg-POZTe0YyiOgL2qK7 .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-POZTe0YyiOgL2qK7 .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-POZTe0YyiOgL2qK7 .statediagram-note text{fill:black;}#mermaid-svg-POZTe0YyiOgL2qK7 .statediagram-note .nodeLabel{color:black;}#mermaid-svg-POZTe0YyiOgL2qK7 .statediagram .edgeLabel{color:red;}#mermaid-svg-POZTe0YyiOgL2qK7 #dependencyStart,#mermaid-svg-POZTe0YyiOgL2qK7 #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-POZTe0YyiOgL2qK7 .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-POZTe0YyiOgL2qK7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ProcessResponse
调用当前 Agent 的模型
包含可执行工具
包含交接或最终文本
收集工具结果与副作用
满足最终输出条件
NextStepRunAgain
NextStepHandoff(target_agent)
NextStepInterruption
当前 Agent 不变,进入下一轮
current_agent = target_agent
输出护栏与状态提交
保存 RunState
等待审批后恢复
获得 ModelResponse
调用已注册目标 Agent 的模型
CurrentAgentTurn
ClassifyOutput
ExecuteTools
EvaluateNext
FinalOutput
ContinueSameAgent
SwitchAgent
Interruption
TargetAgentTurn
PersistAndExit
SerializedPause
这里需要区分两种"继续":
NextStepRunAgain:当前 Agent 不变,把工具结果加入上下文,再调用同一个 Agent 的模型。NextStepHandoff(target_agent):先把current_agent替换为该 handoff 指向的目标 Agent,保存交接产生的项目并触发 Agent 更新事件,然后由目标 Agent 发起下一轮模型调用。常用的handoff(agent=...)接口会固定返回注册时传入的 Agent;源码中的new_agent只是"本次 handoff 解析出的接手者"这一局部变量名,并不意味着现场创建新对象。
handoff 后确实还会发生一次 model turn,但它属于接手后的目标 Agent,不是回到原 Agent。NextStepHandoff 也不是最终返回值,而是主循环中的一次"切换执行主体并继续运行"的控制指令。
Handoff 改变执行者,compaction 改变历史表示
这里还需要排除另一个容易产生的误解:handoff 本身不等于上下文压缩。两者作用在不同维度上。
| 机制 | 主要改变 | 默认是否改变另一个维度 |
|---|---|---|
NextStepHandoff(target_agent) |
当前执行者:Agent A → 目标 Agent B | 默认把此前对话继续交给 B,不自动压缩历史 |
nest_handoff_history / input_filter |
B 在交接后看到的历史表示 | 不决定目标 Agent 是谁 |
OpenAIResponsesCompactionSession |
Session 中长期保存的对话历史 | 不切换当前 Agent |
Agent.as_tool() |
临时执行一次子 Agent,并把结果返回调用者 | 主 Agent 仍掌握控制权 |
当前框架里确实存在真正的 compaction:OpenAIResponsesCompactionSession 会在满足阈值或被手动触发时调用 Responses API 的 responses.compact,再以压缩结果替换底层 Session 历史。默认触发条件是候选项目达到 10 条;替换过程还包含失败回滚、取消恢复和并发写入保护。
handoff 有一项相关但不同的能力:可选的 nest_handoff_history。它只在交接边界整理下一个 Agent 看到的历史------把适合整理的调用轨迹放入带 <CONVERSATION HISTORY> 标记的 assistant 历史段,同时保留需要无损转交的消息。默认 mapper 主要做结构化整理、嵌套和去重,并没有调用模型生成语义摘要。因此,更准确的称呼是是是交接历史重写是是,而不是与 responses.compact 等价的全局 compaction。换言之:
text
handoff 回答"接下来由谁负责?"
compaction 回答"接下来带多少、以什么形式带历史?"
它们可以分别使用,也可以组合使用。将两者拆开,正是这个框架没有把"专家路由"和"上下文工程"混成同一个魔法操作的地方。在这一抽象下:
Agent与执行彻底解耦 :Agent是以配置为主的数据类(Instructions、Tools、Handoffs)。它不持有某次运行的调用栈与会话历史,因此同一份定义可以被复用或派生。- 状态的离散归约 :每一轮交互被严格归约为四种确定性的
NextStep。无论是复杂的多工具并发、长链条交接,还是安全拦截,最终都要在单步裁决器(turn_resolution)中收敛为单一的枚举分支。 - 现场即数据 :执行现场不再只存在于内存调用栈,而是被整理为结构化的
RunState。在 Agent 定义、工具身份和 SDK 版本保持兼容的前提下,调用方可以把快照持久化,并在之后的进程中恢复运行。
一致性边界:不可逆世界中的副作用如何记账
大模型最显著的特点是"输出概率性",而软件系统最基本的要求是"执行确定性"。当这两者在工具调用(Tool Calls)相遇时,就产生了一个尖锐的工程矛盾:副作用的不可逆性。
如果一个 Agent 调用了 transfer_funds(amount=1000),随后大模型在生成结语时触发了安全策略(Output Guardrail),被系统判定为违规并拦截,系统应该怎么做?
- 做法 A(朴素回滚) :把这一轮的所有内容全部丢弃。结果:转账动作在外部系统已经发生,但 Agent 的历史记忆里却没有任何记录,下一轮 Agent 会认为转账从未发生,从而而而再次发起转账而而!
- 做法 B(粗暴保存) :把违规的输出和转账记录全存下来。结果:违规文本污染了会话历史,甚至作为下一次推理的上下文造成越狱。
#mermaid-svg-pPqxjkA9DGoZu4Aq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pPqxjkA9DGoZu4Aq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pPqxjkA9DGoZu4Aq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pPqxjkA9DGoZu4Aq .error-icon{fill:#552222;}#mermaid-svg-pPqxjkA9DGoZu4Aq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pPqxjkA9DGoZu4Aq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pPqxjkA9DGoZu4Aq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pPqxjkA9DGoZu4Aq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pPqxjkA9DGoZu4Aq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pPqxjkA9DGoZu4Aq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pPqxjkA9DGoZu4Aq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pPqxjkA9DGoZu4Aq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pPqxjkA9DGoZu4Aq .marker.cross{stroke:#333333;}#mermaid-svg-pPqxjkA9DGoZu4Aq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pPqxjkA9DGoZu4Aq p{margin:0;}#mermaid-svg-pPqxjkA9DGoZu4Aq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-pPqxjkA9DGoZu4Aq .cluster-label text{fill:#333;}#mermaid-svg-pPqxjkA9DGoZu4Aq .cluster-label span{color:#333;}#mermaid-svg-pPqxjkA9DGoZu4Aq .cluster-label span p{background-color:transparent;}#mermaid-svg-pPqxjkA9DGoZu4Aq .label text,#mermaid-svg-pPqxjkA9DGoZu4Aq span{fill:#333;color:#333;}#mermaid-svg-pPqxjkA9DGoZu4Aq .node rect,#mermaid-svg-pPqxjkA9DGoZu4Aq .node circle,#mermaid-svg-pPqxjkA9DGoZu4Aq .node ellipse,#mermaid-svg-pPqxjkA9DGoZu4Aq .node polygon,#mermaid-svg-pPqxjkA9DGoZu4Aq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pPqxjkA9DGoZu4Aq .rough-node .label text,#mermaid-svg-pPqxjkA9DGoZu4Aq .node .label text,#mermaid-svg-pPqxjkA9DGoZu4Aq .image-shape .label,#mermaid-svg-pPqxjkA9DGoZu4Aq .icon-shape .label{text-anchor:middle;}#mermaid-svg-pPqxjkA9DGoZu4Aq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pPqxjkA9DGoZu4Aq .rough-node .label,#mermaid-svg-pPqxjkA9DGoZu4Aq .node .label,#mermaid-svg-pPqxjkA9DGoZu4Aq .image-shape .label,#mermaid-svg-pPqxjkA9DGoZu4Aq .icon-shape .label{text-align:center;}#mermaid-svg-pPqxjkA9DGoZu4Aq .node.clickable{cursor:pointer;}#mermaid-svg-pPqxjkA9DGoZu4Aq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pPqxjkA9DGoZu4Aq .arrowheadPath{fill:#333333;}#mermaid-svg-pPqxjkA9DGoZu4Aq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pPqxjkA9DGoZu4Aq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pPqxjkA9DGoZu4Aq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pPqxjkA9DGoZu4Aq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pPqxjkA9DGoZu4Aq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pPqxjkA9DGoZu4Aq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pPqxjkA9DGoZu4Aq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pPqxjkA9DGoZu4Aq .cluster text{fill:#333;}#mermaid-svg-pPqxjkA9DGoZu4Aq .cluster span{color:#333;}#mermaid-svg-pPqxjkA9DGoZu4Aq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pPqxjkA9DGoZu4Aq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pPqxjkA9DGoZu4Aq rect.text{fill:none;stroke-width:0;}#mermaid-svg-pPqxjkA9DGoZu4Aq .icon-shape,#mermaid-svg-pPqxjkA9DGoZu4Aq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pPqxjkA9DGoZu4Aq .icon-shape p,#mermaid-svg-pPqxjkA9DGoZu4Aq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pPqxjkA9DGoZu4Aq .icon-shape .label rect,#mermaid-svg-pPqxjkA9DGoZu4Aq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pPqxjkA9DGoZu4Aq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pPqxjkA9DGoZu4Aq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pPqxjkA9DGoZu4Aq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 非对称持久化策略
触发拦截
模型输出: 执行转账 + 违规结语
- 执行工具调用: transfer_funds()
【外部现实】已扣款 $1000 (副作用已发生) - 输出护栏检查 (Output Guardrail)
- 事务边界裁决 (blocked_output.py)
❌ 违规输出文本: 坚决丢弃 / 拒绝落库
✅ 已执行工具与调用记录: 强制持久化进 Session
下一次唤醒: Agent 能看到转账已完成,绝不重复扣款
这就是生产运行时必须建立的"""非对称事务边界""":
- 护栏的时序约束 :阻塞型输入护栏必须在沙箱环境初始化(如挂载云盘、拉取代码库)之前严格运行,防止不安全请求触发底层环境的物理状态变更;
- 输出拦截与副作用落库的分离 :输出内容被熔断拦截时,系统必须启动专门的游标溯源机制,精准保留已落地的工具执行记录,剔除被污染的响应文本;
- 审批前后的双重校验 :在人工审批前做一次参数预检(避免将明显合规错误的脏数据抛给人类审核),在审批通过、真正执行工具的前一毫秒再次复检(防止在漫长的人工等待期间,底层资源或时间窗口已失效)。
护栏在生产 Agent 框架里,绝对不是简单的字符串正则或者内容分类器,而是是是横跨在模型推理与现实副作用之间的事务控制边界是是。
协作协议:分布式语境下的控制权与所有权
当任务变得复杂,单机单进程内存模型就会彻底失效。现代 Agent 必须能与服务端会话(OpenAI Conversations / Responses API)、远程 MCP 服务以及人类审核员协同工作。
此时,系统必须显式规定:历史由谁保存、下一步由谁决定、哪些动作能够自动重放,以及控制权在什么条件下交还给运行时。
人类审核员 外部持久化 (Redis/DB) 模型 / Provider Runtime 内核 宿主应用 人类审核员 外部持久化 (Redis/DB) 模型 / Provider Runtime 内核 宿主应用 #mermaid-svg-2kQlVSGPItXsANvO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2kQlVSGPItXsANvO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2kQlVSGPItXsANvO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2kQlVSGPItXsANvO .error-icon{fill:#552222;}#mermaid-svg-2kQlVSGPItXsANvO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2kQlVSGPItXsANvO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2kQlVSGPItXsANvO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2kQlVSGPItXsANvO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2kQlVSGPItXsANvO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2kQlVSGPItXsANvO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2kQlVSGPItXsANvO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2kQlVSGPItXsANvO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2kQlVSGPItXsANvO .marker.cross{stroke:#333333;}#mermaid-svg-2kQlVSGPItXsANvO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2kQlVSGPItXsANvO p{margin:0;}#mermaid-svg-2kQlVSGPItXsANvO .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2kQlVSGPItXsANvO text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-2kQlVSGPItXsANvO .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-2kQlVSGPItXsANvO .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-2kQlVSGPItXsANvO .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-2kQlVSGPItXsANvO .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-2kQlVSGPItXsANvO #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-2kQlVSGPItXsANvO .sequenceNumber{fill:white;}#mermaid-svg-2kQlVSGPItXsANvO #sequencenumber{fill:#333;}#mermaid-svg-2kQlVSGPItXsANvO #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-2kQlVSGPItXsANvO .messageText{fill:#333;stroke:none;}#mermaid-svg-2kQlVSGPItXsANvO .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2kQlVSGPItXsANvO .labelText,#mermaid-svg-2kQlVSGPItXsANvO .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-2kQlVSGPItXsANvO .loopText,#mermaid-svg-2kQlVSGPItXsANvO .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-2kQlVSGPItXsANvO .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-2kQlVSGPItXsANvO .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-2kQlVSGPItXsANvO .noteText,#mermaid-svg-2kQlVSGPItXsANvO .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-2kQlVSGPItXsANvO .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2kQlVSGPItXsANvO .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2kQlVSGPItXsANvO .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2kQlVSGPItXsANvO .actorPopupMenu{position:absolute;}#mermaid-svg-2kQlVSGPItXsANvO .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-2kQlVSGPItXsANvO .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2kQlVSGPItXsANvO .actor-man circle,#mermaid-svg-2kQlVSGPItXsANvO line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-2kQlVSGPItXsANvO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 触发 needs_approval 规则状态机转入 NextStepInterruption 审核流挂起数小时/数天... 校验 response_accepted 状态跳过已完成网络交互,直奔工具执行 Runner.run(agent, input)1携带增量请求 (Tracker 追踪)2返回敏感工具调用 (delete_database)3生成离散快照 RunState4返回 RunResult (含 interruptions)5持久化 RunState (进程安全退出/释放资源)6审批决策: Approve(call_id)7重新加载 RunState8Runner.run(agent, state)9执行工具并继续后续流转10
在这个分布式拓扑中,运行时需要确立三重所有权机制:
1. 记忆所有权的互斥性
客户端本地 Session 与 OpenAI 服务端托管会话(conversation_id / previous_response_id)))在底层是两套互斥的权威源))。生产框架必须在运行时做强校验,严禁在同一 Run 中混用,并配备专门的增量追踪器(Tracker)。在遇到网络重试时,能够主动将已发送但未确认的增量数据执行回滚(Rewind),避免在服务端产生脏数据。
2. 两阶段挂起的人工在环(HITL)
人工审批不是阻塞等待的 UI 交互,而是一个个个解耦的两阶段协议个个。系统需要记录 response_accepted(服务端是否已经确认)与 llm_end_hooks_started(生命周期是否已推进)两个细粒度标志,确保在跨机器恢复时,系统能够精确识别断点,既不漏掉任何副作用,也不重复发起已计费的模型调用。
3. Replay-Safe 的重试协商
重试绝不是在 catch 块里写一个重试次数。如果模型上下文里包含写操作的程序化工具(ProgrammaticToolCallingTool),重试该请求就是是是极度危险的重放攻击风险是是。框架必须引入模型建议、Provider 权威与硬否决(Veto)机制,只有在证明请求"纯净且可幂等重放"时才允许自动重试。
工程约束:用代码契约驯服不确定性系统
Agent 开发中最令人头疼的是:底层的 LLM 具有天生的随机性,传统的软件工程测试与版本管理方法在此处几乎处处碰壁。
如果每次跑 CI 都要真实调用一遍远端模型,不仅昂贵、缓慢,而且会因为模型的微小输出抖动导致测试用例随机挂掉。
生产运行时的第四大抽象,是用高密度的确定性工具与严格的接口契约,把整个不确定性系统锁进可靠的工程笼子里。
#mermaid-svg-0Z4ijJk8u7RllDOM{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0Z4ijJk8u7RllDOM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0Z4ijJk8u7RllDOM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0Z4ijJk8u7RllDOM .error-icon{fill:#552222;}#mermaid-svg-0Z4ijJk8u7RllDOM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0Z4ijJk8u7RllDOM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0Z4ijJk8u7RllDOM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0Z4ijJk8u7RllDOM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0Z4ijJk8u7RllDOM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0Z4ijJk8u7RllDOM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0Z4ijJk8u7RllDOM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0Z4ijJk8u7RllDOM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0Z4ijJk8u7RllDOM .marker.cross{stroke:#333333;}#mermaid-svg-0Z4ijJk8u7RllDOM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0Z4ijJk8u7RllDOM p{margin:0;}#mermaid-svg-0Z4ijJk8u7RllDOM .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0Z4ijJk8u7RllDOM .cluster-label text{fill:#333;}#mermaid-svg-0Z4ijJk8u7RllDOM .cluster-label span{color:#333;}#mermaid-svg-0Z4ijJk8u7RllDOM .cluster-label span p{background-color:transparent;}#mermaid-svg-0Z4ijJk8u7RllDOM .label text,#mermaid-svg-0Z4ijJk8u7RllDOM span{fill:#333;color:#333;}#mermaid-svg-0Z4ijJk8u7RllDOM .node rect,#mermaid-svg-0Z4ijJk8u7RllDOM .node circle,#mermaid-svg-0Z4ijJk8u7RllDOM .node ellipse,#mermaid-svg-0Z4ijJk8u7RllDOM .node polygon,#mermaid-svg-0Z4ijJk8u7RllDOM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0Z4ijJk8u7RllDOM .rough-node .label text,#mermaid-svg-0Z4ijJk8u7RllDOM .node .label text,#mermaid-svg-0Z4ijJk8u7RllDOM .image-shape .label,#mermaid-svg-0Z4ijJk8u7RllDOM .icon-shape .label{text-anchor:middle;}#mermaid-svg-0Z4ijJk8u7RllDOM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0Z4ijJk8u7RllDOM .rough-node .label,#mermaid-svg-0Z4ijJk8u7RllDOM .node .label,#mermaid-svg-0Z4ijJk8u7RllDOM .image-shape .label,#mermaid-svg-0Z4ijJk8u7RllDOM .icon-shape .label{text-align:center;}#mermaid-svg-0Z4ijJk8u7RllDOM .node.clickable{cursor:pointer;}#mermaid-svg-0Z4ijJk8u7RllDOM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0Z4ijJk8u7RllDOM .arrowheadPath{fill:#333333;}#mermaid-svg-0Z4ijJk8u7RllDOM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0Z4ijJk8u7RllDOM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0Z4ijJk8u7RllDOM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0Z4ijJk8u7RllDOM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0Z4ijJk8u7RllDOM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0Z4ijJk8u7RllDOM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0Z4ijJk8u7RllDOM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0Z4ijJk8u7RllDOM .cluster text{fill:#333;}#mermaid-svg-0Z4ijJk8u7RllDOM .cluster span{color:#333;}#mermaid-svg-0Z4ijJk8u7RllDOM div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0Z4ijJk8u7RllDOM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0Z4ijJk8u7RllDOM rect.text{fill:none;stroke-width:0;}#mermaid-svg-0Z4ijJk8u7RllDOM .icon-shape,#mermaid-svg-0Z4ijJk8u7RllDOM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0Z4ijJk8u7RllDOM .icon-shape p,#mermaid-svg-0Z4ijJk8u7RllDOM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0Z4ijJk8u7RllDOM .icon-shape .label rect,#mermaid-svg-0Z4ijJk8u7RllDOM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0Z4ijJk8u7RllDOM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0Z4ijJk8u7RllDOM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0Z4ijJk8u7RllDOM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 版本与契约治理 (fixtures/)
1.13MB 契约快照
(released_api_contract.json)
发布门禁检查
(CI 自动化 Diff 校验)
规范导入策略
(canonical_imports)
确定性测试体系 (testing/)
ScriptedModel
(可脚本化假模型)
严格多步剧本
(ModelStep / Call)
未消费步骤断言
(Unconsumed Steps)
可复现、零成本、毫秒级的 Agent 回归测试
杜绝隐式破坏向后兼容与导入规范
- 确定性多轮仿真(
ScriptedModel) :
框架不依赖网络 Mock,而是建立了一套完整的"多轮剧本模型"。你可以精确指定第 1 轮返回什么 Tool Call、第 2 轮在收到特定参数后返回什么结果。如果 Agent 在执行中跳过了某个预定步骤,或者发起了未曾声明的调用,测试会立即抛出UnexpectedModelCall或UnconsumedModelSteps。这让极其复杂的多 Agent 协作与状态恢复测试可以在毫秒级、零成本、零随机性的环境下进行单测。 - 版本化 API 契约冻结(API Contract Snapshot) :
在tests/fixtures/目录下,静静躺着一个高达 1.13 MB 的released_api_contract.json及其策略文件。它把整个 SDK 导出的每一个类、方法签名、参数默认值甚至是规范的 Canonical Import 路径全部固化为测试资产。任何一行不小心的代码修改只要改变了公共契约表面,CI 就会无情熔断。
贯穿全篇的核心图景
当执行模型、一致性边界、协作协议与工程约束彼此连接起来,就构成了生产级 Agent 运行时的完整剖面。
- 【人工审批与恢复协议】 :剖析 5000 行
RunState背后的两阶段挂起、状态落盘与跨进程断点唤醒; - 【副作用与事务边界】 :详解输出拦截后已执行工具的非对称保留算法,以及四层护栏的时序控制;
- 【四种记忆策略与所有权】 :厘清客户端 Session 与服务端 Tracker 的增量同步与回滚,看清为什么它们绝对不能混用;
- 【失败重试与异常治理】 :深入
retry.py的 Veto 否决机制、异常组拓扑脱敏与可自愈的兼容开关;