
做Java这行好多年去年开始研究LangChain4j把大模型能力往业务系统里接。一开始的想法很朴素能调用对话接口就完事了结果做着做着发现根本不是那么回事——让大模型能自己查数据、调接口、跨步骤完成任务才是真正让AI落地的方式。这块涉及到的不是某一个模型的API而是一整套衔接逻辑工具定义、上下文管理、多步执行、记忆维护。也就是从单个Tool到一个Agent流水线的演进。我最后选择了LangChain4j一个Java生态里相对低调但能力覆盖很全的库。要说它多强大也不至于但一个库打全套这个说法放在它身上很贴切模型接入、工具调用、记忆管理、RAG、Agent编排全都有现成方案且API设计得很有Java味——接口、注解、泛型用起来不别扭。这篇内容主要面向已经在Java里跑过大模型API、但没深入做过Agent层开发的开发者也适合刚接触LangChain4j、想一步到位把工具调用和Agent流水线串起来的人。我会先讲为什么选它再讲Tool怎么用到位、Agent流水线怎么编排最后把我在真实项目里踩过的坑和调优经验一并说出来。1. 对比一圈之后我为什么把宝押在 LangChain4j1.1 我在集成LLM时面对的三条路先说真实经历。我第一次把ChatCompletion接口接进项目的时候觉得这太轻松了HTTP请求包一层JSON解析一下完事。但后面需求一多就傻眼了。要让大模型查数据库、查库存、调优惠券接口你得自己维护一套工具描述和参数Schema还得处理模型返回的tool call消息把参数解析出来再路由到对应方法。这些东西做到第三四个工具的时候代码已经乱成一锅粥而且没有任何框架帮你兜底。Spring AI我也踩过。作为Spring生态的官方库它的模型接入、Prompt模板做得很规整但在Agent这一层的可用组件和文档相对少。我需要的恰恰是这块工具自动调用、多轮循环、记忆管理、知识库召回的组装。Spring AI当时给我的感觉是有但不多真要跑出一个带工具决策的Agent得自己拼不少东西。LangChain4j是我对比下来觉得最顺手的。它的核心思路很直接——把模型、记忆、工具、RAG这些零件通过AiService这个入口一次性组装成一条流水线。模型这一侧OpenAI、各类国产模型都有LanguageModel实现业务这一侧你要做的只是写Tool方法剩下的交给库去调度。GitHub上的star涨得也快社区活跃度在Java AI库里算是第一梯队。也有朋友推荐直接上可视化平台搭知识库流水线原型阶段确实快但一进Java业务系统还是回到代码层面更可控毕竟Agent要深度绑定你们自己的订单、物流、营销系统。1.2 AiService 把对话流程变成了业务接口LangChain4j最打动我的设计是AiService。我一直在想为什么AI集成非得在业务代码里一大坨一大坨地写就不能像写DAL接口一样把AI对话能力抽成一个方法吗它做到了。你只需要定义一个接口public interface CustomerServiceAssistant { String chat(String userMessage); }后面组装好之后业务层只管注入这个接口调用chat()。底层发生的模型调用、上下文维护、工具触发全都在这一个门面背后自动完成。这种感觉很像把RPC服务定义成一个Feign接口——先约定边界再注入实现业务代码保持干净。后面所有Agent流水线的玩法都是建立在这个入口之上的。2. Tool 进阶用法从单工具到多工具协作2.1 Tool 到底做了什么Tool的本质是把一个Java方法暴露给大模型作为可调用工具。大白话说你告诉大模型你可以用这个方法做某件事它在对话中如果觉得需要就会发起一次工具调用LangChain4j负责把请求路由到方法上执行完再把结果以tool message的形式喂回给模型。最基础的写法是这样的public class OrderTools { Tool(根据用户ID查询最近3个订单) public ListOrder findRecentOrders(Long userId) { return orderRepository.findTop3ByUserIdOrderByCreateTimeDesc(userId); } }然后注册进AiServiceCustomerServiceAssistant assistant AiServices.builder(CustomerServiceAssistant.class) .chatLanguageModel(model) .tools(new OrderTools()) .build();这两段代码跑通之后你只需要在对话里说一句帮我查一下上个月的订单模型自己就知道调用findRecentOrders你完全不用写参数解析。LangChain4j基于方法名、方法描述、参数名和类型生成工具定义的JSON Schema参数反序列化也都是自动完成的。这里有个细节我一开始就栽过方法名和描述不能乱来。模型决定调用哪个工具靠的是工具名称和描述的语义匹配。如果你写getData、描述又是获取信息这种大白话模型面对多个工具时会犹豫甚至不调用。我后来养成的习惯是方法名遵守业务命名描述里写清能做什么、不能用它做什么、参数含义是什么。这些信息是模型唯一的工具说明书值得多花几行字。2.2 多工具协作按业务域拆分一次注册项目变大之后Tool不会只有一个。我习惯按业务域拆类订单一个OrderTools物流一个LogisticsTools营销一个PromotionTools。组装的时候全塞进去Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools( new OrderTools(), new LogisticsTools(), new PromotionTools() ) .build();有人会问工具多了模型到底怎么选答案是模型根据用户意图自主决策这正是Agent的核心机制。LangChain4j把每个Tool变成模型的一个可感知能力点模型在一次回复中甚至能并行发起多个工具调用。比如判断退款资格时模型可能同时查订单状态和查优惠券记录框架把多个tool call并行执行结果再按顺序回填给模型。你要是手动写if-else编排不仅要考虑顺序还得处理各种分支组合业务一变代码就废了。不过有两个边界需要心里有数。第一工具返回给模型的内容要克制。很多开发者习惯直接把领域对象抛给模型但这个对象如果是几十个字段的大实体不仅浪费token还会干扰模型理解重点。我一般会专门做一个给模型看的简报版只返回关键字段的汇总文本。第二工具类本身要无状态。多个用户可能同时触发同一个工具实例的方法工具里如果藏了有状态变量早晚出并发问题。3. 从工具到流水线用 AiService 编排完整 Agent3.1 我把售后客服做成了 Agent而不是状态机让我用一个真实案例把流水线讲明白。我要做的是售后客服用户问我的订单咋还没到Agent要自动查订单、查物流用户说我不想要了Agent要能查单、算退款流程、甚至发起退款申请。传统做法画状态机定义待查单→已查单→判断分支→调用对应服务每一步写完还要考虑异常分支体感很差。用LangChain4j我只需要把该暴露的能力暴露成工具public class AfterSaleTools { private final OrderService orderService; private final LogisticsService logisticsService; private final RefundService refundService; public AfterSaleTools(OrderService orderService, LogisticsService logisticsService, RefundService refundService) { this.orderService orderService; this.logisticsService logisticsService; this.refundService refundService; } Tool(根据订单号查询订单状态和商品明细不含物流信息) public String queryOrder(String orderId) { return orderService.buildOrderBrief(orderId); } Tool(查询订单的物流轨迹输入订单号返回最近5条物流记录) public String queryLogistics(String orderId) { return logisticsService.getRecentTrack(orderId); } Tool(发起退款申请输入订单号返回退款单号) public String applyRefund(String orderId) { return refundService.createRefund(orderId); } }组装AiService时因为这是多用户客服场景不能所有人共用一个上下文窗口。我给每个用户分配独立的ChatMemory用ChatMemoryProvider按MemoryId动态提供ChatMemoryProvider memoryProvider memoryId - MessageWindowChatMemory.builder() .id(memoryId) .maxMessages(20) .build(); AfterSaleAgent agent AiServices.builder(AfterSaleAgent.class) .chatLanguageModel(model) .tools(new AfterSaleTools(...)) .chatMemoryProvider(memoryProvider) .build();接口方法上挂MemoryId实现每个用户自己一条对话流水线public interface AfterSaleAgent { String chat(MemoryId Long userId, UserMessage String userMessage); }跑起来的效果就是我想要的用户问怎么还没到模型先queryOrder看到状态是已发货再调queryLogistics拉轨迹最后拼出一段您的订单已从XX发出预计明天到达这是物流轨迹的回答。全程只有chat()这一个入口背后是模型自主的计划-调用-观察-再调用。3.2 多步执行循环模型是怎么刹住车的有人可能好奇LangChain4j怎么让模型反复调工具、最后收敛到最终回答原理不玄乎模型在一次响应中如果返回tool call框架就执行工具把执行结果以tool message拼接回去再次发起对话请求。只要模型还返回新的tool call这个循环就继续直到某次响应没有tool call输出最终文本一轮完成。相当于LLM自己写了一个while循环框架引擎负责驱动它。这个机制适合那种用户只说一句话、但任务要拆多步的场景。比如查一下订单OD20240101如果发货了看物流如果还在库房就申请退款。在老代码里这还是分支判断在Agent模式下模型会自己决定先调哪个工具看到结果再决定下一步。这就是从Tool到Agent流水线的核心Tool提供能力流水线是模型在能力之间自主穿梭的通道。LangChain4j后来也出了Agent这类更显式的编排方式底层机制还是这个循环核心没变。注意这个循环是要烧token的。每一轮工具调用工具结果都会进入上下文。所以工具返回一定要简报化千万别把几十行JSON塞回去。上下文很快就被冲爆模型也会被无关信息带偏。4. 状态与召回Agent 长任务下的两个进阶扩展4.1 记忆窗口怎么设才不会让 Agent 失忆Agent流水线里记忆不是装饰品。上面案例里的MessageWindowChatMemory相当于最近N条消息的滑动窗口。N怎么取我实测普通客服场景maxMessages在10到20之间最稳太小了模型记不住用户前几轮说过的话太大了上下文膨胀影响响应速度和质量。如果业务方硬性要求模型能回溯很久以前的对话别靠窗口硬撑应该把历史写入持久化存储需要时用工具查出来。对持久化有更高要求的可以自己实现ChatMemory接口把消息存到Redis或数据库。LangChain4j留了接口就是让你按需接的。我之前一版用本地内存应用重启用户上下文就全丢了后来换成Redis实现重启也不丢实际体验立刻提升不少。4.2 多路召回让 Agent 同时拥有知识和能力关键词里有个词叫多路召回在Agent场景里特别实用。很多对话机器人不只会调用业务工具还需要回答知识库问题比如退货政策、使用指南。你当然可以把这些知识也做成工具但手动整理成prompt注入效率很低更标准的做法是用RAG把文档向量化按相似度召回答案。LangChain4j的组装方式很清晰给AiService挂一个RetrievalAugmentor指定EmbeddingStore和EmbeddingModel框架会在每次发起对话前自动做知识检索把召回的文本段作为上下文拼进prompt。再配合Tool就形成知识召回工具调用的双通道。示意代码EmbeddingStoreTextSegment embeddingStore new InMemoryEmbeddingStore(); // 实际项目中需要先加载文档用EmbeddingModel向量化后写入store RetrievalAugmentor augmentor DefaultRetrievalAugmentor.builder() .queryTransformer(new CompressingQueryTransformer(model)) .contentRetriever(EmbeddingContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(3) .minScore(0.5) .build()) .build(); Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(new AfterSaleTools(...)) .retrievalAugmentor(augmentor) .build();这里有个版本注意点RetrievalAugmentor在较新的版本里可能需要编入QueryTransformer等组件不同小版本API略有差异实际使用时以当前版本的源码或官网示例为准。但设计理念一致——Agent既会查库也会调工具模型根据意图自主选择路径。到了这一步一个库打全套的意义真正显现你不需要为了RAG再引一套搜索框架也不需要为了Agent单独写编排器LangChain4j把它们全包了。5. 避坑实录让 Agent 流水线稳定的几个关键细节5.1 工具描述和返回体是稳定性命门我在真实项目里最深的一个坑就是工具描述不清导致模型频繁误调用。刚开始我写queryOrder的描述就是查询订单结果用户问物流时模型也先调queryOrder发现返回里压根没物流再转向queryLogistics。浪费一轮不说用户等待时间也变长。改成查询订单状态和商品明细不含物流信息之后选择准确性立刻上来了。所以我的建议是每个工具的描述里明确写能做什么和不能做什么。工具参数的规则性也值得提前约束。订单号这种参数用户可能说OD123456也可能说od123456模型如果不知道规则很容易把原文原样传过来。我的做法是在工具方法入口做一次规范化和校验识别到非法输入就返回参数不合法请让用户补充信息而不是把脏数据一直往下透传。这能有效打断那些因为连续拿到坏数据而反复调用工具的循环。再说返回体。工具返回给模型的内容尽量是给模型看的结果简报而不是完整领域模型。比如Tool(查询订单返回订单号、状态、金额、商品名称列表) public String queryOrderBrief(String orderId) { Order order orderService.getByOrderId(orderId); return String.format(订单%s 状态%s 金额%.2f 商品:%s, order.getOrderId(), order.getStatus(), order.getAmount(), String.join(、, order.getItemNames())); }这样一条消息不过百来字模型一次性能看懂token开销也小。我见过项目里直接返回整个Order JSON几十个字段堆在上下文里模型反而抓不住重点回答质量明显下降。5.2 死循环、并发和可观测性三个容易翻车的地方多步调用虽然爽但有个现实风险刹不住车。工具连续返回异常数据时模型可能会反复调用同一个工具把上下文爆掉甚至卡住请求。我在线上起码遇过两次这种情况。兜底手段有两个一是工具内部做好输入校验识别到无意义输入直接返回终止性提示二是在业务侧给Agent调用设置超时和最大工具调用轮数。别把一个没有上限的循环裸奔在真实用户面前。并发也要提一句。LangChain4j的AiServices实例本身线程安全可以多线程复用。但工具类里注入的那些Service、Repository也得是线程安全的。我出过一次事故工具类里用了一个非线程安全的HashMap做缓存用户量一上来订单结果张冠李戴排查了很久才发现是它。工具类保持无状态、所有依赖都走Spring容器管理是最稳的。最后是可观测性。Agent流水线本质是个黑盒。用户说回答不对你得能快速知道模型调了哪些工具、每轮结果是什么、最后为什么这么答。我的做法是给每个Tool方法加日志记录入参和返回摘要再用MDC把MemoryId串到日志链路里这样按用户维度就能捞出整条Agent轨迹。下面这张表基本概括了我踩过的几个典型问题常见问题典型表现我的对策工具描述含糊模型选错工具或迟迟不调用描述里写清能做什么和不能做什么返回体过大token暴涨、模型抓不住重点返回一句话能读懂的自然语言简报多步循环失控上下文被连续异常结果冲爆工具内做输入校验业务侧设调用轮数上限日志难追踪用户反馈回答错误但定位不到环节每个工具打日志MDC串MemoryId有了这套日志体系调Agent的体验才真正接近调普通业务代码。6. 实测下来的体会和一点建议6.1 不要为了 Agent 而 Agent用LangChain4j快一年我最大的体会是Agent流水线不是越自动化越好。如果业务链路是固定的比如每次都是先查订单再调物流直接用Tool加AiService就够了没必要引入太复杂的自动决策和超长工具链。工具链越长模型失误的可能性和token开销都越大。但像售后客服这种用户意图千变万化、工具组合路径丰富的场景用工具注册加自主编排的流水线思路就是最顺手的解法。6.2 新工具上线前先做信用评估我给自己定了条规矩每次新增Tool都先问一句这个能力暴露成工具模型真的能准确调用吗如果不能宁可保留在Agent之外用传统代码处理也别硬塞进工具集合。工具是Agent的能力边界也是Agent的信用边界——一个描述含糊、返回臃肿的坏工具会拖累整条流水线的可靠性一个边界清晰、返回简报的好工具才是流水线上真正的润滑剂。最后再分享一个小技巧把工具返回统一设计成一句话能读懂的自然语言文本而不是结构化JSON模型理解起来几乎零成本这是我从一次次的token超时和答非所问里学到的。