
最近在 Java 服务里折腾 LangChain4j从最早只是用Tool暴露一个查询方法到后来把十几个工具按场景编排成一条完整的 Agent 流水线整个过程踩了不少坑也沉淀下来一套能直接复用的套路。如果你也在做 Java 生态的 AI 应用想搞清楚Tool、Agent、流水线这三层到底怎么组织这篇文章把我的实战路径完整拆给你看。我不会只讲概念而是把从注解到编排、从单轮调用到多路召回、从单机演示到扛并发的完整过程都摆出来你跟着走一遍基本就能在自己的项目里落地了。1. 整体设计思路为什么要把 Tool 升级成 Agent 流水线1.1 一个工具方法和一个 Agent 引擎差在哪最早我用Tool只是想把一个 Java 方法暴露给大模型。比如用户说“帮我查一下订单状态”模型识别出这个意图然后把订单号填进我的方法参数里方法执行完把结果返回给模型模型再组织语言回复用户。这套流程在局部场景下是够用的本质上就是一次“意图识别 函数调用 结果回填”。但真正做业务落地时你会发现单工具的链条太短了。用户问“这个月哪类商品退款率最高顺便说说原因”这个问题涉及订单查询、退款统计、商品类目映射、甚至售后备注分析不是一个Tool方法能直接回答的。更常见的是模型需要先调用 A 工具拿到一批数据根据数据判断下一步调 B 还是调 C如果结果不理想还要再调 A 换参数重试。这种“模型自主决策 多工具循环调用”的过程就是我说的 Agent 引擎。所以我一贯的思路是能用Tool直接解决的绝不硬上 Agent但是一旦业务需要多步骤决策必须把工具调用放进一个循环里让模型反复观察结果、调整动作直到它认为信息足够再输出最终答案。这个循环就是 Agent 流水线的内核。1.2 LangChain4j 的“一个库打全套”定位很多 Java 团队一提到 AI 应用第一反应是接 OpenAI SDK或者去搞一套 Python 的 LangChain。但 LangChain4j 提供的是一整套 Java 原生抽象覆盖了我上面说的所有环节。模型接入层统一的ChatLanguageModel、EmbeddingModel接口底层可以用 OpenAI、Ollama、 vLLM、各类国产模型网关只要提供兼容接口就能切。工具调用层Tool注解扫描方法签名自动生成模型需要的 JSON Schema自动解析模型返回的 tool call 参数并反射调用 Java 方法。Agent 编排层AiServices把工具注册、记忆管理、流式输出、RAG 检索、输出校验全部串起来声明式就能构建一个智能体服务。RAG 与检索层EmbeddingStore、ContentRetriever、多路召回都可以在这个抽象下实现。我的经验是框架的意义不是帮你省掉所有代码而是把 AI 应用最繁琐的“协议适配”和“循环调度”固定成标准姿势让你把精力花在业务工具本身的正确性上。这也是我推荐 Java 团队直接选它的核心理由。1.3 流水线分层的通用架构我用一句话概括我最后落地的架构入口路由 → Agent 决策循环 → 工具执行层 → 知识检索层 → 输出后处理。每个请求进来先判断走哪个 Agent客服、分析、运维Agent 内部维护多轮对话记忆模型每一步选择调用哪些工具工具层负责与业务系统交互知识检索层负责从向量库和关键词索引里找上下文最后统一做流式返回。这里有个很关键的分层原则Tool方法只做“原子操作”不要包含决策逻辑。Agent 循环负责决策工具只负责执行和返回结构化结果。如果你把决策逻辑写进工具方法里后面每次调整策略都要动业务代码非常痛苦。2. Tool 注解再深入从简单方法到可信工具集2.1 把第一个 Tool 方法跑起来Tool的使用门槛极低。定义一个类方法上打注解方法名和参数名都能被模型感知。下面是最常见的写法。public class OrderTools { Tool(根据订单号查询订单状态返回订单状态、金额和最近更新时间) public String queryOrderStatus(String orderNo) { Order order orderService.findByNo(orderNo); if (order null) { return 未找到订单请核实订单号; } return String.format(订单号:%s, 状态:%s, 金额:%.2f, 更新时间:%s, order.getOrderNo(), order.getStatus(), order.getAmount(), order.getUpdateTime()); } }注意几个细节。第一Tool注解里的描述要写清楚“方法是干什么的”模型靠这段描述来决定要不要调用这个方法。描述越具体误调用越少。第二返回值建议直接返回人类可读的字符串因为模型的最终回复是基于这段文字组织的你返回一个对象 JSON 也能用但可读性差模型转述时容易丢信息。第三方法名要动词开头比如queryXxx、calculateXxx让模型一看就知道这是动作类工具。注册方式很简单。在AiServices构建时传入工具实例即可。CustomerAssistant assistant AiServices.builder(CustomerAssistant.class) .chatLanguageModel(model) .tools(new OrderTools()) .build();tools()支持传多个对象你只需要把工具类实例放进去框架会扫描该类中所有Tool方法并注册。2.2 参数绑定的进阶玩法与类型选择Tool方法参数的绑定方式本质上是由框架把所有参数转成 JSON Schema 发给模型模型按 schema 生成参数值。底层逻辑不复杂但参数类型的选择直接决定调用成功率。常见的建议是这样的参数类型推荐度原因String、int、double、boolean最推荐模型生成简单值准确率高少出错枚举类型推荐枚举值固定 schema 能约束可选项复杂 Java 对象List、嵌套对象慎用模型生成嵌套 JSON 极易出错字段少时可用Map 泛型不推荐模型不知道 key 应该填什么经常乱传如果你确实需要让模型传入一组结构化参数比如“筛选条件包含时间范围和状态”建议定义一个简单类字段用基本类型并且每个字段加上 JSON 注释让 schema 更清晰。Tool(按条件查询退款订单) public String queryRefundOrders(ToolParameter(开始时间格式yyyy-MM-dd) String startDate, ToolParameter(结束时间格式yyyy-MM-dd) String endDate, ToolParameter(退款状态: PENDING, PROCESSING, DONE) String status) { // 业务逻辑 }ToolParameter是可选的但我的建议是业务关键字段只要有可能让模型误解就一定要写描述。尤其是日期格式、状态枚举取值模型如果不知道格式会生成五花八门的内容导致方法内部解析失败。2.3 工具注册的批量管理与隔离当工具数量超过十个把全部工具塞进一个 Agent 里会让模型“选择困难”。我的做法是把工具按业务域拆成多个类再按 Agent 场景分组注册。比如客服 Agent 只注册订单查询、物流查询、售后申请工具数据运营 Agent 只注册统计、报表、多路检索工具。这和微服务拆分的思想是一致的目的都是缩小模型的决策范围提升工具命中准确率。另外如果你有几十个工具需要统一管理可以用ToolProvider接口做动态供给。框架在每次模型请求时调用ToolProvider拿到工具列表你可以根据当前上下文、用户身份、甚至灰度开关动态决定暴露哪些工具。这在多租户场景下特别有用。提示工具不是越多越好。我实测过把一个 Agent 的工具从 15 个减到 6 个工具误调用率下降了一半以上。做工具集要做减法。3. Agent 编排与流水线让模型自己学会叫工具3.1 理解 Agent 循环的手工版本在学习AiServices之前我强烈建议先理解 Agent 循环的底层实现。说白了就是三步模型生成内容解析其中是否有工具调用请求如果有执行工具并把结果作为消息追加进对话继续让模型生成直到模型不再请求工具为止。下面是我早期手写的简化版流程。ListChatMessage messages new ArrayList(); messages.add(new SystemMessage(systemPrompt)); messages.add(new UserMessage(userInput)); int maxSteps 5; int step 0; boolean finished false; while (!finished step maxSteps) { ResponseAiMessage response model.generate(messages); AiMessage aiMessage response.content(); messages.add(aiMessage); ListToolExecutionRequest requests aiMessage.toolExecutionRequests(); if (requests null || requests.isEmpty()) { finished true; break; } for (ToolExecutionRequest request : requests) { String result toolExecutor.execute(request); messages.add(ToolExecutionResultMessage.from(request, result)); } step; }这段代码解释了 Agent 循环的一切关键点。第一循环必须有最大步数限制不设上限的 Agent 在小模型上经常陷入死循环或高频调用白白烧钱。第二模型每次生成的内容都必须加到消息列表里工具执行结果也要以ToolExecutionResultMessage加上去这样模型才能“看到”工具返回的内容。第三一定要处理模型一次请求多个工具的情况循环里逐个执行再回填。我早期踩过一个坑把工具执行结果拼接成普通字符串塞进 user message结果模型完全不理解那段文本是工具结果开始胡编乱造。改成ToolExecutionResultMessage之后模型对工具结果的引用准确了非常多。3.2 用 AiServices 把循环封装成声明式服务手写循环能帮你理解原理但生产环境我不会用裸循环。AiServices才是 LangChain4j 封装 Agent 循环的核心入口。它的原理其实和我上面的代码一样但帮你处理了记忆、流式、多工具并发、消息转换等一堆细节。public interface CustomerAssistant { String chat(String userId, String message); } CustomerAssistant assistant AiServices.builder(CustomerAssistant.class) .chatLanguageModel(model) .chatMemory(MessageWindowChatMemory.withMaxMessages(30)) .tools(new OrderTools(), new RmaTools()) .build(); String answer assistant.chat(U123456, 帮我查一下订单 NO20240101 到哪了);注意到这里我传了userIdLangChain4j 的chatMemory会按 userId 自动隔离每个用户的对话上下文。不同用户的消息不会串这是生产级多用户服务必须要有的能力。流式输出也一样简单接口返回类型换成FluxString或者StreamString框架会自动把模型输出按 token 流式推给你。前端再配合 SSE体验拉满。3.3 Agent 流水线扇出、扇入与路由很多教程讲完AiServices就停了但真实业务里 Agent 往往不只一条链。我在自己的项目里落地了三种编排模式。顺序流水线一步的结果是下一步的输入。比如先查订单归属仓库再用仓库 ID 查库存状态。并行扇出一个请求同时触发多个独立工具收集所有结果后统一回复。比如用户问“这个订单发货没、能退不、运费谁出”三个问题互相独立可以在一次模型决策中并行调用三个工具。条件路由模型根据当前状态决定走哪条子流水线。比如判断用户是投诉还是咨询投诉走升级流程咨询走标准答复。对于并行扇出我遇到过比较多的坑是模型往往会请求多个工具如果串行执行总耗时等于所有工具耗时之和用户体验很差。LangChain4j 在较新版本支持了ToolExecutionRequest的并行执行你可以通过AiServices的配置开启。如果你用的是早期版本可以自己在工具方法内部用CompletableFuture做并发聚合但注意工具方法要是幂等的。3.4 记忆管理与上下文裁剪Agent 流水线跑久了聊天记录会无限增长。每轮对话都把全部历史发给模型token 成本会快速飙升而且超出模型上下文窗口之后直接报错。我的建议是用MessageWindowChatMemory做滑动窗口保留最近 N 条消息就够了。比如客服场景 20 条足够复杂分析场景可以到 40 条。如果你需要跨会话持久化记忆LangChain4j 提供了PersistentChatMemoryStore接口。我生产环境是把它实现到 Redis 里以 userId 为 key 存消息列表这样服务重启后用户还能接着上次对话聊。别小看记忆持久化用户换一台设备就忘记之前聊过的内容这种体验在客服场景是完全不能接受的。4. RAG 与多路召回让 Agent 的回答“有据可依”4.1 从单路向量检索到多路召回Agent 光会调工具还不够很多问题需要结合企业内部知识库来回答。RAG 的标准链路是用户问题 → 检索相关文档 → 拼接成上下文 → 交给模型生成答案。最朴素的实现是只用向量相似度检索但它的缺陷非常明显专有名词、缩写、编号这类内容向量召回效果不稳定经常漏掉关键信息。所以我后来在生产环境换成了多路召回。简单说就是同时用向量检索和关键词检索再把各路结果融合排序。向量召回擅长语义匹配关键词召回擅长精确匹配两者互补之后召回质量明显提升。4.2 LangChain4j 里的多路召回实现LangChain4j 的Retriever接口就是做召回用的你可以自己写一个CompositeRetriever内部并发调用多个召回源。public class HybridRetriever implements Retriever { private final EmbeddingStoreRetriever vectorRetriever; private final Retriever keywordRetriever; private final int topK; Override public ListDocument findRelevant(String query) { CompletableFutureListDocument vectorFuture CompletableFuture.supplyAsync(() - vectorRetriever.findRelevant(query)); CompletableFutureListDocument keywordFuture CompletableFuture.supplyAsync(() - keywordRetriever.findRelevant(query)); ListDocument vectorDocs vectorFuture.join(); ListDocument keywordDocs keywordFuture.join(); return mergeByRRF(vectorDocs, keywordDocs, topK); } }多路召回的融合排序我统一用 RRFReciprocal Rank Fusion。它的思路很巧妙不看分数的绝对值只看每一路里的排名然后计算每个文档在多个排名里的倒数和。公式是score(d) Σ 1 / (k rank(d))这里rank(d)是文档 d 在某一路召回结果里的排名k 一般取 60。举个例子假设向量召回结果里文档 A 是第 2 名、文档 B 是第 1 名关键词召回结果里文档 A 是第 5 名、文档 B 没被召回那么score(A) 1/(602) 1/(605) 0.0161 0.0154 0.0315 score(B) 1/(601) 0 0.0164虽然文档 B 在向量召回中是第 1 名但在融合排序后 A 反而排在前面因为 A 在两路都被命中。这正是 RRF 的价值它偏爱“在多个来源中都有证据”的文档比单一来源的最高分更稳定。4.3 检索上下文与 Agent 流水线的结合多路召回的结果最终是要拼进 prompt 的。在AiServices中注册ContentRetriever框架就会在每次模型调用前自动检索并把文档作为系统消息注入你不需要手动拼接。ContentRetriever hybridRetriever new HybridRetriever(embeddingStore, embeddingModel, keywordClient); Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .contentRetriever(hybridRetriever) .chatMemory(memory) .tools(new OrderTools()) .build();这里的细节是控制检索文档数量和长度。我建议单轮检索最多返回 4 到 6 个文档每个文档裁剪到 1000 字符以内。文档太多太长模型会分不清主次回答时会东拉西扯文档太少又不够支撑。我的一般做法是向量和关键词各取前 5 个文档做 RRF 融合后保留 Top 4效果比较稳定。5. 生产化与并发AI Agent 怎么扛住真实流量5.1 Agent 并发比普通接口难在哪一个普通的 REST 接口瓶颈基本在数据库或下游服务。但 AI Agent 的请求不是简单的“请求-响应”它内部是多次模型调用和工具调用的循环单请求耗时长且每次模型调用都要消耗外部 API 配额和带宽。流量一上来最常见的现象是线程池被占满后面的请求排队超时模型 API 触发限流报错内存中保存的对话消息列表膨胀导致 OOM。我拆解过 Agent 请求的耗时分布模型生成占大头一个复杂的多步工具调用链整体耗时可能从几秒到几十秒不等。因此它确实比普通接口更难“扛并发”但这不代表不能扛。5.2 限流、超时、重试与幂等设计先说完整体系再给参数建议。信号量限流限制同时进行的 Agent 请求数量。比如一台 4C8G 的容器我给单容器并发上限设为 10 到 20。超出的请求进队列或者直接返回忙。超时控制模型调用必须有超时我一般设 60 秒工具调用设 10 到 15 秒。Agent 总循环步数限制为 5 步。重试策略对模型 API 的瞬时错误做退避重试但工具调用不能盲目重试尤其是写操作。工具重试前要确认是不是幂等方法。幂等设计工具方法必须支持重复调用。最典型的例子是“创建工单”模型可能会因为网络超时重试同一次请求导致创建两张工单。我的做法是在工具参数里要求模型传一个业务幂等键比如 userId 时间戳后端收到重复键直接返回已有结果。5.3 Java 虚拟线程Agent 并发的救星一个 Agent 循环里往往有多次外部调用每次调用都在等待网络 IO。传统线程池在这种“IO 密集型 长时间等待”的场景下非常浪费线程资源。我从 Java 21 开始全面切了虚拟线程效果立竿见影。ExecutorService agentExecutor Executors.newVirtualThreadPerTaskExecutor(); CompletableFutureString future CompletableFuture.supplyAsync( () - assistant.chat(userId, message), agentExecutor );虚拟线程的调度开销极低几万路请求也能轻松创建线程你不必再纠结线程池大小。如果你的服务还在 Java 17建议至少用异步客户端加CompletableFuture做并发聚合不要用同步调用串行执行工具。注意虚拟线程下所有工具方法也要避免阻塞操作。如果你的Tool方法里有synchronized代码块或者数据库连接池取连接它们依然可能成为并发瓶颈。这跟线程模型无关是业务底层资源限制。5.4 缓存被很多人忽略的 Agent 并发利器对于高频且结果可复用的请求缓存能大幅降低模型 API 调用量。我做了两层缓存。第一层是语义缓存把用户问题的嵌入向量存入向量库新请求来了先算出向量在缓存里找相似度超过 0.95 的旧问答直接命中返回。第二层是工具结果缓存比如天气预报、库存数量这类短时内不变的数据工具方法内部加本地缓存避免每个请求都去拉一次外部接口。语义缓存需要控制相似度阈值设太高命中率低设太低容易答非所问。我实测 0.93 到 0.97 之间比较合理你可以根据自己的业务调。6. 常见问题与排查技巧实录6.1 工具调用不触发或者反复调用这是新手最常见的问题现象分两种。一种是模型始终不调用工具自己凭记忆瞎回答。这种情况九成原因是工具描述不清楚模型根本不知道你有这个工具。排查方法是打印发给模型的 system prompt 和 tool schema看工具是否真的被注册进去描述是否写清了“什么场景用什么工具”。另一种是模型陷入死循环反复调用同一个工具。这通常是工具返回结果让模型觉得“信息不够”又不知道下一步该干嘛。我的解法是工具返回里明确告诉模型下一步建议或者在系统提示词里加上“如果同一工具调用超过三次仍无法解决请直接告知用户需要人工介入”。6.2 模型报错provider rejected the request schema or tool payload这个报错我遇到过好几次它说的是我们发给模型的工具 schema 非法或者工具调用参数无法被模型服务端校验通过。常见诱因有三个Tool方法参数包含了框架无法序列化成合法 JSON Schema 的类型比如 Java 8 的Optional字段、自循环引用的对象。参数名或者描述里含有非法字符模型生成时出错。工具返回的字符串过大塞爆了模型上下文服务端直接拒绝。解法很直接工具参数一律用基本类型加简单注解返回值做长度裁剪超长内容分页或者只截取摘要手动用ToolSpecifications构建一遍 schema 发给模型调试工具校验。6.3 并发一高就超时或 OOM超时通常不是模型慢而是你的执行线程被占满后排队。先看线程池是不是太小再看是否有同步阻塞调用卡住了线程。如果线程池不小还超时抓线程栈看热点。OOM 则要怀疑聊天记忆膨胀。如果你的ChatMemory没有做窗口限制消息列表会无限增长长期运行的 Agent 内存必爆。务必设置MessageWindowChatMemory.withMaxMessages(...)并且给持久化存储加 TTL。下面是我整理的排查速查表你可以直接收藏。现象可能原因优先排查项工具从不触发工具描述不清 / 未注册打印 tool schema 检查注册信息工具反复调用返回信息不足 / 步数限制缺失设置 maxSteps优化工具返回提示schema payload 被拒绝参数类型非法 / 返回内容过大简化参数类型裁剪返回长度请求超时线程池满 / 模型 API 慢看线程池活跃数抓线程栈内存持续上涨聊天记忆无窗口限制加 maxMessages 和持久化 TTL并发后回答质量下降上下文太长被截断 / 工具过多裁剪历史缩小工具集6.4 用 traceId 追踪 Agent 全链路最后分享一个我自己一直在用的习惯。Agent 请求链路长涉及模型调用、工具调用、检索调用排查问题如果没有统一链路 ID会非常痛苦。我在AiServices外层包了一个拦截器请求进来时生成 traceId透传到所有工具方法和检索模块结构化日志全部带上这个 traceId。这样用户反馈“回答不对”时我能直接查到这个请求整个链路模型决策了什么、调了哪些工具、每步耗时多少、哪一步返回了错误。没有这套追踪机制排查 Agent 问题就像在黑屋子里找东西效率极低。我的建议是Agent 从开发第一天就接入 traceId不要等出了问题再补。一路写下来我自己最大的感受是LangChain4j 能不能“一个库打全套”关键不在于库本身有多少功能而在于你如何把工具、记忆、检索、并发这几个维度组织成适合自己业务的流水线。Tool只是起点Agent 循环和检索融合才是把 AI 能力落到业务里的真正分水岭。如果你正准备从单工具调用迈向完整的 Agent 流水线建议第一步就把上面第 2 章的工具设计规范落实到位再往后铺检索和并发框架后面你会少踩很多我踩过的坑。