1. 为什么我最终选了 LangChain4j 做 Agent 流水线1.1 从一次真实踩坑说起去年年底我接了一个企业内部知识助手的活儿需求听起来不复杂把散落在 Confluence、飞书文档和本地 PDF 里的资料整合起来做一个能问答、能调工具、能自动跑多步任务的助手。一开始我用 Python 那套生态搭了个原型跑得挺顺但交付时客户提了两个硬性条件——必须跑在他们的 Java 服务里而且要和现有的 Spring Boot 微服务共用一套鉴权和监控。那一刻我就知道Python 原型再香也得推倒重来。选型阶段我对比过几条路纯手写 HTTP 调模型 API、Spring AI、LangChain4j。纯手写最灵活但什么都得自己造工具调用、记忆管理、RAG 检索全是从零开始工期根本扛不住Spring AI 那会儿生态还比较薄Agent 相关的抽象不够成熟LangChain4j 刚好卡在一个舒服的位置——它把 LLM 交互、工具调用也就是Tool、RAG 检索、Agent 编排这些高频需求都封装好了同时又是纯 Java 库能无缝塞进 Spring Boot 项目里。更关键的是它的抽象层次拿捏得不错简单场景几行代码就能跑复杂场景又能往下钻到自定义ChatModel、自定义EmbeddingStore这一层。这篇文章我想聊的不是LangChain4j 是什么这种入门科普而是怎么用它从单个Tool一路搭到完整的 Agent 流水线。我会把选型逻辑、核心机制、实操步骤、参数计算、踩过的坑都摊开讲适合已经写过一点 LangChain4j、想往 Agent 方向进阶的 Java 开发者也适合正在做技术选型、想搞清楚一个库到底能不能打全套的架构同学。1.2 一个库打全套到底指什么标题里说的一个库打全套具体是指这几件事能不能在同一个依赖体系里闭环模型接入层对接 OpenAI 兼容接口、Ollama 本地模型、通义千问等统一成ChatLanguageModel抽象工具调用层用Tool注解把 Java 方法暴露给模型让模型自己决定什么时候调、传什么参数RAG 检索层文档切分、向量化、存储、多路召回、重排一整套检索增强链路Agent 编排层多步推理、工具循环、状态管理、失败重试把上面几层串成流水线。我实测下来的结论是能但有边界。LangChain4j 把 80% 的通用需求都覆盖了剩下 20% 的定制需求比如特殊的召回策略、非标准的状态存储需要你自己扩展接口。这个比例其实很健康全封装的黑盒反而不好用。下面我按工具 → RAG → Agent的顺序一层层拆给你看。2. Tool 注解Agent 的手和脚是怎么长出来的2.1 Tool 的本质把 Java 方法翻译成模型能懂的说明书很多人第一次用Tool会觉得神奇——我写个 Java 方法模型怎么就知道该调它了原理其实不复杂。大模型本身只能输出文本所谓工具调用是这么一回事你把每个工具的名称、功能描述、参数结构按照特定格式塞进发给模型的 prompt 里或者通过 API 的 tools 字段传过去模型在生成时如果判断需要调工具就输出一段结构化的调用意图比如调用 getWeather参数 city北京。你的代码解析这段意图反射执行对应方法再把结果塞回对话历史让模型继续生成。所以Tool注解干的事就是把 Java 方法的签名和注释自动翻译成模型能理解的那份说明书。方法名变成工具名Tool(...)里的字符串变成功能描述方法参数变成参数 schema。这里有个关键点描述写得好不好直接决定模型调得准不准。我见过太多人把描述写成获取天气结果模型在用户问明天出门要带伞吗的时候压根想不到调它。描述要写成根据城市名查询当前天气和未来预报当用户询问天气、温度、是否下雨、穿衣建议时使用把触发场景也写进去。2.2 一个能跑的最小工具示例先看代码再讲门道public class WeatherTools { Tool(根据城市名称查询当前天气返回温度、天气状况和湿度。当用户询问某地天气、温度、是否下雨时调用此工具。) String getWeather(P(城市名称例如北京、上海) String city) { // 实际项目里这里调第三方天气 API return weatherService.query(city); } Tool(根据城市和日期查询未来天气预报。当用户询问明天、后天或指定日期的天气时调用。) String getForecast( P(城市名称) String city, P(日期格式 yyyy-MM-dd) String date) { return weatherService.forecast(city, date); } }P注解是给参数加描述的别省。模型看到city这个参数名可能猜得出是城市但看到date不一定知道你要什么格式写清楚格式 yyyy-MM-dd能省掉大量格式错误的来回。2.3 工具设计的四条实战铁律用了一段时间后我总结出几条工具设计的经验都是踩坑换来的第一工具粒度要原子化别做万能工具。我一开始图省事写了个executeQuery(String sql)工具想让模型自己拼 SQL。结果模型要么拼错表名要么写出有注入风险的语句调试起来极其痛苦。后来拆成searchOrderByUser、getOrderDetail、listRecentOrders三个专用工具准确率立刻上来了。工具越专用模型越不容易用错。第二返回值要控制体积。工具返回的内容会全部塞进上下文如果返回一个几万字的 JSONtoken 直接爆炸还会把关键信息淹没。我的做法是工具内部就做好裁剪只返回模型决策需要的那几个字段长列表做分页默认返回前 10 条。第三工具要有明确的失败语义。工具执行失败时别直接抛异常让整个链路崩掉而是返回一段人类可读的错误说明比如未找到该订单请确认订单号是否正确。模型看到这个会自己调整策略比如换个参数重试或者告诉用户查不到。这比抛异常优雅得多。第四注意工具的副作用。查询类工具随便调没事但涉及写操作下单、发消息、改数据的工具一定要加确认机制。我的做法是这类工具不直接执行而是返回一个待确认的意图由外层逻辑拦截后让用户确认再真正执行。Agent 自动帮你下单这种事出一次事故就够喝一壶的。2.4 工具注册与调用链路工具写好了怎么挂到模型上LangChain4j 提供了AiServices这个门面interface Assistant { String chat(String userMessage); } Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .tools(new WeatherTools(), new OrderTools()) .build(); String answer assistant.chat(北京明天要带伞吗);底层发生的事是这样的AiServices用动态代理生成了Assistant的实现每次chat调用时它把工具列表转成模型 API 需要的格式发请求拿到响应后判断有没有工具调用意图有就反射执行、把结果追加到消息历史、再发一轮请求直到模型给出最终文本回答。这个调用-执行-回填-再调用的循环就是 Agent 最朴素的形态。理解了这个循环后面搭复杂流水线就是在这个骨架上加东西。3. RAG 检索层让 Agent 有据可依3.1 RAG 到底解决了什么问题模型的知识有截止日期也不知道你公司内部的文档。RAG检索增强生成的思路很直白在模型回答之前先从你的知识库里检索出相关片段塞进 prompt 里让模型基于这些片段回答。这样既用上了私有知识又避免了重新训练模型的高昂成本。但 RAG 不是把文档丢进向量库就完事。我见过太多项目卡在检索质量上——用户问一个问题检索出来的片段驴唇不对马嘴模型只能瞎编。RAG 的瓶颈几乎从来不在生成端而在检索端。下面我把整条链路拆开讲。3.2 文档切分切得好检索就成功了一半切分Chunking是 RAG 里最容易被忽视、却最影响效果的环节。切太大一个片段里混了好几个主题检索时噪声大切太小语义不完整模型拿到半句话也没法用。我的经验参数是这样的中文文档按 300 到 500 字切英文按 500 到 800 token 切片段之间保留 10% 到 15% 的重叠。重叠是为了防止关键信息正好被切在边界上。LangChain4j 提供了DocumentSplitters.recursive()它会优先按段落切段落太长再按句子切尽量保持语义完整DocumentSplitter splitter DocumentSplitters.recursive(400, 60); ListTextSegment segments splitter.split(document);但纯按长度切有个问题Markdown 文档里的标题层级、代码块、表格会被切碎。我的做法是针对不同格式写不同的切分策略——Markdown 按标题切代码文档按函数切表格整体保留不切。LangChain4j 允许你自定义DocumentSplitter接口实现一个按标题切的版本并不难。3.3 向量化与存储模型选型与维度计算切好的片段要转成向量存起来。这里涉及两个决策用哪个 Embedding 模型存到哪个向量库。Embedding 模型的选择上中文场景我一般用 BGE 系列或者通义千问的 embedding 接口英文场景 OpenAI 的text-embedding-3-small性价比很高。维度方面常见的有 768、1024、1536 维。维度越高表达能力越强但存储和计算成本也越高。我做过一个粗略的估算100 万条 768 维的向量用 float32 存储大约占 3GB 内存100万 × 768 × 4 字节 ≈ 2.93GB如果换成 1536 维直接翻倍。所以别盲目追高维度768 维对大多数中文知识库够用了。向量库的选择上LangChain4j 支持一大堆EmbeddingStore实现。小规模几万条以内我直接用内存版InMemoryEmbeddingStore重启就重建简单省事中等规模用 PostgreSQL 的 pgvector 扩展能复用现有数据库大规模再上专门的向量数据库。这里有个坑换 Embedding 模型必须重建整个向量库因为不同模型的向量空间不兼容。所以选型时就要想清楚别上线了再换。3.4 多路召回单一检索为什么不够只用向量检索有个明显短板它对精确匹配不敏感。用户搜一个具体的订单号、产品型号、人名向量检索可能返回一堆语义相近但不对的结果。这时候就需要多路召回——同时跑向量检索和关键词检索比如 BM25再把两路结果融合。LangChain4j 里实现多路召回的思路是分别用EmbeddingStoreContentRetriever和自定义的关键词检索器各查一遍然后用Reciprocal Rank FusionRRF算法融合。RRF 的公式很简单score(d) Σ 1 / (k rank_i(d))其中rank_i(d)是文档 d 在第 i 路检索里的排名k 一般取 60。这个算法的好处是不需要归一化不同检索器的分数只看排名融合起来很稳。我实测下来多路召回对包含专有名词的查询提升非常明显召回率能涨 20% 以上。3.5 重排把最相关的顶到最前面召回之后还有一步重排Rerank。向量检索为了快用的是近似算法排序不一定精准。重排用一个更精细的模型比如 Cross-Encoder对召回的 Top 50 重新打分选出 Top 5 给模型。这一步能显著提升精度代价是多一次模型推理。LangChain4j 的ScoringModel接口就是干这个的。我一般把召回数量设成最终需要的 5 到 10 倍比如最终要 5 条就召回 30 到 50 条再重排。这个比例是权衡出来的召回太少重排没得选召回太多重排又慢。3.6 RAG 与 Agent 的结合点RAG 检索器在 LangChain4j 里可以包装成一个工具让 Agent 自己决定什么时候检索RetrievalAugmentor augmentor DefaultRetrievalAugmentor.builder() .contentRetriever(contentRetriever) .build(); Assistant assistant AiServices.builder(Assistant.class) .chatLanguageModel(model) .retrievalAugmentor(augmentor) .tools(new WeatherTools()) .build();这样每次对话前会自动检索并注入上下文。但更灵活的做法是把检索做成Tool让模型自己判断这个问题需不需要查知识库。闲聊类问题就不用检索省 token 也省时间。两种方式我都用过固定注入适合知识问答为主的场景工具化检索适合混合场景。4. Agent 流水线把工具和检索串成会思考的流程4.1 Agent 和普通工具调用的区别很多人分不清带工具的对话和Agent。我的理解是带工具的对话是单轮决策——模型看一眼问题决定调不调工具调完给答案。而 Agent 是多步规划——模型会把一个大任务拆成若干子任务一步步执行中间根据每步结果调整下一步直到任务完成。举个例子用户说帮我分析上个月销售数据找出下滑最严重的三个区域并给每个区域生成一份改进建议。这不是一次工具调用能搞定的Agent 需要先调工具查数据再调工具做聚合分析再调工具生成报告中间还要判断数据是否完整、要不要补充查询。这个规划-执行-观察-再规划的循环就是 Agent 的核心。4.2 用 LangChain4j 搭一个多步 AgentLangChain4j 本身没有提供一个叫 Agent 的现成类但它的组件足够你搭出来。核心是把工具调用循环显式化public class SalesAgent { private final ChatLanguageModel model; private final ListObject tools; private final int maxIterations 10; public String run(String task) { ListChatMessage history new ArrayList(); history.add(SystemMessage.from( 你是一个数据分析助手。你可以调用工具完成任务。 每次只做一步观察结果后再决定下一步。 任务完成后用 FINISH: 开头输出最终答案。)); history.add(UserMessage.from(task)); for (int i 0; i maxIterations; i) { ChatResponse response model.generate(history, ToolSpecifications.toolSpecificationsFrom(tools)); AiMessage aiMessage response.content(); history.add(aiMessage); if (aiMessage.hasToolExecutionRequests()) { for (ToolExecutionRequest req : aiMessage.toolExecutionRequests()) { String result executeTool(req); history.add(ToolExecutionResultMessage.from(req, result)); } } else { return aiMessage.text(); } } return 任务超过最大步数限制请拆分后重试。; } }这段代码是整个 Agent 的心脏。maxIterations是必须的保险丝——没有它模型可能陷入死循环反复调同一个工具。我一般设 8 到 15 步具体看任务复杂度。超过就中断并提示用户拆分任务比无限跑下去强。4.3 状态管理与记忆多步 Agent 必须管理好状态。上面代码里history就是最简单的状态——完整的消息历史。但历史会越来越长token 消耗直线上升。我的处理策略是分层短期记忆最近 N 轮对话完整保留N 一般取 5 到 10中期记忆更早的对话做摘要压缩用一个小模型把历史浓缩成几句话长期记忆关键事实用户偏好、任务结论存到外部存储需要时检索回来。LangChain4j 的ChatMemory接口支持这些策略MessageWindowChatMemory就是滑动窗口实现。但要注意工具调用的中间结果不要全塞进长期记忆那些是过程数据任务结束就没用了留着只会污染上下文。4.4 并发场景下 Agent 怎么扛这是热词里高频出现的问题AI Agent 怎么扛并发。我的实测经验是Agent 的并发瓶颈通常不在你的 Java 服务而在模型 API 的速率限制和工具执行的外部依赖。先说模型侧。大多数模型 API 都有 RPM每分钟请求数和 TPM每分钟 token 数限制。一个多步 Agent 任务可能产生 5 到 10 次模型调用所以你的服务并发数要按API 限额 / 平均步数来估算。比如限额 500 RPM平均 8 步那理论并发上限就是 60 左右还得留余量。我的做法是在服务层加一个信号量限流超过就排队而不是硬冲导致大面积 429。再说工具侧。如果工具里有慢查询、外部 API 调用一定要设超时和熔断。我踩过一个坑某个工具调用的外部服务挂了没设超时导致 Agent 线程全部卡死整个服务雪崩。后来给每个工具都包了一层超时控制默认 5 秒超时返回工具暂时不可用让模型自己决定是重试还是换方案。还有一个容易被忽视的点Agent 任务是有状态的不能简单水平扩展。如果任务中途换了个实例继续跑历史就丢了。我的方案是把任务状态外置到 Redis每个实例都能读写这样扩容缩容都不影响进行中的任务。4.5 失败重试与降级策略Agent 跑飞是常态关键是怎么兜底。我整理了一套分层策略失败类型表现处理策略模型调用超时请求无响应指数退避重试 2 次仍失败则降级到简单问答工具执行异常工具返回错误把错误信息回填给模型让它换策略参数格式错误模型传错参数在工具描述里强化格式说明或加参数校验层死循环反复调同一工具检测重复调用超过阈值强制中断上下文超限token 溢出触发记忆压缩或截断早期历史这张表是我从真实故障里总结出来的每一条都对应过一次线上事故。特别是死循环检测我现在的实现是记录最近 5 次工具调用如果出现完全相同的调用同工具同参数超过 3 次直接中断并返回提示。5. 常见问题与排查技巧实录5.1 工具不被调用怎么办这是最高频的问题。模型明明该调工具却直接编了个答案。排查顺序是这样的先看工具描述。描述里有没有写清楚什么时候用只写功能不写触发场景模型经常想不起来。再看参数描述格式要求写了吗然后看系统提示词有没有明确告诉模型你有工具可用遇到 X 类问题必须调工具。最后看模型本身有些小模型工具调用能力很弱换个强一点的模型立刻就好。我遇到过一个特别隐蔽的案例工具描述里用了英文但系统提示词是中文模型在中文语境下对英文工具描述的注意力明显下降。改成中文描述后调用率从 60% 涨到 95%。所以工具描述的语言最好和主要对话语言一致。5.2 RAG 检索结果不相关怎么调检索不准按这个顺序排查切分是否合理把检索出来的片段打印出来看是不是被切得七零八落Embedding 模型是否匹配语种用英文模型处理中文效果必然差是否需要多路召回纯向量检索对专有名词不敏感加关键词检索是否需要重排召回 Top 50 里其实有对的只是没排到前面查询是否需要改写用户的口语化提问和文档的书面表达差距大可以先让模型把问题改写成更适合检索的形式。我做过一个对比测试同一套知识库只做向量检索的准确率是 62%加了多路召回涨到 74%再加重排涨到 85%最后加查询改写涨到 89%。每一步都有明确收益但成本也递增按需选择。5.3 上下文爆炸怎么控制Agent 跑几步之后 token 就爆了这是必然的。控制手段有这么几个工具返回值裁剪前面说过、历史消息压缩、检索片段数量限制别一次塞 20 条5 条足够、以及把不必要的信息从 prompt 里拿掉。我见过有人在系统提示词里写了 2000 字的角色设定其实 200 字就能说清楚剩下的全是浪费。5.4 排查速查表症状可能原因快速验证模型不调工具描述不清/模型弱换强模型试若好了就是模型问题工具参数传错参数描述缺失打印模型原始输出看它怎么理解的检索结果差切分/模型/召回策略逐层替换验证响应特别慢步数多/工具慢/模型慢打点计时定位耗时环节结果不稳定温度参数高把 temperature 降到 0.1 以下内存持续增长历史未清理检查 ChatMemory 是否设了上限6. 我踩过的几个坑和一点个人体会先说几个具体的坑。第一个是 Embedding 模型和向量库维度不匹配我换模型时忘了重建库结果检索出来的全是乱码般的结果排查了大半天才反应过来。第二个是工具方法用了非 public 修饰符LangChain4j 反射调不到静默失败模型一直说工具不可用查文档才发现要求 public。第三个是并发下共享了 ChatMemory多个用户的历史串在一起A 用户看到了 B 用户的对话这个事故性质就比较严重了后来改成每个会话独立 memory 实例。再说说我对一个库打全套的真实看法。LangChain4j 确实能覆盖从工具到 RAG 到 Agent 的完整链路省掉了大量胶水代码这是它最大的价值。但它不是银弹Agent 的可靠性最终取决于你的工具设计质量、检索质量和兜底策略这些库帮不了你得靠自己对业务的理解去打磨。我现在的习惯是任何 Agent 上线前都要跑一轮对抗测试——故意问模糊问题、故意让工具失败、故意超长对话看它怎么应对。能扛住这些的 Agent才敢放到生产环境。最后分享一个我觉得很实用的小技巧给 Agent 加一个思考日志。每次它决定调什么工具、为什么调都记一条结构化日志。出问题时翻日志比看最终输出有用得多。这个日志不用给用户看但对你调试和优化 Agent 是刚需。我靠这个日志定位过好几次模型为什么突然抽风的问题往往能发现是某条工具返回的数据格式变了或者某段检索内容误导了它。