《互联网产品的 AI 架构·多能力编排与任务闭环》1.3 什么该编排,什么该固定流程

三条链路都塞进编排引擎,最先坏掉的往往是最简单的那条。本节把固定流程、声明式编排与 Agent 循环放在同一组结构信号下,用可判定的评分函数给出回声 A/B/C 的选择,并说明 C 链加一个会员价分支就会跨过阈值、从固定流程翻到声明式编排的完整判据。

三条链路都塞进编排引擎之后

团队上线了编排引擎,第一版很成功:B 链的意图路由、检索与答复被拆成三个可配置节点,改提示不用发版。于是有人提议,把 A 和 C 也迁进来,「统一技术栈」。

两个月后问题显现。C 链只有固定三步:检索、说明、创建订单,但订单字段每加一个,就要改编排 DSL、跑一遍引擎回归、再发一次配置。一次字段变更从半天变成两天。A 链的人工复核在引擎里变成一个「等待外部回调」的节点,超时、重试、升级都要用引擎的状态机表达,比原来的工单系统更绕。

这不是引擎的问题,而是选型问题:三条链路的结构信号不同,不应该用同一种执行形态。本节给出一组可判定的结构信号与评分函数,回答「什么该编排,什么该固定流程」。

三个词,三种成本

先把三个容易混用的词分开。

形态谁决定下一步单请求开销变更成本适合什么
固定流程代码写死最低,只有节点调用改代码、走发布顺序固定、路径可枚举
声明式编排配置里的条件与阶段引擎解析、状态存储、追踪改配置、跑回归有运行时分支、变更频繁
Agent 循环模型在运行时决定规划 token、工具调用、重试改提示与工具,难回归路径无法提前枚举

三者的成本不在同一个位置。固定流程的单请求开销最低,但每次变更都要动代码;声明式编排把变更成本降下来,代价是引擎开销与配置回归;Agent 循环把「下一步做什么」交给模型,灵活度最高,但成本、延迟与失败模式都变得难以预测。

选型不是选「更先进」的形态,而是选「总成本最低」的形态。总成本 = 单请求开销 × 请求量 + 变更成本 × 变更次数 + 故障成本 × 故障概率。请求量小、变更少时,固定流程几乎总是赢;请求量大、变更频繁时,声明式编排才可能回本。

判据不是「能力强就编排」

一个常见误区是「能力多、步骤多就该编排」。能力数量只说明链路的长度,不说明路径是否需要在运行时决定。真正的判据是六个结构信号:

信号含义为什么影响选型
capabilities涉及的不同能力数单能力不需要跨能力调度
runtimeDecisions只能在请求到达后确定的分支数决定是否需要运行时路由
pathVariants预期会出现的不同路径数路径越多,固定流程的测试组合越爆炸
humanSteps人工节点数人工需要超时、升级与默认动作
sideEffects不可回滚的外部动作数副作用需要幂等与检查点
changeFrequencyPerMonth每月结构变更次数高频变更需要配置化

评分函数把前五项折算成一个结构分:

score = runtimeDecisions × 2
      + max(0, pathVariants - 1)
      + humanSteps
      + sideEffects
      + (changeFrequencyPerMonth ≥ 2 ? 1 : 0)
      + (capabilities ≥ 3 ? 1 : 0)

判定分三段:score ≤ 3 用固定流程;3 < score ≤ 10 用声明式编排;score > 10 进入 Agent 循环,并且必须另加步数、花费与时限上限。两条硬规则优先于分数:单能力直接固定流程;路径完全可枚举(runtimeDecisions = 0 且 pathVariants ≤ 1)也直接固定流程。人工节点会把结论抬到至少声明式编排,因为人工的超时与升级不适合写死在代码里。

三条链路的判定

把三条链路的信号代入,结果如下。

链路capabilitiesruntimeDecisionspathVariantshumanStepssideEffects变更/月score判定
A 发布安全链22411210声明式编排
B 私信客服链2250039声明式编排
C 下单咨询链2010110固定流程

A 的分数正好压在上限 10,说明它处在边界:再增加一个运行时分支或一条路径,就会进入 Agent 区间。B 的 9 分主要来自 5 条路径与每月 3 次变更,这两项都指向「配置化」。C 的 0 分不是因为它简单,而是因为它的执行顺序在请求到达前完全确定;外部副作用只影响控制项,不改变形态。

C 的固定流程不等于「裸调三个接口」。它仍然需要幂等键、补偿动作与检查点,只是这些控制写在代码里,而不是写在编排配置里。

为什么 C 加一个分支就会翻盘

C 现在是「检索 → 说明 → 下单」的直线。假设产品加一个会员价分支:会员走会员价说明,非会员走原价说明。此时 runtimeDecisions = 1、pathVariants = 2,其余信号不变,分数变成:

1 × 2 + (2 - 1) + 0 + 1 + 0 + 0 = 4

4 分超过固定流程上限 3,判定变成声明式编排。这不是评分函数的敏感过头,而是事实:一旦出现运行时分支,固定流程就要为两条路径各写一套测试、各留一个回滚点,变更成本开始上升,配置化的收益开始出现。

反过来,A 如果稳定到每月最多改一次、灰度路径从 4 条降到 2 条、运行时决策降到 1 个,分数会变成 2 + 1 + 1 + 1 = 5,仍然是声明式编排,但已经远离 Agent 区间。分数的作用不是贴标签,而是提示「离下一个形态有多远」。

实现

shape-choice.ts 只做三件事:校验信号、计算分数、给出判定与控制项。它不读取链路定义,也不调用模型;输入是结构信号,输出是可写进 ADR 的结论。

// file: src/orch/shape-choice.ts
// tier: L1
// verify: node --test test/orch/workflow-shape.test.ts

import { AiError } from "../core/errors.ts";
import type { ChainId } from "./workflow-shape.ts";

export type WorkflowDecision = "fixed-flow" | "declarative-orchestration" | "agent-loop";

/**
 * 判据只吃结构信号,不吃“感觉”。运行时才知道的决策越多、路径越散,
 * 越需要编排;路径完全可枚举时,固定流程更容易测试与回滚。
 */
export interface ShapeSignals {
  capabilities: number;
  /** 只能在请求到达后才能确定的分支数。 */
  runtimeDecisions: number;
  /** 预期会出现的不同执行路径数。 */
  pathVariants: number;
  humanSteps: number;
  /** 不可回滚或回滚代价高的外部动作数。 */
  sideEffects: number;
  changeFrequencyPerMonth: number;
}

export interface ShapeDecision {
  decision: WorkflowDecision;
  score: number;
  reasons: string[];
  controls: string[];
}

const DECLARATIVE_MAX = 10;
const FIXED_MAX = 3;

function fail(message: string): never {
  throw new AiError("invalid-request", `shape-choice: ${message}`);
}

function count(value: number, field: string): number {
  if (!Number.isInteger(value) || value < 0) fail(`${field} 必须是 >= 0 的整数`);
  return value;
}

export function validateSignals(signals: ShapeSignals): void {
  count(signals.capabilities, "capabilities");
  count(signals.runtimeDecisions, "runtimeDecisions");
  count(signals.pathVariants, "pathVariants");
  count(signals.humanSteps, "humanSteps");
  count(signals.sideEffects, "sideEffects");
  count(signals.changeFrequencyPerMonth, "changeFrequencyPerMonth");
  if (signals.runtimeDecisions > 0 && signals.pathVariants < 2) {
    fail("存在运行时决策,但 pathVariants 小于 2,信号自相矛盾");
  }
}

/**
 * 三段判定:
 * 1) 单能力或路径完全可枚举 → 固定流程;
 * 2) 有运行时分支但结构可声明 → 声明式编排;
 * 3) 分支与路径超出可枚举范围 → Agent 循环,必须另加硬上限。
 */
export function decideShape(signals: ShapeSignals): ShapeDecision {
  validateSignals(signals);
  const reasons: string[] = [];
  const controls: string[] = [];

  if (signals.capabilities <= 1) {
    reasons.push("只涉及一个能力,不需要跨能力调度");
    return { decision: "fixed-flow", score: 0, reasons, controls };
  }

  if (signals.runtimeDecisions === 0 && signals.pathVariants <= 1) {
    reasons.push("执行顺序在请求到达前完全确定,固定流程即可覆盖");
    if (signals.sideEffects > 0) {
      controls.push("外部动作使用幂等键,失败走补偿而不是盲目重放");
      controls.push("动作前保留可回滚检查点");
    }
    return { decision: "fixed-flow", score: 0, reasons, controls };
  }

  const score = signals.runtimeDecisions * 2 +
    Math.max(0, signals.pathVariants - 1) +
    signals.humanSteps +
    signals.sideEffects +
    (signals.changeFrequencyPerMonth >= 2 ? 1 : 0) +
    (signals.capabilities >= 3 ? 1 : 0);

  reasons.push(`运行时决策 ${signals.runtimeDecisions} 个`);
  reasons.push(`预期路径 ${signals.pathVariants} 条`);
  if (signals.humanSteps > 0) reasons.push(`人工节点 ${signals.humanSteps} 个`);
  if (signals.sideEffects > 0) reasons.push(`外部副作用 ${signals.sideEffects} 个`);
  if (signals.changeFrequencyPerMonth >= 2) {
    reasons.push(`每月变更 ${signals.changeFrequencyPerMonth} 次,需要配置化`);
  }

  let decision: WorkflowDecision;
  if (score > DECLARATIVE_MAX) {
    decision = "agent-loop";
    reasons.push(`结构分 ${score} 超过声明式上限 ${DECLARATIVE_MAX}`);
    controls.push("设置步数、花费与时限三重上限,并定义终止条件");
    controls.push("每一轮循环都必须可观测,禁止无界自省");
  } else if (score > FIXED_MAX) {
    decision = "declarative-orchestration";
    reasons.push(`结构分 ${score} 落在 (${FIXED_MAX}, ${DECLARATIVE_MAX}],路径可声明`);
    controls.push("把阶段、条件与重试写成可版本化的声明");
    controls.push("每次路径变更跑一组固定回归样本");
  } else {
    decision = "fixed-flow";
    reasons.push(`结构分 ${score} 不超过 ${FIXED_MAX},固定流程更易测试`);
  }

  if (signals.humanSteps > 0) {
    if (decision === "fixed-flow") decision = "declarative-orchestration";
    controls.push("人工节点设超时、升级路径与默认动作");
  }
  if (signals.sideEffects > 0) {
    controls.push("写动作前保留人工确认或可回滚检查点");
  }
  if (signals.changeFrequencyPerMonth >= 3) {
    controls.push("高频变更走配置开关,不直接改编排代码");
  }

  return { decision, score, reasons, controls };
}

/** 三条链的结构信号。数字来自链路定义,不是事后拟合的评分。 */
export const CHAIN_SIGNALS: Record<ChainId, ShapeSignals> = {
  A: {
    capabilities: 2,
    runtimeDecisions: 2,
    pathVariants: 4,
    humanSteps: 1,
    sideEffects: 1,
    changeFrequencyPerMonth: 2,
  },
  B: {
    capabilities: 2,
    runtimeDecisions: 2,
    pathVariants: 5,
    humanSteps: 0,
    sideEffects: 0,
    changeFrequencyPerMonth: 3,
  },
  C: {
    capabilities: 2,
    runtimeDecisions: 0,
    pathVariants: 1,
    humanSteps: 0,
    sideEffects: 1,
    changeFrequencyPerMonth: 1,
  },
};

validateSignals 会拦一个自相矛盾的输入:有运行时决策,但路径数小于 2。这类输入通常来自「把分支写进了文档,却没算进路径数」,放过去会让评分偏低。

decideShape 的硬规则顺序很重要:先处理单能力与完全可枚举路径,再算分。否则一个「2 能力、0 决策、1 路径」的链路会因为 sideEffects 得 1 分,被误判成需要编排。控制项与判定分开返回,是因为同一个判定在不同链路下的控制强度不同:C 是固定流程,但它的外部动作控制比 B 多。

判定之后做什么

判定只回答形态,控制项回答「怎么保证这个形态不出事」。

形态必须有的控制
固定流程契约测试、幂等键、失败补偿、发布前回归
声明式编排配置版本化、路径覆盖样本、引擎级追踪、灰度开关
Agent 循环步数/花费/时限上限、终止条件、每轮可观测、人工兜底
含人工节点超时、升级路径、默认动作、人工决策留痕
含外部副作用动作前检查点、幂等键、补偿或人工确认

这张表可以直接粘进设计评审模板。评审时先看形态判定,再看对应控制项是否齐全;缺哪一项,就在发布前补齐,而不是等事故后补。

决策记录模板

把判定写成一条 ADR,至少包含五列:

字段C 链示例
决策采用固定流程,不接入编排引擎
依据信号0 个运行时决策、1 条路径、1 个外部副作用、每月 1 次变更
放弃的方案声明式编排:引擎开销与配置回归换不来路径灵活性
控制项创建订单使用幂等键;失败走补偿;动作前写检查点
复核触发新增运行时分支、路径数 ≥ 2、月变更 ≥ 2 时重新评估

最后一列最重要。形态不是一次性决定,信号变化就要重新判定;没有复核触发的 ADR,半年后就会变成「历史遗留」。

小结

三条链路里,A 与 B 有运行时分支、人工节点或高频变更,结构分是 10 与 9,适合声明式编排;C 顺序固定、路径唯一,结构分是 0,适合固定流程。C 一旦加入会员价分支,分数变成 4,立刻跨过固定流程上限,这说明形态判定对结构变化是敏感的,不是拍脑袋的标签。

判据的价值不在给出一个标准答案,而在把「为什么这样选」写成可复核的信号、分数与控制项。形态选错,通常不是因为技术能力不够,而是因为没有人把变更成本与故障成本算进同一张表。

阅读导航:上一节:1.2 串行、并行与条件分支 · 下一节:2.1 能力之间的数据契约 。

常见错误与边界

  • 把所有链路迁进编排引擎。顺序固定的链路会为不存在的灵活性支付引擎开销。
  • 用能力数量判断是否编排。能力多只说明链路长,不说明路径需要运行时决定。
  • 忽略变更频率。每月改三次的链路,固定流程的发布成本会超过引擎开销。
  • 把人工节点写进代码。超时、升级与默认动作会让代码迅速失控。
  • 给外部副作用配自动重试。订单可能已创建,重试会制造重复副作用。
  • 只看单请求开销。固定流程最便宜,但变更成本可能吃掉全部节省。
  • 判定后不写复核触发。信号变了,形态却没人重新评估。
  • 把分数当绝对真理。它只是把结构信号折算成一个可讨论的起点。
  • 认为 Agent 循环更先进。路径可枚举时,Agent 只会增加成本与不确定性。
  • 用示例分数替代真实信号。路径数与变更频率必须来自自己的链路记录。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《互联网产品的 AI 架构·平台化与治理》附录 C 平台化评审清单
  2. 《互联网产品的 AI 架构·平台化与治理》附录 B 能力目录与配额字段速查
  3. 《互联网产品的 AI 架构·平台化与治理》附录 A 治理角色与职责速查