Agent 工程体系 · 第 96/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。

Agent 发布与版本治理:模型、Prompt、Tool、Memory、灰度和回滚

Agent 发布不是把一段代码部署到服务器,而是把一组会共同改变行为的运行时部件切换到生产环境。这些部件至少包括:

  • 模型(Model):负责生成文本、结构化输出、工具调用或下一步决策的模型及其参数。
  • Prompt:发送给模型的系统指令、任务模板、动态上下文和输出约束。
  • Tool:Agent 可以调用的函数、检索服务、数据库、外部 API 或人工审批接口。
  • Memory:跨轮次、跨任务保存并再次注入 Agent 的状态、事实、偏好和历史信息。
  • 灰度(Canary / Gradual Rollout):只让部分请求、用户或租户使用新版本,以较小影响面观察行为。
  • 回滚(Rollback):停止新版本接收流量,恢复到已知可用的版本组合;必要时还要修复新版本已经写入的数据或外部系统状态。

如果只给应用服务打一个 Git tag,而没有同时固定模型、Prompt、Tool schema、Memory schema 和路由规则,那么这个 tag 并不能代表一个可复现的 Agent 版本。生产中的 Agent 版本更接近一个版本向量:

V=(m,p,t,μ,g,r,s)V = (m, p, t, \mu, g, r, s)

其中:

  • mm:模型标识及模型参数;
  • pp:Prompt 模板及其参数;
  • tt:Tool 集合、工具描述和输入输出 schema;
  • μ\mu:Memory 读写逻辑及存储 schema;
  • gg:Guardrail,包括输入、输出和工具调用前后的安全检查;
  • rr:路由、重试、超时、并发和降级策略;
  • ss:服务代码与依赖版本。

因此,发布的基本对象不是“新代码”,而是一个可寻址、可观测、可比较、可恢复的 Agent 运行时版本


一、先定义“版本”:行为版本,而不是代码版本

传统 Web 服务通常把版本理解成二进制或容器镜像版本。Agent 的行为由多个动态因素共同决定,同一份代码只要更换模型或 Prompt,行为就可能发生显著变化。

例如,下面三个部署实际上是三个不同的 Agent 版本:

release-a:
  application: agent-service:2026.09.01
  model:       reasoning-model-x
  prompt:      support-agent@17
  tools:       order.read@3, refund.create@2
  memory:      customer-memory@5

release-b:
  application: agent-service:2026.09.01
  model:       reasoning-model-x
  prompt:      support-agent@18
  tools:       order.read@3, refund.create@2
  memory:      customer-memory@5

release-c:
  application: agent-service:2026.09.01
  model:       reasoning-model-y
  prompt:      support-agent@18
  tools:       order.read@3, refund.create@2
  memory:      customer-memory@5

release-b 主要改变 Prompt,release-c 同时改变模型和 Prompt。出现回答质量下降时,如果只记录 agent-service:2026.09.01,就无法判断问题来自模型、Prompt 还是二者的交互。

1. 版本应当包含内容摘要

推荐为每个发布组合生成不可变的 manifest:

release_id: agent-prod-2026-09-01.3
application:
  image: registry.example.com/agent-service@sha256:...
  git_commit: 9f2a1d7

model:
  provider: openai
  name: reasoning-model-x
  settings:
    temperature: 0.2
    max_output_tokens: 1200

prompt:
  name: support-agent
  revision: 18
  sha256: 4a2e...

tools:
  - name: order.read
    revision: 3
    schema_sha256: 18bc...
  - name: refund.create
    revision: 2
    schema_sha256: 5d01...

memory:
  read_policy: customer-memory@5
  write_policy: customer-memory@5
  schema_version: 5

guardrails:
  input: 4
  output: 7
  tool: 3

routing:
  strategy: stable
  timeout_ms: 8000
  max_turns: 12

这里的 sha256 不是为了安全装饰,而是为了区分“名称相同但内容不同”的配置。例如 Prompt 仍然叫 support-agent@18,但如果模板文件被直接覆盖,名称就无法证明实际内容。发布系统应当只引用不可变对象,禁止生产进程根据“latest”或可变别名动态拉取配置。

2. 发布版本和配置版本要分开

应用版本、Prompt 版本和 Tool 版本不必拥有相同的递增序号:

application: 2026.09.01
prompt:      support-agent@18
tool:        refund.create@2
memory:      customer-memory@5
release:     agent-prod-2026-09-01.3

这样做的原因是它们的兼容性不同:

  • 应用代码可能兼容多个 Prompt 版本;
  • Prompt 可能要求某个 Tool 必须返回新字段;
  • Memory 可能需要数据库迁移,但模型本身没有变化;
  • Tool 的外部 API 可能发生破坏性变化,必须先部署兼容层。

发布系统需要保存的是完整版本向量,而不是试图让所有组件共享一个编号。


二、Agent 的执行闭环:发布改变的是状态转换函数

一个 Agent 请求可以抽象为状态转换:

Si+1=F(Si,M,P,T,μ,Ii)S_{i+1} = F(S_i, M, P, T, \mu, I_i)

其中:

  • SiS_i:第 ii 步的会话状态;
  • MM:模型及模型参数;
  • PP:Prompt 构造逻辑;
  • TT:Tool 集合及其结果;
  • μ\mu:Memory 的读取和写入行为;
  • IiI_i:用户输入或外部事件;
  • FF:编排器执行的状态转换。

一次完整运行通常包含以下路径:

sequenceDiagram
    participant U as 用户
    participant R as 路由器
    participant A as Agent Runtime
    participant M as Memory Store
    participant L as 模型
    participant T as Tool
    participant O as 外部系统
    participant X as Trace/Eval

    U->>R: 请求 + 用户/租户标识
    R->>R: 选择 release_id
    R->>A: 请求 + 版本 manifest
    A->>M: 读取历史记忆
    M-->>A: 记忆快照
    A->>L: Prompt + 输入 + Memory
    L-->>A: 文本或 Tool Call
    A->>T: 校验工具调用
    T->>O: 查询或写入外部系统
    O-->>T: 工具结果
    T-->>A: 结构化工具结果
    A->>L: 工具结果 + 后续上下文
    L-->>A: 最终答案
    A->>M: 写入候选记忆
    A->>X: trace、指标、版本信息
    A-->>R: 答案 + 状态
    R-->>U: 响应

OpenAI Agents SDK 的 tracing 将一次工作流表示为 Trace,并以 Span 记录模型生成、工具调用、handoff、guardrail 和自定义事件等操作;Trace 可以关联一个端到端运行,Span 则包含父子关系、开始结束时间和具体操作数据。(openai.github.io)

这一区分对发布治理很重要:如果只有最终答案,没有中间状态,就无法知道新版本是:

  1. 选错了工具;
  2. 选择了正确工具但参数错误;
  3. Tool 返回了错误数据;
  4. 模型读取了错误的 Memory;
  5. Guardrail 阻断了正常路径;
  6. Prompt 改变了后续路由;
  7. 外部系统已成功写入,但 Agent 在生成答案前超时。

因此,Agent 的发布验证必须同时观察结果指标路径指标


三、模型版本治理:模型不是普通依赖

1. 模型变更会改变概率分布

给定相同输入 xx,模型输出不是一个固定函数,而是条件概率分布:

P(yx,M,P,T,μ)P(y \mid x, M, P, T, \mu)

模型升级后,即使 API schema 没有变化,输出分布也可能变化:

  • 工具调用频率变化;
  • 工具选择顺序变化;
  • 参数边界行为变化;
  • 对拒答、澄清和不确定性的处理变化;
  • 结构化输出的字段内容变化;
  • 同一 Prompt 下的答案风格变化。

因此,模型升级不是“替换一个 HTTP endpoint”,而是改变了 Agent 的策略分布。

2. 模型版本至少固定四类信息

模型发布记录应包含:

model:
  provider: openai
  name: reasoning-model-x
  snapshot: snapshot-or-provider-version
  temperature: 0.2
  top_p: 1.0
  max_output_tokens: 1200
  reasoning_effort: medium
  response_format: structured_json

需要区分:

  • 模型名称:逻辑名称,可能指向供应商可变配置;
  • 模型快照或固定版本:用于重放和比较;
  • 采样参数:影响输出分布;
  • 输出约束:例如 JSON schema、工具 schema 和最大输出长度。

如果供应商只提供逻辑模型名而没有承诺永久固定快照,则系统不能把“完全字节级复现”当成规范保证。此时应保存请求、响应、工具结果和 trace,用于行为级重放,并接受模型侧变化带来的差异。

3. 不要用平均分掩盖尾部风险

假设旧模型和新模型在 1,000 条评测样本上的总体成功率如下:

旧模型:94.2%
新模型:94.8%

这并不自动说明新模型更好。将样本按风险分层:

场景 旧版本 新版本
普通查询 97.0% 98.1%
退款解释 95.2% 95.8%
退款执行 93.5% 91.0%
高风险账户操作 89.0% 84.0%

新模型可能提高了普通查询,却损害了不可逆操作。发布门禁应使用分层条件,而不是只比较总体平均值:

允许发布    ΔQoverallϵQcriticalQcritical,minEunsafeEunsafe,max\text{允许发布} \iff \Delta Q_{\text{overall}} \geq -\epsilon \land Q_{\text{critical}} \geq Q_{\text{critical,min}} \land E_{\text{unsafe}} \leq E_{\text{unsafe,max}}

其中:

  • QoverallQ_{\text{overall}}:总体质量;
  • QcriticalQ_{\text{critical}}:关键业务场景质量;
  • EunsafeE_{\text{unsafe}}:不安全或越权行为比例;
  • ϵ\epsilon:允许的总体回归容忍度。

关键场景可以不允许回归,即使总体得分上升。


四、Prompt 版本治理:Prompt 是可执行策略

Prompt 不只是文案。对于 Agent,它同时规定:

  • 任务目标;
  • 工具使用条件;
  • 何时澄清;
  • 何时拒绝;
  • 何时交给人工;
  • 如何解释 Tool 结果;
  • 哪些字段必须进入最终响应;
  • 哪些内容不得写入 Memory。

因此,Prompt 变化应当像代码变化一样进行评审、测试和发布。

1. 将 Prompt 拆成稳定层和动态层

一个可治理的 Prompt 通常分为:

稳定策略:
  - 角色和权限边界
  - 工具调用规则
  - 安全约束
  - 输出格式

动态上下文:
  - 用户输入
  - 当前时间
  - 账户信息
  - 检索结果
  - Memory 摘要
  - 工具结果

稳定层应有版本号和摘要;动态层应在 trace 中记录其来源和摘要。不要只记录最终拼接后的大字符串,否则无法判断回归来自模板还是上下文。

2. Prompt 变化要做差异分类

可以将变更分成三类:

语义不变变更

例如调整 Markdown 换行、变量排序、无意义措辞。这类变更理论上不应改变行为,但由于模型对上下文敏感,仍应通过回归样本验证。

约束增强变更

例如新增:

当订单状态不是“已支付”时,不得调用 refund.create。

这会减少某些工具调用,但可能增加澄清或拒答比例。验证时要同时观察:

  • 错误调用是否减少;
  • 正常订单是否被误拦截;
  • 用户是否获得可行动的解释。

策略改变变更

例如把“默认直接退款”改成“先询问退款原因”。这不是 Prompt 优化,而是业务行为改变,必须提高版本级别,并更新验收标准。

3. 反例:只测最终回答

旧 Prompt:

如果用户要求退款,查询订单后直接处理。

新 Prompt:

如果用户要求退款,先确认退款原因,再查询订单并处理。

输入:

用户:订单 1001 可以退款吗?

两个版本可能都生成“可以,我来帮你查询订单”。但对输入:

用户:帮我把订单 1001 退掉。

旧版本可能执行查询和退款,新版本可能先追问原因。若只断言最终回答中包含“退款”,测试就无法发现工具调用路径已经改变。

正确的评测对象至少包括:

{
  "expected": {
    "tool_calls": [
      {
        "name": "order.read",
        "args": {"order_id": "1001"}
      }
    ],
    "must_not_call": ["refund.create"],
    "final_answer_contains": ["退款原因"]
  }
}

OpenAI 的 Agent 评测文档将 trace、grader、dataset 和 eval run 分为不同层次:调试阶段先检查单次 trace,行为标准明确后再使用数据集和评测运行进行可重复比较。(developers.openai.com)


五、Tool 版本治理:接口兼容不等于行为兼容

Tool 包含两部分:

T=(D,I)T = (D, I)

其中:

  • DD:模型看到的工具描述、名称、参数 schema;
  • II:真正执行工具调用的实现。

很多事故只管理了 II,忽略了 DD。但模型是根据工具描述决定是否调用和如何填参的。修改字段说明同样可能改变行为。

1. Tool 的兼容性分为三层

Schema 兼容性

调用方能否继续解析输入和输出。例如输出从:

{"status": "paid"}

变成:

{"state": "paid"}

即使业务含义相同,也可能导致旧 Prompt 或旧代码失效。

语义兼容性

同样的参数是否仍有相同含义。例如:

{"amount": 100}

旧版本表示人民币 100 元,新版本表示分,则属于语义破坏。

副作用兼容性

调用是否仍然是幂等的、是否产生相同外部影响。例如 refund.create 从“创建退款申请”变成“立即扣款并退款”,这是副作用级别的破坏性变更。

2. Tool 的发布顺序

对于输出字段变更,推荐采用“先兼容、后切换、再清理”的顺序:

阶段 1:Tool 同时返回旧字段和新字段
阶段 2:新 Agent 读取新字段,旧 Agent 仍读取旧字段
阶段 3:新版本稳定运行
阶段 4:停止旧 Agent 流量
阶段 5:删除旧字段

对于输入字段变更:

阶段 1:服务端同时接受 old_arg 和 new_arg
阶段 2:Prompt 和 Tool schema 切换到 new_arg
阶段 3:监控 old_arg 使用量归零
阶段 4:删除 old_arg

这不是为了迁就 Agent,而是因为灰度期间通常会同时存在旧版本和新版本。若 Tool 只支持新协议,旧版本请求就可能在灰度期间失败。

3. 写操作必须有幂等键

Agent 可能因为模型重试、网络超时、客户端重试或 Worker 重启而重复发起相同 Tool 调用。写操作应携带业务幂等键:

{
  "order_id": "1001",
  "reason": "用户申请退款",
  "idempotency_key": "run_8e1c:toolcall_03"
}

run_id + tool_call_id 只适合防止同一次运行重复执行。如果业务上允许用户再次确认同一动作,还需要业务级幂等键,例如:

refund:{tenant_id}:{order_id}:{request_id}

Tool 服务必须明确返回以下状态:

{
  "status": "already_applied",
  "operation_id": "refund_7788"
}

不能把“请求超时”直接解释成“操作失败”。超时只说明调用方没有获得结果,外部系统可能已经成功。


六、Memory 版本治理:最容易被忽略的持久化状态

Memory 是 Agent 跨请求保留的信息。它可能包括:

  • 会话历史;
  • 用户偏好;
  • 事实摘要;
  • 任务进度;
  • 工具结果缓存;
  • 检索索引;
  • Agent 自己写入的长期记忆。

Memory 与 Prompt 的关键差别是:Prompt 通常可以随发布切换,Memory 已经持久化在生产数据中,不能简单地随代码回滚。

1. Memory 不是一个字段,而是一种数据协议

可以把一条记忆表示为:

{
  "memory_id": "mem_123",
  "tenant_id": "tenant_a",
  "subject_id": "user_42",
  "kind": "preference",
  "content": "用户偏好中文回答",
  "source": {
    "run_id": "run_8e1c",
    "message_id": "msg_91"
  },
  "confidence": 0.93,
  "schema_version": 5,
  "created_at": "2026-09-01T09:00:00+08:00",
  "expires_at": null
}

其中 sourceconfidenceschema_version 很重要:

  • 没有来源,无法追溯错误记忆;
  • 没有置信度,无法区分用户明确声明和模型推断;
  • 没有 schema 版本,无法安全迁移和回滚。

2. 读取版本和写入版本要分离

建议显式配置:

memory:
  read:
    accepted_schema_versions: [4, 5]
    projection: customer-memory-read@5
  write:
    schema_version: 5
    policy: customer-memory-write@5

发布初期可以让新代码读取旧格式:

新版本读取:v4、v5
新版本写入:v5
旧版本读取:仅 v4

这时如果直接把流量回滚到旧版本,旧版本可能无法读取新写入的 v5 数据。解决方案有三种:

  1. 旧版本先升级为可读 v5 的兼容版本;
  2. 新版本写入双格式;
  3. 使用读取投影,将 v5 转换为 v4。

第三种方案通常更适合紧急回滚,因为它不要求立即修改持久化数据。

3. Memory 写入应当延迟提交

Agent 运行中可能出现:

模型生成“用户偏好退款”
→ Agent 写入 Memory
→ 最终响应失败

如果写入发生在最终结果确认之前,Memory 可能保存一条未经验证的事实。更安全的做法是使用候选记忆:

candidate memory
    ↓ 通过事实性、来源、权限检查
committed memory

可以用数据库事务表示:

CREATE TABLE memory_entries (
    memory_id       TEXT PRIMARY KEY,
    subject_id      TEXT NOT NULL,
    kind            TEXT NOT NULL,
    content         TEXT NOT NULL,
    schema_version  INTEGER NOT NULL,
    status          TEXT NOT NULL CHECK (status IN ('candidate', 'committed', 'revoked')),
    source_run_id   TEXT NOT NULL,
    created_at      TIMESTAMP NOT NULL,
    revoked_at      TIMESTAMP
);

CREATE INDEX idx_memory_subject_status
ON memory_entries(subject_id, status);

当一次运行失败时,候选记忆不应自动升级为 committed。当发现新版本产生错误记忆时,优先使用 revoked 标记和读取过滤,而不是直接物理删除,以保留审计证据。


七、发布前验证:从单元测试到工作流评测

Agent 发布不能只依赖传统单元测试。单元测试适合验证确定性逻辑,例如:

  • 版本 manifest 解析;
  • Tool 参数校验;
  • Memory 迁移;
  • 灰度路由;
  • 回滚开关;
  • 幂等键生成。

而模型行为和多步骤工作流需要使用 trace、数据集和评测运行。

1. 四层验证结构

第一层:静态检查

检查版本组合是否满足声明的兼容矩阵:

prompt@18 requires:
  order.read >= 3
  refund.create >= 2
  memory schema >= 4

第二层:确定性组件测试

使用模型替身和 Tool Stub:

class ModelStub:
    def __init__(self, outputs):
        self.outputs = iter(outputs)

    async def generate(self, request):
        return next(self.outputs)


class ToolStub:
    def __init__(self, responses):
        self.responses = responses
        self.calls = []

    async def call(self, name, args):
        self.calls.append((name, args))
        return self.responses[name]

输入固定、模型输出固定、Tool 结果固定时,测试的是编排器本身,而不是模型能力:

async def test_refund_requires_paid_order():
    model = ModelStub([
        {"type": "tool_call", "name": "order.read",
         "arguments": {"order_id": "1001"}},
        {"type": "final", "text": "订单未支付,不能退款"}
    ])

    tools = ToolStub({
        "order.read": {"order_id": "1001", "status": "created"}
    })

    result = await run_agent(model=model, tools=tools, user_input="帮我退款")

    assert [name for name, _ in tools.calls] == ["order.read"]
    assert "refund.create" not in [name for name, _ in tools.calls]
    assert "不能退款" in result.text

第三层:离线 Agent 评测

数据集中的每条样本应包含:

{
  "input": "帮我把订单1001退掉",
  "memory": [],
  "tool_fixtures": {
    "order.read": {
      "order_id": "1001",
      "status": "paid",
      "refundable": true
    }
  },
  "expected": {
    "required_tools": ["order.read", "refund.create"],
    "forbidden_tools": [],
    "must_confirm_before_write": true
  }
}

评测器不应只比较字符串。更可靠的维度包括:

Q=w1C+w2A+w3P+w4Sw5Lw6KQ = w_1 C + w_2 A + w_3 P + w_4 S - w_5 L - w_6 K

其中:

  • CC:任务完成度;
  • AA:事实准确性;
  • PP:工具路径正确性;
  • SS:安全性;
  • LL:延迟成本;
  • KK:Token 或外部工具成本;
  • wiw_i:业务权重。

对于写操作,安全性和副作用正确性通常应设置为硬门槛,而不是通过提高其他项的分数抵消。

第四层:生产 trace 评测

离线数据集覆盖不了真实用户的表达、脏数据、超时和并发。生产 trace 用于发现:

  • 工具描述导致的误调用;
  • 某类用户输入触发的循环;
  • Memory 污染;
  • handoff 错误;
  • 外部服务慢导致的级联超时。

OpenAI 文档建议在行为仍处于调试阶段时先使用 trace grading,待“什么是好行为”稳定后,再转向数据集和重复评测运行。(developers.openai.com)


八、可比性:评测运行必须固定实验条件

两个版本的分数只有在实验条件可比时才有意义。至少固定以下内容:

数据集版本
模型版本
模型参数
Prompt 摘要
Tool schema
Tool fixture
Memory 快照
随机种子(若实现支持)
评测器版本
评分阈值

如果新版本使用了新的 Tool 实时数据,而旧版本使用录制数据,那么差异同时包含:

Agent 版本差异 + 外部数据差异

此时不能把结果归因于模型或 Prompt。

反例:评测集被悄悄改变

旧版本:dataset@12,1000 条样本
新版本:dataset@13,删除了 80 条失败样本

新版本成功率上升,但这不是 Agent 改进,而是样本选择改变。发布系统应记录:

dataset@12 → dataset@13
新增样本:120
删除样本:80
修改样本:35

对关键场景,评测报告应同时输出:

  • 固定基线集得分;
  • 新增样本得分;
  • 删除样本的历史得分;
  • 按场景、风险、租户类型分层的结果。

九、灰度发布:控制的是风险暴露,而不是只控制流量比例

灰度是让新版本只服务一部分请求,但“10% 流量”并不等于“10% 风险”。

如果高风险请求只占总流量的 1%,却全部被路由到新版本,那么新版本暴露的是高风险业务,而不是总体流量的随机样本。

1. 灰度键必须稳定

路由可以使用用户、租户、会话或请求级键:

import hashlib

def bucket(key: str) -> int:
    digest = hashlib.sha256(key.encode("utf-8")).hexdigest()
    return int(digest[:8], 16) % 100

def choose_release(*, tenant_id: str, experiment: str,
                   canary_percent: int) -> str:
    key = f"{experiment}:{tenant_id}"
    return "candidate" if bucket(key) < canary_percent else "stable"

使用租户级稳定键的结果是:同一租户在灰度期间始终使用同一版本,减少同一会话在两个版本之间切换造成的记忆和上下文差异。

但这也带来边界:如果某个大租户恰好进入灰度,其风险可能远大于按请求随机抽样。因此生产系统通常需要组合约束:

租户白名单
+ 用户比例
+ 高风险请求排除
+ 单租户流量上限
+ 失败自动熔断

2. 灰度期间必须记录版本归因

每一次请求至少记录:

{
  "trace_id": "trace_...",
  "release_id": "agent-prod-2026-09-01.3",
  "routing_rule": "tenant_canary_10",
  "tenant_id_hash": "sha256:...",
  "model": "reasoning-model-x",
  "prompt_revision": 18,
  "tool_revisions": {
    "order.read": 3,
    "refund.create": 2
  },
  "memory_schema_version": 5
}

Tracing 的价值不仅是查看最终答案,也在于把模型生成、工具调用、guardrail 和 handoff 放在同一条工作流记录中,从而能按版本比较完整路径。Agents SDK 默认启用 tracing,也提供全局、代码级和单次运行级的关闭方式;使用 Zero Data Retention 策略的组织则不提供该 tracing 能力,需要配置符合数据策略的替代观测方案。(openai.github.io)

3. 灰度指标必须分为三类

结果指标

任务完成率
用户重新提问率
人工转接率
投诉率
关键业务成功率

路径指标

工具调用成功率
错误工具调用率
工具调用次数
最大轮数触发率
handoff 率
guardrail 阻断率

系统指标

P50/P95/P99 延迟
模型超时率
Tool 超时率
Token 使用量
外部 API 错误率
队列积压

如果最终答案成功率稳定,但 refund.create 的重复调用率上升,仍然应暂停灰度。因为结果指标可能尚未反映已经发生的外部副作用。

4. 自动停止条件要分硬门槛和软门槛

rollout:
  hard_stop:
    - metric: unsafe_tool_call_rate
      operator: ">"
      threshold: 0.001
    - metric: refund_duplicate_rate
      operator: ">"
      threshold: 0.0001

  soft_alert:
    - metric: task_success_rate
      operator: "<"
      threshold: 0.94
    - metric: p95_latency_ms
      operator: ">"
      threshold: 5000

硬门槛表示必须停止或回滚;软门槛表示需要人工分析。不能把所有指标都放入自动回滚,否则短时外部依赖抖动可能造成不必要的版本震荡。


十、回滚:回滚流量,不一定能回滚状态

传统服务回滚通常是:

停止新二进制 → 启动旧二进制

Agent 回滚更复杂,因为新版本可能已经:

  • 写入 Memory;
  • 创建订单、退款或工单;
  • 发送消息;
  • 修改数据库;
  • 触发下游异步任务;
  • 产生用户可见但不正确的答案。

因此需要区分三种回滚:

1. 配置回滚

只恢复:

model
prompt
tool routing
guardrail
灰度比例

适合发现答案质量下降但没有外部副作用的情况。

2. 代码回滚

恢复应用代码和编排逻辑。要求旧代码能够读取新版本可能产生的状态。

3. 状态补偿

对已经发生的外部副作用执行业务补偿,例如:

错误创建退款申请 → 取消待处理退款
错误发送通知 → 发送更正通知
错误写入 Memory → 撤销错误记忆
错误创建工单 → 标记为自动创建并转人工复核

补偿不是数据库回滚。跨系统调用一旦成功,就不能依赖本地事务撤销。

4. 回滚前检查清单的实际顺序

生产操作应按以下顺序执行:

1. 关闭 candidate 的新增流量
2. 保留现有 candidate 请求的 trace 和状态
3. 将路由切回 stable
4. 禁止 candidate 执行高风险写 Tool
5. 查询 candidate 时间窗口内的外部副作用
6. 对需要补偿的操作生成补偿任务
7. 验证 stable 能读取 candidate 写入的 Memory
8. 观察 stable 的错误率和延迟
9. 冻结 candidate manifest,不覆盖现场

第 2 步不能省略。直接删除新版本日志会使事故失去证据;而没有 release_idtrace_idtool_call_id 的关联,也无法准确判断哪些外部写操作来自 candidate。


十一、失败路径:Agent 发布必须建模中间状态

下面是一个典型的“Tool 成功但 Agent 失败”场景:

T0  Agent 调用 refund.create
T1  外部退款系统成功创建退款 refund_7788
T2  Agent 服务等待响应时连接断开
T3  客户端重试同一请求
T4  Agent 再次调用 refund.create

如果 Tool 没有幂等保护,就会产生重复退款申请。

正确的状态机应把“未知结果”单独建模:

stateDiagram-v2
    [*] --> Prepared
    Prepared --> Calling: 发送 Tool 请求
    Calling --> Succeeded: 收到成功响应
    Calling --> Failed: 明确失败响应
    Calling --> Unknown: 超时/连接断开
    Unknown --> ReconciledSuccess: 查询到外部成功
    Unknown --> ReconciledFailed: 查询到外部失败
    Unknown --> Retryable: 具备安全重试条件
    Retryable --> Calling: 使用同一幂等键重试
    Succeeded --> [*]
    Failed --> [*]
    ReconciledSuccess --> [*]
    ReconciledFailed --> [*]

Unknown 不能直接转成 Failed。因为失败意味着可以安全重试,而未知结果并不满足这个条件。

诊断时应关联:

run_id
trace_id
tool_call_id
idempotency_key
external_operation_id
release_id

如果缺少其中任意一个字段,故障处理可能只能通过时间和业务参数猜测,无法证明某一次外部操作是否已经发生。


十二、发布事故的错误分类和处理边界

不同错误的止损动作不同。

错误类型 典型表现 首要动作 是否需要补偿
模型质量回归 答案不准确、工具选择变差 停止灰度、切回稳定模型 通常不需要
Prompt 策略错误 澄清过多、拒答过多、越权指令未生效 回滚 Prompt 取决于是否执行 Tool
Tool schema 错误 参数解析失败、工具调用率骤降 恢复兼容 schema 可能需要
Tool 外部服务故障 超时、5xx、限流 熔断或降级 视未知结果而定
Memory 污染 错误偏好、错误事实持续影响回答 停止写入、撤销错误记忆 通常需要数据修复
路由错误 candidate 获得超出预期流量 关闭路由规则 通常不需要
观测缺失 无法确认版本或工具路径 先冻结变更和扩大日志 可能无法安全回滚

这里的“回滚成功”不能只看服务健康检查。至少要验证:

旧版本流量占比恢复
新版本流量归零或符合预期
关键 Tool 调用恢复
Memory 读取无 schema 错误
外部副作用没有继续增长
关键业务成功率恢复

十三、一个可执行的发布目录结构

可以用如下目录组织版本资产:

agent/
├── releases/
│   ├── agent-prod-2026-09-01.2.yaml
│   └── agent-prod-2026-09-01.3.yaml
├── prompts/
│   ├── support-agent/
│   │   ├── 17.md
│   │   └── 18.md
├── tools/
│   ├── order.read/
│   │   ├── 3.schema.json
│   │   └── 3.adapter.py
│   └── refund.create/
│       ├── 2.schema.json
│       └── 2.adapter.py
├── memory/
│   ├── migrations/
│   │   ├── 004_to_005.sql
│   │   └── rollback_005_to_004.sql
│   └── projections/
│       └── customer-memory-read-v5.py
├── evals/
│   ├── datasets/
│   │   └── support-regression-v12.jsonl
│   └── graders/
│       ├── tool-path.py
│       └── safety.py
└── runbooks/
    ├── canary.md
    └── rollback.md

发布命令可以要求 manifest 先通过兼容性检查:

python -m agentctl validate-release \
  --manifest releases/agent-prod-2026-09-01.3.yaml \
  --dataset evals/datasets/support-regression-v12.jsonl

预期输出:

manifest: valid
prompt/tool compatibility: valid
memory read compatibility: valid
required evals: 4
passed evals: 4
hard-gate regressions: 0
release: eligible

如果输出为:

memory read compatibility: failed
old runtime supports schema [1, 2, 3, 4]
candidate writes schema 5
release: blocked

发布系统应阻断,而不是由人工“先发上去看看”。原因是灰度期间旧版本仍会处理请求,兼容问题必然在生产中出现。


十四、常见误解

误解一:回滚应用镜像就等于回滚 Agent

不成立。模型、Prompt、Tool 和 Memory 可能仍然是新版本。应回滚完整版本向量,或者明确执行“只回滚代码”的局部操作。

误解二:Prompt 只是配置,不需要版本评审

不成立。Prompt 改变工具路径、拒答边界和记忆写入策略时,实际改变的是执行策略。

误解三:Tool 返回 200 就说明 Agent 执行正确

不成立。HTTP 成功只说明请求被服务接受。还要验证业务状态、幂等性、返回字段语义和 Agent 后续是否正确解释结果。

误解四:灰度 5% 足以代表生产行为

不一定。如果灰度样本没有覆盖高风险请求、长对话、异常 Tool 结果和已存在 Memory,5% 可能只验证了最容易的路径。

误解五:相同输入和相同模型就能完全重放

不一定。还需要固定 Prompt、Memory、Tool 结果、时间、随机性、外部数据和编排器版本。若模型服务本身不提供固定快照,重放通常只能验证行为类别,而不是字节级输出。

误解六:日志越多越好

不成立。trace 可能包含用户输入、Memory、工具参数和敏感业务数据。必须进行脱敏、访问控制、保留周期管理,并区分“用于调试的完整证据”和“用于指标聚合的最小数据”。OpenAI Agents SDK 文档也将敏感数据处理作为 tracing 的独立主题;具体保留和脱敏策略需要结合组织的数据政策决定。(openai.github.io)


十五、推荐的发布状态机

一个完整的 Agent 发布可以建模为:

stateDiagram-v2
    [*] --> Draft
    Draft --> Validated: manifest/兼容性检查通过
    Validated --> Evaluated: 离线评测通过
    Evaluated --> Shadow: 影子流量观察
    Shadow --> Canary: 关键指标正常
    Canary --> Expanding: 灰度门槛通过
    Expanding --> Stable: 全量完成且观察期结束

    Shadow --> Halted: 硬门槛触发
    Canary --> Halted: 硬门槛触发
    Expanding --> Halted: 硬门槛触发

    Halted --> RolledBack: 流量切回旧版本
    RolledBack --> Compensating: 存在外部副作用或错误记忆
    Compensating --> Closed: 补偿验证完成
    RolledBack --> Closed: 无需补偿

    Stable --> Deprecated: 新版本替代
    Deprecated --> Archived: 保留证据后归档

每次状态转移都应产生审计记录:

{
  "release_id": "agent-prod-2026-09-01.3",
  "from": "canary",
  "to": "halted",
  "reason": "unsafe_tool_call_rate_above_threshold",
  "actor": "rollout-controller",
  "observed_at": "2026-09-01T10:15:00+08:00",
  "evidence": {
    "trace_query": "release_id=... AND tool=refund.create",
    "sample_count": 842,
    "violations": 3
  }
}

这样,发布控制器的决策不是“监控报警后人工凭感觉切流量”,而是“基于固定版本、固定阈值和可查询证据的状态转换”。


十六、最终的治理原则

Agent 发布的核心问题不是“新版本能不能启动”,而是:

新版本是否能在可控风险下改变行为,并在失败时恢复服务和业务状态\text{新版本是否能在可控风险下改变行为,并在失败时恢复服务和业务状态}

要满足这个条件,至少需要:

  1. 用完整 manifest 固定模型、Prompt、Tool、Memory、Guardrail、路由和代码;
  2. 将版本作为向量管理,而不是只管理容器镜像;
  3. 将 Tool schema、语义和副作用分别进行兼容性分析;
  4. 将 Memory 视为持久化协议,区分读取版本、写入版本和候选记忆;
  5. 使用模型替身、Tool Stub 和录制数据验证确定性编排逻辑;
  6. 使用 trace、grader、dataset 和 eval run 分别处理调试、评分和持续回归;
  7. 灰度时按风险分层,而不是只按请求百分比;
  8. 为每个请求记录 release_id、trace_id、Tool 调用和外部操作标识;
  9. 将回滚分成流量回滚、代码回滚和状态补偿;
  10. 对“外部调用结果未知”单独建模,避免把超时误判为失败;
  11. 保留事故现场和版本证据,不覆盖或删除 candidate 的运行记录;
  12. 将发布状态转换、停止条件和恢复动作写入可执行 runbook。

当模型、Prompt、Tool 和 Memory 都能被准确标识,灰度行为能够被观测,失败状态能够被隔离,外部副作用能够被补偿时,Agent 才具备接近传统生产系统的发布可控性。此时版本治理不再是给 Prompt 加编号,而是对一整个概率性、持久化、可调用外部系统的运行时进行工程化控制。


系列导航与关联阅读

官方资料

本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。