资讯详情

资讯详情

Java接入大模型:用LangChain4j构建监督者Agent协调多智能体

老哥们最近在团队内部落地了一个 Java 后端接入大模型能力的项目核心是用 langchain4j 搭了一个监督者 agentSupervisor Agent。简单说就是把好几个原本各自为战的智能体统一收口由监督者负责听用户的指令判断该找谁干活再把结果汇总回来。这套架构很适合那种“业务域已经拆得比较细但对外又希望是一个统一入口”的场景。如果你正好在用 Java 写代码又不想为了一个大模型演示项目单独起一套 Python 服务那这篇文章值得你看完。我会把思路、依赖、代码、提示词设计、以及实际踩过的坑都摊开讲尽量做到拿来就能用。1. 为什么需要监督者 Agent多智能体协作的痛点与解法1.1 从单 Agent 到多 Agent协作中的真实问题先聊一个最常见的场景。某电商客服系统业务上已经拆成了三块订单查询、退货退款、物流跟踪。如果不用多智能体传统做法是写一堆 if/else 加关键词匹配用户问一句“我那个订单退了吗钱什么时候到账”硬编码基本就要写吐。于是很多团队开始试着给每个业务域都配一个 Agent让不同 Agent 分管不同事情。但这会引出新的问题用户一句话可能横跨多个域。比如“订单 12345 退货了快递显示签收但钱没退”这就是订单域、退货域、物流域三个 Agent 之间的协作问题。如果三个 Agent 同时被唤起它们各说各话用户看到的就是一段拼凑出的混乱答复。更麻烦的是每个 Agent 各自的上下文是独立的它们之间没有信息交换机制谁先回答、谁后回答、怎么合并没有人说了算。生活中其实有非常形象的类比一个项目组同时进来一个客户需求三四个工程师都直接开口回答你说一句技术方案、我说一句报价、他说一句排期客户听完更懵。这时候就需要一个项目经理先听完客户的完整诉求统一拆解成任务再分给对应的人。监督者 Agent 就是这个项目经理。它本身不干活或者只干很少的活核心职责是听懂用户要什么、决定哪个子 Agent 能处理、把任务投递过去、拿到结果后再决定要不要继续追问下一个角色、最后统一组织答复。1.2 监督者模式的核心设计分派、仲裁、汇总监督者设计上要把握三个关键词。第一是分派。监督者必须足够理解用户的意图才能把任务分配给正确的子 Agent。这里的“理解”不是靠关键词硬匹配而是靠大模型的语义能力。比如用户问“我的 phone 什么时候能送到”监督者应该能识别出这属于物流查询而不是把它当成一个售后投诉。第二是仲裁。当多个子 Agent 都可能响应时监督者需要做优先级判断。比如用户问“退货申请被拒了但我已经寄回去了”这里既涉及退货规则解释又涉及物流信息核验监督者可以按依赖关系先查物流签收状态再结合退货申请结论给出解释。它决定的是“先做什么、后做什么”而不是把所有工具的结果一股脑堆出来。第三是汇总。子 Agent 返回的往往是结构化数据或中间结论不一定适合直接面向用户。监督者还要负责把这些干巴巴的结果整理成自然、连贯、有直接答案的回复。比如退货状态是“退款中”、物流状态是“已签收”监督者会组织成“包裹已签收退款正在处理中预计 1-3 个工作日到账”。在项目落地时还要注意不同方案之间的取舍。我自己做选型时做过一个对比写在这里供参考方案协作方式优点缺点固定 ChainA 完成后交给 B线性流水线流程清晰、易调试灵活度低用户一个简单问题也要走完整个链路一个超大的 Agent 一堆工具单 Agent 自主选工具实现简单无协作开销提示词和工具一多模型容易混乱上下文也容易爆监督者 Agent 子 Agent 工具监督者决策子 Agent 执行意图路由清晰业务边界明确子 Agent 可独立演进多一次模型调用成本和延迟略高全自动多 Agent 编排Agent 间互相协商适应复杂动态场景不可控性强Java 生态落地成本高容易死循环我的结论是绝大多数业务场景监督者 子 Agent 这种“一主多从”的模式最稳。langchain4j 之所以适合做这件事是因为它提供了 AiServices 和 Tool 注解让子 Agent 的能力可以像插件一样注册给监督者而底层大模型是否要调用某个子 Agent交给模型的 function calling 能力去判断Java 侧甚至不用自己写复杂的状态机。2. 准备工作怎么做环境、依赖与子 Agent 能力设计2.1 基础环境与依赖引入我这边工程用的是 JDK 17 Spring Boot 3.x这是 langchain4j 官方目前支持比较好的组合。JDK 8 也不是完全不能用但因为 langchain4j 的新版本对虚拟线程、HTTP 客户端都有依赖建议直接上 17省得后面遇到各种兼容问题。Maven 依赖我先贴一个最小可用集后面可以根据需要再加。dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.36.2/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version0.36.2/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-easy-rag/artifactId version0.36.2/version /dependency版本号这里不用死记去 Maven 仓库搜 langchain4j 看最新的稳定版就行。我习惯把 langchain4j 核心依赖和 open-ai 兼容包放一起因为现在绝大多数模型服务商都提供兼容 OpenAI 协议的服务端本地部署的开源模型也可以通过兼容层接入后面接什么模型都方便。模型接入这块我配的是模型服务商提供的 API Key 和基础地址配置写在 application.yml 里langchain4j: open-ai: chat-model: base-url: ${MODEL_BASE_URL} api-key: ${MODEL_API_KEY} model-name: ${MODEL_NAME} temperature: 0.2 timeout: 60s有个细节必须提醒监督者 Agent 的核心依赖是大模型的 function calling / tool calling 能力。如果你用的模型不支持工具调用那么“监督者自动决定调用哪个子 Agent”这件事基本做不成立只能退回纯提示词路由可靠性会差很多。选模型时一定要确认这一点。2.2 子 Agent 能力怎么暴露用 Tool 封装业务方法langchain4j 里把子 Agent“包装成工具”是实现监督者模式的关键。子 Agent 的本质就是一段能够解决特定问题的业务能力它可以是一个普通 Java 类的方法也可以是一个封装了提示词和模型调用的完整服务。我的做法是每个子 Agent 能力都用 Tool 注解暴露成一个方法方法名和描述都写得直白。描述里千万不要写“这是一个强大的智能助手可以处理很多问题”这种废话大模型是靠描述来决定要不要调用它的描述越精确路由越准。Component public class OrderTools { private final OrderService orderService; public OrderTools(OrderService orderService) { this.orderService orderService; } Tool(根据订单ID查询订单基本信息包括订单状态、商品清单、创建时间) public String getOrderInfo(String orderId) { Order order orderService.getByOrderId(orderId); if (order null) { return 未找到订单 orderId; } return String.format(订单状态%s商品%s金额%.2f, order.getStatus(), order.getItems(), order.getAmount()); } }再比如物流查询工具Component public class LogisticsTools { private final LogisticsService logisticsService; public LogisticsTools(LogisticsService logisticsService) { this.logisticsService logisticsService; } Tool(根据订单ID查询物流轨迹以及最新签收状态) public String getShippingStatus(String orderId) { LogisticsTrack track logisticsService.getLatestTrack(orderId); if (track null) { return 该订单暂无物流信息; } return String.format(最新节点%s网点%s状态%s, track.getNode(), track.getStation(), track.getStatus()); } }退货退款工具同理Component public class ReturnTools { private final ReturnService returnService; public ReturnTools(ReturnService returnService) { this.returnService returnService; } Tool(查询退货退款单的处理进度包括审核状态、退款金额和预计到账时间) public String getReturnProgress(String returnId) { ReturnProgress progress returnService.getProgress(returnId); if (progress null) { return 未找到退货单 returnId; } return String.format(审核状态:%s退款金额:%.2f预计到账:%s, progress.getAuditStatus(), progress.getRefundAmount(), progress.getEstimatedArrival()); } }有人会问子 Agent 不就是个 Spring Service 方法吗何必叫 Agent我的理解是子 Agent 的边界不在于代码形式而在于“它封装了某个专业问题的判断逻辑”。有的子 Agent 确实只是一个查询方法有的子 Agent 内部还会调用模型、走多轮对话、读知识库但对外它们的表现都一样——给定入参返回结论。监督者不需要关心它内部是怎么实现的只要知道“这个工具能处理哪类问题”。这种设计带来的好处是后续任何一个子 Agent 内部的实现可以随便换只要方法签名和描述不变对监督者来说就是透明的。3. 监督者 Agent 的完整实现接口定义、装配与调用链路3.1 监督者接口与 AiServices 装配有了子 Agent 工具接下来就是把它们注册给监督者。langchain4j 的 AiServices 提供了一套简洁的装配方式。先定义监督者对外接口public interface CustomerServiceSupervisor { String chat(String userMessage); }然后写一个装配用的配置类把模型、子 Agent 工具、以及监督者接口绑定在一起。这里用 ChatMemory 记录会话历史保证多轮对话时监督者还记得用户刚才说过什么。Configuration public class SupervisorConfig { Bean public ChatLanguageModel chatLanguageModel() { return OpenAiChatModel.builder() .baseUrl(System.getenv(MODEL_BASE_URL)) .apiKey(System.getenv(MODEL_API_KEY)) .modelName(System.getenv(MODEL_NAME)) .temperature(0.2) .timeout(Duration.ofSeconds(60)) .build(); } Bean public CustomerServiceSupervisor customerServiceSupervisor( ChatLanguageModel chatLanguageModel, OrderTools orderTools, LogisticsTools logisticsTools, ReturnTools returnTools) { return AiServices.builder(CustomerServiceSupervisor.class) .chatLanguageModel(chatLanguageModel) .chatMemory(MessageWindowChatMemory.withMaxMessages(20)) .tools(orderTools, logisticsTools, returnTools) .build(); } }这里的核心就一行.tools(orderTools, logisticsTools, returnTools)。langchain4j 会扫描这些类里带 Tool 注解的方法把方法名、参数、描述都整理成模型的工具列表。当用户提问时模型会先判断是否需要调用这些方法如果需要就会返回对应的工具调用请求由 langchain4j 执行并把结果回传给模型模型再基于结果生成最终回答。这个机制的循环过程说白了就是:用户说“订单 12345 寄回去了查一下退款进度。”监督者模型看到工具列表里有 getReturnProgress于是决定调用它入参是用户消息里提到的退货单号。langchain4j 执行该方法拿到“审核通过退款中预计 3 日到账”。模型基于这个结果生成一句完整人话返回给调用方。3.2 完整调用链路一次监督者请求的运作流程用一个 Controller 暴露入口然后观察整个链路RestController public class SupportController { private final CustomerServiceSupervisor supervisor; public SupportController(CustomerServiceSupervisor supervisor) { this.supervisor supervisor; } PostMapping(/api/support/chat) public String chat(RequestBody String userMessage) { return supervisor.chat(userMessage); } }这一步极其简单真正的复杂度都在模型内部。我实际测试时用一个混合了多个业务域的问题看过日志大致流程是这样的第一条用户消息“订单 8899 显示签收了但我退货申请还在审核中到底什么时候退款”监督者模型做的第一件事不是直接回答而是把这个消息拆解成两个信息需求订单 8899 的退货申请状态物流是否已经签收从工具列表里它选择了 getReturnProgress 和 getShippingStatus 两个工具把订单号作为入参分别调用。第一次调用返回“退货审核已通过等待财务打款”第二次调用返回“快递已签收”。这两段结果回填给模型后模型组织出最终答案“包裹已签收您的退货申请也已审核通过退款预计 1-3 个工作日到账。”我特意强调这段流程是因为很多第一次接触监督者模式的同事有个误区以为需要在代码里写死“先查物流再查退货”的顺序。实际上不需要。只要工具描述清楚模型的推理能力足够它会自己判断依赖关系。代码里要做的只是把工具都暴露出来以及把返回结果整理好。如果你希望监督者在某些场景下强制按固定优先级执行可以在系统提示词里显式说明比如“必须先查退货单状态再查物流信息”。提示词对模型行为的影响非常大这个后面单讲。3.3 监督者的系统提示词设计细节系统提示词是监督者 Agent 的灵魂。同样一个模型、同样一组工具提示词写得烂路由就乱提示词写得好准确率能上好几个台阶。我试过一版比较可靠的监督者提示词模板核心结构如下你是电商客服系统的总调度员。你的职责是理解用户的问题选择合适的工具获取信息并组织最终答案。 工具使用规则 1. 用户问题只涉及单一业务域时只调用对应工具。 2. 用户问题涉及多个业务域时必须分别调用所有相关工具后再回答。 3. 你只能使用工具返回的数据来回答问题绝不猜测、编造订单状态、物流节点或退款信息。 4. 如果工具返回“未找到”或空结果如实告诉用户没有查到相关信息。 5. 如果用户问题和订单、物流、退货无关请直接礼貌说明你无法处理。 6. 最终回答必须简洁、口语化不要罗列工具调用过程。第 3 条非常关键。默认情况下模型在没有工具结果时有一种“强行作答”的倾向你如果不明确告诉它“只能基于工具返回数据作答”它很可能在你还没调用工具时就自己编一个物流状态出来。加上这条约束后至少在我测试的模型上幻觉问题明显减少。另外还有一个细节如果你希望某个工具返回的数据能被监督者理解子 Agent 方法的返回字符串里一定要带明确的字段名。比如“状态:审核通过金额:199.00预计到账:3个工作日”而不是只返回“审核通过 199.00 3个工作日”。不要小看这个格式模型对前者理解起来要稳健得多。后面我也会讲到格式混乱会导致汇总结果翻车。4. 调试、踩坑与调优实录4.1 常见问题速查表监督者 Agent 实际跑起来之后我遇到了不少问题。有些问题花了我大半天才定位到原因这里整理一个速查表希望你能避开现象可能原因解决方案模型完全不调用工具直接回答模型不支持 tool calling或提示词没约束住换支持工具调用的模型在提示词中明确“只能通过工具获取信息”工具调用后模型还是乱猜工具返回结果格式模糊让工具返回带字段名的结构化字符串最好用 JSON路由经常选错工具工具描述写得太宽泛或太技术化重写 Tool 描述用业务语言描述带典型示例一次请求反复触发同一工具上下文里有误解工具结果没有被正确回填检查 ChatMemory 配置确认每轮工具结果都正确附带工具抛出业务异常整个流程失败代码没有兜底返回在工具方法内部捕获异常返回“查询失败原因”而不是抛出去上下文过长费用高、响应慢会话消息无限制累积用 MessageWindowChatMemory 限制窗口条数或做摘要压缩监督者把无关问题硬套给某个工具提示词缺少“无法处理”的兜底规则增加第 5 条类似的规则要求跳出业务域时直接拒答4.2 两个典型问题的排障实录第一个问题模型根本不走工具调用。我最早接进去时监督者回答得还挺像模像样但仔细一核对它回答里的订单状态全是编的。当时日志显示模型一次工具调用都没发起直接生成了最终答案。排查过程是这样的第一步检查模型服务商工具调用能力确认支持第二步把提示词里那一句“你可以使用以下工具”改成更强硬的“你只能通过工具调用获取订单、物流、退货信息禁止直接给出最终答案”并在最后加了一句“如果未调用工具你不允许回答业务相关问题”。改完后模型才开始老老实实走工具。这个问题的本质是提示词里对“必须调用工具”的约束强度不够。不同模型对提示词服从度差异很大遇到这种情况建议把规则写得更死、更绝对宁可少一点“礼貌”也要保证行为正确。第二个问题子 Agent 返回结果格式混乱导致监督者汇总时张冠李戴。我最初在退货工具里返回的是“当前状态审核通过退款金额 199.0预计到账 2025-06-01”本来觉得没问题。但有一次用户问“我的退款到账了吗”监督者居然回答“退款预计到账 2025-06-01”把“预计”说成了“已到账”。原因在于返回字段没有明确标注“是否已到账”模型在推理时掺入了自己的推测。后来我统一把子 Agent 返回改成 JSON 结构字符串Tool(查询退货退款单的处理进度包括审核状态、退款金额和预计到账时间) public String getReturnProgress(String returnId) { ReturnProgress progress returnService.getProgress(returnId); if (progress null) { return {\found\: false, \message\: \未找到退货单\}; } return String.format( {\found\: true, \auditStatus\: \%s\, \refundAmount\: %.2f, \estimatedArrival\: \%s\}, progress.getAuditStatus(), progress.getRefundAmount(), progress.getEstimatedArrival()); }改成 JSON 之后监督者对字段语义的理解立刻稳定了。这类问题不一定是模型笨而是返回结果里没有带上足够的语义边界。任何可能引起歧义的字段都要显式写清楚。4.3 调优参数与后续扩展方向监督者 Agent 上线后我陆续做了几轮调优有些参数值得你抄作业temperature 尽量调低0.1 到 0.3 之间比较合适。监督者本质是做路由和汇总不需要创造力温度高只会让它“自由发挥”。ChatMemory 窗口别拉太长。在这个项目里我用 20 条窗口已经覆盖了大多数客服会话再长既费 token 又增加无关上下文干扰。工具执行要加超时。监督者调子 Agent本质是本地 Java 方法调用但如果子 Agent 内部也调了模型最外层模型等待时间就容易被拖长。建议把整体模型超时设成 60 秒子 Agent 内部调用放到另一个线程池并单独限制。关于扩展方向我认为最值得做的是把监督者从单层变成两层监督者下面不只挂业务工具还挂“能调度其他工具的二级监督者”。如果你有个业务域内部特别复杂比如售后域既要查退款、又要查优惠券补偿、还要查人工客服工单那就可以在售后域内部再做一个售后监督者对外暴露成一个统一的 Tool 方法。这样上层监督者不关心售后域内部细节下层售后监督者也有自己的子工具和上下文分工更清晰。从并行性角度讲langchain4j 目前对工具调用是串行执行的。如果监督者一次要调多个工具且这些工具之间没有依赖你可以考虑手动做并行。不过并行会带来结果回填顺序的问题模型对工具结果的顺序比较敏感我建议先串行跑通有性能压力了再做并行。最后分享一点实在的体会。监督者 Agent 听着很玄乎真正落地时你会发现工作量主要集中在两件事一是把工具描述写得足够清楚二是把系统提示词的行为约束写得足够严格。代码量反而很少AI Services 帮我们省掉了很多内部的编排逻辑。如果你现在的业务连单 Agent 都还没跑通我建议先不要急着搞监督者。先让一个 Agent 把工具调用跑顺把幻觉问题压下去再拆分业务域引入监督者。监督者解决的是“多域协作”的问题不是一个 Agent 都还没调通时的救命稻草。先把基础做扎实再上这套架构你会回来感谢自己的。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →