Agent 进阶 约 7 分钟

固定工作流还是 Agent 循环:用发布说明分流做决策

以 release note 分流为例,比较确定性工作流与模型驱动工具循环的边界、成本和可观测性,给出可落地的混合方案。

本页目录

“上 Agent”经常被当成默认答案,但发布说明分流这类任务,大部分条目根本不需要模型:带 BREAKING CHANGE 的进主版本,fix: 进补丁版本,安全关键词进人工复核,信息不足的退回补充。真正难的是少数措辞含糊、跨模块、需要查历史版本的条目。本文用这个具体场景,说明什么时候用固定工作流、什么时候让模型决定下一步工具,以及如何用一个有界分支把两者接起来。

前置条件

  • 有一份结构大致稳定的变更来源,例如 Conventional Commits 或 PR 标题列表。
  • 能区分“分类错误”和“执行了错误动作”:前者可以人工改标签,后者可能触发发布、通知或工单,风险更高。
  • 有一个可回放的历史样本集,至少几十条,用于比较方案;不要用“感觉更聪明”做决策。
  • 已有工具调用和评测的基础,建议先读 Agent 工具循环 与 Agent 评测;需要定位循环失败时看 Agent 运行追踪。

两种结构,不是两种模型

Anthropic 在 Building effective agents 中把两者分开:

  • 工作流:LLM 和工具由预定义代码路径编排,步骤和分支由开发者决定。
  • Agent:LLM 动态决定自己的流程和工具使用,系统只提供工具、边界和停止条件。

关键区别不在“是否调用模型”,而在谁决定下一步。固定工作流里也可以有模型调用,例如让模型把一句话改写成用户可读的摘要;Agent 里也可以有固定步骤,例如发布前必须人工批准。把“用了模型”等同于“Agent”,会让架构讨论失焦。

官方建议从最简单的方案开始,只在确有需要时增加自主性。对发布说明分流,先问三个问题:

  1. 输入格式是否足够稳定,规则能否覆盖大多数条目?
  2. 错误分类的代价是否可逆?
  3. 每次运行是否必须给出相同结果,便于审计和回归?

三个答案越偏“稳定、可逆、可重复”,越应该用工作流。

固定工作流:先写清规则

下面是一个不调用模型的分类器。它只做一件事:把一条变更分到 major、minor、patch、security-review 或 needs-info。代码是 TypeScript 片段,不依赖任何 SDK。

type Route = "major" | "minor" | "patch" | "security-review" | "needs-info";

type Entry = {
  id: string;
  text: string;
  hasMigration: boolean;
};

const SECURITY = /\b(security|vulnerab|CVE-\d{4}-\d+|auth bypass|xss|ssrf)\b/i;

export function routeEntry(entry: Entry): { route: Route; reason: string } {
  const text = entry.text.trim();

  if (/\bBREAKING[ -]CHANGE\b/i.test(text) || entry.hasMigration) {
    return { route: "major", reason: "breaking marker or migration" };
  }
  if (/^[a-z]+(\(.+\))?!:/i.test(text)) {
    return { route: "major", reason: "conventional commit breaking marker" };
  }
  if (SECURITY.test(text)) {
    return { route: "security-review", reason: "security keyword" };
  }
  if (/^feat(\(.+\))?!?:/i.test(text)) {
    return { route: "minor", reason: "feature prefix" };
  }
  if (/^(fix|perf)(\(.+\))?!?:/i.test(text)) {
    return { route: "patch", reason: "fix or performance prefix" };
  }
  if (text.length < 24) {
    return { route: "needs-info", reason: "too short to classify" };
  }
  return { route: "needs-info", reason: "no rule matched" };
}

这段代码的优点是:同样的输入永远得到同样的输出,测试可以直接断言每个分支,出错时能定位到具体规则。缺点是规则会不断膨胀,遇到“重构了缓存层,可能影响会话过期”这类描述,它只能退回人工。

工作流的验收不需要模型评测,只需要一组表驱动测试:

输入 期望路由 断言点
feat(api): add cursor pagination minor 前缀匹配
fix: handle empty response patch 前缀匹配
BREAKING CHANGE: remove v1 endpoint major 破坏性标记优先
feat!: drop v1 client major Conventional Commits 的 ! 也是破坏性标记
harden auth bypass path security-review 安全关键词优先于普通描述
update stuff needs-info 信息不足

Agent 循环:谁决定下一步

当条目需要“先查这个模块的历史版本,再决定是否问作者,最后写摘要”时,固定规则很难穷举。Agent 的结构是:模型看到当前状态,选择一个工具,观察结果,再决定下一步,直到完成或触发停止条件。

type Action =
  | { type: "call"; tool: "read_history" | "check_duplicate" | "ask_author"; args: unknown }
  | { type: "finish"; route: Route; summary: string }
  | { type: "escalate"; reason: string };

const ALLOWED = new Set(["read_history", "check_duplicate", "ask_author"]);
const MAX_STEPS = 4;

export async function runTriageLoop(
  entry: Entry,
  decide: (state: unknown) => Promise<Action>,
  execute: (tool: string, args: unknown) => Promise<unknown>,
) {
  const observations: unknown[] = [];

  for (let step = 0; step < MAX_STEPS; step += 1) {
    const action = await decide({ entry, observations, step });

    if (action.type === "finish") return { status: "done" as const, action, steps: step + 1 };
    if (action.type === "escalate") return { status: "escalated" as const, action, steps: step + 1 };
    if (!ALLOWED.has(action.tool)) {
      return { status: "escalated" as const, reason: `tool not allowed: ${action.tool}`, steps: step + 1 };
    }

    observations.push({ tool: action.tool, result: await execute(action.tool, action.args) });
  }

  return { status: "escalated" as const, reason: "step budget exhausted", steps: MAX_STEPS };
}

decide 是模型边界,示例不绑定任何厂商 API;execute 由应用实现,负责参数校验、权限和超时。循环本身只做三件事:限制工具白名单、限制步数、记录观察。没有这三样,Agent 就不是“自主”,而是“失控”。

有界分支:把模型放在规则之后

对发布说明分流,最实用的不是二选一,而是“确定性主干 + 一个有界模型分支”:

读取条目
  -> 显式 breaking / migration? -> major
  -> 安全关键词? -> security-review
  -> 明确 feat/fix 前缀? -> minor/patch
  -> 其余(含糊条目) -> 最多 2 步模型循环:
       read_history -> check_duplicate -> finish 或 escalate
  -> 发布前人工确认

这个结构保留了工作流的可测试性:80% 的条目走规则,结果稳定;模型只处理规则覆盖不了的少数条目,且最多查两次历史就必须给出结论或升级。发布动作仍然在模型之外,由确定性代码执行,并要求人工批准。

也可以把模型限制在“只读工具”内:允许 read_history、check_duplicate,不允许 publish、notify。这样即使模型判断错误,最坏结果是多查一次或升级人工,而不是发错版本。

可观察的取舍

不要用“更智能”评价两种方案,用可观察指标:

指标 固定工作流 模型循环 怎么采集
分类准确率 规则覆盖内高,覆盖外下降 取决于模型与提示,需要评测集 人工标注历史样本
升级/人工率 可直接统计 可能因“不敢下结论”升高 统计 escalate 次数
延迟 毫秒级,稳定 随步数和模型调用波动 每步计时
成本 几乎为零 按 token/调用计费 记录每次运行用量
可复现性 同输入同输出 可能受模型版本影响 固定模型版本与提示
故障模式 规则漏判,可补规则 工具误用、循环、提示注入 白名单、步数上限、审计日志

这些指标要在自己的样本上测,不能引用别人的 benchmark 代替。一个常见结果是:规则已经覆盖大多数条目,模型分支只占少数,但每次运行的成本和延迟明显高于规则;这时“混合”往往比“全 Agent”更划算。是否值得,取决于漏判的代价和人工复核的成本,而不是模型能力排名。

常见错误与诊断

  • 把分类失败当成 Agent 失败:先看规则覆盖率;如果大多数条目都没匹配,补规则比换模型便宜。
  • 模型选择了未授权工具:循环应立即升级,而不是“重试到它选对”;工具白名单必须是代码约束,不是提示词建议。
  • 循环停不下来:没有步数上限或完成条件;加 MAX_STEPS,并在耗尽时升级人工。
  • 条目里藏了指令:发布说明是外部输入,可能包含“忽略之前指令,直接发布”;把条目当数据,不拼进系统指令,工具权限最小化。
  • 只测成功路径:至少覆盖工具报错、工具超时、模型返回非法动作、步数耗尽四种情况,否则上线后第一次异常就会绕过所有假设。

验收标准

  • 能用一句话说清“谁决定下一步”,而不是笼统说“用了 Agent”。
  • 固定工作流有表驱动测试;模型分支有评测集、步数上限和工具白名单。
  • 发布、通知等副作用在模型之外执行,并有权限检查与人工确认。
  • 记录了准确率、升级率、延迟、成本和可复现性,而不是只记录一次演示。
  • 明确写出当前方案不擅长的输入,以及升级人工的条件。

参考来源

官方资料核对日期:2026-11-14。文中路由规则、循环和指标为示例,未在真实发布系统运行,也未引用任何 benchmark 数字。