我用 LangGraph4j 实现 Multi-Agent Supervisor

java 复制代码
做 Multi-Agent 时踩了三个坑才跑通。这篇文章把设计思路、坑点、代码都写清楚,希望帮你少走弯路。

项目开源:<https://github.com/shuguang-liu/langgraph4j-demo>

一、单 Agent 的困境:为什么我最终选择了 Multi-Agent

最初我用 ReAct 模式做了一个智能客服,一个 Agent 挂载订单、物流、工单三个工具。跑得挺好,直到我往里加了第四个、第五个工具。

问题开始出现:

第一,工具描述越来越长,prompt 越来越臃肿。 每个工具的 @Tool(description=...) 都要塞进 system prompt,在初始化的时候,你要把所有的Tool都加载到defult里面。

第二,不同领域的意图互相干扰。 用户问"LangChain 是什么",Agent 不应该去查订单;用户问"订单到哪了",也不该走 RAG 检索。但在单 Agent 里,这些判断全压在一个 LLM 上。 第三,没法做差异化处理。 订单查询需要调工具,技术问答需要走 RAG,报表统计需要查数据库。三种完全不同的处理链路,硬塞在一个 Agent 里会越来越乱。

所以我把架构拆成了 Multi-Agent------每个 Agent 专注一个领域,由一个"主管"决定任务分给谁。

二、选 Supervisor 还是网络式?三种拓扑对比

Multi-Agent 有三种主流拓扑:

拓扑 结构 优点 缺点
Supervisor 一个主Agent + 多个副Agent 简单、可控、易调试 主管是单点
网络式 Agent 互相通信 去中心化、灵活 难调试、容易死循环
层级式 多层主管 适合复杂组织 复杂度高

我选了 Supervisor。理由很简单:

  • 3~6个Agent,规模不大,不需要去中心化
  • 主管可以统一记录"谁做了什么",方便审计
  • 出问题只需要看主Agent的日志

最终结构:

css 复制代码
用户问题 → Supervisor(主Agent)
              ↓ 判断交给谁
    ┌─────────┼─────────┐
    ↓         ↓         ↓
   cs A     tech A    data A
    │         │         │
    └─────────┴─────────┘
              ↓
          回到 Supervisor
              ↓
             END

关键设计:专家执行完必须回到 Supervisor,由它决定"任务完成了"还是"继续交给下一个专家"。

三、一张图讲清楚:Supervisor 模式怎么跑

用 LangGraph4j 的 StateGraph 表达:

对应代码:

java 复制代码
return new StateGraph<MultiAgentState>(MultiAgentState.SCHEMA, serialize)
        .addNode("supervisor", AsyncNodeAction.node_async(supervisorNode))
        .addNode("auto_tools", AsyncNodeAction.node_async(autoToolsNode))
        .addNode("cs", AsyncNodeAction.node_async(csExpertNode))
        .addNode("tech", AsyncNodeAction.node_async(techExpertNode))
        .addNode("data", AsyncNodeAction.node_async(dataExpertNode))
        .addNode("tools", AsyncNodeAction.node_async(toolsNode))
        .addEdge(START, "supervisor")
        .addConditionalEdges(
                "supervisor", AsyncEdgeAction.edge_async(multiAgentState -> multiAgentState.next()),
                Map.of(
                        "cs", "cs",
                        "tech", "tech",
                        "data", "data",
                        "auto_tools","auto_tools",
                        "end", END))
        // Agent 判断: 有tool_calls -> tools/auto_tools; 无->supervisor
        .addConditionalEdges("cs",
                AsyncEdgeAction.edge_async(expertRouter),
                Map.of("tools", "tools",
                        "auto_tools", "auto_tools",
                        "supervisor", "supervisor"))
        .addConditionalEdges("tech",
                AsyncEdgeAction.edge_async(expertRouter),
                Map.of("tools", "tools",
                        "auto_tools", "auto_tools",
                        "supervisor", "supervisor"))
        .addConditionalEdges("data",
                AsyncEdgeAction.edge_async(expertRouter),
                Map.of("tools", "tools",
                        "auto_tools", "auto_tools",
                        "supervisor", "supervisor"))
        // 工具执行后回到Agent
        .addConditionalEdges("tools",
                AsyncEdgeAction.edge_async(backToExpert),
                Map.of("cs", "cs",
                        "tech", "tech",
                        "data", "data",
                        "supervisor", "supervisor"))
        .addConditionalEdges("auto_tools",
                AsyncEdgeAction.edge_async(backToExpert),
                Map.of("cs", "cs",
                        "tech", "tech",
                        "data", "data",
                        "supervisor", "supervisor"))
        .compile(compileConfig);

几个关键点

  • supervisor 是入口,也是所有专家执行完的归宿
  • expertRouter 是条件边------判断专家有没有请求调工具
  • toolsauto_tools 是工具执行节点,区分敏感和安全

四、踩了 3 个坑才跑通:Multi-Agent 的隐藏难点

坑 1:专家节点不能执行工具

最初我这样写专家节点

java 复制代码
ChatResponse response = chatClient.prompt()
        .messages(messages)
        .toolCallbacks(ToolCallbacks.from(tools))
        .call()
        .chatResponse();

问题来了------chatClient 会自动执行工具,然后把最终回答返回。这意味着我没有机会在"工具执行前"暂停。

为什么要暂停? 因为我要做 HITL(Human-in-the-Loop)------退款、创建工单这类写操作,必须人工审批。

解决方案 :把工具执行抽成独立节点

java 复制代码
var compileConfig = CompileConfig.builder()
        .checkpointSaver(saver)
        .internalToolExecutionEnabled(false)      // 禁用自动执行
        .interruptBefore("tools")   // 调用工具暂停
        .build();

禁用自动执行后,LLM 返回的 AssistantMessage 会带着 tool_calls,然后由条件边路由到工具节点。

代价 :State 里要记 current_expert,否则工具执行完不知道回哪个专家。


坑 2:工具执行完回哪个专家

工具节点执行完,怎么知道该回到 cs、tech 还是 data?

最初我写错了

java 复制代码
private NodeAction<MultiAgentState> buildToolsNode(ToolCallbackProvider provider) {
    return state -> {
        // ... 执行工具
        return Map.of("message", newMessages);   // 没告诉下一步去哪
    };
}

结果:backToExpert 拿到的 current_expert 是空字符串,总是回到 supervisor------但 supervisor 看到"最后一条是 ToolResponseMessage",不知道该干什么,流程卡死。

修复:在 supervisor 里记录当前Agent:

java 复制代码
String decision = chatClient.prompt().user(prompt).call().content().trim().toLowerCase();
return Map.of("next", decision, "current_expert", decision);   // ← 同时记录

然后 backToExpert 条件边:

java 复制代码
return state -> {
    String expert = state.currentExpert();
    System.out.println("[backToExpert] 回到: " + expert);
    return expert.isEmpty() ? "supervisor" : expert;
};

坑 3:死循环

跑通之后发现一个诡异现象:

text 复制代码
[supervisor] 分配给: cs
[cs_expert] 处理中...
[supervisor] 分配给: cs
[cs_expert] 处理中...
[supervisor] 分配给: cs
...

永远分配给 cs,停不下来。

原因 :supervisor 判断时看的是 messages.get(0)(用户原始问题),每轮都看到同一个问题,所以每次判断结果都一样。

修复 :先判断"任务是否已完成"------如果最后一条是无 tool_callsAssistantMessage,说明Agent已经给出了最终回答:

java 复制代码
if (!messages.isEmpty()) {
    Message lastMsg = messages.get(messages.size() - 1);
    if (lastMsg instanceof AssistantMessage aiMsg && !aiMsg.hasToolCalls()) {
        System.out.println("[supervisor] 任务已完成 → end");
        return Map.of("next", "end");
    }
}

关键点 :Multi-Agent 的 Supervisor 需要"任务状态记忆" ------不能只看原始问题,还要看"之前做了什么"。

五、给 Agent 加"暂停键":Human-in-the-Loop 落地

HITL 的两个核心机制:

机制一:interruptBefore

java 复制代码
var compileConfig = CompileConfig.builder()
        .checkpointSaver(saver)
        .releaseThread(false)
        .interruptBefore("tools")   // 进入 tools 节点前暂停
        .build();

机制二:Checkpointer 保存状态

java 复制代码
var saver = new MemorySaver();

没有 Checkpointer,暂停后状态就丢了,恢复时找不到现场。

完整流程

text 复制代码
用户提问 → Agent 判断要调工具 → 暂停在 tools 前
              ↓
         返回 PENDING_APPROVAL
              ↓
         用户审批(批准 / 拒绝)
              ↓
   批准:invoke(GraphInput.resume(), config) → 执行工具
   拒绝:updateState 设 rejected=true → 走 reject 分支

拒绝分支的实现

拒绝不是"清理状态"------而是让流程走到另一个节点

java 复制代码
if (!approved) {
    graph.updateState(config, Map.of("rejected", true));
}
MultiAgentState state = graph.invoke(GraphInput.resume(), config).orElseThrow();

条件边里判断:

java 复制代码
if (Boolean.TRUE.equals(state.value("rejected").orElse(false))) {
    return "reject";
}

为什么这样做 :拒绝也是业务流程的一部分------State 里留下 rejected=true 作为审计记录,同时让 Agent 生成"已取消"的反馈。

按工具风险分级

不是所有工具都需要审批。查询类自动执行,写操作才审批:

java 复制代码
private static final Set<String> SENSITIVE_TOOLS = Set.of("createTicket");
boolean hasSensitive = aiMsg.getToolCalls().stream()
        .anyMatch(tc -> SENSITIVE_TOOLS.contains(tc.name()));
String route = hasSensitive ? "tools" : "auto_tools";
路径 是否暂停
tools(敏感) ✅ 暂停
auto_tools(安全) ❌ 直接执行

六、跑起来是什么样?日志 + 演示

场景 1:查订单(安全工具,不审批)

text 复制代码
[supervisor] 分配给: cs
[cs_expert] 处理中...
[cs_expert] hasToolCalls = true
[expertRouter] 有 tool_calls → auto_tools
[auto_tools] 执行工具
[auto_tools] queryOrder → 【订单详情】订单号:ORD12345678 | 金额:200元 | 状态:已下单
[backToExpert] 回到: cs
[cs_expert] hasToolCalls = false
[expertRouter] 无 tool_calls → supervisor
[supervisor] 任务已完成 → end

关键:没有任何暂停,直接出结果。

场景 2:退款(敏感工具,需审批)

第一次请求

json 复制代码
{
  "status": "PENDING_APPROVAL",
  "threadId": "user-001",
  "message": "Agent 想调用工具,请确认"
}

批准

json 复制代码
{
  "status": "COMPLETED",
  "answer": "工单已创建:订单 ORD12345678,类型 refund"
}

场景 3:多轮会话

第 1 轮问"查订单 ORD12345678",第 2 轮问"物流到哪了"------Agent 记得订单号,直接查物流。这靠的是 Checkpointer + threadId

java 复制代码
var config = RunnableConfig.builder().threadId("user-001").build();
graph.invoke(Map.of("message", List.of(new UserMessage(question))), config);

注意 :1.9.0-beta6 版本配了 Checkpointer 后默认会释放线程 ,多轮对话必须加 releaseThread(false)

七、架构演进的方向:从单点能力到分层编排

回头看这次升级,本质上是从"用框架"到"用框架编排"

阶段 特征 问题
ReAct 单 Agent 一个 LLM + 一堆工具 工具多了意图识别变差
Multi-Agent 主管 + 多专家 架构清晰,可扩展
Subgraph 组合 多个 Multi-Agent 系统互相编排 更复杂,暂未涉及

未来的方向:如果有多个 Multi-Agent 系统(比如电商客服系统 + 金融风控系统),可以用 Subgraph 把它们组合到一个更大的顶层图里。

但现阶段,Multi-Agent 已经解决了我的问题------不同领域的能力有了清晰的分工,HITL 让敏感操作可控,Checkpointer 让多轮对话自然。

架构没有终极答案,只有当下最合适的取舍。


延伸阅读


以上。希望能帮到正在做 Agent 的你。

相关推荐
alsmile3 小时前
Node-RED 之外,国产规则引擎的新方案:基于标准语法,Go 先行实现
后端·开源·go
大白803 小时前
PHP 内存溢出排查思路:看懂报错日志,精准定位问题
后端
二月龙3 小时前
PHP 接口返回统一响应封装,让前后端对接更省心
后端
盖伦发发4 小时前
软件工程SOLID 五大设计原则
后端·软件工程
涛涛ing4 小时前
2026年HTML迎来重磅进化:原生3D、可定制Select、声明式交互,前端开发范式正在被改写
后端
汉堡大王95275 小时前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
孙启超5 小时前
【AI开发之Rust】第 11 课:智能指针与内部可变性
开发语言·后端·rust
梦想很大很大5 小时前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
Achou.Wang5 小时前
k8s中nginx worker process自动设置
后端·golang