ARTICLE DETAIL

资讯详情

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

LangChain4j 实战:从 @Tool 到 Agent 流水线的进阶之路

LangChain4j 实战:从 @Tool 到 Agent 流水线的进阶之路 1. 为什么我最终把整套 Agent 流水线压进了 LangChain4j第一次接触 LangChain4j 的时候我的心态其实很功利Java 生态里能用的 LLM 编排库本来就不多Spring AI 那会儿还偏早期Python 的 LangChain 又没法直接塞进现有的 Spring Boot 服务里。所以我当时的诉求特别简单——找个能在 JVM 上跑、能调模型、能接向量库的库把 RAG 那条链路先跑通就行。结果真上手之后发现LangChain4j 的野心比我想的大得多。它不只是个Java 版 LangChain而是把Tool 注解、AiServices 声明式接口、Agent 编排、RAG 检索增强、记忆管理这一整套东西都做进了同一个依赖里。这意味着什么意味着你不需要在项目里同时引入三四个框架一个langchain4j加上几个langchain4j-xxx-integration就能从最简单的调一次大模型一路做到多工具协作的 Agent 流水线。这篇东西我想聊的就是这条完整的进阶路径从最基础的Tool怎么用到 AiServices 怎么把接口变成 Agent再到 RAG 怎么接、多路召回怎么做、Agent 的记忆和编排怎么设计。中间会穿插大量我实际踩过的坑比如Tool的参数描述写不好模型就乱调、RAG 召回率上不去到底是切分的问题还是 embedding 的问题、Agent 循环停不下来怎么加护栏。适合谁看如果你是有 Java 基础、想在自己项目里落地 AI 能力的后端开发或者你已经用过 LangChain4j 但只停留在调个模型的阶段想往 Agent 方向走那这篇应该能帮你省不少试错时间。如果你完全没接触过也没关系我会把每个概念都用生活化的例子讲清楚。先说结论LangChain4j 目前是我在 JVM 上做 Agent 开发的首选不是因为它完美而是因为它在声明式和可控性之间找到了一个很舒服的平衡点。下面我按进阶顺序一层层拆。2. Tool 注解Agent 能干活的第一步2.1 Tool 到底解决了什么问题大模型本身只会说话它不能查数据库、不能发请求、不能算复杂公式。所谓 Tool工具就是给模型装上的手——你告诉模型你有这些能力可以调用模型在需要的时候就会输出一个结构化的调用请求你的代码执行完再把结果喂回去。LangChain4j 里最优雅的地方在于你不需要手写 JSON Schema 去描述工具。你只要在一个 Java 方法上打Tool注解框架会自动反射出方法名、参数、参数类型生成模型能理解的工具描述。public class WeatherTools { Tool(查询指定城市的当前天气返回温度和天气状况) String getWeather(P(城市名称例如北京、上海) String city) { // 实际调用天气 API return weatherApi.query(city); } }就这么几行getWeather就成了模型可以调用的工具。Tool里的字符串是给模型看的工具说明P是给参数做说明。别小看这两处文字它们直接决定了模型调不调、调得对不对。2.2 工具描述写得好不好决定 Agent 的智商上限我踩过最典型的坑写了个工具叫search描述就一句搜索。结果模型在该调数据库查询的时候去调了它在该调它的时候又自己瞎编答案。后来我把描述改成根据用户问题在内部知识库中做语义检索返回最相关的文档片段适用于事实性问题命中率立刻上来了。这里有个经验法则工具描述要写清楚什么时候用和什么时候不用。模型不是人它没有常识兜底你写得越明确它判断越准。我一般会按这个模板写这个工具做什么一句话输入是什么、格式要求适用场景不适用场景可选但很有用参数描述同理。P(城市名称)不如P(城市名称中文不带市字例如北京)。后者能避免模型传进来北京市朝阳区这种它自己脑补的粒度。2.3 工具方法的返回值设计有讲究很多人忽略的一点工具返回给模型的内容也会影响后续推理。如果你返回一大坨 JSON模型可能抓不住重点如果你返回自然语言模型反而更容易理解。我的做法是结构化数据在工具内部消化返回给模型的是精炼后的自然语言或关键字段。比如查订单不要返回整个订单对象而是返回订单号 A123状态已发货预计 3 月 5 日送达。这样模型下一步推理会顺畅很多。另外要注意异常处理。工具方法抛异常时LangChain4j 会把异常信息也传给模型。这其实是好事——模型看到数据库连接超时可能会决定重试或换个工具。但如果你抛的是NullPointerException这种没信息量的异常模型就懵了。所以工具内部一定要 catch 住底层异常转成有意义的错误描述再抛。2.4 工具数量不是越多越好我一开始很兴奋给 Agent 塞了十几个工具结果发现模型选择困难经常调错。后来砍到 5 个以内准确率明显提升。原因很简单工具越多模型在决策时的搜索空间越大越容易选错。这跟人一样你给一个新员工十个系统权限他反而不知道先用哪个。所以我的建议是按场景拆分 Agent每个 Agent 只带它真正需要的工具而不是搞一个万能 Agent。3. AiServices把接口变成 Agent 的声明式魔法3.1 从手动拼 Prompt 到声明式接口早期我用 LangChain4j 是这样的手动拼 system prompt、手动管理 message 列表、手动解析模型输出。代码又长又容易出错。直到用上AiServices才发现原来可以这么写interface Assistant { SystemMessage(你是一个专业的客服助手回答要简洁准确) String chat(String userMessage); } Assistant assistant AiServices.create(Assistant.class, chatModel); String reply assistant.chat(我的订单什么时候到);你定义一个接口框架帮你生成实现。方法调用就是一次对话SystemMessage定义人设参数就是用户输入。这种声明式风格是 LangChain4j 最舒服的设计之一。3.2 把工具挂到 AiServices 上Agent 就成型了真正让 AiServices 变成 Agent 的是它能挂工具Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(chatModel) .tools(new WeatherTools(), new OrderTools()) .chatMemory(MessageWindowChatMemory.withMaxMessages(20)) .build();这几行代码背后发生的事模型收到用户问题后会判断是否需要调工具需要就输出工具调用请求框架执行工具把结果回传模型继续推理直到给出最终答案。这个推理-调用-再推理的循环就是 Agent 的核心。我特别喜欢这种设计的原因是它把 Agent 的复杂度藏起来了但没藏死。你想控制循环次数、想加拦截器、想换记忆策略都有对应的 API。不像某些框架要么全黑盒要么全手写。3.3 记忆管理Agent 的记性怎么调chatMemory这块我踩坑最多。默认的MessageWindowChatMemory只保留最近 N 条消息超出就丢。这在短对话里没问题但长对话里 Agent 会失忆——用户前面说的关键信息被挤掉了。我的处理策略分三档短对话场景客服问答MessageWindowChatMemory.withMaxMessages(20)够用简单省 token。中等长度多轮任务用TokenWindowChatMemory按 token 数而不是消息数来限制更精确。长对话/需要长期记忆得自己实现ChatMemoryStore把历史存到 Redis 或数据库配合摘要压缩。这里有个反直觉的点记忆不是越多越好。塞太多历史进去一是费 token二是会干扰模型对当前问题的判断。我见过 Agent 因为记住了三轮前的无关信息把当前问题答偏的。所以我现在倾向于窗口 摘要近期消息保留原文远期消息压缩成一段摘要。3.4 流式输出和 AiServices 的配合做前端交互的时候流式输出几乎是刚需。LangChain4j 支持返回TokenStream配合StreamingChatLanguageModel用。但要注意流式模式下工具调用的处理会复杂一些因为工具调用请求可能分散在多个 token 里。我的经验是如果 Agent 需要频繁调工具先用非流式跑通逻辑确认工具调用没问题再切流式。别一上来就流式调试起来很痛苦。4. RAG 接入让 Agent 有外部知识4.1 RAG 的本质是开卷考试大模型的知识有截止日期也不知道你公司的内部文档。RAG检索增强生成的思路很朴素回答问题前先去知识库里查相关资料把资料塞进 prompt让模型基于资料回答。就像开卷考试模型不用死记硬背翻书就行。LangChain4j 里 RAG 的链路是这样的文档加载DocumentLoader切分DocumentSplitter向量化EmbeddingModel存入向量库EmbeddingStore检索EmbeddingStoreContentRetriever注入到 AiServicesEmbeddingStoreTextSegment store new InMemoryEmbeddingStore(); EmbeddingStoreIngestor ingestor EmbeddingStoreIngestor.builder() .documentSplitter(DocumentSplitters.recursive(500, 50)) .embeddingModel(embeddingModel) .embeddingStore(store) .build(); ingestor.ingest(document);4.2 切分策略RAG 效果的第一道分水岭我见过太多人 RAG 效果差第一反应是换 embedding 模型其实问题往往出在切分上。DocumentSplitters.recursive(500, 50)的意思是每段最多 500 个 token段与段之间重叠 50 个 token。重叠是为了避免一句话被硬生生切断导致语义丢失。但固定长度切分有个致命问题它不理解文档结构。一份技术文档按 500 字切很可能把参数说明和注意事项切到两段里检索时只召回一半。我的做法是分层切分先按标题层级切Markdown 的#、##再在每层内部按长度切。LangChain4j 提供了DocumentByParagraphSplitter、DocumentByLineSplitter等也可以自己实现DocumentSplitter接口。对于结构化强的文档按结构切的效果远好于按长度切。4.3 多路召回单一检索策略的天花板纯向量检索有个天然短板它对精确匹配不敏感。用户问订单号 A12345 的状态向量检索可能召回一堆订单状态查询的通用文档却漏掉那条精确包含 A12345 的记录。这就是多路召回Multi-Route Retrieval要解决的问题。我的实践是组合三种检索检索方式擅长场景实现方式向量检索语义相似、模糊问题EmbeddingStoreContentRetriever关键词检索精确匹配、专有名词全文索引如 Lucene元数据过滤按时间/类型/权限筛选MetadataFilterLangChain4j 里可以用EmbeddingStoreContentRetriever配合dynamicFilter做元数据过滤多路召回则需要自己组合多个 Retriever再用ReRankingContentAggregator做重排。重排这一步很关键。多路召回回来的结果会有重复和噪声用一个重排模型比如 Cohere Rerank 或本地的 cross-encoder重新打分排序能显著提升最终注入 prompt 的质量。我实测下来加了重排之后答案准确率能提升 15% 到 25%具体取决于知识库的杂乱程度。4.4 RAG 的瓶颈到底在哪热词里有个rag瓶颈我深有同感。RAG 跑通容易跑好难。我总结下来瓶颈主要在三处第一是召回质量。检索不到正确文档后面全白搭。这需要反复调切分、调 embedding、加多路召回、加重排。第二是上下文窗口。召回太多文档塞不进 prompt塞进去模型也抓不住重点。所以要控制召回数量一般 top-3 到 top-5 就够配合重排精选。第三是模型对上下文的利用能力。有些模型给了资料也不会用还是按自己的记忆答。这时候要在 system prompt 里明确要求只基于提供的资料回答资料中没有的信息要说明无法回答。4.5 知识库的三种形态RAG、KG 和结构化知识库热词里提到kg知识库、rag知识库和结构知识库区分以及应用场景这个区分很重要我展开说下。RAG 知识库存的是非结构化文本的向量适合文档问答这类场景比如产品手册、政策文件。它的优势是灵活什么文本都能塞劣势是推理能力弱回答 A 和 B 有什么关系这种需要多跳推理的问题时容易断链。KG 知识库知识图谱存的是实体和关系适合需要推理的场景比如张三的上级的部门负责人是谁。它能做多跳查询但构建成本高需要抽取实体关系。结构化知识库就是传统数据库适合精确查询和统计比如上个月销售额是多少。它准确但不懂自然语言。实际项目里我经常三者混用Agent 先判断问题类型事实查询走结构化库文档问答走 RAG关系推理走 KG。LangChain4j 的工具机制天然适合这种组合——把三种查询各封装成一个Tool让模型自己选。5. Agent 编排从单 Agent 到流水线5.1 单 Agent 的能力边界一个 Agent 挂几个工具能解决不少问题。但复杂任务它扛不住。比如帮我分析这份销售报告找出异常然后生成一封给客户的说明邮件——这涉及读文件、数据分析、写邮件三个步骤单 Agent 容易在中途迷失。原因在于单 Agent 的上下文是共享的任务越长上下文越乱。前面步骤的中间结果会干扰后面步骤的判断。5.2 流水线式编排把大任务拆成小 Agent我的解法是把任务拆成多个专职 Agent串成流水线。每个 Agent 只干一件事上下文干净工具精简。用户请求 → 路由 Agent → 检索 Agent → 分析 Agent → 生成 Agent → 输出路由 Agent 判断任务类型检索 Agent 负责找资料分析 Agent 处理数据生成 Agent 写最终答案。每个 Agent 用独立的 AiServices 实例独立的记忆和工具。LangChain4j 本身没有强制的流水线抽象但用普通 Java 代码串起来就行。我一般会定义一个AgentPipeline类每个节点是一个FunctionInput, Output中间可以加日志、加校验、加降级。5.3 Agent 循环停不下来怎么办这是 Agent 开发里最危险的问题模型陷入调工具-看结果-再调工具的死循环烧 token 还出不来。LangChain4j 的 AiServices 默认有最大工具调用次数限制但我觉得不够。我的做法是加三层护栏硬性次数限制设置最大迭代次数比如 10 次超过就强制返回当前结果。重复检测记录最近几次工具调用的参数如果发现完全重复直接中断。超时控制整个 Agent 执行加一个总超时比如 30 秒超了就降级到简单回答。这些护栏不是限制 Agent 能力而是保证它在异常情况下不会失控。生产环境里一个失控的 Agent 烧掉的 token 成本可能比你一个月工资还高。5.4 Agent 安全别让工具变成后门热词里有agent安全这个必须聊。Agent 能调工具就意味着它能执行真实操作。如果工具里有删除数据发送邮件转账这类敏感操作一旦模型被诱导后果很严重。我的原则是读写分离敏感操作加人工确认。查询类工具可以放开让 Agent 随便调写操作类工具要么不暴露给 Agent要么在执行前加一道确认。LangChain4j 里可以通过自定义ToolExecutor来拦截工具调用在敏感工具执行前插入校验逻辑。另外工具的参数一定要做校验。模型可能传进来奇怪的参数比如 SQL 注入片段。工具内部该转义转义该白名单白名单别假设模型传的都是干净的。6. 一些实战中的零碎经验6.1 模型选型不是越贵越好我做过对比在工具调用这种结构化任务上中等规模的模型往往就够用大模型反而因为想太多而调错工具。我的策略是路由和工具调用用小模型最终生成用大模型。这样成本和效果都能兼顾。6.2 Prompt 里的防幻觉话术在 system prompt 里加一句如果资料中没有相关信息请明确说明不知道不要编造能显著降低幻觉率。这句话看起来简单但效果立竿见影。我还会加回答时引用资料中的原文片段这样既提高可信度也方便我排查问题。6.3 日志和可观测性Agent 的调试比普通接口难得多因为它的决策过程是不透明的。我的做法是把每次模型调用、每次工具调用、每次检索结果都打日志最好带上 traceId。出问题时能完整回放整个决策链路。LangChain4j 支持注册ChatModelListener可以在里面埋点。6.4 关于一个库打全套回到标题。LangChain4j 确实做到了从Tool到 Agent 流水线的全覆盖一个依赖搞定。但一个库打全套不等于一个配置打全套。不同场景下的模型、记忆策略、检索策略、护栏配置都不一样该拆还得拆。库是统一的架构是要设计的。我现在的项目里LangChain4j 承担了 80% 的编排工作剩下的 20% 是业务逻辑和自定义组件。这个比例我觉得挺健康——框架帮你处理通用的部分你把精力放在业务特有的地方。最后分享一个我最近才想明白的点Agent 的能力上限不取决于模型多强而取决于你的工具设计得多好、知识库整理得多干净、护栏加得多合理。模型只是大脑工具是手脚知识是记忆护栏是本能。这四样配齐了Agent 才真正能用。
返回列表