ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LangChain4j实战:从@Tool注解到生产级Agent流水线

LangChain4j实战:从@Tool注解到生产级Agent流水线 1. 为什么我最终把整套 Agent 流水线压在了 LangChain4j 上最早接触 LangChain4j 是从一个很朴素的需求开始的我手上有一堆 Java 服务业务逻辑全在 Spring 生态里但团队想接大模型做智能问答和自动化任务。Python 那套 LangChain 生态确实成熟可让一个纯 Java 团队为了几个 Agent 功能去维护一套 Python 服务运维成本、序列化成本、调试链路全都翻倍。LangChain4j 出现之后我第一反应就是——终于可以在同一个 JVM 里把模型调用、工具调用、检索增强、Agent 编排全串起来了。这篇内容我想聊的不是LangChain4j 是什么而是怎么从一个Tool注解出发一步步搭出一条能上生产的 Agent 流水线。核心关键词就几个LangChain4j、Tool、Agent、RAG、Agentic。适合谁看如果你是会写 Java、懂 Spring、想在自己项目里落地 AI Agent 的后端开发或者你已经在用 LangChain4j 但卡在工具调用能跑、但编排一复杂就乱的阶段那这篇基本就是给你写的。我踩过的坑包括工具方法签名写错导致模型永远调不对、RAG 召回质量差到模型开始胡说、多轮 Agent 循环停不下来烧 token、工具之间状态传递丢失等等。下面我会把这些经验按设计思路 → 核心细节 → 实操落地 → 问题排查的顺序完整拆开尽量做到你看完能直接抄作业。2. 整体设计思路从单点工具到 Agent 流水线2.1 为什么不是直接上 Agent而是从 Tool 起步很多人一上来就想搞一个全自动 Agent结果发现模型要么不调用工具要么乱调用要么陷入死循环。我的经验是Agent 的能力上限取决于你工具定义的质量。工具是 Agent 的手脚手脚不灵活大脑再聪明也白搭。LangChain4j 里Tool注解就是把一个普通 Java 方法暴露给大模型让模型在需要时调用它。它的本质是把方法名、参数、描述序列化成一段模型能理解的 schema模型输出一个我要调用这个工具、参数是这些的结构化请求框架负责反射调用并把结果回填给模型。所以我的设计思路是分三层递进第一层单工具可用。先保证一个Tool方法能被模型正确识别和调用参数类型、描述、返回值都清晰。第二层多工具协同。多个工具之间能配合模型能根据上下文选择正确的工具甚至连续调用多个。第三层Agent 流水线。把工具、RAG 检索、记忆、循环控制组合成一个有状态的 Agent能处理多步骤任务。这个递进顺序很重要因为每一层的调试手段完全不同。第一层看日志就行第二层要看模型的工具选择逻辑第三层要考虑状态管理和终止条件。2.2 Agentic 的核心让模型决定下一步做什么Agentic这个词这两年很热但落到代码里其实就一句话模型不再只是回答而是决定下一步动作。传统调用是你问 → 模型答Agentic 是你给目标 → 模型决定调哪个工具 → 看结果 → 再决定下一步 → 直到完成。LangChain4j 里实现这个循环核心是AiServices配合工具和记忆。模型每一轮拿到的是系统提示 历史对话 可用工具列表 上一轮工具返回结果。它输出要么是最终答案要么是下一个工具调用请求。这里有个关键设计决策循环由谁控制。LangChain4j 默认的 AiServices 会在模型请求工具时自动执行并回填形成内部循环。但如果你要做的 Agent 步骤很多或者需要在每步之间插入人工审核、日志、限流就得自己控制循环。我一般会在原型阶段用默认循环快速验证生产阶段改成显式循环方便加监控和熔断。2.3 RAG 在 Agent 里的定位不是替代工具而是补充上下文很多人把 RAG 和工具调用混为一谈其实它们解决的是不同问题。RAG 解决的是模型不知道的私有知识工具解决的是模型做不到的动作。一个查知识库一个执行操作。在 Agent 流水线里RAG 通常作为一个特殊的工具或者前置的上下文注入存在。我倾向于把 RAG 做成工具因为这样模型可以自己决定这个问题需不需要查知识库。但要注意RAG 召回质量直接决定 Agent 表现召回错了后面全错。这块后面会专门讲。2.4 方案选型为什么是 LangChain4j 而不是别的选 LangChain4j 的理由很实际维度LangChain4jPython LangChain自研语言一致性Java 原生和业务同 JVM需跨语言服务完全可控生态成熟度中等够用最成熟从零开始运维成本低复用现有部署高多一套服务中工具调用Tool注解极简装饰器成熟自己写 schemaRAG 支持内置多种向量库集成最丰富自己接调试体验和 Java 调试一致需跨服务追踪自己搭对 Java 团队来说LangChain4j 最大的价值是不用为了 AI 功能重构整个技术栈。你现有的 Spring Bean、数据库连接、缓存、监控全都能直接复用工具方法就是普通 Service 方法加个注解。3. 核心细节解析Tool、Agent、RAG 的关键实现3.1 Tool 注解的隐藏规则模型怎么看懂你的方法Tool看起来简单但模型能不能正确调用取决于三个东西方法名、参数名、描述文本。这三者会被序列化成 JSON Schema 给模型看。先看一个能用的例子public class OrderTools { Tool(根据订单号查询订单状态返回订单的当前状态和物流信息) public OrderStatus queryOrderStatus( P(订单号格式为 ORD 开头的 12 位字符串) String orderId) { return orderService.getStatus(orderId); } Tool(取消指定订单仅在订单状态为待发货时可取消) public CancelResult cancelOrder( P(订单号) String orderId, P(取消原因用于记录) String reason) { return orderService.cancel(orderId, reason); } }这里有几个我踩过坑才明白的点描述要写什么时候用不只是是什么。比如仅在订单状态为待发货时可取消这句直接告诉模型调用前提能大幅减少错误调用。参数描述要带格式约束。ORD 开头的 12 位字符串比订单号有用得多模型会据此校验用户输入。方法名用动词开头。queryOrderStatus比orderStatus更容易让模型理解这是动作。返回值要可序列化且信息完整。模型只能看到你返回的内容返回一个只有 ID 的对象等于没返回。注意Tool方法的参数类型尽量用基本类型和 String复杂对象虽然也能序列化但模型理解成本高容易传错。3.2 工具调用的完整链路从模型输出到方法执行理解这条链路对排查问题至关重要。一次工具调用的完整流程是用户输入 系统提示 工具 schema 一起发给模型模型返回一个结构化的工具调用请求工具名 参数 JSONLangChain4j 解析请求通过反射找到对应方法反序列化参数执行方法把返回值序列化作为一条工具结果消息追加到对话再次调用模型模型基于工具结果决定下一步任何一步出问题表现都是模型不调用工具或调用报错。我的排查顺序是先看模型有没有返回工具调用请求第 2 步再看参数能不能反序列化第 4 步最后看返回值模型能不能理解第 6 步。3.3 Agent 循环的终止条件别让模型无限转圈Agent 最危险的问题就是停不下来。模型可能反复调用同一个工具或者在不同工具间来回横跳。LangChain4j 的默认循环有最大迭代次数保护但生产环境我建议自己加更细的控制最大工具调用次数比如 10 次超过就强制返回当前结果。重复调用检测如果连续两次调用同一工具且参数相同直接中断。超时控制整个 Agent 流程设置总超时避免单次请求拖太久。成本预算累计 token 超过阈值就停防止烧钱。这些控制逻辑我一般包在一个AgentExecutor里而不是依赖框架默认行为。因为默认行为在黑盒里出问题不好定位。3.4 RAG 召回质量Agent 表现的天花板RAG 在 Agent 里最常见的用法是做成一个检索工具。但很多人做完发现模型查了知识库还是答错问题往往出在召回环节。影响召回质量的因素按重要性排序切分策略按语义切分比按固定长度切分好但成本高。我一般用递归切分块大小 500-800 token重叠 100 token。Embedding 模型选择中文场景下通用多语言模型往往不如专门优化的中文模型。多路召回单一向量召回容易漏结合关键词召回BM25能显著提升。这就是热词里提到的多路召回。重排序召回 top 20用 rerank 模型精排到 top 5质量提升明显。提示RAG 知识库能不能存图片能但存的是图片的向量表示或图片描述文本。纯图片检索需要多模态 embeddingLangChain4j 对这块支持还在演进生产环境我一般先把图片转成文字描述再入库。4. 实操落地搭一条完整的 Agent 流水线4.1 环境准备与依赖引入先明确版本。LangChain4j 迭代很快我写这篇时用的是 0.35 系列核心依赖dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.35.0/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version0.35.0/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-easy-rag/artifactId version0.35.0/version /dependency如果你用别的模型提供方换成对应的集成包即可。向量库我一般用内存版做原型生产换 PGVector 或 Milvus。4.2 定义工具集把业务能力暴露给模型工具集的设计原则是单一职责 描述清晰。我以一个客服场景为例定义三个工具public class CustomerServiceTools { private final OrderService orderService; private final KnowledgeBase knowledgeBase; Tool(查询订单详情包括状态、金额、下单时间。当用户询问订单相关问题时使用) public String queryOrder(P(订单号) String orderId) { Order order orderService.findById(orderId); if (order null) { return 未找到订单 orderId; } return String.format(订单%s状态%s金额%s元下单时间%s, order.getId(), order.getStatus(), order.getAmount(), order.getCreateTime()); } Tool(搜索知识库获取产品使用说明和常见问题解答。当用户询问产品功能、使用方法时使用) public String searchKnowledge(P(搜索关键词尽量具体) String query) { ListString docs knowledgeBase.search(query, 3); return docs.isEmpty() ? 知识库中未找到相关内容 : String.join(\n---\n, docs); } Tool(创建售后工单。当用户明确要求投诉或需要人工介入时使用) public String createTicket( P(工单内容描述) String content, P(用户联系方式) String contact) { String ticketId orderService.createTicket(content, contact); return 工单已创建编号 ticketId; } }注意每个工具的描述都写清楚了什么时候用这是减少误调用的关键。4.3 组装 AgentAiServices 的配置要点把工具、记忆、模型组装成一个 Agent 接口public interface CustomerServiceAgent { SystemMessage( 你是一个电商客服助手。你的职责是帮助用户查询订单、解答产品问题、处理售后。 规则 1. 用户询问订单时先调用 queryOrder 工具不要凭记忆回答。 2. 用户询问产品功能时先调用 searchKnowledge 工具。 3. 用户明确要求投诉或你无法解决时调用 createTicket 工具。 4. 每次只调用一个工具拿到结果后再决定下一步。 5. 如果工具返回未找到如实告知用户不要编造。 ) String chat(MemoryId String sessionId, UserMessage String message); }组装代码CustomerServiceAgent agent AiServices.builder(CustomerServiceAgent.class) .chatLanguageModel(model) .tools(new CustomerServiceTools(orderService, knowledgeBase)) .chatMemoryProvider(sessionId - MessageWindowChatMemory.withMaxMessages(20)) .build();几个关键配置说明SystemMessage是 Agent 的灵魂。规则写得越具体模型行为越可控。我一般会把先调工具再回答不要编造这类约束写进去。MemoryId实现多会话隔离。每个用户一个 sessionId记忆互不干扰。MessageWindowChatMemory控制上下文长度。20 条消息是个经验值太多会稀释注意力太少会丢上下文。4.4 RAG 检索工具的实现细节把 RAG 做成工具核心是检索逻辑。我用 Easy RAG 做原型EmbeddingStoreTextSegment embeddingStore new InMemoryEmbeddingStore(); EmbeddingStoreIngestor ingestor EmbeddingStoreIngestor.builder() .documentSplitter(DocumentSplitters.recursive(600, 100)) .embeddingModel(embeddingModel) .embeddingStore(embeddingStore) .build(); // 加载文档 ListDocument documents FileSystemDocumentLoader.loadDocuments(/path/to/docs); ingestor.ingest(documents);检索时public ListString search(String query, int topK) { Embedding queryEmbedding embeddingModel.embed(query).content(); ListEmbeddingMatchTextSegment matches embeddingStore.findRelevant(queryEmbedding, topK); return matches.stream() .map(m - m.embedded().text()) .collect(Collectors.toList()); }这里recursive(600, 100)的意思是递归切分目标块大小 600 token相邻块重叠 100 token。重叠是为了避免关键信息被切断在边界。4.5 多路召回与重排序的落地单一向量召回在中文场景下经常漏召回。我的做法是向量召回 关键词召回并行然后合并去重public ListString multiRecall(String query, int topK) { // 向量召回 ListString vectorResults vectorSearch(query, topK); // 关键词召回可以用 Lucene 或数据库全文索引 ListString keywordResults keywordSearch(query, topK); // 合并去重保留顺序 LinkedHashSetString merged new LinkedHashSet(); merged.addAll(vectorResults); merged.addAll(keywordResults); return new ArrayList(merged); }如果对质量要求高再加一层 rerankpublic ListString rerank(String query, ListString candidates, int topN) { // 用交叉编码器或专门的 rerank 模型打分 return rerankModel.rerank(query, candidates, topN); }实测下来多路召回 重排序能把召回准确率提升 20% 以上代价是延迟增加。生产环境我会根据场景权衡简单问答用单路复杂查询用多路。4.6 显式 Agent 循环生产环境的可控方案原型阶段用 AiServices 默认循环够了但生产环境我建议自己控制循环。核心逻辑public String runAgent(String sessionId, String userMessage) { ListChatMessage messages new ArrayList(); messages.add(SystemMessage.from(SYSTEM_PROMPT)); messages.addAll(memory.get(sessionId)); messages.add(UserMessage.from(userMessage)); int maxSteps 10; int step 0; while (step maxSteps) { ChatResponse response model.generate(messages); AiMessage aiMessage response.content(); if (!aiMessage.hasToolExecutionRequests()) { // 模型给出最终答案 memory.add(sessionId, aiMessage); return aiMessage.text(); } // 执行工具调用 messages.add(aiMessage); for (ToolExecutionRequest request : aiMessage.toolExecutionRequests()) { String result toolExecutor.execute(request); messages.add(ToolExecutionResultMessage.from(request, result)); } step; } return 处理步骤过多请简化您的问题后重试。; }这个显式循环的好处是每一步都能加日志、加监控、加熔断。maxSteps是硬性保护防止无限循环。5. 常见问题与排查技巧实录5.1 模型不调用工具怎么办这是最高频的问题。排查顺序现象可能原因解决方向完全不调用工具没注册检查.tools()是否传入完全不调用模型不支持 function calling换支持工具调用的模型偶尔不调用描述不清晰补充什么时候用的描述该调不调系统提示没引导在 SystemMessage 里明确要求先调工具调了但参数错参数描述缺失给每个参数加P描述和格式约束我的经验是90% 的不调用问题都是描述写得不够具体。模型不是人它只能根据你给的文字判断。把查询订单改成当用户提供订单号并询问订单状态时调用此工具查询效果立竿见影。5.2 工具调用报参数反序列化错误典型报错是Cannot deserialize value of type ...。原因通常是模型传的参数类型和你的方法签名不匹配。比如你定义int count模型传了3字符串。解决办法参数类型尽量用 String在方法内部自己转换和校验。如果必须用数字在参数描述里明确传入数字不要加引号。加一层容错用P描述约束格式。5.3 Agent 陷入循环停不下来表现是模型反复调用同一个工具或者两个工具来回调。原因和解决工具返回值没有提供新信息模型看不到进展就重复调用。解决是让返回值包含足够信息让模型能判断已经完成了。系统提示没有终止条件在 SystemMessage 里加如果已经获取到足够信息直接回答用户不要继续调用工具。缺少硬性步数限制一定要有maxSteps这是最后防线。5.4 RAG 召回了但模型不用有时候检索结果明明包含答案模型却忽略它自己编。这通常是上下文注入方式的问题。检索结果要以清晰的结构注入比如以下是知识库检索结果请优先基于这些内容回答 [1] xxx [2] xxx 如果检索结果不包含答案请明确告知用户。同时系统提示里要强调优先基于检索结果回答不要编造。5.5 多轮对话记忆丢失如果发现 Agent 记不住上一轮的内容检查MemoryId是否在每次调用时传了相同的 sessionId。ChatMemoryProvider是否配置正确。记忆窗口是否太小导致早期消息被挤掉。我一般用MessageWindowChatMemory.withMaxMessages(20)并在关键信息上用工具结果的形式固化而不是依赖记忆。5.6 性能与成本优化Agent 流水线跑起来后延迟和成本是两个绕不开的问题。我的优化手段工具结果缓存相同参数的查询结果缓存减少重复调用。RAG 结果缓存高频查询的检索结果缓存。模型分级简单意图识别用小模型复杂推理用大模型。并行工具调用如果多个工具之间无依赖可以并行执行。上下文裁剪定期清理无关的历史消息减少 token 消耗。实测下来缓存 模型分级能把平均成本降低 40% 左右延迟降低 30%。6. 一些关于 Agent 架构的延伸思考6.1 Agent 和普通工作流的边界在哪不是所有任务都需要 Agent。我的判断标准是如果步骤是固定的、可枚举的用工作流如果步骤取决于中间结果、需要动态决策才用 Agent。比如查订单 → 判断状态 → 决定是否取消这种如果分支有限用工作流更稳定、更便宜。Agent 的价值在于处理那些你事先想不到所有分支的场景。6.2 工具粒度怎么把握工具太粗模型不好组合工具太细模型选择困难。我的经验是一个工具对应一个完整的业务动作。比如查询订单是一个工具而不是查订单表查物流表两个工具。粒度以业务语义完整为准而不是以技术实现为准。6.3 Agent 安全的基本考量Agent 能调用工具就意味着它能产生副作用。生产环境必须考虑权限控制不同用户能调用的工具不同。敏感操作二次确认比如取消订单、退款这类操作要有人工确认环节。输入校验工具方法内部必须做参数校验不能信任模型传的参数。审计日志每次工具调用都记录便于追溯。这些不是 LangChain4j 帮你做的是你自己在工具实现层要保证的。6.4 后续可以扩展的方向这套流水线搭起来后往上还能加不少东西。比如接入更复杂的记忆机制把短期对话记忆和长期用户画像结合比如引入规划能力让 Agent 先拆解任务再执行比如做多 Agent 协作一个负责检索、一个负责决策、一个负责执行。这些在 LangChain4j 里都有对应的扩展点但核心还是那句话——工具定义的质量决定 Agent 的上限把基础打牢上层怎么搭都稳。我个人在实际项目里的体会是别一上来追求全自动智能体先把单工具调稳再把 RAG 召回做准最后才是编排。每一步都验证到位整条流水线才靠得住。踩过的坑基本都集中在跳步上——工具还没调通就急着上 AgentRAG 还没召回准就急着做多轮最后问题叠在一起根本不知道从哪查起。
返回列表