分工的艺术:多Agent编排与Human-in-the-Loop

前面几篇文章,Agent都是"一个人干活"。但当工具数量膨胀、任务变复杂时,单个Agent会力不从心。今天聊聊:怎么把一个大Agent拆成多个专家,以及什么时候Agent应该自己决定,什么时候该等人类点头。


一、单Agent撑不住了:三个具体的问题

回顾Day1中Single Agent的工作方式:一个LLM,面前摆着所有工具的说明书。用户提问 → LLM从所有工具里选 → 执行 → 回答。

这在工具少的时候没问题。但看看Day2和Day3之后的情况——内置3个Skill、外部可能10+个Skill、8个MCP模板每装一个就多几个工具。Agent面前可能摆着20+个工具。

三个问题随之而来:

问题一:JSON Schema爆炸。 每个工具的Schema大约占100-300 tokens。20个工具就是2000-6000 tokens——仅工具描述就占了上下文窗口的5%-10%。这些tokens本该用于记住对话历史和生成回答。

问题二:选择困难。 LLM面对20个工具,从中选一个最合适的——这是一个分类问题。人类面对20个选项也会犹豫,LLM也一样。工具越多,选错的概率越高。

问题三:安全边界模糊。 一个Agent同时拥有"读文件"和"删文件"的工具。用户让它"读pom.xml",它可能手滑调了删除。因为它们全在同一个工具列表里,LLM无法区分"安全工具"和"危险工具"。

二、多Agent架构:分而治之

2.1 核心思路

给每个Agent只分配它职责范围内的工具,不让它看到不需要的。就像公司里的分工——财务部门不用知道怎么操作服务器,运维部门不用懂财务报表。

我们拆成了三个专家 + 一个调度员:

Orchestrator(调度员)
  ├── 搜索专家:只有RAG搜索工具 + MCP文件/网络工具
  ├── 工具专家:只有计算器、翻译、骰子 + MCP工具
  └── 记忆专家:只有记笔记、查笔记、查用户画像

2.2 为什么"看不见"比"看见但被告知不能用"更安全?

一个常见的误解是:在System Prompt里告诉LLM"不要用删除工具"就够了。

但这是软约束。Prompt本质上只是改变了概率,不能保证100%。就像限速标志——大部分人会遵守,但总有人超速。

而"根本不让工具出现在工具列表里"是硬约束。LLM不知道删除工具存在,就绝对不可能调用它。这和数据库的最小权限原则完全相同——只授予需要的权限,不授予不需要的。

2.3 调度员(Orchestrator)的工作流程

用户提问:"帮我查一下知识库中的Spring AI内容,然后翻译成英文"

Orchestrator:
  第1步 [Plan]: 快速分析 → 这需要搜索 + 翻译 → 输出 "SEARCH, TOOL"

  第2步 [Execute]:
    → 调用搜索专家: "搜索知识库中的Spring AI内容"
    → 拿到搜索结果
    → 调用工具专家: "把以下内容翻译成英文: {搜索结果}"

  第3步 [Summarize]:
    → 汇总搜索专家和工具专家的结果
    → 流式输出最终回答

每一步都有追踪记录。哪一步出问题一目了然——不像单Agent模式,出了问题不知道LLM在哪一步犯了错。

2.4 调度员用什么模型?

Planner(规划)只需要做分类——输出几个关键词。这个任务不需要GPT-4级别的大模型,用 deepseek-chat 这种轻量模型就够。Prompt也极简:

判断需要哪些专家,只输出标签(逗号分隔)。
SEARCH(查知识库) TOOL(计算/翻译) MEMORY(笔记)。
都不需要输出 NONE。

Summarizer(汇总)需要生成高质量回答,用同模型或更好的模型。Planner和Summarizer分开,意味着你可以给不同任务用不同模型——省钱的同时保证质量。

三、Human-in-the-Loop:什么时候该等人?

3.1 一个真实的危险场景

用户:“帮我把/tmp/logs下所有文件清空”

如果Agent全自动执行,一下就删了。万一用户其实想说的是 /var/logs 而打错了呢?或者用户根本不知道这会删掉重要日志?

Agent应该停在这里,问:“即将删除 /tmp/logs 下的所有文件。确认吗?”

3.2 怎么判断什么操作需要审批?

一个简单实用的标准:不可逆操作需要审批

  • 读取文件 → 可逆(读一下不会改变什么)→ 自动执行
  • 写文件 → 不可逆(内容可能被覆盖)→ 需要审批
  • 删除 → 不可逆 → 需要审批
  • 执行脚本 → 不可逆(不知道脚本会做什么)→ 需要审批
  • 调用付费API → 技术上可逆但涉及成本 → 需要审批

3.3 审批流的工程实现:智能的"等待"

技术难点:Agent的处理是一个HTTP请求,审批来自另一个HTTP请求。怎么让第一个请求"等"第二个请求?

我们的方案:CompletableFuture阻塞等待

Agent工具调用 ApprovalController.requestApproval("删除/tmp/logs")
  → 创建一个 CompletableFuture
  → .join() 阻塞等待(当前线程挂起)

前端弹出确认框,用户点"确认"
  → POST /api/approval/{id}/approve
  → CompletableFuture.complete(true)
  → 阻塞的线程被唤醒,拿到结果 true

Agent看到审批通过 → 继续执行删除操作

对Agent来说,审批就是一个"有点慢的工具调用"——它不知道(也不需要知道)这几秒是在等人类确认。Agent只知道"调用了审批工具,几秒后返回了true"。

配合60秒超时:如果用户走开了,60秒后自动拒绝。防止无限等待。

四、中断机制:用户可以随时喊停

还有一个重要功能:用户点击"停止"按钮,Agent应该立即停下。

传统程序优雅停止比较复杂(事务回滚、资源释放)。Agent的停止相对简单——在每个步骤前检查一个标志。

多Agent架构让这一点更自然:每个专家调用前都是检查点。Orchestrator在调用搜索专家前检查 → 用户已按停止 → 直接结束Flux流。不会留下脏状态。

这是"合作式中断"——不是暴力kill线程,而是在安全点上检查、自行退出。比kill优雅得多。

五、SSE:为什么不用WebSocket?

前端和后端之间的流式推送用的是SSE(Server-Sent Events),不是WebSocket。为什么?

对比两者:

  • SSE:单向(服务器→客户端),基于HTTP,自动重连
  • WebSocket:双向,独立协议,需要心跳保活

Agent对话的数据流是高度不对称的:

  • 服务器→客户端:海量数据(思考步骤、文本片段、追踪数据)
  • 客户端→服务器:极少(一条消息 + 偶尔点停止)

这种"重下行、轻上行"的特征,SSE是完全匹配的选择。比WebSocket简单太多,而且天然兼容所有HTTP基础设施(负载均衡、代理)。

六、总结

今天从单Agent的困境出发,引入了两个重要设计:

  1. 多Agent编排:把大Agent拆成专家+调度员。核心价值是隔离——每个专家只处理自己领域的事,看不到不需要的工具。
  2. Human-in-the-Loop:对不可逆操作引入人工确认。CompletableFuture让审批流实现得很简洁,Agent不需要感知"人类"的存在。

加上前几天的ReAct循环、Skill系统、MCP协议、RAG检索,Agent的能力体系已经相当完整了。但还有一个关键问题没解决——记忆。每次新对话Agent都"失忆"。下一篇就来解决这个问题。


项目链接

项目代码:day6-agent

更多推荐