阅读视图

发现新文章,点击刷新页面。

「JS全栈AI学习」九、Multi-Agent 系统设计:架构与编排

📌 系列简介:「JS全栈AI Agent学习」系统学习 21 个 Agent 设计模式,篇数随学习进度持续更新。

📖 原书地址adp.xindoo.xyz

前端转 JS 全栈,正在学 AI,理解难免有偏差,欢迎批评指正 ~


往期系列导航

主题
第一篇 提示链 · 路由 · 并行化
第二篇 反思 · 工具使用 · 规划
第三篇 多智能体 · 记忆管理 · 学习适应
第四篇 MCP:给AI工具世界造一个USB接口
第五篇 目标设定与监控 · 异常处理与恢复
第六篇 Human-in-the-Loop 设计
第七篇 深入理解 RAG(检索增强生成)技术
第八篇 A2A 协议完全指南:理解 Agent 协作体系
本篇 Multi-Agent 系统设计:架构与编排

写在前面

这个系列前几篇,我们从 RAG 开始——把简历切片、向量化、检索,让 AI 能"翻书再答题"。 再到 A2A 协议——搞清楚多个 Agent 之间怎么互相发现、互相协作、互相信任。

但学到这里,我意识到还有一个问题没解决:

Agent 之间协作,谁来指挥?谁来编排?出了问题谁来兜底?

这就是 Multi-Agent 系统设计要回答的问题。

这篇是这个话题的第一篇,聚焦在三件事:架构选择、动态编排、状态管理。 用"旅行规划"这个场景贯穿始终——不是因为它特别,而是因为它足够复杂,能把问题说清楚。

九、十、十一 3篇对应学习的 第15章:Multi-Agent 系统架构、第16章:工作流编排与规划、第17章:成本优化与执行策略 很多孤立起来说没意义,加上 multi-agent 比较重要就放一起了


目录

  1. 为什么需要 Multi-Agent?
  2. 架构设计:中心化 vs 去中心化
  3. 动态工作流编排
  4. 上下文管理
  5. 状态管理与一致性
  6. Human-in-the-loop
  7. 完整流程串联
  8. 总结

Multi-Agent 系统设计


1. 为什么需要 Multi-Agent?

单 Agent 的局限

假设用户说:"帮我规划一次去三亚的旅行,预算 5000 元。"

如果用单个 Agent 处理,它需要同时具备:理解意图、查航班、查酒店、查景点、规划路线、计算预算……

这些能力混在一起,代码会变得臃肿且难以维护。更重要的是,每个环节都有专业知识和外部 API,单个 Agent 很难做到精通所有领域。

分工协作的思路

借鉴现实世界的分工,我们可以设计多个专业的 Agent,各司其职:

NLU Agent      → 理解用户意图
Profile Agent  → 分析用户偏好
Planner Agent  → 制定整体计划
Flight Agent   → 查询航班信息
Hotel Agent    → 查询酒店信息

好处很直接:

  • 职责清晰:每个 Agent 只做一件事,做好一件事
  • 易于维护:修改航班查询逻辑,只需要改 Flight Agent
  • 可复用:Flight Agent 可以用在其他旅行相关场景

这和前面学 A2A 时的思路是一脉相承的——A2A 解决的是 Agent 之间"怎么通信",Multi-Agent 设计解决的是"怎么协作"。


2. 架构设计:中心化 vs 去中心化

Multi-Agent 系统有两种常见的架构模式,选哪个,取决于场景。

中心化架构(Coordinator 模式)

有一个中心协调者(Coordinator)负责调度所有 Agent:

class Coordinator {
  async execute(userInput: string): Promise<Result> {
    // 串行:理解意图 → 分析画像 → 制定计划
    const intent   = await this.agents.nlu.execute({ userInput });
    const profile  = await this.agents.profile.execute({ intent });
    const plan     = await this.agents.planner.execute({ intent, profile });

    // 并行:同时查询航班、酒店、景点
    const [flights, hotels, attractions] = await Promise.all([
      this.agents.flight.execute({ plan }),
      this.agents.hotel.execute({ plan }),
      this.agents.attraction.execute({ plan }),
    ]);

    return this.integrate({ flights, hotels, attractions });
  }
}

流程清晰,统一的错误处理和状态管理,便于调试——代价是 Coordinator 是单点,压力大。

去中心化架构(P2P 模式)

Agent 之间通过消息总线直接通信,没有中心协调者:

// Flight Agent 完成后,发布事件,其他 Agent 自行订阅响应
class FlightAgent {
  async execute(context: Context): Promise<Result> {
    const result = await this.queryFlights(context);
    this.messageBus.publish({ type: 'flights_ready', data: result });
    return result;
  }
}

没有单点瓶颈,扩展性好——代价是流程不直观,调试困难。

我的选择

对于旅行规划这种有明确步骤的场景,我选择了中心化架构

原因很简单:旅行规划有清晰的先后顺序(理解意图 → 制定计划 → 查询信息 → 整合结果),需要强一致性(预算控制不能各个 Agent 各自为政),也需要便于调试。

如果是实时监控、事件驱动的场景,去中心化可能更合适。架构没有对错,只有合不合适。


3. 动态工作流编排

有了架构,接下来的问题是:如何编排这些 Agent 的执行顺序?

静态编排的问题

最简单的方式是写死流程——但太死板了:

  • 如果用户直接说"帮我订明天去北京的机票",还需要分析画像吗?
  • 如果用户已经订好了酒店,还需要查询酒店吗?

动态主导权转移

我想到一个思路:让 Agent 自己决定下一步该谁执行

就像接力赛,当前跑的人决定把棒交给谁——流程就灵活了:

class Agent {
  async execute(context: Context): Promise<ExecutionResult> {
    const result = await this.doWork(context);

    // 根据当前状态,决定把主导权交给谁
    const nextAgent = this.decideNextAgent(context, result);

    return { result, nextAgent, context: this.updateContext(context, result) };
  }

  private decideNextAgent(context: Context, result: any): string | null {
    if (context.needsFlightInfo && !context.hasFlightInfo) return 'flight_agent';
    if (context.needsHotelInfo  && !context.hasHotelInfo)  return 'hotel_agent';
    return null; // 没有下一步了
  }
}

Coordinator 只需要不断传递主导权,直到没有下一步:

class DynamicCoordinator {
  async execute(userInput: string): Promise<Result> {
    let context     = this.initContext(userInput);
    let currentAgent = 'nlu_agent';

    while (currentAgent) {
      const { result, nextAgent, context: newContext } =
        await this.agents.get(currentAgent).execute(context);

      context      = newContext;
      currentAgent = nextAgent; // 主导权转移
    }

    return context.finalResult;
  }
}

这个思路让我想起乾卦的"时乘六龙以御天"——不是死守固定的步骤,而是顺应时机,动态调整。

充分条件原则

动态编排带来一个新问题:Agent 怎么知道自己能不能执行?

我的答案是:定义每个 Agent 的前置条件,条件不满足就反向补全

class FlightAgent {
  canExecute(context: Context): boolean {
    return context.has('destination') &&
           context.has('departureCity') &&
           context.has('travelDate');
  }

  async execute(context: Context): Promise<Result> {
    const missing = this.checkMissing(context);

    if (missing.length > 0) {
      // 反向传播:请求 NLU Agent 补全缺失信息
      context.requestInfo(missing);
      return { status: 'pending', nextAgent: 'nlu_agent' };
    }

    return await this.queryFlights(context);
  }
}

这就像神经网络的反向传播:从目标反推需要什么输入,然后向前传播补全信息。


4. 上下文管理

多个 Agent 协作,必然涉及信息共享。Context 的设计很关键。

上下文的结构

interface Context {
  requestId: string;
  traceId: string;       // 链路追踪

  user: { id: string; preferences: UserPreferences };

  intent: Intent;
  destination: string;
  budget: number;

  completedAgents: string[];
  results: Map<string, any>;
}

只传递必要的信息

不是所有信息都需要传递。我的原则是:每个 Agent 只提取自己需要的,只返回必要的结果

class FlightAgent {
  async execute(context: Context): Promise<Result> {
    // 只提取需要的字段
    const { destination, departureCity, travelDate, budget } = context;

    const flights = await this.queryFlights({
      destination, departureCity, travelDate,
      maxPrice: budget * 0.4,  // 航班预算占总预算 40%
    });

    // 只返回必要的结果,不把原始数据全部往下传
    return {
      flights: flights.slice(0, 5),
      cheapestPrice: flights[0].price,
      recommendedFlight: this.selectBest(flights),
    };
  }
}

信息过载和信息不足一样危险——这是做 RAG 时就踩过的坑,在 Multi-Agent 里同样成立。


5. 状态管理与一致性

当多个 Agent 并行执行时,会遇到状态一致性问题。

问题场景

t0: 用户说"预算 5000 元"
t1: Flight Agent  Hotel Agent 同时开始查询(基于 5000 元)
t2: 用户说"我想把预算改成 8000 元"
问题:Flight Agent 已经查完了,结果还有效吗?

版本控制

解决方案:给上下文加版本号

class StateManager {
  private version = 0;

  // 更新上下文时,版本号递增
  updateContext(updates: Partial<Context>): void {
    this.version++;
    this.context = { ...this.context, ...updates, version: this.version };
  }

  // Agent 开始执行时,创建快照(记录当前版本)
  createSnapshot(agentId: string): ContextSnapshot {
    return { version: this.version, context: { ...this.context }, agentId };
  }

  // Agent 提交结果时,检查版本是否一致
  submitResult(agentId: string, result: any, snapshotVersion: number): boolean {
    if (snapshotVersion < this.version) {
      console.log(`${agentId} 的结果已过期,需要重新执行`);
      return false;
    }
    return true;
  }
}

乐观锁 vs 悲观锁

对于状态冲突,有两种策略:

  • 乐观锁:先执行,提交时检查版本——适合读多写少的场景(查询航班)
  • 悲观锁:先加锁,执行完再释放——适合写操作(预订机票)

我的选择是混合策略:查询用乐观锁,性能高;预订用悲观锁,保证一致性。

这个思路和数据库事务设计是一回事——底层的逻辑,跨越了层次,是相通的。


6. Human-in-the-loop

完全自动化不一定是最好的。有时候,让用户参与决策反而更好。

最小干预原则

我的原则是:只在关键决策点询问用户,其他信息能推断就推断,能用默认值就用默认值

class ProgressiveConfirmation {
  async execute(userInput: string): Promise<Result> {
    const intent = await this.nluAgent.execute({ userInput });

    // 只问缺失的关键信息
    if (!intent.destination) {
      intent.destination = await this.askUser("您想去哪里?");
    }

    // 非关键信息:推断或使用默认值
    intent.budget     = intent.budget     || this.inferBudget(intent);
    intent.travelDate = intent.travelDate || this.getDefaultDate();

    // 非关键信息在后续流程中再问,不要一次性问完
    return this.continueExecution(intent);
  }
}

一次性问用户十个问题,用户会直接关掉。逐步确认,每次只问最关键的那一个。

何时必须让用户介入?

回顾前面学 A2A 时总结的四种情况,在 Multi-Agent 编排里同样适用:

  1. 权限/能力边界:Agent 遇到了自己无权处理的事
  2. 死锁/僵局:系统自己解不开
  3. 高风险不可逆操作:预订、付款、发送——做了就很难撤回
  4. 置信度低于阈值:Agent 不够确定,不该自己做主

7. 完整流程串联

把上面的思路串起来,看一个完整的执行流程:

class TravelPlanningSystem {
  async plan(userInput: string): Promise<TravelPlan> {
    // 1. 初始化上下文
    const context = { traceId: generateId(), version: 0, userInput, results: new Map() };

    // 2. NLU → 补全缺失信息 → Profile → Planner(串行)
    const intent = await this.nluAgent.execute(context);
    if (!intent.destination) {
      intent.destination = await this.askUser("您想去哪里?");
    }
    context.intent  = intent;
    context.profile = await this.profileAgent.execute(context);
    context.plan    = await this.plannerAgent.execute(context);

    // 3. 并行查询(带版本快照)
    const snapshot = this.stateManager.createSnapshot('parallel_query');
    const [flights, hotels, attractions] = await Promise.all([
      this.flightAgent.execute(snapshot.context),
      this.hotelAgent.execute(snapshot.context),
      this.attractionAgent.execute(snapshot.context),
    ]);

    // 4. 检查版本冲突(用户可能中途修改了预算)
    if (snapshot.version < this.stateManager.currentVersion) {
      return this.plan(userInput); // 重新规划
    }

    // 5. 整合结果
    return this.integrate({ flights, hotels, attractions, plan: context.plan });
  }
}

流程里有几个细节值得注意:

  • 串行和并行混用——有依赖关系的步骤串行,独立的步骤并行
  • 版本快照在并行开始前创建,不是在结束后
  • 版本冲突时直接重新规划,不是尝试修补

8. 总结

这篇学到的几个判断

架构选择没有对错,只有合不合适。 有明确流程的场景用中心化,事件驱动的场景用去中心化。

动态编排比静态编排更灵活,但更难调试。 主导权转移的思路很好,但要做好链路追踪,不然出了问题很难定位。

充分条件原则是 Multi-Agent 设计的基础。 每个 Agent 都应该知道自己需要什么、能做什么、做不了的时候该怎么办。

状态一致性是并行执行的核心挑战。 版本控制 + 混合锁策略,是目前我觉得最实用的解法。

和前面内容的关系

回头看这个系列走过的路:

RAG          → 让 Agent 能"翻书再答题"(知识检索)
A2A 协议     → 让 Agent 之间能互相发现、协作、信任(通信协议)
Multi-Agent  → 让多个 Agent 能有序地协同完成复杂任务(编排调度)

每一层都在解决上一层留下的问题。


写在最后

学这一章的时候,有一个细节让我停下来想了一下。

动态主导权转移那里,每个 Agent 在执行完之后,都要做一个判断:下一步该谁?

不是由外部强行指定,而是由当前执行者根据现状来决定。

这让我想起易经里的一个说法:"知几其神乎"——几,是事物变化的苗头,是时机的信号。 真正懂得顺势而为的人,不是按计划行事,而是在每一个当下,感知现状,做出最合适的那个判断。

Multi-Agent 的动态编排,其实是在用代码实现这件事: 不是写死流程,而是让每个节点都有感知、有判断、有选择。

系统如此,人也如此。


昇哥 · 2026年4月 学 Multi-Agent 系统设计途中,把想清楚的事写下来

「JS全栈AI Agent学习」六、当AI遇到矛盾,该自己决定还是问你?—— Human-in-the-Loop

📌 系列简介:「JS全栈AI Agent学习」系统学习 21 个 Agent 设计模式,篇数随学习进度持续更新。

⏱️ 预计阅读时间:15 分钟

📖 原书地址adp.xindoo.xyz

前端转 JS 全栈,正在学 AI,理解难免有偏差,欢迎批评指正 ~


🗺️ 系列导航

主题 状态
第一篇 提示链 · 路由 · 并行化
第二篇 反思 · 工具使用 · 规划
第三篇 多智能体 · 记忆管理 · 学习适应
第四篇 MCP 协议
第五篇 目标设定与监控 · 异常处理与恢复
本篇 Human-in-the-Loop 设计

前言

上一篇讲目标监控和异常处理,结尾提到了 Human-in-the-loop——什么时候该让人介入。

当时我给了一个简单的判断原则:影响最终结果 + 难以撤回,就介入

但这只是"要不要介入"的问题。这一章要讲的,是更难的那个问题:

在什么时候,用什么方式,把决策权交还给人?

这个问题在 my-resume 项目里非常具体。很多开源项目也有嘛,就是分析自己简历,然后提出参考意见并优化。

每一条信息都是用户的真实经历——Agent 没有权利自己"脑补",更不能随便改。

怎么在"帮用户做事"和"不越权替用户做决定"之间找到平衡?这就是 HITL 要解决的事。

PS:现在还在跟着学,代码实战的推进到这部分再一起放出来了,目前刚重构完还没还没把AI的功能串起来

image.png

后面设计的一个功能就是能帮识别下简历问题,有时手滑年份错了,可能还好,但对HR来说很致命。从实际问题出发,自己当产品,自己即是用户就好,慢慢完善,一边学习一边做。


一、一个让 Agent 卡住的问题

假设你正在用简历优化 Agent,它在扫描你的简历时,发现了这样一个问题:

  • A公司任职时间:2018年3月 — 2020年6月
  • C公司任职时间:2017年9月 — 2019年4月

两段时间有将近两年的重叠。

这时候 Agent 面临一个选择:

  • 自己改? 改哪个?改成什么?它不知道哪个才是真实的。
  • 不管它? 这个矛盾如果出现在正式简历里,会让 HR 直接质疑真实性。
  • 问用户? 问,但怎么问?问什么?

这个看似简单的问题,背后藏着 AI Agent 设计中最核心的一个命题:

在什么时候,用什么方式,把决策权交还给人?

这就是本章的主题:Human-in-the-Loop(HITL)


二、HITL 是什么?

Human-in-the-Loop,直译是"把人放在循环里"。

用三句话理解它:

模式 描述 问题
全自动 AI 自己做所有决定 遇到信息不足时,只能瞎猜
全人工 每一步都问用户 用户体验极差,跟没有 AI 一样
HITL AI 做能做的,人做该做的 ✅ 两者平衡

HITL 的核心不是"让 AI 更笨",而是:

承认有些决定本来就该人来做,AI 的职责是识别出这些时刻,并优雅地把决策权交出去。

接下来,拆解实现 HITL 的六大核心机制。


三、机制①:介入时机——Agent 先自己找答案

最容易犯的错误:发现问题就问用户。

这会导致用户被频繁打断,体验极差。正确的做法是:

Agent 先尝试自己解决,真的解决不了,才介入。

判断标准:有没有足够的上下文自行决策?

还是简历场景。Agent 看到用户写了:

"我是一个积极主动、善于沟通的人"

这句话太泛了,Agent 想把它改得更具体。这时候该问用户吗?

不该。 Agent 应该先去项目经历里找支撑证据——这件事它自己能做:

async function enrichSelfDescription(profile) {
  const { selfDescription, projects } = profile;

  // 先在项目经历里找支撑证据
  const evidence = await findSupportingEvidence(projects, selfDescription);

  if (evidence.length > 0) {
    // 找到了 → 直接补充,不打扰用户
    return {
      action: 'auto_enrich',
      result: buildEnrichedDescription(selfDescription, evidence),
    };
  } else {
    // 找不到 → 才介入
    return {
      action: 'require_human',
      reason: '自我评价缺乏具体项目支撑,需要用户补充',
    };
  }
}

这个判断逻辑用一句话总结:

能自己解决 → 不介入
不能自己解决 → 才介入

看起来简单,但它是后续所有机制的基础前提。


四、机制②:结构化选项——别问开放问题

当 Agent 决定介入时,怎么问同样重要。

开放问题 vs 结构化选项

糟糕的问法:

"您的两段工作经历时间有重叠,请问是怎么回事?"

用户看到这个问题,需要自己思考、自己组织语言、自己判断该改哪里——认知负担极高。

正确的问法:

"发现您的工作经历存在时间重叠,请选择处理方式:

  • A:A公司时间有误,应为 2019年3月 — 2020年6月
  • B:C公司时间有误,应为 2019年9月 — 2020年4月
  • C:两段经历确实重叠(如兼职),我来手动说明"

用户只需要选一个字母,认知成本降到最低。

这个设计思路,和我们做前端交互设计是一个道理——不要让用户面对空白输入框,给他选项,降低决策成本

A/B/C 选项的设计原则

function buildInterventionOptions(conflict) {
  return {
    question: conflict.description,
    options: [
      {
        key: 'A',
        label: conflict.suggestion_a,       // Agent 推断的方案A
        action: 'auto_fix_a',
      },
      {
        key: 'B',
        label: conflict.suggestion_b,       // Agent 推断的方案B
        action: 'auto_fix_b',
      },
      {
        key: 'C',
        label: '以上都不对,我来手动说明',  // 兜底选项,永远存在
        action: 'pause_for_human',          // 暂停,等用户补充
      },
    ],
  };
}

注意 C 选项永远存在。它的作用是:

保留用户的最终控制权,无论 Agent 推断得多准,用户都可以说"都不对,我自己来"。

这不是产品的妥协,而是对用户自主权的尊重——也是用户信任 Agent 的基础。


五、机制③:介入粒度——问题有大有小,介入要分级

并不是所有的介入都一样重。Agent 需要识别当前问题属于哪个粒度级别,再决定如何介入。

三个粒度级别

字段级(Field-level):缺一个具体数据,补上就好。

场景:手机号只有10位,少了一位数字。 处理:直接问"您的手机号是否为 138XXXX?",一句话解决。

段落级(Block-level):某个模块的内部逻辑有问题,需要用户理清一块内容。

场景:项目经历里有三个项目,时间线混乱,无法判断先后顺序。 处理:列出三个项目,请用户确认排序依据。

全局级(Global-level):输入内容与任务目标根本不匹配,需要重新确认方向。

场景:用户投的是前端工程师岗位,但简历通篇没有提到任何技术栈。 处理:这不是逻辑问题,而是内容本身无法支撑任务,需要从全局重新确认。

粒度判断逻辑

function classifyInterventionLevel(issue) {
  switch (issue.scope) {
    case 'single_field':
      // 缺一个字段值,补上即可
      return 'field';

    case 'block_logic':
      // 某模块内部逻辑不完整,缺少判断依据
      return 'block';

    case 'global_mismatch':
      // 整体内容与目标任务不匹配
      return 'global';
  }
}

粒度越高,用户需要做的事越多,也越容易产生疲劳感——这就引出了下一个机制。


六、机制④:批量介入——别一个一个问,打包说

用户疲劳是真实存在的

想象一下:Agent 问了你第1个问题,你回答了。问了第2个,你回答了。第3个、第4个、第5个……

到第3个问题开始,大多数用户已经开始不耐烦了。更糟糕的是,如果前3个都是小问题(字段级),第4个突然是全局级的大问题,用户早就没耐心认真回答了。

做过用户访谈或者产品测试的同学应该有体会——用户的耐心是有限的,而且消耗得比你想象的快。

解法:先做完能做的,再打包告诉用户

Agent 扫描全文
      ↓
收集所有问题,分类整理
      ↓
能自己解决的 → 先默默处理掉
      ↓
剩下不能解决的 → 打包成一份"阶段总结"
      ↓
一次性告知用户,用户一次性补充
      ↓
继续后续流程

阶段总结的模板示例

✅ 已完成优化:
  - 自我评价已结合项目经历补充了具体案例
  - 技能标签已按岗位要求重新排序
  - 教育经历格式已统一

⚠️ 需要您补充以下信息,以便继续优化:
  1. [字段级] 手机号疑似缺少一位,请确认
  2. [段落级] A公司与C公司任职时间有重叠,请选择处理方式(A/B/C)
  3. [全局级] 未发现前端相关技术栈,请确认目标岗位方向

补充完成后,我将继续为您完成剩余优化 ~
async function runBatchedIntervention(profile) {
  const issues = [];

  // 第一遍扫描:收集所有问题
  const scanResult = await scanProfile(profile);

  for (const issue of scanResult.issues) {
    if (issue.canAutoFix) {
      // 能自己解决的,直接处理
      await autoFix(profile, issue);
    } else {
      // 不能解决的,加入待询问列表
      issues.push(issue);
    }
  }

  if (issues.length === 0) return { status: 'complete' };

  // 打包成一次介入,而不是多次打断
  return {
    status: 'need_human',
    summary: buildSummaryMessage(profile, issues),
    issues,
  };
}

这个设计的核心思想:

把"打扰用户"这件事的次数压到最低,但每次打扰都要有价值、有上下文、让用户看到进度。


七、机制⑤:前后回溯——用户回答后,不是结束

用户补充完信息,Agent 不能直接继续往下走。它需要做两件事:

往后看:后续内容跟着改

用户确认了"A公司时间有误,应为2019年3月",那么:

  • 简历里所有引用了这段时间的地方,都要同步更新
  • 基于这段时间计算的"工作年限",也要重新计算

往前看:之前内容有没有新矛盾

用户的补充可能引入新的矛盾。比如:

用户把 A公司时间改成了 2019年3月 — 2020年6月 但之前已经处理好的 B公司时间是 2019年1月 — 2020年3月 现在又重叠了……

这让我想到写代码改 bug 的感受——改了一个地方,另一个地方又冒出来了。Agent 的回溯机制,就是在系统层面把这件事自动化。

async function postInterventionRevalidation(profile, updatedFields) {
  // 往后看:同步更新所有受影响的字段
  await propagateChanges(profile, updatedFields);

  // 往前看:重新扫描,检查是否引入了新矛盾
  const newIssues = await scanProfile(profile);

  if (newIssues.issues.length > 0) {
    // 发现新矛盾 → 进入升级循环
    return {
      status: 'new_conflict_found',
      issues: newIssues.issues,
    };
  }

  return { status: 'clean' };
}

八、机制⑥:升级循环——新矛盾出现,再次介入

前后回溯发现了新矛盾,怎么办?

再次进入介入流程。 这就是"升级循环(Escalation Loop)"。

但循环不能无限进行,需要一个收敛条件——这和上一篇讲反思模式时的"最多3次"是同一个道理:边际收益递减,超过上限就该人工接手,而不是让 Agent 继续转圈。

async function escalationLoop(profile, maxRounds = 3) {
  let round = 0;

  while (round < maxRounds) {
    const result = await runBatchedIntervention(profile);

    if (result.status === 'complete') {
      // 没有新问题,循环结束
      return { status: 'done', rounds: round };
    }

    // 有问题,等待用户响应
    const userResponse = await waitForUserInput(result.summary);
    await applyUserResponse(profile, userResponse);

    // 前后回溯
    const revalidation = await postInterventionRevalidation(
      profile,
      userResponse.updatedFields
    );

    if (revalidation.status === 'clean') break;

    round++;
  }

  if (round >= maxRounds) {
    // 超过最大轮次,诚实告知用户
    return {
      status: 'max_rounds_reached',
      message: '检测到复杂冲突,建议您手动检查以下内容后重新提交',
    };
  }
}

超过最大轮次的处理方式,我觉得这里有一个很重要的设计原则:

诚实地告诉用户"这个我处理不了",比假装处理完要好得多。

Agent 承认自己的边界,反而会让用户更信任它。


九、完整流程图

把六大机制串在一起,完整的 HITL 流程如下:

用户提交内容
      ↓
Agent 扫描全文,收集所有问题
      ↓
┌─────────────────────────────┐
│  对每个问题:                │
│  有足够上下文?              │
│  ├─ 是 → 自动处理(机制①)  │
│  └─ 否 → 加入待询问列表      │
└─────────────────────────────┘
      ↓
待询问列表为空?
├─ 是 → 输出结果,流程结束
└─ 否 → 按粒度分级(机制③)
            ↓
       打包成阶段总结(机制④)
            ↓
       展示给用户:A/B/C 选项(机制②)
            ↓
       用户响应
            ↓
       前后回溯(机制⑤)
            ↓
       有新矛盾?
       ├─ 有 → 升级循环,回到扫描(机制⑥)
       └─ 没有 → 输出结果,流程结束

上述是和AI讨论出来的结论,实际上,已有功能都是 已有简历 -> 反推回填内容;

这一块设计后面是想做一个Agent功能,能快速高效生成简历模版。慢慢来,边学边完善吧。


十、核心洞察总结

机制 核心思想 一句话记住
①介入时机 Agent 先自己找答案 能自己解决的,不打扰用户
②结构化选项 给选项,不问开放问题 A/B/C 选项 + 永远有兜底的 C
③介入粒度 问题分三级,介入方式不同 字段级 · 段落级 · 全局级
④批量介入 打包打扰,不零散打断 把打扰次数压到最低
⑤前后回溯 用户回答后,双向检查 往后同步,往前验证
⑥升级循环 新矛盾再次介入,有收敛条件 超过上限,诚实告知,交给人

结语

读完这一章,我最大的感受是:

HITL 不是 AI 能力不足的妥协,而是一种设计哲学。

它承认了一件事:有些决定,本来就该人来做。AI 的职责不是替代人的所有判断,而是:

  1. 识别出哪些决定超出了自己的能力范围
  2. 优雅地把这些决定交还给用户
  3. 降低用户做决定的认知成本
  4. 保护用户不被无意义的打扰淹没

这六个机制,本质上都在回答同一个问题:

怎么让 AI 和人的协作,比任何一方单独工作都更好?

对于 my-resume 的全栈改造来说,这章给了我一个很清晰的产品设计原则:

Agent 的边界感,和开发者的边界感是一回事。 知道什么该自己做,什么该交出去,什么时候该说"这个我不确定,你来决定"——这是靠谱的标志,不是能力不足的表现。

学到这里,越来越觉得:AI 工程和软件工程,底层真的是同一套思维。 边界感、容错、分层处理——工程师早就在做了,只不过现在的执行者从代码变成了模型。


下一篇预告: 第14章——RAG(检索增强生成)。Agent 有了工具、有了目标、有了人机协同,下一步是让它真正"有记忆"——从外部知识库里检索信息,而不是只靠训练数据回答问题。


💬 系列地址:持续更新中

📖 原书地址adp.xindoo.xyz

🛠️ 实战项目:my-resume(静态页面 → NestJS + 数据库 + AI + 部署上线,进行中)

如果这篇对你有帮助,欢迎点赞收藏,我们下篇见 👋

❌