资讯详情

LangChain4j Agent 流水线实战:从 @Tool 到多 Agent 编排与 RAG 集成

📅 2026/10/5 9:28:28 | 华诺云谱 👁 阅读
LangChain4j Agent 流水线实战:从 @Tool 到多 Agent 编排与 RAG 集成
1. 为什么单靠 Tool 注解撑不起一个真正的 Agent很多人第一次接触 LangChain4j都是从Tool注解开始的。写一个方法加个注解注册到AiServices里模型就能调用它了。这个体验确实很爽几行代码就能让大模型帮你查天气、算数学、读数据库。但如果你真的拿这套东西去做一个稍微复杂点的业务场景很快就会撞墙。我最初也是这样觉得Tool就是 Agent 的全部。直到有一次做一个客服工单自动处理的需求需要模型先判断工单类型再决定是查知识库、调工单接口还是转人工中间还要根据查询结果做二次判断。用纯Tool的方式写出来代码变成了一坨意大利面模型有时候该调工具不调有时候调了工具拿到结果却不知道怎么继续多轮对话里上下文丢失得一塌糊涂。问题的根源在于Tool只解决了模型能调用什么这一个问题它没有解决什么时候调、调完之后干什么、多个工具之间怎么编排这些更上层的问题。这就是 Agent 流水线要解决的事情。LangChain4j 在这方面的设计思路其实很清晰Tool是原子能力Agent 是编排逻辑而流水线是把多个 Agent 串起来形成完整业务闭环。这三层各司其职混在一起用就会出问题。这篇文章我会按照这个层次从最基础的Tool讲起一路讲到怎么用 LangChain4j 搭一条能扛住真实业务的 Agent 流水线中间会穿插 RAG 的集成、多路召回的处理、并发场景下的注意事项以及我自己踩过的那些坑。适合的读者是已经用过 LangChain4j 基础功能、想往生产级别 Agent 方向走的人。如果你还没写过第一个Tool建议先跑通官方 Quick Start 再回来看不然有些细节会显得跳跃。2. Tool 注解的真实能力边界与常见误用2.1 Tool 到底做了什么先把这个注解的机制说清楚。当你在一个方法上标注ToolLangChain4j 在构建AiServices的时候会做几件事读取方法的名称和参数生成一份 JSON Schema 描述把这份描述塞进发给模型的请求里。模型看到这份描述后如果判断需要调用就会返回一个特定格式的响应LangChain4j 解析这个响应反射调用你对应的方法再把返回值拼回对话上下文。整个过程里模型看到的只是有一个叫 queryOrderStatus 的工具需要一个 String 类型的 orderId 参数。它不知道你的方法内部是查数据库还是调 HTTP 接口也不关心。这个抽象层次决定了Tool的能力边界它适合做单一职责、输入输出明确、无副作用的操作。我见过不少人把Tool当成万能胶什么逻辑都往里塞。比如一个processOrder方法内部既校验参数、又扣库存、又发通知、还写日志最后返回一个巨大的对象。这种写法在简单场景下能跑但一旦模型调用出错你根本没法定位是哪一步的问题。更麻烦的是模型可能会在不该调用的时候调用它因为工具描述里写的是处理订单模型看到用户说我想下单就触发了但实际上用户可能只是在咨询。2.2 工具描述写得好不好直接决定调用准确率这是我最想强调的一点。Tool注解本身可以带一个 value用来写工具描述。很多人不写或者随便写一句查询订单。实测下来工具描述的详细程度对调用准确率的影响比换模型还大。一个好的工具描述应该包含三部分这个工具做什么、什么情况下应该用、什么情况下不应该用。比如Tool(根据订单号查询订单的当前状态和物流信息。当用户明确提供了订单号并且询问订单进度、物流位置、是否发货时使用。如果用户没有提供订单号或者询问的是退换货政策不要调用此工具。) public OrderStatus queryOrderStatus(P(订单号通常是10到20位的数字字符串) String orderId) { // ... }注意参数上的P注解它给参数加了描述。模型在生成调用参数时会参考这个描述。如果你的参数是枚举类型最好在描述里把可选值列出来否则模型可能编造一个不存在的值。我做过一个对比测试同一个模型工具描述从查询订单改成上面那种详细版本调用准确率从大概六成提升到了九成以上。这个投入产出比非常高值得花时间打磨。2.3 返回值的设计比你想的重要工具返回什么直接影响模型下一步的行为。如果你返回一个巨大的 JSON模型可能抓不住重点如果你返回一个纯字符串成功模型又不知道具体成功在哪。我的经验是返回值要结构化但精简。用 record 或者简单的 POJO字段名要有语义。比如查订单返回OrderStatus(String status, String location, LocalDateTime estimatedArrival)就比返回一整张订单表的所有字段要好。模型拿到这个结果能直接理解状态是运输中当前位置在杭州预计明天到然后组织成自然语言回复用户。还有一个坑如果工具执行失败不要直接抛异常让整个链路崩掉。应该捕获异常返回一个模型能理解的错误信息比如查询失败订单号格式不正确。这样模型可以决定是重试、换工具还是告诉用户。直接抛异常的话LangChain4j 会把异常往上抛整个对话就断了。2.4 什么时候不该用 Tool有些场景用Tool是过度设计。比如纯信息检索如果知识是静态的直接塞进 System Prompt 或者用 RAG 检索更合适没必要包成工具。再比如需要多步推理才能完成的任务单个Tool表达不了硬塞进去只会让工具变得臃肿。判断标准很简单如果这个操作模型不需要决定是否调用而是每次都必须执行那它就不该是工具应该是流程的一部分。工具的本质是给模型提供选择权没有选择权的地方就不需要工具。3. 从 AiServices 到 Agent编排层到底在编排什么3.1 AiServices 的定位与局限AiServices是 LangChain4j 里把接口变成 AI 实现的核心类。你定义一个接口用SystemMessage标注系统提示用Tool标注可调用的方法然后AiServices.create(YourInterface.class, model)就能得到一个实现。这个机制在单轮或者简单多轮场景下非常好用。但AiServices的局限在于它把决策和执行绑在了一起。模型在一次响应里既决定要不要调工具又决定了调哪个然后框架直接执行。你作为开发者在中间没有插入自定义逻辑的机会。比如你想在工具调用前做权限校验或者根据调用结果决定是否要再走一轮检索AiServices原生是不支持的。这就是为什么需要 Agent 层。Agent 的本质是把决策-执行-观察这个循环显式化让你能在每一步之间插入自己的逻辑。3.2 用 LangChain4j 构建显式 Agent 循环LangChain4j 提供了ChatLanguageModel和ToolSpecification这些底层 API你可以自己控制循环。核心逻辑大概是这样ListChatMessage messages new ArrayList(); messages.add(SystemMessage.from(你是一个客服助手...)); messages.add(UserMessage.from(userInput)); while (true) { ChatResponse response model.generate(messages, toolSpecifications); if (response.hasToolExecutionRequests()) { for (ToolExecutionRequest request : response.toolExecutionRequests()) { // 这里可以插入权限校验、日志、限流等逻辑 String result executeTool(request); messages.add(ToolExecutionResultMessage.from(request, result)); } } else { // 没有工具调用说明模型给出了最终回复 return response.content().text(); } // 防止无限循环 if (round MAX_ROUNDS) { throw new IllegalStateException(Agent 循环超过最大轮次); } }这段代码看起来简单但它给了你完全的控制权。你可以在executeTool里做任何事检查用户权限、记录审计日志、对结果做二次处理、甚至根据结果动态决定下一轮给模型看哪些工具。我强烈建议在生产环境里加上MAX_ROUNDS限制。我遇到过一次模型陷入死循环的情况它反复调用同一个工具每次拿到结果都觉得不满意又调一次。没有轮次限制的话这个循环会一直跑下去烧钱不说还会把用户晾在那里。3.3 动态工具选择不是所有工具都要一次性给模型看当你的工具数量超过十个模型的调用准确率会明显下降。它会在相似的工具之间犹豫或者干脆选错。解决办法是动态工具选择根据当前对话的上下文只把相关的工具子集传给模型。实现方式可以很简单维护一个工具注册表每个工具打上标签然后根据用户输入的关键词或者上一轮的意图分类结果筛选出候选工具。比如用户问的是订单相关就只传订单查询、订单修改、退款申请这几个工具物流跟踪、商品推荐这些先不放进去。LangChain4j 的ToolSpecification是可以在每次generate调用时动态传入的所以这个方案完全可行。实测下来工具数量从 15 个缩减到 4 个之后调用准确率能提升 20 个百分点以上。3.4 记忆管理Agent 的上下文不能无限增长多轮对话里消息列表会越来越长最终超出模型的上下文窗口。LangChain4j 提供了ChatMemory接口和几种实现比如MessageWindowChatMemory保留最近 N 条消息TokenWindowChatMemory按 token 数保留。但直接用这些实现有个问题它们是无差别地保留最近的消息可能会丢掉早期的重要信息。比如用户在第三轮说了自己的订单号到第十轮时如果被挤掉了模型就不知道订单号了。我的做法是分层记忆短期记忆用MessageWindowChatMemory保留最近几轮长期记忆把关键信息订单号、用户身份、已确认的需求抽取出来以 System Message 的形式固定在上下文里。这样即使对话很长核心信息也不会丢。4. RAG 在 Agent 流水线里的正确接入姿势4.1 RAG 不是 Agent 的替代品是补充经常看到有人把 RAG 和 Agent 对立起来讨论其实它们解决的是不同问题。RAG 解决的是模型不知道的知识怎么让它知道Agent 解决的是模型知道了之后怎么行动。一个完整的业务系统往往两者都需要。在 LangChain4j 里RAG 的典型接入方式是EmbeddingStoreContentRetriever配合AiServices的contentRetriever参数使用。但如果你在做 Agent 流水线更灵活的方式是把检索也做成一个工具让模型自己决定什么时候检索、检索什么。这两种方式各有适用场景。如果每次对话都必须基于知识库回答用contentRetriever自动检索更省事。如果检索只是可选动作之一做成工具更合适。我一般会先做成工具观察一段时间模型的调用模式如果发现它几乎每次都调再改成自动检索。4.2 多路召回在 LangChain4j 里怎么落地单一向量检索的问题很明显语义相似但关键词不匹配的内容召不回来关键词匹配但语义偏离的内容又排不到前面。多路召回就是同时用多种检索策略然后合并结果。在 LangChain4j 里你可以组合多个ContentRetriever。比如一个用向量检索一个用全文检索如果底层存储支持然后用ReRankingContentAggregator做重排序。如果没有 rerank 模型也可以用简单的倒数排名融合RRF算法自己合并。ContentRetriever vectorRetriever EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(10) .minScore(0.6) .build(); ContentRetriever keywordRetriever /* 基于全文索引的检索器 */; ContentAggregator aggregator new ReRankingContentAggregator(rerankModel, 5); ContentRetriever multiRetriever new MultiRetriever(List.of(vectorRetriever, keywordRetriever), aggregator);这里有个细节minScore的设置很关键。设太高会漏召回设太低会引入噪声。我的经验是从 0.6 开始试根据实际召回质量调整。如果用的是归一化后的余弦相似度0.6 大概对应中等相关0.75 以上才算高度相关。4.3 知识库能存图片吗以及怎么存这是被问得很多的问题。答案是能但不是直接存。向量数据库存的是向量图片本身要存在对象存储或者文件系统里向量库里存的是图片的向量表示和指向原图的元数据。流程是这样的图片先用多模态 embedding 模型比如 CLIP 类的转成向量存进向量库元数据里带上图片的 URL 或者文件路径。检索的时候如果用户输入是文本用文本 embedding 去匹配如果用户输入是图片用图片 embedding 去匹配。召回之后把图片 URL 一起返回给模型模型可以在回复里引用。LangChain4j 对多模态的支持还在演进中目前比较稳妥的做法是自己封装一个ContentRetriever在检索结果里把图片信息作为 metadata 带出来。这样模型拿到的是文本描述加图片链接可以组织成带图的回复。4.4 RAG 的瓶颈往往不在检索在切分很多人优化 RAG 效果第一反应是换 embedding 模型、调检索参数。但我实际排查下来大部分效果问题出在文档切分上。切分粒度太粗一个 chunk 里混了好几个主题检索出来噪声大切分太细上下文不完整模型拿到半句话没法用。我的做法是按语义切分而不是按固定字数。LangChain4j 提供了DocumentSplitter接口默认实现是按段落和句子切。对于结构化文档比如产品手册我会先用规则把章节拆开再在每个章节内部按段落切。对于非结构化文本用重叠窗口切分重叠部分大概占 chunk 的 10% 到 20%。还有一个容易被忽略的点chunk 的元数据。每个 chunk 除了文本内容还应该带上来源文档、章节标题、页码这些信息。检索的时候可以按元数据过滤比如只搜某个产品线的文档。回复的时候也可以引用来源增加可信度。5. 把多个 Agent 串成流水线编排模式与并发处理5.1 什么时候需要多个 Agent单个 Agent 能处理的任务复杂度是有上限的。当你的业务涉及多个专业领域或者需要严格的阶段划分时就该考虑多 Agent 了。比如一个合同审核系统可能需要一个 Agent 负责提取合同关键条款一个 Agent 负责对照法规检查合规性一个 Agent 负责生成审核报告。每个 Agent 有自己的系统提示、自己的工具集、自己的知识库。这种拆分的好处是每个 Agent 的职责单一提示词可以写得很精准工具集可以很小调用准确率高。坏处是 Agent 之间的通信和状态传递需要额外设计。5.2 串行流水线最简单的编排串行就是前一个 Agent 的输出作为后一个 Agent 的输入。在 LangChain4j 里这其实就是把多个AiServices实例按顺序调用ExtractionResult extracted extractionAgent.extract(contractText); ComplianceResult compliance complianceAgent.check(extracted); String report reportAgent.generate(extracted, compliance);简单直接但要注意数据格式的约定。前一个 Agent 的输出如果是自然语言后一个 Agent 解析起来会很不稳定。我的做法是让上游 Agent 输出结构化数据JSON用 LangChain4j 的AiServices返回类型直接映射成 POJO。这样下游拿到的是强类型对象不依赖模型的解析能力。5.3 并行与条件分支有些步骤之间没有依赖可以并行执行。比如合同审核里条款提取和格式检查可以同时做。Java 里用CompletableFuture就能实现CompletableFutureExtractionResult extractionFuture CompletableFuture.supplyAsync(() - extractionAgent.extract(contractText)); CompletableFutureFormatCheckResult formatFuture CompletableFuture.supplyAsync(() - formatAgent.check(contractText)); ExtractionResult extracted extractionFuture.join(); FormatCheckResult formatResult formatFuture.join();条件分支则是根据上游结果决定走哪条路。比如合规检查发现严重问题就直接跳到报告生成跳过后续的优化建议环节。这个用普通的 if-else 控制就行不需要什么框架。5.4 AI Agent 怎么扛并发这是生产环境必须面对的问题。Agent 的每次调用都涉及模型 API 请求而模型 API 通常有速率限制。并发高了之后要么被限流要么响应时间飙升。几个实用的策略。第一是信号量限流控制同时进行的 Agent 调用数量Semaphore semaphore new Semaphore(10); public String process(String input) { semaphore.acquire(); try { return agent.chat(input); } finally { semaphore.release(); } }第二是请求队列加超时。超过等待时间的请求直接返回降级结果而不是让用户一直等。第三是缓存对于相同或相似的输入缓存 Agent 的输出。注意 Agent 的输出可能有随机性缓存要设置合理的过期时间并且在提示词或参数变化时失效。还有一个容易被忽略的点模型 API 的重试。网络抖动或者临时限流导致的失败重试往往能成功。但重试要有退避策略不能立即重试否则会加剧限流。LangChain4j 本身没有内置重试需要自己包一层。5.5 Agent 安全工具调用的权限控制Agent 能调工具就意味着它能执行实际操作。如果工具里有删除数据、发送消息、修改配置这类有副作用的操作权限控制就非常重要。我的做法是在工具执行层加一道校验。每个工具有一个权限标签执行前检查当前用户是否有对应权限。没有权限就返回一个错误信息给模型让模型告诉用户你没有权限执行此操作。这样既安全用户体验也合理。另外对于高风险操作可以要求二次确认。模型第一次调用时返回需要确认等用户确认后再真正执行。这个逻辑可以在 Agent 循环里实现检测到高风险工具调用时不直接执行而是把确认请求返回给上层。6. 实测中的坑与排查链路6.1 模型不调工具或者调错工具这是最常见的问题。排查链路是这样的先看工具描述是否清晰有没有说明使用场景和排除场景。再看工具数量是不是太多模型选择困难。然后看系统提示里有没有引导模型使用工具。最后看模型本身的能力小模型在工具调用上的表现确实不如大模型。我遇到过一次模型死活不调工具检查了半天发现是系统提示里写了一句尽量直接回答用户问题模型理解成了不要用工具。把这句话删掉就好了。所以系统提示和工具描述之间不能有冲突。6.2 工具调用参数错误模型生成的参数格式不对比如该传数字传了字符串该传枚举传了不存在的值。解决办法是在参数描述里写清楚类型和可选值另外在工具方法内部做参数校验不合法就返回错误信息让模型重试。LangChain4j 对参数类型有一定的转换能力但不要依赖它。该是 String 的就用 String内部再解析比直接用 int 更稳妥因为模型可能传123而不是 123。6.3 上下文丢失导致的多轮对话断裂前面提过长对话里早期信息被挤掉。除了分层记忆还有一个技巧是把关键信息在每轮对话开始时重新注入。比如从数据库里查出用户的订单信息以 System Message 的形式放在上下文最前面。这样即使对话很长这些信息也不会丢。6.4 RAG 检索结果不相关排查顺序先看切分是否合理chunk 里是不是混了多个主题。再看 embedding 模型是否适合你的领域通用模型在专业领域上效果会打折。然后看检索参数maxResults和minScore是否合适。最后考虑加 rerank这是提升相关性最有效的手段之一。6.5 Agent 循环不终止前面提过MAX_ROUNDS这里再强调一次。除了轮次限制还可以加超时限制。整个 Agent 循环设置一个总超时超过就返回当前已有的结果或者降级回复。生产环境里没有任何限制的循环是定时炸弹。7. 一些关于工具选型和架构演进的个人体会LangChain4j 这个库的定位很务实它不追求大而全而是把 LLM 应用开发中最常用的抽象做好。Tool、AiServices、ContentRetriever这几个核心概念覆盖了大部分场景。当你需要更复杂的编排时它提供的底层 API 也足够你自由发挥。我的建议是不要一上来就追求多 Agent 流水线。先用单个 Agent 加几个工具把业务跑通观察模型的调用模式找到瓶颈之后再拆分。很多所谓的需要多 Agent的场景其实一个 Agent 加上动态工具选择就能解决。另外Agent 的可观测性非常重要。每次模型调用、每次工具执行、每次检索都要有日志。出问题的时候这些日志是唯一的排查依据。LangChain4j 提供了ChatModelListener接口可以实现它来记录请求和响应。我一般会把完整的消息列表和工具调用记录都打出来虽然日志量大但排查问题时真的救命。最后说一个关于模型选择的体会。工具调用和结构化输出这两件事不同模型的表现差异很大。在选型阶段建议用你的真实工具集和真实用户输入做一轮对比测试不要只看 benchmark 分数。我测下来有些在通用榜单上排名很高的模型在工具调用准确率上反而不如一些专门优化过的模型。这个测试花不了多少时间但能避免后期大量的调试成本。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑