客服 Agent 昨天还会从商品库读取当前价格,今天却在回复里引用了一段旧 FAQ。调用链没有异常,JSON 结构也完全正确。
排查后才发现,团队做了三个看似独立的小改动:换了默认模型,重新索引知识库,把查询工具从 v2 升到 v3。每个改动单独看都能运行,组合起来却已经不是上周批准上线的那个岗位版本。
Agent 进入长期生产后,真正麻烦的不是 crash,而是 silent drift:它还在工作,却不再按岗位约定工作。
先做 diff,再问"要不要修"
最近 Google Cloud 在 VM Extension Manager 的官方更新中提到声明式策略、持续漂移检测、分阶段发布和自动回滚。官方讨论的是 VM 扩展治理,并不是 AI Agent。这里借用的是控制面的思路:先描述目标状态,再持续比较观测状态。

不要把目标状态压成一段 system prompt。一个岗位版本至少要锁住这些内容:
ts
type RoleRelease = {
id: string;
modelPolicy: {
primary: string;
fallbacks: string[];
paramsHash: string;
};
instructionBundle: string;
skills: Array<{ name: string; version: string; checksum: string }>;
tools: Array<{
name: string;
schemaHash: string;
permission: "read" | "draft" | "execute";
approval: "never" | "before_execute";
}>;
knowledgeSnapshot: string;
outputSchema: string;
evalSuite: string;
rollbackTo: string;
};
这份 release 不负责动态发现所有能力。它只记录当前岗位已经批准使用的组合,是下一次 diff、评测和回滚的锚点。
配置相同,也可能已经漂移
如果 reconciler 只对比 checksum,它最多发现"装错了什么",发现不了"同样的配置为什么做差了"。因此观测数据至少分两组:
ts
type RoleObservation = {
inventory: {
model: string;
skillChecksums: string[];
toolSchemas: string[];
knowledgeSnapshot: string;
approvalPolicy: string;
};
behavior: {
contractPassRate: number;
citationCoverage: number;
policyViolationCount: number;
humanRewriteRate: number;
costPerTask: number;
p95Ms: number;
};
};
模型别名背后的实现变化、知识检索分布变化、上游接口返回值变化,都可能让 inventory 一致但 behavior 退化。
这里有个重要边界:humanRewriteRate 上升不一定是模型退化,也可能是本周任务更难。行为指标适合触发复核,不能独立充当裁决器。
Reconciler 应该输出决策,不要直接"自愈"
轻量 reconciler 可以保持为纯函数:
ts
type Action =
| { type: "NOOP" }
| { type: "RESTORE_PINNED_CONFIG"; paths: string[] }
| { type: "RUN_EVAL"; suite: string }
| { type: "REQUEST_APPROVAL"; reasons: string[] };
function decide(
desired: RoleRelease,
observed: RoleObservation,
diff: Diff,
): Action {
if (diff.empty) return { type: "NOOP" };
if (diff.expandsPermissions || diff.removesApproval) {
return { type: "REQUEST_APPROVAL", reasons: diff.riskReasons };
}
if (diff.onlyPinnedArtifactsChanged && diff.isReversible) {
return { type: "RESTORE_PINNED_CONFIG", paths: diff.paths };
}
return { type: "RUN_EVAL", suite: desired.evalSuite };
}
自动恢复只适用于可确定比较、可逆、不会扩大权限的字段。模型行为、知识语义、外部写权限都不应被一个 autoHeal=true 粗暴覆盖。
给岗位升级加一个 canary cohort

一次发布可以先通过离线 fixture,再按稳定哈希分配少量真实任务:
ts
function chooseRelease(taskId: string, plan: RolloutPlan): string {
const bucket = stableHash(`${plan.salt}:${taskId}`) % 100;
return bucket < plan.canaryPercent
? plan.candidateRelease
: plan.stableRelease;
}
为什么用稳定哈希,而不是每次随机?同一个任务重试时应该留在同一 cohort,否则一次失败重试可能换版本,排查证据会被污染。
canary 的门禁不要只看总成功率。至少分别监控:
- 输出契约通过率;
- 来源引用覆盖率;
- 权限违规次数;
- 人工改写或驳回率;
- 单任务成本与 P95 时延;
- fallback 和人工接管比例。
高风险指标一旦越界,停止扩大 cohort。外部发布、付款、删除文件等动作,即使质量指标漂亮,也应保留独立审批门槛。
本地 override 不是"脏数据"
全局岗位模板经常允许门店、项目或个人做定制。全局版本升级时,本地 override 不能被静默覆盖。
官方的全局 VM 扩展策略文档也专门定义了冲突行为,默认不覆盖独立修改,并允许显式选择覆盖。映射到岗位版本,可以保存三方 diff:
ts
type MergeInput = {
base: RoleRelease;
desired: RoleRelease;
local: Partial<RoleRelease>;
};
type MergeResult = {
merged?: RoleRelease;
conflicts: Array<{
path: string;
severity: "semantic" | "permission";
}>;
};
门店地址这类数据可能安全合并,价格来源、审批策略和写权限则应进入人工裁决。把所有 override 当作脏数据清掉,短期整齐,长期一定伤害业务。
回滚的是 release,不是某一个模型名
一个 Agent release 是经过测试的组合:模型、指令、Skill、工具 schema、知识快照和权限策略。只把模型切回旧版,剩余组件仍是新版,就构造了一个从未验证过的状态。
所以回滚应该是原子的:把流量路由回上一份不可变 release。候选 release 可以继续保留,用于复现失败,不要在事故中直接覆盖它。
重试也要幂等:
ts
const idempotencyKey = [
tenantId,
roleId,
candidateRelease,
"rollout",
].join(":");
同一个 key 返回原发布任务,避免网络抖动创建两组 canary。
别把控制面做成新的配置税
小团队不需要先建设"Agent Kubernetes"。可以从最小闭环开始:
- 选一个会对外写入的高风险岗位;
- 保存一份不可变 release manifest;
- 准备十几个覆盖失败边界的 fixture;
- 只设一个 canary 百分比和一个稳定回滚版本;
- 把 diff、评测、批准和回滚写入同一审计记录。
低风险草稿、一次性脑暴没有必要通过完整发布链。治理的目标是降低不确定性,不是增加一套每天要维护的表单。
岗位产品真正要保留什么
这也是我们做 Tipkay 时的一个取舍。Tipkay 面向小微企业、一人公司和小团队,提供按需使用的垂类 AI 员工。不同岗位助手可以复制、定制和协作,携带各自的 Skill、MCP、工具与流程。
这样的系统不能只解决"第一次配置成功",还要避免岗位经验在模型、工具和知识变化时悄悄丢失。本文的基线与发布控制面是一种工程方法,不代表语义差异都应自动修复;高风险变化仍需要评测与明确批准。
Agent 上线后的关键指标,不只是今天成功调用了多少次。更值得追问的是:当前运行的到底是哪份岗位 release,它和批准版本差在哪里,失败后能否回到一个经过验证的组合。