1. 为什么我最终把整套 Agent 流水线压进了 LangChain4j
1.1 从“能跑”到“能维护”的转折点
最早做 AI Agent 项目的时候,我和很多人一样,是拿 Python 生态起步的。原型阶段确实爽,几十行代码就能把大模型、工具调用、向量检索串起来,Demo 演示效果拉满。但一旦进入真实业务,问题就来了:线上服务是 Java 写的,团队里大部分工程师日常写 Spring Boot,运维体系、监控体系、权限体系全是 Java 那一套。你硬塞一个 Python 服务进去,等于凭空多出一条技术栈,部署、扩缩容、日志采集、链路追踪全都要重新搭一遍。
我印象特别深的一次,是一个内部知识库问答项目。Python 侧的原型两周就跑通了,但接入公司统一网关、统一鉴权、统一配置中心的时候,前后折腾了一个多月。那段时间我就在想,能不能把 Agent 这套东西直接做在 Java 里,让业务代码和智能逻辑待在同一个进程、同一套工程规范里。后来接触到LangChain4j,这个念头才算真正落地。
LangChain4j 的定位很清晰:它是 LangChain 思想在 Java 世界的实现,把大模型调用、提示词模板、工具调用、记忆、RAG 检索、Agent 编排这些能力,用 Java 开发者熟悉的方式封装起来。它不是一个玩具库,而是能直接进生产工程的框架。我现在手上的几个项目,从最简单的“单工具问答”到“多 Agent 协作流水线”,全部用 LangChain4j 一套库搞定,没有再引入第二套编排框架。
这篇文章我想聊的,就是这条完整的进阶路径:从最基础的@Tool注解开始,一步步走到 RAG 检索增强,再走到多 Agent 流水线。中间会讲清楚每一步为什么这么设计、参数怎么算、坑在哪里。如果你也是 Java 背景、想认真做 Agent 而不是玩票,这篇应该能帮你少走不少弯路。
1.2 这套内容适合谁,能解决什么问题
先说清楚受众。如果你是完全没碰过大模型的纯后端,这篇能让你理解 Agent 到底是怎么回事,并且知道怎么用 Java 落地;如果你已经用 Python 做过 RAG 或 Agent,想迁移到 Java 工程体系,这篇能帮你把概念映射过来,避开 Java 生态特有的坑;如果你已经在用 LangChain4j 但只停留在“调个模型问答”的阶段,那@Tool、多路召回、Agent 流水线这几块应该能让你把项目档次提上去。
它能解决的核心问题有三个。第一是技术栈统一,Agent 逻辑和业务逻辑同进程、同语言、同构建工具,不用维护跨语言调用。第二是能力递进,从工具调用到 RAG 到多 Agent,是一条平滑的升级路线,不用中途换框架。第三是可维护性,LangChain4j 的抽象层次比较克制,没有过度封装,出问题的时候你能顺着调用栈一路查下去,而不是对着一堆黑盒魔法干瞪眼。
我个人的判断是,Java 生态做 Agent,LangChain4j 目前是最省心的选择。它不像某些框架那样什么都想管,反而留出了足够的空间让你按自己的工程习惯去组织代码。接下来我按进阶顺序,一层层拆开讲。
2. 打地基:把 @Tool 用到位的几个关键认知
2.1 @Tool 到底做了什么,为什么它是 Agent 的起点
很多人第一次看到@Tool注解,会觉得它就是个语法糖,把方法暴露给模型调用而已。这个理解不算错,但太浅了。@Tool真正的价值,在于它把“Java 方法”翻译成了“模型能理解的工具描述”,这个翻译过程包含三件事:方法名和参数被转成自然语言描述、参数类型被映射成模型能识别的 schema、返回值被约定成模型可消费的格式。
我举个具体例子。假设你有一个查询订单状态的方法:
@Tool("根据订单号查询订单的当前状态,返回状态码和描述") public OrderStatus queryOrderStatus( @P("订单号,格式为 ORD 开头的 12 位字符串") String orderId) { return orderService.getStatus(orderId); }模型看到的不是 Java 代码,而是一段类似“有一个工具叫 queryOrderStatus,作用是查询订单状态,需要一个参数 orderId,是 ORD 开头的 12 位字符串”的描述。模型根据用户的问题,判断要不要调用这个工具、传什么参数。所以@Tool里的描述文字,直接决定了模型调用工具的准确率。
这里有个我踩过的坑:早期我图省事,@Tool描述写得很短,比如就写“查询订单”。结果模型经常在用户问“我的包裹到哪了”的时候不调用这个工具,因为它不知道“包裹”和“订单”是一回事。后来我把描述改成“根据订单号查询订单状态和物流进度,适用于用户询问包裹、快递、订单进展等场景”,调用准确率肉眼可见地提升了。工具描述不是注释,是给模型看的提示词,必须写得像在跟一个新人解释这个工具什么时候用。
2.2 参数设计:让模型少犯错的第一道防线
参数设计比工具描述更容易被忽视。模型调用工具时,参数是它自己“填”的,填错的原因通常有两个:一是参数类型太自由,二是参数含义有歧义。
先说类型。能用枚举就别用字符串,能用数字就别用字符串。比如一个“设置提醒”的工具,时间参数如果定义成String time,模型可能传“明天下午三点”“3pm”“15:00”各种格式,你后端解析起来要命。更好的做法是拆成int hour和int minute,或者定义一个明确的枚举。LangChain4j 会把参数类型转成 JSON Schema,类型越明确,模型填错的概率越低。
再说歧义。我做过一个“发送通知”的工具,参数是String target,本意是接收人。结果模型有时候传人名,有时候传部门名,有时候传邮箱。后来我把它拆成String userName和String channel两个参数,并在描述里写清楚“userName 是接收人的登录名,channel 是通知渠道如 email、sms”,问题就解决了。参数名和描述要消除一切可能的解释空间,宁可多写几个字,也不要让模型去猜。
还有一个细节:参数尽量必填,少用可选参数。可选参数会让模型在“要不要填”上纠结,增加不确定性。如果确实有可选逻辑,不如拆成两个工具,让模型根据场景选择调用哪个。
2.3 工具粒度:太粗和太细都是灾难
工具粒度是个经验活。太粗,一个工具干太多事,模型不知道该传什么参数;太细,工具数量爆炸,模型选择困难,而且每次调用都要消耗 token 去理解工具列表。
我的经验法则是:一个工具对应一个明确的业务动作,参数控制在 1 到 4 个之间。比如“查询订单”和“取消订单”应该是两个工具,而不是一个“订单操作”工具加一个 action 参数。因为查询和取消的语义差异很大,拆开之后模型判断起来更干脆。
但也不能拆得太碎。我曾经把一个“创建用户”拆成了“校验用户名”“校验邮箱”“写入数据库”三个工具,结果模型经常只调用前两个就以为完事了。后来合并成一个“创建用户”工具,内部自己走校验和写入,反而稳定。判断标准是:这个动作在业务上是不是一个原子操作,用户会不会把它当成一件事来说。如果用户会说“帮我建个账号”,那创建用户就是一个工具,内部的校验步骤不该暴露给模型。
工具数量上,我实测下来,单个 Agent 挂 5 到 15 个工具是比较舒服的区间。超过 20 个,模型的选择准确率会下降,这时候就该考虑用多 Agent 拆分,或者用路由先做一层筛选。这个后面讲 Agent 流水线的时候会展开。
2.4 工具调用的错误处理与幂等设计
工具被模型调用,和被人调用,最大的区别是:模型可能会用你想不到的方式调用。所以错误处理和幂等设计必须做扎实。
错误处理上,工具方法不要往外抛异常,而是返回一个结构化的结果对象,里面包含成功标志和错误信息。因为异常信息模型不一定能正确理解,但结构化的错误描述它能读懂,甚至能根据错误信息调整参数重试。比如:
@Tool("根据订单号查询订单状态") public ToolResult queryOrderStatus(@P("订单号") String orderId) { try { OrderStatus status = orderService.getStatus(orderId); return ToolResult.success(status); } catch (OrderNotFoundException e) { return ToolResult.failure("订单号不存在,请确认后重试"); } }这样模型收到 failure 之后,可能会反问用户“您确认一下订单号是否正确”,而不是直接报错给用户。
幂等设计同样重要。模型有可能在同一个对话里重复调用同一个工具,尤其是它不确定上一次调用是否成功的时候。所以写操作类的工具,比如“创建工单”“发送通知”,一定要做幂等,比如用请求 ID 去重,或者先查后写。我吃过一次亏,模型连续调了两次“发送通知”,用户收到了两条一样的消息,体验很差。凡是会产生副作用的工具,都要假设它会被调用多次。
3. RAG 进阶:从单路检索到多路召回的真实调优
3.1 RAG 的瓶颈到底在哪,先定位再优化
RAG 这东西,入门容易,做好难。很多人搭完第一版 RAG,发现效果不理想,就开始盲目换 embedding 模型、换向量库、调 chunk size,折腾一圈还是不行。问题在于没有先定位瓶颈。
RAG 的链路可以拆成四段:文档切分、向量化、检索、生成。瓶颈可能出在任何一段。我的排查顺序是这样的:先看检索出来的内容对不对,如果检索结果里根本没有正确答案,那是切分或向量化的问题;如果检索结果里有正确答案但模型没用上,那是提示词或生成的问题;如果检索结果相关但不够全,那是召回策略的问题。
怎么判断检索结果对不对?我会准备一批测试问题,每个问题标注好“标准答案所在的文档片段”,然后跑检索,看这些片段有没有被召回、排在第几位。这个评估集不用很大,二三十个问题就能看出问题。没有评估集的 RAG 调优就是盲人摸象,你永远不知道改动是变好了还是变坏了。
我遇到最多的瓶颈是切分。默认的按固定长度切分,经常把一段完整的逻辑切碎,导致检索出来的片段缺头少尾。后来我改成按语义切分,优先在段落、标题、列表项边界切,效果明显好转。LangChain4j 提供了多种文档分割器,选对分割器比调参数重要得多。
3.2 多路召回:为什么单路检索总是不够
单路检索的问题在于,它只用一种方式理解“相关性”。向量检索擅长语义相似,但对精确匹配、关键词、专有名词不敏感。比如用户问“ORD20240115001 这个订单怎么了”,向量检索可能召回一堆讲订单流程的文档,但真正包含这个订单号的记录反而排不上来。
多路召回的思路是:用多种检索方式各召回一批,再融合排序。常见的组合是向量检索加关键词检索(BM25 之类),一个管语义,一个管精确匹配。LangChain4j 里可以分别配置两个检索器,然后用一个融合策略合并结果。
融合策略我试过几种。最简单的是加权求和,给向量检索和关键词检索各一个权重,按分数加权排序。这个方法的坑在于两边的分数尺度不一样,向量相似度通常在 0 到 1 之间,BM25 分数可能是任意正数,直接加权会失真。所以要先做归一化,把两边分数都映射到 0 到 1,再加权。
另一种是 RRF(Reciprocal Rank Fusion),不看分数只看排名,把两边的排名做倒数加权。这个方法对分数尺度不敏感,实现简单,我实测下来在多数场景下比加权求和更稳。它的逻辑是:一个文档如果在两路检索里都排得靠前,那它大概率真的相关。
// 伪代码示意 RRF 融合 Map<String, Double> fusedScores = new HashMap<>(); for (int i = 0; i < vectorResults.size(); i++) { fusedScores.merge(vectorResults.get(i).id(), 1.0 / (60 + i), Double::sum); } for (int i = 0; i < keywordResults.size(); i++) { fusedScores.merge(keywordResults.get(i).id(), 1.0 / (60 + i), Double::sum); }那个 60 是 RRF 的经验常数,作用是平滑排名差异,让靠前的几名差距不要拉得太大。这个值可以调,但一般 60 附近都差不多。
3.3 重排序:把真正相关的顶上来
多路召回解决了“召回不全”的问题,但召回多了之后,排序又成了问题。这时候就需要重排序(Rerank)。
重排序的本质是用一个更精细但更慢的模型,对召回的一批文档重新打分。向量检索用的是双塔模型,快但粗;重排序用的是交叉编码器,把问题和文档拼在一起过模型,慢但准。所以典型流程是:向量检索召回 50 条,重排序精选 5 条,再送给大模型生成。
LangChain4j 支持接入重排序模型。我一般会把召回数量设成最终需要的 5 到 10 倍,比如最终要 5 条,就召回 30 到 50 条给重排序。召回太少,重排序没得选;召回太多,重排序耗时上去了。这个比例要根据你的延迟要求来定。
重排序的收益在什么场景下最明显?我观察下来,是那种“问题里有多个关键词,但只有一个文档同时命中所有关键词”的场景。向量检索可能因为语义泛化,把只命中一个关键词的文档排前面;重排序能识别出同时命中多个关键词的文档更相关,把它顶上来。如果你的 RAG 场景里用户问题经常包含多个限定条件,重排序几乎是必选项。
3.4 知识库结构:结构化、半结构化、非结构化怎么混
热词里有人问“kg 知识库、rag 知识库和结构知识库区分以及应用场景”,这个问题很实在。我的理解是,它们不是互斥的,而是可以叠加的。
纯非结构化的 RAG,就是把文档切块向量化,适合问答、摘要这类场景。但如果你要回答“A 和 B 是什么关系”这种问题,纯向量检索就吃力了,因为它不理解实体之间的关系。这时候知识图谱(KG)就有用武之地,把实体和关系显式建模,查询的时候走图遍历。
结构化知识库,比如数据库表,适合精确查询。用户问“上个月销售额多少”,这种问题走 SQL 比走向量检索靠谱得多。所以我的做法是:把结构化查询也封装成工具,让 Agent 自己判断该走向量检索还是走 SQL。这就是 RAG 和工具调用结合的地方,也是从“检索增强”走向“Agentic RAG”的关键一步。
具体到 LangChain4j,你可以把向量检索器、SQL 查询、图谱查询都封装成工具,挂给 Agent。Agent 根据用户问题,决定调用哪个。这样一套系统既能处理模糊的语义问题,也能处理精确的数据问题,覆盖面比单一 RAG 宽得多。
3.5 RAG 实战里那些文档不会写的坑
第一个坑是文档更新。知识库不是一次建好就完事的,文档会增删改。如果每次更新都全量重建向量库,成本高、耗时长。我的做法是给每个文档块打上文档 ID 和版本号,更新时只重建变化的文档对应的块,删除时按文档 ID 批量删。LangChain4j 的向量库接口支持按元数据过滤删除,用起来还算顺手。
第二个坑是chunk 重叠。切分的时候设置一定的重叠长度,能避免关键信息正好被切在边界上导致两边都不完整。重叠长度一般是 chunk 大小的 10% 到 20%。但重叠太多会引入冗余,检索时可能召回一堆内容重复的块,浪费上下文窗口。我一般设 15% 左右。
第三个坑是元数据过滤。真实场景里,用户的问题往往隐含了过滤条件,比如“今年的政策”“技术部的文档”。如果检索时不带过滤,可能召回一堆过期或无关部门的文档。所以切分的时候要把文档的年份、部门、类型这些元数据存进去,检索时根据问题解析出过滤条件。LangChain4j 支持在检索时传过滤表达式,这块一定要用起来。
第四个坑是上下文窗口管理。召回 5 条文档,每条 500 字,加起来 2500 字,再加上对话历史和系统提示,很容易超模型上下文。我的做法是给召回内容设一个总长度上限,超了就按相关性截断,优先保留高分片段。同时对话历史也要做滑动窗口或摘要压缩,不能无限增长。
4. Agent 流水线:从单 Agent 到多 Agent 协作的架构演进
4.1 单 Agent 的天花板在哪里
单 Agent 加一堆工具,能解决不少问题,但它有天花板。第一个天花板是工具数量,前面说过,超过 20 个工具,模型选择准确率下降。第二个天花板是职责混杂,一个 Agent 既要理解用户意图,又要查知识库,又要调业务接口,还要生成回复,提示词会变得又长又矛盾。第三个天花板是上下文污染,不同任务的中间结果混在一个上下文里,互相干扰。
我遇到过一次典型情况:一个客服 Agent,既要回答产品问题(走 RAG),又要处理退换货(走业务工具)。结果用户问“这个产品怎么退货”,Agent 一会儿去检索产品文档,一会儿去调退货接口,最后回复得驴唇不对马嘴。问题就在于它没有一个清晰的“先判断意图再分派”的机制。
这时候就该上多 Agent 了。多 Agent 的核心思想是分而治之:每个 Agent 只负责一个明确的领域,有自己的工具集和提示词,通过一个协调者(或者叫路由、编排器)来分派任务。这样每个 Agent 的提示词可以写得很聚焦,工具集也很干净,整体准确率反而比单 Agent 高。
4.2 路由 Agent:流水线的入口怎么设计
路由 Agent 是整个流水线的入口,它的职责只有一个:判断用户的问题该交给哪个下游 Agent 处理。它自己通常不挂业务工具,只挂“分派”这个能力。
路由的实现方式有两种。一种是基于分类,把用户问题分类到预定义的几个类别,每个类别对应一个下游 Agent。这种方式简单直接,适合类别边界清晰的场景。另一种是基于工具调用,把每个下游 Agent 封装成一个工具,路由 Agent 通过调用工具来分派。这种方式更灵活,因为下游 Agent 的描述可以写得很详细,路由 Agent 根据描述来判断。
我倾向于第二种,因为它复用了@Tool这套机制,不用额外写分类逻辑。比如:
@Tool("处理产品咨询类问题,包括功能、价格、使用方法等") public String handleProductInquiry(@P("用户问题") String question) { return productAgent.chat(question); } @Tool("处理退换货和售后类问题") public String handleAfterSales(@P("用户问题") String question) { return afterSalesAgent.chat(question); }路由 Agent 看到用户问题,判断该调哪个工具,调用之后把结果返回给用户。整个流程很自然。
路由的准确率是关键。我一般会在路由 Agent 的系统提示词里写清楚每个下游 Agent 的职责边界,并且给出几个边界案例。比如“如果问题同时涉及产品和售后,优先走售后”。这种边界规则能显著减少路由错误。
4.3 串行与并行:流水线的两种编排模式
多 Agent 编排有两种基本模式:串行和并行。
串行就是 A 的输出是 B 的输入,像流水线一样。典型场景是“先检索再生成”“先分析再执行”。串行的好处是逻辑清晰,每一步的输入输出都明确;坏处是延迟累加,而且前一步错了后面全错。
并行是多个 Agent 同时处理,最后汇总。典型场景是“多个专家同时给意见,然后综合”。并行的好处是快,而且能覆盖多个角度;坏处是汇总逻辑要设计好,不然就是一堆信息的堆砌。
LangChain4j 里实现串行很直接,就是方法调用链。实现并行可以用 Java 的CompletableFuture或者虚拟线程,把多个 Agent 的调用并发出去,然后join汇总。我用虚拟线程做过一个“多专家会诊”的场景,三个 Agent 分别从技术、成本、风险角度分析同一个方案,并发执行,最后汇总成一个报告,延迟比串行低了三分之二。
选择串行还是并行,取决于任务之间有没有依赖。有依赖必须串行,没依赖尽量并行。但并行也不是越多越好,并发太多会给下游模型服务压力,而且汇总逻辑会变复杂。我一般控制在 3 到 5 个并行分支。
4.4 状态传递与上下文隔离
多 Agent 协作里,状态传递是个容易出问题的地方。每个 Agent 有自己的对话历史,Agent 之间传递的是“任务”和“结果”,而不是完整的对话上下文。如果把所有 Agent 的历史都混在一起,上下文会爆炸,而且会互相干扰。
我的做法是:每个 Agent 维护自己的记忆,Agent 之间只传递结构化的任务描述和结果。比如路由 Agent 分派任务时,传的是“用户问题原文”加“必要的上下文摘要”,而不是整个对话历史。下游 Agent 处理完,返回的是“结果”加“置信度”这类元信息,路由 Agent 再决定怎么呈现给用户。
这样做的另一个好处是可测试。每个 Agent 可以单独测试,给定输入看输出,不用管上游是谁。整个流水线的调试也简单,哪个环节出问题,单独拎出来看就行。
上下文隔离还涉及一个安全问题:不同 Agent 能访问的数据权限可能不同。比如售后 Agent 能查订单,产品 Agent 不能。这种权限边界要在 Agent 层面就隔离好,不能靠提示词去约束,因为提示词是可以被绕过的。权限控制要落在代码层,而不是提示词层。
4.5 Agent 安全与并发:生产环境必须面对的两件事
热词里“agent 安全”和“ai agent 怎么扛并发”出现频率很高,说明大家已经从“能不能跑”进入到“能不能上生产”的阶段了。
安全方面,我关注三个点。第一是工具调用的权限校验,每个工具在执行前都要校验当前用户有没有权限,不能因为模型调用了就放行。第二是输入输出过滤,用户输入里可能包含提示词注入,试图让 Agent 执行非预期操作;Agent 输出里可能包含敏感信息,需要过滤。第三是操作审计,所有工具调用都要记日志,谁在什么时候调了什么工具、传了什么参数、返回了什么,都要可追溯。
并发方面,核心是无状态化。Agent 本身应该尽量无状态,状态放在外部存储(比如 Redis)里,这样多个实例可以水平扩展。LangChain4j 的组件大多是无状态的,你只要把对话记忆、向量库这些外部化,就能轻松扩容。另外要注意模型服务的限流,并发太高会被限流,需要做队列和退避重试。
我实测下来,一个 4 核 8G 的实例,用虚拟线程处理 Agent 请求,配合外部记忆存储,扛几百 QPS 是没问题的,瓶颈通常在模型服务那边,不在应用本身。
5. 常见问题与排查技巧实录
5.1 工具调用相关的高频问题
问题一:模型不调用工具,直接编答案。这是最常见的。原因通常是工具描述不够清楚,模型没意识到该用工具。解决办法是把工具描述写得更具体,明确“什么时候用这个工具”。另外可以在系统提示词里强调“涉及事实性问题必须调用工具,不要凭记忆回答”。
问题二:模型调用了错误的工具。通常是工具之间职责重叠。解决办法是检查工具描述,确保每个工具的适用场景互斥。如果确实有重叠,可以在描述里写明优先级,比如“如果同时符合 A 和 B,优先用 A”。
问题三:模型传错参数。检查参数类型和描述。类型尽量用枚举或数字,描述要消除歧义。另外可以在工具方法里做参数校验,返回明确的错误信息,让模型有机会重试。
问题四:工具调用死循环。模型反复调用同一个工具。这通常是因为工具返回的结果模型不理解,或者模型陷入了某种循环。解决办法是设置最大调用次数,超过就强制结束并返回兜底回复。
5.2 RAG 效果不佳的排查路径
RAG 效果不好,按这个顺序排查:
| 排查项 | 判断方法 | 常见原因 | 解决方向 |
|---|---|---|---|
| 召回内容 | 看检索结果里有没有正确答案 | 切分不当、embedding 不匹配 | 换分割器、换 embedding 模型 |
| 排序位置 | 正确答案排在第几 | 单路检索、缺重排序 | 加多路召回、加重排序 |
| 生成质量 | 答案有没有用上召回内容 | 提示词不当、上下文超限 | 优化提示词、控制上下文长度 |
| 覆盖范围 | 问题类型是否都覆盖 | 知识库不全、缺结构化查询 | 补文档、加工具调用 |
我一般会先跑评估集,看召回率。召回率低就优化检索,召回率高但答案不对就优化生成。分开定位,不要一起改。
5.3 多 Agent 流水线的调试技巧
多 Agent 调试比单 Agent 麻烦,因为链路长。我的做法是给每个 Agent 的输入输出都打日志,并且给每次请求分配一个 trace ID,串起整条链路。这样出问题的时候,能一眼看出是哪个 Agent 出的错。
另外,我会给每个 Agent 单独写单元测试,用固定的输入验证输出。这样改一个 Agent 的提示词,不会影响其他 Agent 的测试。整个流水线再写集成测试,验证端到端的效果。
还有一个技巧是降级开关。多 Agent 流水线复杂,出问题的时候要能快速降级到单 Agent 或者纯 RAG。我会给每个 Agent 加一个开关,出问题就关掉,走兜底逻辑。这样线上出问题不至于全挂。
5.4 我踩过的几个印象深刻的坑
第一个坑是向量库的维度不匹配。换 embedding 模型的时候,忘了重建向量库,结果新模型生成的向量和旧库里的维度对不上,检索直接报错。后来我养成了习惯,换 embedding 模型必须重建整个库,并且在配置里把模型名和维度绑定,防止误用。
第二个坑是对话记忆无限增长。早期没做记忆压缩,一个长对话跑下来,上下文越来越长,最后超限报错。后来加了滑动窗口,只保留最近 N 轮,再对更早的历史做摘要。这个摘要本身也可以用一个轻量模型来做,成本不高。
第三个坑是工具调用的超时。有些工具是调外部接口的,外部接口慢的时候,整个 Agent 就卡住了。后来给所有工具调用加了超时,超时就返回失败,让模型决定是重试还是走别的路径。任何外部调用都必须有超时,这是生产环境的基本要求。
第四个坑是提示词里的变量注入。用户输入直接拼进提示词,如果用户输入里包含类似“忽略以上指令”的内容,可能造成提示词注入。解决办法是对用户输入做转义或隔离,不要直接拼进系统提示词,而是放在明确的用户消息位置。
6. 从单库到全套:我的工程组织经验
6.1 项目结构怎么分层
用 LangChain4j 做 Agent 项目,我一般会分成这么几层:config层放模型、向量库、记忆存储的配置;tool层放所有@Tool工具类;agent层放各个 Agent 的定义和编排逻辑;rag层放文档加载、切分、检索相关代码;web层放对外的接口。
这样分层的好处是职责清晰,改哪块找哪块。工具类只关心业务逻辑,不关心被谁调用;Agent 类只关心编排,不关心工具内部实现。测试也好写,工具类单独测,Agent 类 mock 工具测。
配置我建议用 Spring Boot 的@ConfigurationProperties管理,把模型名、API 地址、超时时间这些外部化,不同环境用不同配置。不要把配置硬编码在代码里,尤其是模型相关的参数,调优的时候要频繁改。
6.2 依赖选型与版本管理
LangChain4j 的模块划分比较细,用哪个引哪个,不要一股脑全引进来。核心是langchain4j-core,然后按需引langchain4j-open-ai(或对应的模型集成)、langchain4j-easy-rag(快速 RAG)、langchain4j-spring-boot-starter(Spring 集成)等。
版本管理上,LangChain4j 迭代比较快,建议锁定版本,升级前先看 changelog。我一般会在测试环境先跑一轮评估集,确认效果没退化再上生产。另外注意模型集成的版本要和核心版本匹配,不然可能有兼容问题。
6.3 监控与可观测性
Agent 上生产,监控必须做。我关注几个指标:每次请求的 token 消耗、工具调用次数和成功率、RAG 检索的召回率和延迟、端到端的响应时间。这些指标能帮你发现性能瓶颈和效果退化。
日志方面,每次请求的完整链路都要记,包括用户输入、路由决策、工具调用、检索结果、最终输出。这些日志不仅是排查问题的依据,也是优化提示词和工具描述的素材。我会定期抽样看日志,找那些效果不好的 case,针对性优化。
LangChain4j 本身提供了一些监听器接口,可以在模型调用、工具调用这些关键节点挂回调,用来采集指标和日志。这块建议在项目早期就搭好,不要等出问题再补。
6.4 后续可以怎么扩展
这套架构搭好之后,扩展方向有几个。一是接入更多模型,LangChain4j 支持多种模型集成,可以根据成本和效果做路由,简单问题用便宜模型,复杂问题用强模型。二是增加更多 Agent,按业务领域拆分,每个领域一个专家 Agent。三是引入工作流引擎,对于特别复杂的流程,可以用更正式的工作流编排,LangChain4j 的 Agent 作为其中的节点。
我个人在实际操作中的体会是,Agent 项目最怕的不是技术难,而是需求变。今天加个工具,明天改个流程,如果没有好的分层和抽象,很快就会变成一团乱麻。所以前期在结构上多花点时间,后面会省很多事。LangChain4j 这套库的好处就在于,它的抽象层次刚好,既给了你足够的结构,又没有把你绑死,你可以按自己的工程习惯去组织。这一点,是我用了大半年之后最满意的地方。