分工的艺术:多Agent编排与Human-in-the-Loop
分工的艺术:多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的困境出发,引入了两个重要设计:
- 多Agent编排:把大Agent拆成专家+调度员。核心价值是隔离——每个专家只处理自己领域的事,看不到不需要的工具。
- Human-in-the-Loop:对不可逆操作引入人工确认。CompletableFuture让审批流实现得很简洁,Agent不需要感知"人类"的存在。
加上前几天的ReAct循环、Skill系统、MCP协议、RAG检索,Agent的能力体系已经相当完整了。但还有一个关键问题没解决——记忆。每次新对话Agent都"失忆"。下一篇就来解决这个问题。
项目链接
项目代码:day6-agent
更多推荐
所有评论(0)