一篇活动稿已经完成审核。运营负责人确认标题、正文和两张配图无误,点击“同意发布”。

十分钟后,另一个人替换了一张图、调整了标题和可见范围。定时任务仍读取到 approved=true,于是把修改后的版本发了出去。

审批人没有批准过这个版本,系统却认为它已经批准。这类问题的根因通常不是模型判断错误,而是审批数据模型太弱:它只保存“同意”,没有保存“同意了什么”。

GitHub 2026 年 9 月 1 日发布的 Copilot Pull Request 审批能力,给了一个很有价值的工程参照。官方明确区分两件事:

  • approval assessment 是 Copilot 对“是否可批准”的判断,本身不计入合并要求;
  • 真正可计入规则的 approval 默认关闭,必须由管理员显式授权。

权限还可以在企业、组织、仓库层配置,并限制允许审批的文件路径。Copilot 批准后如果出现新提交,原批准会被解除,需要重新评审。

这篇文章不讨论 Copilot 的评审质量,而是把这套边界抽象成一个通用的 Agent 审批链:适用于内容发布、付款、客户外联、生产变更和批量文件处理。

审批不是布尔值,而是一条带证据的租约

一、先拆开 assessment、authorization 和 execution

最危险的模型通常长这样:

{
  "taskId": "publish-42",
  "approved": true
}

它把三个不同主体的责任压进了一个字段:

层级回答的问题典型主体
Assessment当前证据看起来是否满足要求?模型、规则引擎、质检 Agent
Authorization谁有权允许这个动作?业务负责人、权限策略服务
Execution当前版本是否仍可安全执行?发布器、付款器、部署器

更合适的数据结构是分别记录:

type Assessment = {
  decision: "PASS" | "FAIL" | "UNCERTAIN";
  confidence?: number;
  evidenceIds: string[];
  rationale: string;
};

type Authorization = {
  state: "PENDING" | "APPROVED" | "REJECTED" | "STALE";
  approver: string;
  policyVersion: string;
  allowedAction: string;
};

type Execution = {
  state: "READY" | "RUNNING" | "SUCCEEDED" | "FAILED" | "RESULT_UNKNOWN";
  idempotencyKey: string;
  resultEvidence?: string[];
};

模型可以生成 assessment,但只有拥有权限的主体才能签发 authorization。执行器则必须重新检查 authorization 是否仍适用于当前对象。

二、把 approval 设计成“证据租约”

审批不是永久事实,而是在有限时间、有限版本、有限动作上有效的一份租约。

一份最小可用的 ApprovalLease 可以这样表示:

type ApprovalLease = {
  approvalId: string;
  subject: {
    type: "content_post" | "payment" | "deployment" | "outreach";
    id: string;
    version: string;
  };
  assessmentHash: string;
  evidenceHash: string;
  policyVersion: string;
  approver: {
    principalId: string;
    role: string;
  };
  allowedAction: {
    name: string;
    resource: string;
    constraints: Record<string, string | number | boolean>;
  };
  issuedAt: string;
  expiresAt: string;
  invalidateOn: InvalidationReason[];
};

type InvalidationReason =
  | "SUBJECT_CHANGED"
  | "EVIDENCE_CHANGED"
  | "POLICY_CHANGED"
  | "ACCOUNT_CHANGED"
  | "SCOPE_CHANGED"
  | "LEASE_EXPIRED";

这里最重要的不是字段数量,而是五个绑定关系:

  1. 绑定对象版本,而不是只绑任务 ID;
  2. 绑定审批时看到的证据;
  3. 绑定当时生效的权限策略;
  4. 绑定允许执行的具体动作与范围;
  5. 明确什么变化会让批准失效。

三、为内容发布建立可重复计算的版本哈希

代码评审可以直接绑定 commit SHA。内容发布没有天然的提交哈希,需要先建立规范化清单。

subject:
  platform: csdn
  accountId: writer-001
  title: "AI Agent 审批链设计"
  bodySha256: "..."
  summarySha256: "..."
  imageSha256:
    - "cover:..."
    - "body-1:..."
    - "body-2:..."
  tags:
    - 人工智能
    - AI Agent
  articleType: original
  visibility: public
  syndication: false

计算前必须规范化:

  • 标签排序,避免顺序不同造成无意义变更;
  • 换行统一为 \n
  • 去掉正文末尾无意义空白;
  • 图片使用内容哈希或稳定资源 ID,不能只用临时 URL;
  • 布尔值和枚举使用固定表示;
  • 不把页面时间戳等易变字段放进哈希。
function canonicalize(input: PublishManifest): string {
  const stable = {
    ...input,
    title: input.title.trim(),
    body: input.body.replace(/\r\n/g, "\n").trimEnd(),
    tags: [...input.tags].sort(),
    images: [...input.images]
      .map(({ role, sha256 }) => ({ role, sha256 }))
      .sort((a, b) => a.role.localeCompare(b.role)),
  };
  return stableJson(stable);
}

function subjectVersion(input: PublishManifest): string {
  return sha256(canonicalize(input));
}

这样,“审批后换图”“审批后改可见范围”“审批后换账号”都会得到新的 subjectVersion

四、证据快照不能只保存一句“检查通过”

审批人真正看到的内容,要变成可回读的证据:

{
  "evidenceVersion": 1,
  "capturedAt": "2026-09-03T10:42:00+08:00",
  "source": "real-platform-page",
  "checks": [
    { "name": "account", "expected": "writer-001", "actual": "writer-001", "ok": true },
    { "name": "title", "actualHash": "sha256:...", "ok": true },
    { "name": "bodyImages", "expected": 2, "actual": 2, "ok": true },
    { "name": "cover", "actual": "loaded", "ok": true },
    { "name": "visibility", "actual": "public", "ok": true },
    { "name": "syndication", "actual": false, "ok": true }
  ]
}

不要只信预填接口返回 ok=true。如果真正执行发生在第三方页面,就应从真实页面读取标题、正文首尾、图片状态和发布选项。工具返回证明“自动化尝试过”,页面快照才证明“用户将要提交什么”。

五、状态机:对象一变,批准立刻过期

审批租约的状态变化:批准、失效与重新评审

推荐状态机:

DRAFT
  → ASSESSED
  → WAITING_APPROVAL
  → APPROVED
  → EXECUTING
  → SUCCEEDED

APPROVED
  → STALE_APPROVAL    # 对象、证据、策略或范围变化
  → WAITING_APPROVAL  # 重新构建证据后再审

EXECUTING
  → FAILED
  → RESULT_UNKNOWN    # 已点击,但无法确认平台结果

关键约束是:任何修改事件都不能直接把状态留在 APPROVED

ApprovalLease invalidateIfChanged(
    ApprovalLease lease,
    String currentSubjectVersion,
    String currentEvidenceHash,
    String currentPolicyVersion) {

  if (!lease.subject().version().equals(currentSubjectVersion)) {
    return lease.stale("SUBJECT_CHANGED");
  }
  if (!lease.evidenceHash().equals(currentEvidenceHash)) {
    return lease.stale("EVIDENCE_CHANGED");
  }
  if (!lease.policyVersion().equals(currentPolicyVersion)) {
    return lease.stale("POLICY_CHANGED");
  }
  if (Instant.now().isAfter(Instant.parse(lease.expiresAt()))) {
    return lease.stale("LEASE_EXPIRED");
  }
  return lease;
}

这和 GitHub 在新提交后解除旧批准的逻辑相似:批准针对的是当时那份 diff,不是这个 PR 名称未来产生的所有内容。

六、执行前使用 compare-and-act

仅在页面回读后比较哈希还不够。如果“校验”和“点击”之间还能被并发修改,就存在 TOCTOU(检查时与使用时不一致)问题。

执行器至少应完成:

1. 读取真实目标状态
2. 重新计算 subjectVersion / evidenceHash
3. 校验 approval lease
4. 原子领取执行权
5. 执行动作
6. 回读平台结果
7. 写入结果证据

数据库层可以用条件更新防止重复领取:

UPDATE approval_execution
SET state = 'EXECUTING',
    started_at = CURRENT_TIMESTAMP
WHERE approval_id = :approvalId
  AND subject_version = :subjectVersion
  AND state = 'APPROVED'
  AND expires_at > CURRENT_TIMESTAMP;

受影响行数必须等于 1。若为 0,可能是批准已被消费、已过期或版本不匹配,应停止而不是重试点击。

幂等键建议包含:

sha256(approval_id + subject_version + allowed_action + target_account)

它的作用不是保证第三方平台一定幂等,而是防止本系统把同一份批准并发消费两次。

七、按风险定义失效矩阵

不是所有变化都要找人重审。可以先建立确定性的失效矩阵:

变化低风险动作高风险动作
仅格式空格变化自动重新计算并留痕规则允许时自动续签
同哈希资源重新上传自动确认自动确认
标题、正文语义变化重新评审重新评审
目标账号变化重新授权重新授权
可见范围变化重新授权重新授权
金额、收件人、生产环境变化不适用必须重新授权
工具权限扩大重新授权阻断并升级
结果无法确认停在 RESULT_UNKNOWN停在 RESULT_UNKNOWN

“自动续签”只适合规则能确定证明等价的变化。不要让另一个 LLM 自己判断“这次修改应该不影响审批”,否则审批边界又回到了概率输出。

八、错误码要让操作人员知道下一步

建议至少提供:

[
  "APPROVAL_SUBJECT_CHANGED",
  "APPROVAL_EVIDENCE_CHANGED",
  "APPROVAL_POLICY_CHANGED",
  "APPROVAL_SCOPE_MISMATCH",
  "APPROVAL_EXPIRED",
  "APPROVAL_ALREADY_CONSUMED",
  "EXECUTION_RESULT_UNKNOWN"
]

每个错误码都应配最小恢复动作:

  • APPROVAL_SUBJECT_CHANGED:重新构建快照并请求评审;
  • APPROVAL_SCOPE_MISMATCH:不能扩大动作范围,必须重新授权;
  • EXECUTION_RESULT_UNKNOWN:只读回查结果,不得盲目重复提交。

九、这也是我们做 Tipkay 时的一个取舍

我们在 Tipkay 的博客发布助手里,把研究、写稿、配图、平台预填和最终提交拆成不同阶段。助手可以把内容送到编辑器,但在最后提交前仍要从真实页面回读账号、标题、正文、图片、标签和可见范围。

这不是为了多一个确认步骤,而是让批准绑定实际交付物。生成完成不等于工作完成,工具返回成功也不等于平台里的当前版本已被正确提交。

Tipkay 面向小微企业、一人公司和小团队,提供按实际使用量计费的垂类 AI 员工;每个助手带着自己的岗位经验、流程、Skill、MCP 和工具。对小团队而言,岗位分工真正有价值的前提,是每次交接都有明确对象、证据和责任边界。

一个人的生意,也能有一支专业团队。但专业团队不会把一句“没问题”当作永久授权。

十、可直接执行的落地顺序

  1. 列出所有会产生外部副作用的 Agent 动作;
  2. 将 assessment、authorization、execution 拆成独立状态;
  3. 为每类对象定义规范化 manifest 和版本哈希;
  4. 保存真实执行目标的证据快照;
  5. 定义权限策略版本、批准主体和动作范围;
  6. 列出对象、证据、策略、账户与范围的失效条件;
  7. 用条件更新或锁原子领取执行权;
  8. 提交后回读真实结果,无法确认时进入 RESULT_UNKNOWN;
  9. 用变更后旧批准失效、重复提交、并发领取和超时场景做契约测试。

最后验收时,不要只问“审批按钮能不能点”。应验证:

  • 批准后改标题,旧批准是否失效;
  • 批准后换账号,执行是否被阻断;
  • 同一批准并发执行两次,是否只有一次领取成功;
  • 点击后页面没有明确结果,系统是否停止而非再次点击;
  • 审计日志能否还原批准时对象、证据、策略和权限。

这些测试通过,approval 才从界面上的布尔值,变成执行器可以信任的工程对象。

参考资料

  • GitHub Changelog, Copilot code review can now approve pull requests: https://github.blog/changelog/2026-09-01-copilot-code-review-can-now-approve-pull-requests/
  • GitHub Docs, Configuring code review by GitHub Copilot: https://docs.github.com/en/copilot/how-tos/copilot-on-github/set-up-copilot/configure-code-review
  • GitHub Docs, Available rules for rulesets: https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets
  • GitHub Docs, REST API endpoints for pull request reviews: https://docs.github.com/en/rest/pulls/reviews
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐