从@Tool到Agent流水线:LangChain4j多步编排与RAG融合实战

发布时间:2026/10/7 12:14:05
从@Tool到Agent流水线:LangChain4j多步编排与RAG融合实战 1. 为什么单靠 Tool 注解撑不起一个真正的 Agent很多人第一次接触 LangChain4j都是从Tool注解开始的。写一个方法加个注解注册到AiServices里模型就能调用它了。这个体验确实爽几行代码就能让大模型帮你查天气、算汇率、读数据库。但如果你真的拿这套东西去搭一个稍微复杂点的业务场景很快就会撞墙。我去年接手过一个内部知识助手项目最初的想法很简单把公司文档灌进向量库挂两个Tool让模型能查库存和查订单收工。结果上线第一周就出问题了。用户问“上个月华东区退货率最高的三个品类是什么”模型先调了查订单的工具拿到一堆原始数据然后……就没有然后了。它不知道怎么把数据聚合、怎么排序、怎么关联品类维度。因为Tool只解决了“模型能调用一个函数”这件事它不解决“多个步骤怎么串”“中间结果怎么传”“失败了怎么重试”这些问题。这就是Tool和 Agent 流水线之间的鸿沟。Tool是原子能力Agent 是编排逻辑。一个库要打全套LangChain4j 必须把这两层都覆盖住而它的做法是通过AiServicesToolChatMemoryRetrievalAugmentor这几块拼图组合出一个完整的 Agentic 运行时。先把这个定位说清楚LangChain4j 不是那种“帮你一键生成 Agent”的框架它更像是一套积木。你得自己决定流水线怎么搭但每一块积木的接口都给你留好了。这个设计取舍很关键后面会反复提到。提示如果你现在还在用“一个 Tool 打天下”的思路建议先把 Agent 和 Tool 的区别想明白。Tool 是手Agent 是大脑加手加记忆加反思。缺了后面几样手再多也白搭。1.1 Agent 和 Tool 的本质区别在哪里用生活化的类比Tool就像你给一个实习生配了一把螺丝刀告诉他“拧螺丝用这个”。但 Agent 是你要让这个实习生独立完成“把这张桌子装好”这件事。他得知道先装哪块板、螺丝拧多紧、装错了怎么拆、装到一半发现少零件怎么办。螺丝刀只是工具装桌子的流程才是 Agent。落到代码层面LangChain4j 里一个典型的 Agent 流水线至少包含四个层次感知层接收用户输入可能还要做意图识别、query 改写决策层决定下一步做什么是直接回答、调工具、还是先检索执行层实际调用 Tool、查向量库、调外部 API记忆层保存对话历史、中间状态、工具调用结果Tool只覆盖了执行层的一小部分。决策层靠的是模型的 function calling 能力加上你的 prompt 设计记忆层靠ChatMemory感知层里的检索靠RetrievalAugmentor。这四层缺一层Agent 就会表现得像个“只会调函数的复读机”。我见过太多项目卡在决策层。模型明明有能力做多步推理但因为你的 prompt 里没给它“思考空间”它就直接把工具结果原样吐给用户了。解决办法后面会讲核心是给模型一个明确的“思考-行动-观察”循环结构。1.2 LangChain4j 的 Agentic 能力全景LangChain4j 目前提供的 Agentic 相关能力我按成熟度排个序能力模块成熟度典型用途坑点AiServices Tool高单轮工具调用多步编排需自己写ChatMemory高对话历史管理长对话 token 爆炸RetrievalAugmentor中高RAG 检索增强多路召回配置复杂Tool 链式调用中多工具顺序执行错误传播难控制Agent 循环中自主多步推理容易死循环结构化输出高JSON/POJO 解析schema 设计要小心这张表不是让你背是让你心里有数哪些东西开箱即用哪些东西得自己补。比如“Agent 循环”这一块LangChain4j 没有给你一个现成的AgentExecutor你得用AiServices配合循环逻辑自己搭。这就是为什么我说它是积木不是成品。2. 从 Tool 到流水线的核心设计思路把Tool升级成 Agent 流水线核心要解决三个问题工具怎么注册和发现、多步调用怎么编排、中间状态怎么管理。这三个问题对应三种设计模式我一个个拆。2.1 工具注册从散落到集中最开始大家写Tool都是散在各个 Service 类里用的时候手动ToolSpecifications.toolSpecificationsFrom(XXX.class)。项目小的时候没问题工具一多就乱了。我的做法是建一个ToolRegistry把所有工具类集中注册并且给每个工具打上标签。public class ToolRegistry { private final MapString, Object tools new HashMap(); public void register(String category, Object toolInstance) { tools.put(category, toolInstance); } public ListToolSpecification allSpecs() { return tools.values().stream() .flatMap(t - ToolSpecifications.toolSpecificationsFrom(t).stream()) .collect(Collectors.toList()); } }这样做的好处是后面做多路召回或者按场景动态挂载工具时你可以根据 category 筛选。比如客服场景只挂“订单查询”“退款申请”运维场景只挂“日志检索”“重启服务”。模型看到的工具列表越干净选错的概率越低。注意工具描述Tool(...)里的字符串是模型选工具的唯一依据。我踩过的坑是描述写得太技术化比如“查询数据库”模型根本不知道查什么库、什么表。改成“根据订单号查询订单状态和物流信息”命中率立刻上去了。2.2 多步编排ReAct 循环的 Java 实现ReActReasoning Acting是目前最实用的 Agent 编排模式。它的逻辑很简单让模型先想一步再决定做什么做完看结果再想下一步直到能回答用户。LangChain4j 没有内置这个循环但用AiServices可以很自然地实现。核心思路是把“思考”和“行动”拆成两个 prompt 阶段用一个 while 循环串起来。public String runAgent(String userInput, int maxSteps) { String scratchpad ; for (int i 0; i maxSteps; i) { String prompt buildReActPrompt(userInput, scratchpad); String response model.generate(prompt); if (response.contains(Final Answer:)) { return extractFinalAnswer(response); } String action extractAction(response); String observation executeTool(action); scratchpad \nThought: response \nObservation: observation; } return 达到最大步数限制未能完成; }这个循环里最关键的是maxSteps。我一般设 5 到 8 步。设太小复杂任务做不完设太大模型容易绕圈子。实测下来大部分业务场景 6 步足够。2.3 状态管理ChatMemory 的正确用法ChatMemory很多人只用来存对话历史其实它还能存工具调用结果。我的做法是自定义一个AgentMemory继承ChatMemory的接口但内部把消息分成三类用户消息、模型思考、工具观察。这样在构建下一轮 prompt 时可以只取最近 N 条工具观察避免 token 爆炸。public class AgentMemory implements ChatMemory { private final DequeChatMessage messages new ArrayDeque(); private static final int MAX_TOOL_OBSERVATIONS 5; Override public void add(ChatMessage message) { messages.addLast(message); trimToolObservations(); } private void trimToolObservations() { long toolCount messages.stream() .filter(m - m instanceof ToolExecutionResultMessage) .count(); while (toolCount MAX_TOOL_OBSERVATIONS) { messages.removeFirstOccurrence( messages.stream() .filter(m - m instanceof ToolExecutionResultMessage) .findFirst().orElse(null) ); toolCount--; } } }这个细节很关键。我见过一个项目Agent 跑了 10 步每步的工具返回都是几千字的 JSON最后 prompt 直接超了模型上下文报错退出。加了工具观察裁剪之后稳定多了。3. RAG 与 Agent 的融合多路召回和知识库分层Agent 光有工具还不够很多问题需要先查知识库再回答。这就是 RAG 和 Agent 的交汇点。LangChain4j 的RetrievalAugmentor提供了基础能力但要用好得在召回策略和知识库分层上下功夫。3.1 多路召回在 LangChain4j 里怎么落地多路召回的意思是同一个 query 同时走向量检索、关键词检索、甚至图检索然后把结果融合排序。LangChain4j 支持自定义ContentRetriever你可以实现多个 retriever再用一个ReRankingContentAggregator合并。ContentRetriever vectorRetriever EmbeddingStoreContentRetriever.builder() .embeddingStore(vectorStore) .embeddingModel(embeddingModel) .maxResults(10) .minScore(0.7) .build(); ContentRetriever keywordRetriever new KeywordContentRetriever(keywordIndex); RetrievalAugmentor augmentor DefaultRetrievalAugmentor.builder() .contentRetriever(new MultiRetriever(List.of(vectorRetriever, keywordRetriever))) .contentAggregator(new ReRankingContentAggregator(scoringModel)) .build();这里minScore设 0.7 是我调出来的经验值。设 0.5 会召回一堆不相关的设 0.85 又太严很多边缘相关的漏掉。不同 embedding 模型这个值不一样建议先用一批测试 query 跑一遍看召回率和准确率的平衡点。3.2 向量知识库、KG 知识库、结构化知识库怎么选这是最近被问得最多的问题。三种知识库不是替代关系是互补关系。我整理了一张对照表知识库类型适合存什么查询方式典型场景向量知识库非结构化文本、图片描述语义相似度文档问答、客服话术KG 知识库实体关系、图谱图遍历、路径查询风控、推荐、溯源结构化知识库表格、指标、数值SQL、精确匹配报表、统计、对账实际项目里我通常是组合用。比如用户问“A 产品的退货政策是什么”向量库召回政策文档同时 KG 库查 A 产品的品类归属结构化库查该品类的历史退货率。三路结果一起喂给模型回答质量比单路高一大截。提示向量知识库能不能存图片可以但存的是图片的向量表示通过多模态 embedding 模型不是图片本身。检索出来的是图片 ID 或 URL实际展示还得靠对象存储。别指望向量库当图床用。3.3 RAG 瓶颈的三种典型表现和破解思路RAG 用久了都会遇到瓶颈我总结成三类第一类召回不准。用户问的和文档写的用词不一样。解决办法是 query 改写让模型先把用户问题改写成多个检索 query再分别召回。LangChain4j 里可以用QueryTransformer实现。第二类上下文太长。召回 10 个文档每个 2000 字直接爆 token。解决办法是ContentAggregator里做压缩或者用DocumentSplitter切得更细召回后只取最相关的段落。第三类答案不落地。模型拿着召回内容自己编。解决办法是在 prompt 里强制要求“只根据以下内容回答没有就说不知道”并且把召回内容的来源标注出来让用户能核对。4. 完整 Agent 流水线实操从零搭一个知识助手前面讲的都是零件这一节把它们装成一台能跑的车。目标搭一个能查文档、能调工具、能多步推理的知识助手。4.1 环境准备和依赖配置Maven 依赖核心就几个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-pgvector/artifactId version0.35.0/version /dependency版本号建议锁死LangChain4j 迭代快不同版本 API 有差异。我用 0.35.0 跑了三个月稳定。4.2 定义工具集和 Agent 接口先定义两个工具一个查订单一个查文档public class OrderTool { Tool(根据订单号查询订单状态、金额和物流信息) public OrderInfo queryOrder(P(订单号) String orderId) { return orderService.getById(orderId); } } public class DocTool { Tool(根据关键词搜索内部文档返回最相关的三个片段) public ListString searchDoc(P(搜索关键词) String keyword) { return docRetriever.retrieve(keyword); } }然后定义 Agent 接口public interface KnowledgeAgent { SystemMessage(你是一个知识助手。先思考需要哪些信息再决定调用哪个工具。 如果工具结果不足以回答继续调用其他工具。 最多调用 6 次工具然后必须给出最终答案。) String chat(MemoryId String sessionId, UserMessage String message); }构建 AgentKnowledgeAgent agent AiServices.builder(KnowledgeAgent.class) .chatLanguageModel(model) .chatMemoryProvider(id - new AgentMemory()) .tools(new OrderTool(), new DocTool()) .retrievalAugmentor(augmentor) .build();4.3 关键参数计算和调优记录maxResults和minScore这两个参数我调了一周。测试集是 200 条真实用户 query人工标注了正确答案。结果如下maxResultsminScore召回率准确率平均响应时间50.672%65%1.2s100.789%81%1.8s100.7584%86%1.7s150.791%78%2.4s最后选了maxResults10, minScore0.72召回率和准确率平衡得最好。响应时间 1.8 秒在可接受范围。注意这个测试集必须用你自己的业务数据。不同领域的最优参数差很多。通用文档问答和代码检索的最优 minScore 能差 0.1 以上。4.4 实操现场一次完整的多步调用用户问“订单 12345 的退货政策是什么这个订单能退吗”Agent 的执行轨迹思考需要先查订单状态再查退货政策行动调用queryOrder(12345)观察订单已签收 3 天金额 299品类是电子产品思考需要查电子产品的退货政策行动调用searchDoc(电子产品退货政策)观察电子产品 7 天无理由退货需保持包装完整思考信息够了可以回答最终答案订单 12345 已签收 3 天在 7 天退货期内可以退货需保持包装完整整个过程 2 次工具调用耗时 3.2 秒。如果没有 Agent 编排用户得自己分两步问体验差很多。5. 常见问题排查和避坑经验这一节是我踩过的坑和帮别人排查过的问题整理成速查表。5.1 工具调用失败排查表现象可能原因排查方法解决模型不调工具工具描述不清看模型输出改描述加示例调错工具工具太多太像打印工具列表按场景裁剪工具参数传错参数名不直观看调用日志用 P 加描述调用超时工具内部慢加日志计时异步化或加缓存结果解析失败返回格式复杂看原始返回包装成简单 POJO5.2 Agent 死循环的三种破解方法死循环是 Agent 最烦的问题。模型反复调同一个工具或者在两个工具之间来回跳。我试过三种解法方法一硬限制步数。最简单maxSteps6到了就强制输出。缺点是复杂任务可能被截断。方法二重复检测。记录每次工具调用的参数如果连续两次一样就注入一条提示“你已经调用过这个工具了请换一个或给出答案”。方法三反思提示。每三步插入一次“请回顾你已获得的信息判断是否足够回答”。这个最优雅但会增加 token 消耗。我一般方法一和方法二组合用方法三在复杂场景才开。5.3 记忆管理的独家技巧ChatMemory默认是存所有消息长对话必爆。我的做法是分层短期记忆最近 5 轮对话完整保留中期记忆5 到 20 轮只保留用户消息和最终答案去掉中间思考长期记忆超过 20 轮摘要成一段话public class TieredMemory implements ChatMemory { private final ListChatMessage recent new ArrayList(); private final ListChatMessage midTerm new ArrayList(); private String summary ; Override public void add(ChatMessage message) { recent.add(message); if (recent.size() 10) { compressToMidTerm(); } if (midTerm.size() 40) { compressToSummary(); } } }这个方案实测能把 50 轮对话的 token 消耗压到原来的三分之一而且关键信息不丢。5.4 多路召回的权重调优多路召回融合时向量和关键词的权重怎么分我的经验是看 query 类型。短 query少于 5 个字关键词权重要高长 query 向量权重要高。可以做一个简单的规则double vectorWeight query.length() 10 ? 0.7 : 0.4; double keywordWeight 1 - vectorWeight;这个规则很粗糙但有效。更精细的做法是训一个小的分类模型判断 query 类型但大多数项目不值得。6. 工具选型和扩展方向最后聊聊工具选型。LangChain4j 不是唯一选择但它在 Java 生态里目前是最顺手的。如果你团队是 Java 栈别折腾 Python 那套直接上 LangChain4j。6.1 LangChain4j 和其他方案的对比方案语言优势劣势LangChain4jJava和 Spring 集成好生态比 Python 小LangChainPython生态最全Java 项目集成麻烦自研任意完全可控工作量大我的建议Java 项目优先 LangChain4j遇到它没覆盖的能力再考虑自研补丁。别为了一个功能切语言栈维护成本太高。6.2 后续可以扩展的方向这套流水线搭好之后有几个方向可以继续加Agent 安全加一层工具调用审批敏感操作如退款、删除需要人工确认Agent 记忆持久化把ChatMemory存到 Redis 或数据库支持跨会话多 Agent 协作一个主 Agent 调度多个子 Agent每个子 Agent 负责一个领域可观测性记录每次工具调用的输入输出和耗时方便排查和优化我个人在实际操作中的体会是Agent 流水线最难的不是技术是边界设计。哪些事让模型自主决定哪些事必须人工兜底这个边界划清楚了系统才稳。我见过太多项目追求“全自动”结果模型一个误操作造成业务损失。宁可多一步确认也别让 Agent 裸奔。最后分享一个小技巧每次改完 prompt 或工具描述别急着上线先拿 20 条历史 query 跑一遍回归测试。Agent 的行为对 prompt 极其敏感改一个字可能结果就变了。建一个小的测试集比什么都管用。