
1. 为什么单靠 Tool 注解撑不起一个生产级 Agent很多人第一次接触 LangChain4j都是从Tool注解开始的。写一个方法加个注解注册到AiServices里模型就能调用你的 Java 方法了。这个体验确实爽几行代码就能让大模型帮你查天气、算汇率、读数据库。但如果你真拿这套东西去接业务很快就会撞墙。我最早做的一个内部工单助手就是典型的“Tool 一把梭”。工具方法写了十几个全塞在一个类里模型调用的时候经常选错工具或者把参数拼得乱七八糟。更麻烦的是有些工具需要多步才能完成——比如“查订单状态”要先根据用户手机号查订单号再根据订单号查物流最后汇总。这种链路用单个Tool根本表达不了模型只能靠多轮对话硬凑稳定性极差。这里面的核心问题在于Tool解决的是“单次函数调用”的问题而 Agent 要解决的是“多步决策与状态流转”的问题。两者不在一个层面上。你可以把Tool理解成给模型递了一把螺丝刀但 Agent 需要的是整套工具箱加一张装配图纸。LangChain4j 的进阶路径本质上就是从“给模型递工具”走到“给模型搭流水线”。这条路径大致会经过几个阶段单工具调用、多工具编排、带记忆的 Agent、接入 RAG 的知识增强、以及最终的多 Agent 协作。每一步都有它要解决的具体痛点也都有对应的 API 和设计模式。下面我按自己踩过的顺序把这套东西拆开讲。提示如果你现在的项目里Tool方法超过 5 个或者出现了“模型选错工具”的情况基本可以判断你需要往 Agent 流水线方向走了。2. 从 Tool 到 AiServices工具注册的边界在哪里2.1 Tool 注解的底层机制与常见误用Tool的本质是告诉 LangChain4j“这个方法可以被模型调用”。框架在运行时会扫描带有该注解的方法提取方法名、参数名、参数类型、方法描述然后把这些信息拼成一段 JSON Schema塞进发给模型的请求里。模型看到这段 Schema 后如果判断需要调用工具就会返回一个特定格式的响应框架再解析这个响应反射调用你写的 Java 方法。听起来很顺但坑就在细节里。第一个常见误用是方法描述写得太随意。很多人只写Tool(查询订单)模型根本不知道这个工具需要什么参数、返回什么。正确的做法是把描述当成给模型看的 API 文档来写比如Tool(根据用户手机号查询该用户最近一笔订单的状态返回订单号、状态和预计送达时间)。描述越具体模型选错工具的概率越低。第二个误用是参数类型过于复杂。Tool方法的参数最好是基本类型或简单的 POJO如果你传一个嵌套三层的对象模型很难正确构造出这个参数。我见过有人把整个HttpServletRequest当参数传进去模型直接懵了。参数应该尽量扁平化能用 String 和 int 解决的不要用复杂对象。第三个误用是工具方法有副作用却不做幂等。模型可能会在同一个对话里重复调用同一个工具如果你的工具方法是“扣减库存”这种操作重复调用就是灾难。所有有副作用的工具要么做成幂等要么在方法内部加去重逻辑。2.2 多工具场景下的选择困境当工具数量超过 5 个模型的选择准确率会明显下降。这不是模型不行而是信息过载。你想想如果一个人面前摆着 20 个按钮每个按钮的功能描述只有一句话他也容易按错。解决办法有两个方向一是工具分组把相关的工具放到不同的AiServices接口里按场景加载二是两阶段选择先让模型选“类别”再在类别内选具体工具。LangChain4j 支持通过ToolProvider动态提供工具这就给分组提供了可能。你可以根据当前对话的上下文只把相关的工具注册进去。比如用户问的是订单问题就只注册订单相关的工具物流工具和退款工具先不注册。这样模型的选择空间小了准确率自然上去。2.3 工具描述与参数设计的经验法则我总结了几条写Tool的硬规矩都是踩坑换来的方法名用动词开头queryOrderStatus比orderStatus好模型对动词更敏感。描述里必须包含“什么时候用这个工具”而不只是“这个工具做什么”。参数名要自解释phoneNumber比param1强一万倍。返回值尽量用 String 或简单的 JSON 字符串不要返回复杂的嵌套对象模型解析起来费劲。每个工具方法只做一件事不要写“查询并更新”这种复合操作。这些规矩看起来琐碎但每一条都对应着实际项目里出现过的 bug。工具层是 Agent 的地基地基不稳上面的流水线再花哨也没用。3. Agent 流水线的骨架Chain、Memory 与 Router 的协作3.1 用 Chain 把多步操作串起来单个Tool解决不了多步链路LangChain4j 里的Chain就是干这个的。Chain 的思路很简单把多个步骤按顺序串起来前一步的输出作为后一步的输入。比如“查订单”这个链路可以拆成三步第一步根据手机号查订单号第二步根据订单号查物流第三步把结果汇总成自然语言。在 LangChain4j 里你可以用SequentialChain或者自己实现Chain接口。自己实现的好处是控制力强每一步的输入输出都能精确控制。我一般会定义一个OrderQueryChain内部持有三个Tool方法对应的服务按顺序调用中间加一些校验和异常处理。这里有个关键设计决策Chain 的每一步应该是纯函数式的不依赖外部状态。也就是说给定相同的输入每一步都应该产出相同的输出。这样做的好处是链路可测试、可重放。如果某一步依赖了数据库的当前状态那测试起来就很麻烦。对于必须依赖外部状态的步骤我会把它单独抽出来在 Chain 外面先查好作为参数传进去。3.2 Memory 在 Agent 中的正确用法Memory 是 Agent 区别于普通函数调用的核心能力之一。没有 Memory 的 Agent每轮对话都是失忆的用户说“帮我查订单”Agent 问“请提供手机号”用户给了手机号下一轮 Agent 又忘了要查什么。LangChain4j 提供了ChatMemory接口可以按对话 ID 维护上下文。但 Memory 不是越多越好。我见过有人把整个对话历史都塞进 Memory结果 token 消耗爆炸模型还被无关信息干扰。正确的做法是分层记忆短期记忆只保留最近几轮对话长期记忆把关键信息抽取成结构化数据存起来。比如用户的手机号、订单号这些关键实体抽取出来存到一个Map里需要的时候直接注入不用每次都从对话历史里翻。LangChain4j 的MessageWindowChatMemory可以限制保留的消息数量我一般设成 10 到 20 条。超过这个数量的旧消息会被丢弃但关键实体已经抽取到长期记忆里了不会丢。这个设计在实测中很稳既控制了 token 成本又保证了关键信息不丢失。3.3 Router 模式让 Agent 自己决定走哪条链路当你的 Agent 需要处理多种类型的请求时Router 模式就派上用场了。比如一个客服 Agent用户可能问订单、问退款、问产品功能每种问题的处理链路完全不同。如果全部塞给一个 ChainChain 会变得无比臃肿。Router 的做法是先用一个轻量的模型调用判断用户意图然后根据意图把请求路由到对应的 Chain。LangChain4j 里可以用AiServices定义一个IntentRouter接口方法返回一个枚举表示识别出的意图。这个判断可以用小模型来做成本低、速度快。我实测下来Router 的准确率主要取决于意图定义的粒度。意图太粗路由不准意图太细模型分不清。一般 5 到 8 个意图是比较合适的范围。另外Router 一定要有兜底策略当模型无法判断意图时走一个默认链路或者直接转人工。4. 接入 RAG让 Agent 拥有领域知识4.1 RAG 在 Agent 流水线中的位置Agent 有了工具和链路能干活了但它不知道你公司的业务规则。比如“退款政策是什么”“这个产品的保修期多久”这些知识不在模型的训练数据里也不适合写成Tool。这时候就需要 RAG 出场了。RAG 在 Agent 流水线里的位置很关键它应该作为工具层的一部分而不是独立于 Agent 之外。也就是说把“知识检索”包装成一个Tool方法Agent 在需要的时候主动调用。这样做的好处是 Agent 可以自己判断“这个问题我需要查知识库”而不是每轮对话都无脑检索一遍。LangChain4j 提供了EmbeddingStore和EmbeddingStoreContentRetriever可以很方便地接入向量数据库。我一般用EmbeddingStoreIngestor把文档切块、向量化、存入EmbeddingStore然后在Tool方法里调用ContentRetriever检索相关片段拼成上下文返回给模型。4.2 文档切块与检索精度的关系RAG 的效果七分靠切块三分靠模型。切块策略直接决定了检索精度。我试过几种切块方式各有适用场景切块方式适用场景优点缺点固定长度切块结构松散的文档实现简单容易切断语义按段落切块结构清晰的文档语义完整段落过长时效果差按标题层级切块有层级结构的文档上下文清晰依赖文档格式递归切块混合结构文档兼顾语义和长度参数调优复杂我一般用递归切块先按标题切标题内再按段落切段落内如果还超长就按句子切。每个块的大小控制在 500 到 1000 字符之间重叠 100 字符左右。这个参数不是固定的要根据你的文档特点调。4.3 多路召回与重排序的实战配置单一检索策略往往不够。用户的问题可能和文档里的表述方式完全不同纯向量检索可能召不回相关文档。这时候就需要多路召回向量检索一路关键词检索一路两路结果合并后再重排序。LangChain4j 本身对多路召回的支持比较基础我一般是自己实现一个MultiRetriever内部持有多个ContentRetriever分别检索后合并结果再用一个简单的打分函数重排序。打分函数可以考虑向量相似度、关键词匹配度、文档新鲜度等因素。重排序这一步很关键。我实测过加了重排序之后Top-3 的命中率能提升 20% 以上。重排序可以用一个小型的交叉编码器模型也可以用简单的规则打分。如果对延迟敏感规则打分就够了如果追求精度可以上模型。注意RAG 检索到的内容一定要做相关性过滤。我见过太多案例检索回来的文档和问题八竿子打不着模型却硬着头皮基于这些文档回答结果就是一本正经地胡说八道。设置一个相似度阈值低于阈值的直接丢弃宁可让 Agent 说“我不知道”也不要让它瞎编。5. 多 Agent 协作什么时候需要怎么拆5.1 单 Agent 的能力边界单 Agent 能做的事情是有上限的。当你的 Agent 需要同时具备“查订单”“查知识库”“调外部 API”“做计算”等多种能力时一个 Agent 的提示词会变得极其复杂工具列表也会很长模型的选择准确率会下降。我做过一个测试给一个 Agent 注册 15 个工具让它处理 100 个不同类型的请求工具选择准确率只有 70% 左右。拆成三个 Agent每个 Agent 负责 5 个工具准确率提升到了 92%。这个提升是显著的。单 Agent 的另一个问题是上下文污染。不同任务的上下文混在一起模型容易被无关信息干扰。比如用户在问退款政策但对话历史里有之前查订单的记录模型可能会把订单信息混进退款回答里。5.2 按职责拆分 Agent 的三种模式多 Agent 协作不是随便拆要有明确的拆分逻辑。我总结下来有三种模式比较实用按领域拆分订单 Agent、退款 Agent、产品咨询 Agent每个 Agent 负责一个业务领域。这种拆分最自然每个 Agent 的工具和知识库都是领域相关的。按能力拆分检索 Agent、计算 Agent、执行 Agent每个 Agent 负责一类能力。这种拆分适合能力复用度高的场景比如多个业务都需要检索能力就抽一个检索 Agent 出来。按流程拆分前置 Agent、处理 Agent、后置 Agent按业务流程的阶段拆分。这种拆分适合流程固定的场景比如工单处理前置 Agent 负责分类和提取信息处理 Agent 负责执行后置 Agent 负责汇总和回复。我一般用按领域拆分因为业务边界通常比较清晰拆分后每个 Agent 的职责明确维护起来也方便。5.3 Agent 之间的通信与状态传递多 Agent 协作的难点在于通信。LangChain4j 没有内置的多 Agent 通信机制需要自己实现。我一般用一个AgentOrchestrator来协调Orchestrator 接收用户请求判断应该交给哪个 Agent把请求和必要的上下文传过去拿到结果后再决定下一步。状态传递用的是一个共享的Context对象里面存着对话历史、提取的实体、中间结果等。每个 Agent 处理完后把结果写回Context下一个 Agent 从Context里读。这个设计简单但有效关键是Context的读写要加锁避免并发问题。Agent 之间的调用可以是同步的也可以是异步的。同步调用简单但延迟是累加的异步调用复杂但可以并行处理。我一般对延迟敏感的场景用异步比如同时查订单和查物流两个 Agent 并行跑最后汇总。6. 并发与稳定性Agent 上生产的最后一公里6.1 Agent 并发场景下的资源竞争Agent 上生产并发是绕不过去的坎。一个 Agent 请求可能涉及多次模型调用、多次工具调用、多次数据库查询整个链路的耗时可能从几百毫秒到几秒不等。如果并发量上来资源竞争问题就会暴露。最常见的资源竞争是模型调用的速率限制。大多数模型服务都有 QPS 限制如果你的 Agent 并发量超过这个限制请求就会被拒绝。解决办法是在 Agent 层加一个令牌桶限流器控制模型调用的速率。LangChain4j 本身没有限流功能需要自己实现我一般用 Guava 的RateLimiter。另一个资源竞争是共享状态的读写。前面提到的Context对象如果多个 Agent 并发读写不加锁就会出问题。我一般用ConcurrentHashMap存Context每个对话 ID 对应一个Context对话内的操作串行化对话间的操作并行化。6.2 超时、重试与降级策略Agent 链路的任何一环都可能超时。模型调用可能超时工具调用可能超时数据库查询也可能超时。如果不做超时控制一个慢请求会拖垮整个线程池。我的做法是给每一层都设超时模型调用设 10 秒工具调用设 5 秒数据库查询设 3 秒。任何一层超时就触发降级逻辑。降级策略分几种如果是知识检索超时就跳过检索让模型基于已有知识回答如果是工具调用超时就返回“服务暂时不可用请稍后重试”如果是模型调用超时就返回一个兜底回复。重试要谨慎。模型调用失败可以重试一次但工具调用失败不要轻易重试尤其是写操作。我见过有人对“扣减库存”的工具做了自动重试结果库存扣了两次。重试一定要区分幂等和非幂等操作。6.3 可观测性日志、指标与链路追踪Agent 上生产没有可观测性就是盲人摸象。我一般会在几个关键节点打日志请求进入时、每次模型调用前后、每次工具调用前后、请求结束时。日志里带上对话 ID、用户 ID、耗时、token 消耗等信息。指标方面我关注几个核心数据请求量、成功率、平均延迟、P95 延迟、token 消耗、工具调用成功率。这些指标用 Micrometer 暴露出去接到 Prometheus 和 Grafana 上一目了然。链路追踪用 OpenTelemetry把一次 Agent 请求的完整链路串起来。这样当某个请求出问题时可以快速定位是哪一步出了问题。LangChain4j 对 OpenTelemetry 的支持还在完善中我一般是在自己的代码里手动埋点。7. 我在实际项目里踩过的几个坑第一个坑是工具方法的异常处理。Tool方法抛异常时LangChain4j 默认会把异常信息传给模型模型看到异常后可能会尝试“修复”问题比如换个参数重新调用。这听起来很智能但实际上经常导致死循环。我的做法是在工具方法内部捕获所有异常返回一个结构化的错误信息而不是抛出去。第二个坑是RAG 的文档更新。知识库不是一成不变的文档更新后需要重新向量化。我一开始没做增量更新每次都是全量重建耗时很长。后来改成增量更新只处理变化的文档效率提升了很多。增量更新的关键是给每个文档块打上版本号或时间戳检索时优先返回最新版本。第三个坑是多 Agent 的循环调用。Agent A 调用 Agent BAgent B 又调用 Agent A形成死循环。这个坑很隐蔽因为单次调用看起来都正常。我的解决办法是在Context里加一个调用链记录每次 Agent 调用前检查是否已经调用过如果调用链超过一定深度就强制终止。第四个坑是模型版本升级导致的提示词失效。模型升级后同样的提示词可能产生不同的行为。我吃过一次亏模型升级后原本正常的工具选择逻辑全乱了。后来我养成了习惯每次模型升级前先跑一遍回归测试确认提示词和工具描述在新模型上仍然有效。这些坑的共同点是它们都不会在开发环境暴露只有上了生产、有了真实流量才会出现。所以我的建议是Agent 项目一定要尽早做压力测试和灰度发布不要等到全量上线才发现问题。最后分享一个实用技巧给 Agent 加一个“思考过程”的输出。让模型在调用工具前先输出一段简短的思考说明它为什么要调用这个工具、期望得到什么结果。这段思考不返回给用户只记在日志里。这样当 Agent 行为异常时你可以通过思考过程快速定位问题。这个技巧在调试复杂链路时特别有用我几乎每个 Agent 项目都会加。