ARTICLE DETAIL

资讯详情

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

LangChain4j实战:@Tool、Agent与RAG流水线搭建及并发优化

LangChain4j实战:@Tool、Agent与RAG流水线搭建及并发优化 1. 为什么我最终把整套 Agent 流水线压进了 LangChain4j1.1 从一次“工具越写越乱”的真实翻车说起去年下半年我接手一个企业内部知识助手项目需求听起来不复杂能查内部文档、能调几个业务接口、能根据用户问题自动决定要不要检索知识库。最开始我的做法很朴素用最原始的 HTTP 客户端加手写 prompt 拼接工具调用靠正则匹配模型输出里的 JSON。前两周跑得挺顺第三周开始崩模型偶尔把工具名写错、参数少一个字段、连续调用两个工具时上下文丢失、并发一上来日志里全是解析失败。那段时间我每天的工作就是给正则打补丁。后来我意识到问题不在模型而在我把“工具定义、调用协议、上下文管理、结果回填”这些本该由框架兜底的事情全自己扛了。于是我开始认真评估 Agent 框架最后落到 LangChain4j 上。原因很直接我的主技术栈是 Java团队没人愿意为了一个助手项目再维护一套 Python 服务而 LangChain4j 把Tool注解、Agent 编排、RAG 检索这几块能力都做进了同一个库一个依赖就能打全套。这篇内容我想聊的不是“LangChain4j 是什么”而是一个 Java 后端怎么从零把Tool用到 Agent 流水线再把 RAG 接进来最后扛住并发。适合已经写过一点大模型调用、但工具一多就乱、RAG 一上就慢的同学。全程按我实际项目的落地顺序讲参数和踩坑都会给到。1.2 先厘清几个容易混的概念不然后面全乱热词里有一堆看着像但完全不同的词Agent、Agentic、RAG、tool、agent 框架、harness 和 agent 区别。我在团队内部分享时发现很多人卡住不是因为不会写代码而是概念没对齐导致选型时把不同层的东西混在一起比。我一般这么区分Tool工具一个具体的能力单元比如“查订单”“搜文档”“发邮件”。在 LangChain4j 里就是一个带Tool注解的方法。Agent智能体一个能自己决定“要不要调工具、调哪个、调几次”的执行体。它 模型 工具集 循环控制逻辑。Agentic智能体化一种设计风格指系统具备自主规划、多步执行、自我修正的特征不是某个具体组件。RAG检索增强生成在生成前先检索外部知识把结果塞进上下文。它可以是 Agent 的一个工具也可以独立于 Agent 存在。harness 和 agent 的区别harness 更像“测试/驱动外壳”负责给 Agent 喂输入、收集输出、做评测Agent 是真正干活的主体。两者不是竞争关系。把这些摆清楚之后你会发现 LangChain4j 的定位很清晰它同时提供了 Tool 抽象、Agent 编排、RAG 组件所以你不需要在三个库之间来回倒腾。这也是标题里“一个库打全套”的真正含义——不是它什么都能干而是这条链路上的关键环节它都覆盖了省掉了胶水层。2. 核心设计拆解Tool、Agent、RAG 到底怎么串起来2.1 Tool 注解背后的机制别只当成语法糖很多人第一次用Tool会觉得它就是个标记跟 Spring 的Component差不多。其实它承担了三件事描述暴露、参数 schema 生成、调用路由。模型看到的工具说明就是从注解的value和参数类型推断出来的。一个我实际项目里的工具长这样public class OrderTools { Tool(根据订单号查询订单状态返回状态码和预计送达时间) public OrderStatus queryOrder(P(订单号格式为 ORD 开头的12位字符串) String orderId) { return orderService.find(orderId); } }这里有两个细节值得说。第一Tool里的描述不是给人看的是给模型看的所以写法要像给一个新同事交代任务说清楚“什么时候用、返回什么”。我见过有人写“查询订单”模型经常在用户问“我的包裹到哪了”时不敢调因为描述里没有“包裹/物流”这类语义线索。第二P注解的参数描述同样重要模型生成参数时全靠它。参数格式、取值范围、示例能写就写。注意工具方法的参数类型尽量用简单类型String、int、枚举复杂对象会让模型生成参数时出错率飙升。如果确实需要结构化输入拆成多个简单参数或者让模型先调一个“构造参数”的工具。2.2 Agent 编排为什么我放弃了手写 ReAct 循环早期我自己写过 ReAct 循环把工具列表拼进 prompt让模型输出Thought/Action/Observation然后解析、执行、回填、再循环。写出来不到 200 行但维护成本极高——模型换个版本输出格式就飘解析逻辑就得改。LangChain4j 的 Agent 抽象把这层接过去了。它内部维护了工具调用协议、多轮循环、最大迭代次数控制。我实际用的是AiServices配合工具类的方式大致结构interface Assistant { String chat(String userMessage); } Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(new OrderTools(), new KnowledgeTools()) .chatMemory(MessageWindowChatMemory.withMaxMessages(20)) .build();这段代码背后框架会自动完成把工具描述注入系统提示、解析模型的工具调用请求、反射调用对应方法、把结果作为工具消息回填、继续下一轮直到模型给出最终回答。我要做的只是定义工具和记忆策略。为什么选它而不是自己写一是协议稳定性框架跟着模型 API 更新二是记忆管理MessageWindowChatMemory这种滑动窗口策略自己写容易出边界 bug三是可观测性框架留了监听接口方便打日志。2.3 RAG 在 Agent 里的两种接法我踩过坑RAG 接进 Agent 有两条路我两种都试过第一种把检索做成一个 Tool。模型自己决定要不要检索、检索什么关键词。优点是灵活用户问“你好”时不会白白检索一次缺点是模型可能该检索时不检索尤其是问题里没有明显“查资料”信号时。第二种前置检索把结果直接塞进上下文。每次请求先检索再让模型基于检索结果回答。优点是稳定缺点是浪费——闲聊也检索而且检索质量差时反而干扰模型。我最终采用的是混合策略默认走工具式检索但在系统提示里明确写“涉及内部政策、产品参数、流程规范的问题必须先调用知识库检索工具”。同时在检索工具内部做了一层判断如果 query 太短或明显是寒暄直接返回“无需检索”。这个判断逻辑很土但很有效Tool(检索内部知识库用于回答政策、流程、产品参数类问题) public String searchKnowledge(P(检索关键词尽量具体) String query) { if (query.length() 4 || smallTalkPattern.matcher(query).find()) { return 该问题无需检索知识库; } ListContent docs retriever.retrieve(query); return docs.stream().map(Content::text).collect(Collectors.joining(\n---\n)); }2.4 多路召回热词里问得最多的一块“langchain4j 多路召回”这个词搜索量很高说明大家确实卡在这。单路向量检索的问题很明显语义相似但关键词不匹配的文档召不回专有名词、型号、编号这类内容向量模型经常抓瞎。我的做法是向量召回 关键词召回并行再融合排序。LangChain4j 本身提供了EmbeddingStoreContentRetriever关键词那路我用数据库的全文索引或者简单的 BM25 实现然后做 RRFReciprocal Rank Fusion融合。RRF 的好处是不需要调权重对两路分数尺度不一致的情况很鲁棒MapString, Double fused new HashMap(); for (int i 0; i vectorResults.size(); i) { fused.merge(vectorResults.get(i).id(), 1.0 / (60 i 1), Double::sum); } for (int i 0; i keywordResults.size(); i) { fused.merge(keywordResults.get(i).id(), 1.0 / (60 i 1), Double::sum); }60 这个常数是 RRF 论文里的经验值实测下来对大多数场景都够用不用纠结。融合后取 Top-K 再送进模型。实操心得多路召回真正的瓶颈往往不在融合算法而在两路召回的候选集大小。我一开始每路只取 5 条融合后效果还不如单路。后来每路取 20 条再融合取 5 条召回率明显提升。候选集要足够大融合才有意义。3. 完整实操从零搭一条能跑的 Agent 流水线3.1 依赖与模型接入先把地基打稳我用的构建工具是 Maven核心依赖就两个LangChain4j 核心包和对应模型提供方的集成包。版本上我建议锁定一个稳定版别追最新Agent 相关 API 在早期版本变动比较频繁。dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.35.0/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version0.35.0/version /dependency模型接入这块我用的是兼容 OpenAI 协议的本地推理服务配置方式ChatLanguageModel model OpenAiChatModel.builder() .baseUrl(http://localhost:8000/v1) .apiKey(not-needed) .modelName(qwen2.5-7b-instruct) .temperature(0.2) .timeout(Duration.ofSeconds(60)) .build();参数选择理由temperature设 0.2 是因为 Agent 场景要的是稳定决策不是创意工具调用时温度高了模型容易“发挥”生成不存在的工具名。timeout给 60 秒是因为本地 7B 模型在长上下文下首 token 延迟可能到十几秒设太短会频繁超时。注意如果你用的是需要工具调用能力的模型务必确认该模型支持 function calling 或至少能稳定输出结构化内容。我试过几个不支持工具调用的小模型框架会退化成“让模型输出 JSON 再解析”稳定性差很多。3.2 工具类的组织方式别全塞一个类工具一多全塞一个类会变成几千行的怪物而且模型看到的工具描述会互相干扰。我的组织原则是按业务域拆类订单工具、知识库工具、通知工具各一个类注册时按需传入。Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(new OrderTools(orderService), new KnowledgeTools(retriever), new NotifyTools(mailClient)) .chatMemoryProvider(memoryId - MessageWindowChatMemory.withMaxMessages(20)) .build();chatMemoryProvider这个用法很关键。单用户场景用chatMemory就行但多用户并发时必须用 provider按会话 ID 隔离记忆。我一开始没注意所有用户共享一份记忆结果 A 用户问的订单号出现在 B 用户的上下文里差点出事故。3.3 记忆策略窗口大小不是越大越好MessageWindowChatMemory.withMaxMessages(20)里的 20 指的是保留最近 20 条消息。这个数字我调过好几轮设 10 时多轮任务容易丢上下文设 50 时 token 消耗暴涨且模型开始被无关历史干扰。20 是我在“任务连续性”和“成本”之间的平衡点。如果你的场景涉及很长的多步任务可以考虑做摘要记忆把早期消息压缩成一段摘要只保留最近几轮原文。LangChain4j 提供了相关接口但需要自己实现摘要逻辑我一般用同一个模型做摘要prompt 就一句“用三句话概括以下对话的关键信息”。3.4 RAG 知识库的构建切分比模型更重要RAG 效果差八成问题出在切分不是 embedding 模型。我见过太多人上来就换更大的 embedding 模型结果毫无改善因为文档切得稀碎语义单元都被切断了。我的切分策略是按语义结构切不按固定字数切。Markdown 文档按标题层级切每个二级标题下的内容作为一个 chunkPDF 按段落切遇到表格单独处理。chunk 大小控制在 300 到 800 字之间太短语义不完整太长检索精度下降。DocumentSplitter splitter DocumentSplitters.recursive(500, 50);recursive分割器会优先按段落、句子边界切500是目标 chunk 大小50是重叠长度。重叠是为了防止关键信息正好落在切分点上被割裂。实操心得中文文档的 chunk 大小要比英文小一些。英文一个 token 约等于 4 个字符中文一个汉字往往就是一个 token所以同样 500 的配置中文实际信息量更大。我中文场景一般用 300 到 400。3.5 把 RAG 接进 Agent 的完整代码把前面几块拼起来一个能跑的 Agent 流水线大概是这样// 1. 构建检索器 EmbeddingStoreTextSegment store new InMemoryEmbeddingStore(); EmbeddingModel embeddingModel new AllMiniLmL6V2EmbeddingModel(); EmbeddingStoreIngestor ingestor EmbeddingStoreIngestor.builder() .documentSplitter(DocumentSplitters.recursive(400, 50)) .embeddingModel(embeddingModel) .embeddingStore(store) .build(); ingestor.ingest(documents); ContentRetriever retriever EmbeddingStoreContentRetriever.builder() .embeddingStore(store) .embeddingModel(embeddingModel) .maxResults(20) .minScore(0.6) .build(); // 2. 组装 Agent Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(new KnowledgeTools(retriever), new OrderTools(orderService)) .chatMemoryProvider(id - MessageWindowChatMemory.withMaxMessages(20)) .build(); // 3. 调用 String answer assistant.chat(我们的退货政策对生鲜类商品是怎么规定的);maxResults(20)配合minScore(0.6)是我实测比较稳的组合先多召回再用分数阈值过滤掉明显不相关的。阈值设太低会引入噪声设太高会漏召回0.6 是个不错的起点具体要按你的 embedding 模型调。4. 并发、性能与常见问题排查实录4.1 AI Agent 怎么扛并发这是绕不过去的坎“ai agent 怎么扛并发”是热词里最实际的问题。Agent 比普通接口重得多一次请求可能触发多轮模型调用、多次工具执行、多次检索。我压测过单实例不加优化的情况下本地 7B 模型 QPS 大概只有个位数。我的优化顺序是这样的第一模型调用层做限流和排队。用一个信号量控制同时进行的模型调用数超出的请求排队而不是直接拒绝。信号量大小按你的推理服务承载能力设我本地单卡设 4。Semaphore modelSemaphore new Semaphore(4); public String chatWithLimit(String msg) throws InterruptedException { modelSemaphore.acquire(); try { return assistant.chat(msg); } finally { modelSemaphore.release(); } }第二检索层加缓存。相同或相似的 query 没必要重复检索。我用 Caffeine 做了一层 LRU 缓存key 是归一化后的 queryTTL 设 10 分钟。命中率在真实流量下能到 30% 左右直接省掉这部分检索开销。第三工具执行异步化。如果一次 Agent 循环里要调多个互不依赖的工具可以并行执行。不过要注意LangChain4j 默认是串行执行工具调用的并行需要自己控制且要小心模型对结果顺序的依赖。第四控制最大迭代次数。Agent 循环如果不设上限模型可能陷入“调工具-不满意-再调”的死循环。我一般设maxIterations为 5 到 8超过就强制返回当前结果。4.2 常见问题速查表下面这张表是我和团队实际遇到并解决过的问题按出现频率排序问题现象可能原因排查与解决模型不调用工具直接编答案工具描述不清晰或系统提示没强调在工具描述里写清使用场景系统提示加“涉及X类问题必须调用工具”工具参数生成错误参数描述缺失或类型复杂补全P描述参数改简单类型给出格式示例多用户上下文串了用了共享 chatMemory改用 chatMemoryProvider 按会话隔离RAG 召回不相关chunk 切分不合理或阈值太低调整切分策略提高 minScore加多路召回响应特别慢模型调用无限制、检索无缓存加信号量限流、加检索缓存、减少 maxResults工具调用死循环没设最大迭代次数设置 maxIterations并在提示里说明“信息足够就回答”长对话后答非所问记忆窗口太大引入噪声缩小窗口或改用摘要记忆4.3 几个只有踩过才知道的坑坑一工具方法抛异常会中断整个 Agent 循环。我一开始工具里直接抛业务异常结果模型收到的是框架包装后的错误经常理解不了。后来改成工具内部捕获异常返回一句人类可读的说明比如“订单号格式不正确请确认后重试”模型反而能据此引导用户。坑二embedding 模型和检索模型要匹配。换 embedding 模型后必须重新灌库否则向量空间不一致检索结果全是乱的。这个坑我在换模型时踩过一次排查了半天才发现是旧向量没清。坑三系统提示里的工具说明和Tool描述会叠加。如果两边都写得很啰嗦会挤占上下文。我的做法是Tool里写“怎么用”系统提示里只写“什么情况下必须用”各司其职。坑四本地小模型的工具调用能力参差不齐。7B 级别模型在工具数量超过 5 个时选错工具的概率明显上升。如果工具很多考虑做工具分组或者用路由先判断该用哪组工具。4.4 关于 RAG 的几个进阶方向热词里还有“ontology rag”“kg 知识库和 rag 知识库区分”这类问题说明大家开始不满足于纯向量检索。我的看法是纯 RAG 适合非结构化文档问答知识图谱适合关系推理和精确查询两者不是替代关系。我实际项目里做过一个混合方案实体和关系类问题走图谱查询开放性问题走向量检索用一个轻量路由判断走哪条路。图谱那部分我用的是简单的三元组存储没有上重型图数据库因为业务关系不复杂。如果你的场景涉及大量实体关联推理再考虑上专业图库。至于“rag 知识库能存储图片嘛”答案是能但要看怎么用。图片本身不能直接进向量库需要先用多模态模型生成图片描述把描述文本入库检索到后再把原图一起返回。我做过一个产品手册的场景图片配文字描述入库效果不错。5. 我在这套流水线上的几点真实体会从最初手写正则解析工具调用到现在用 LangChain4j 把Tool、Agent、RAG 串成一条流水线最大的感受是框架的价值不在于让你少写代码而在于让你少写那些容易出错的代码。工具调用协议、记忆管理、循环控制这些地方自己写能跑但一到并发和边界情况就露馅。另一个体会是Agent 项目的成败往往不在模型而在工程细节。工具描述写得好不好、chunk 切得合不合理、并发控制做没做这些看起来“不 AI”的东西才是决定线上能不能用的关键。我见过太多 demo 惊艳、上线就崩的项目问题几乎都出在这些地方。最后分享一个我一直在用的小技巧给 Agent 加一个“调试模式”把每一轮的模型输入、工具调用、工具返回、模型输出全打到日志里。排查问题时这份日志比任何监控指标都管用。我甚至会在开发环境把这份日志渲染成一个可折叠的页面一眼就能看出模型在哪一步走偏了。这个投入在项目后期回报极高强烈建议一开始就做。
返回列表