1. 为什么我最终把整套 AI 应用都压在了 LangChain4j 上
先说结论:如果你是一个 Java 后端,想在不换语言栈的前提下把大模型能力接进现有系统,LangChain4j 目前是我用过最顺手的方案。我从最早写裸 HTTP 调接口,到后来自己封装 Prompt 模板、自己拼上下文,再到现在用@Tool注解加 Agent 流水线把一整套业务流程串起来,中间踩的坑足够写一本小册子。这篇就按我实际项目的演进路径,把从单个工具方法到完整 Agent 流水线的过程拆开讲,顺带把 RAG、多路召回、并发这些绕不开的话题一起聊透。
标题里说的“一个库打全套”,不是夸张。LangChain4j 把模型接入、Prompt 管理、工具调用、记忆、检索增强、Agent 编排这几层都覆盖了,你不需要在 Spring 项目里再塞三四个框架互相打架。适合谁看?有 Java 基础、想把 AI 能力落地到真实业务里的后端开发,以及正在评估 Agent 框架选型的技术负责人。看完你至少能判断:哪些场景该用@Tool,哪些该上 Agent,RAG 的知识库到底该怎么切、怎么召回、怎么扛住并发。
我下面讲的所有内容,都基于一个假设:你手上已经有一个能跑起来的 Java 服务,可能是 Spring Boot,也可能是别的。至于模型,本地 Ollama 和云端 API 我都试过,代码层面切换成本很低,重点在于编排逻辑,而不是模型本身。
2. 从一次接口调用到 @Tool:先把“能调”变成“会调”
2.1 裸调模型的三个致命问题
最早我的做法很朴素:HTTP 客户端发一个请求,把用户问题拼进 Prompt,拿回文本,返回给前端。能跑,但很快就出问题。
第一个问题是上下文管理失控。多轮对话时我得自己维护一个 List,手动截断,手动拼角色。稍微复杂一点的多轮场景,代码里全是字符串拼接,改一个标点都要重新测。
第二个问题是模型不知道你的业务。你问它“帮我查一下订单 12345 的状态”,它只会编一个看起来合理的答案。要让它真的去查库,你得在 Prompt 里写死“如果用户问订单,就输出一个特定格式的 JSON”,然后自己解析。这个方案在 demo 阶段能用,上线就是灾难,因为模型输出格式不稳定,解析失败率极高。
第三个问题是没有工具的概念。模型只能“说”,不能“做”。而真实业务里,查订单、发通知、算价格,全是“做”的动作。
2.2 @Tool 注解到底解决了什么
LangChain4j 的@Tool注解,本质上是把 Java 方法暴露成模型可以调用的“函数”。你写一个普通方法,加个注解,描述清楚它是干什么的、参数是什么,框架会自动把这个方法的元信息塞进给模型的请求里。模型判断需要调用时,会返回一个结构化的调用请求,框架再反射执行你的方法,把结果回填给模型,模型基于结果生成最终回答。
这个过程听起来简单,但它把前面说的三个问题一次性解决了。上下文由框架的ChatMemory管理,工具调用由框架解析和路由,你只需要专注写业务方法。
我举个实际例子。假设有一个订单服务:
public class OrderTool { @Tool("根据订单号查询订单状态,返回状态描述和预计送达时间") public String queryOrderStatus(@P("订单号,纯数字") String orderId) { // 实际查库逻辑 Order order = orderRepository.findById(orderId); return "订单状态:" + order.getStatus() + ",预计送达:" + order.getEta(); } }注意@P注解,它用来描述参数。这个描述非常重要,模型就是靠它来判断该传什么值。我见过太多人只写方法描述不写参数描述,结果模型传参乱七八糟。
2.3 工具方法设计的四条铁律
用了几个月@Tool之后,我总结了几条设计原则,每一条都是踩坑换来的。
第一,方法粒度要适中。一个工具方法只做一件事。我一开始写了一个handleOrder方法,里面根据参数不同走查询、取消、修改三条分支。结果模型经常传错参数,因为它分不清这个工具到底该在什么时候用。拆成queryOrder、cancelOrder、modifyOrder三个方法后,调用准确率明显上升。
第二,返回值要结构化且简短。模型不是数据库,你返回一个巨大的 JSON,它反而抓不住重点。我现在的做法是返回一个精简的字符串,关键字段用固定格式,比如状态:已发货|预计:明天下午。这样模型解析起来稳定,后续如果要程序化处理也方便。
第三,工具描述要写“什么时候用”,而不只是“是什么”。比如“查询订单状态”就不如“当用户询问订单进度、物流信息、预计送达时间时使用”。模型是靠描述做路由的,描述里带上触发场景,准确率会高很多。
第四,异常要吞掉,返回可读的错误信息。工具方法里抛异常,框架处理起来很别扭,模型也看不懂堆栈。我现在的做法是 try-catch 包住,返回“查询失败:订单号不存在”这样的字符串,模型会把这个信息自然地转述给用户。
提示:
@Tool方法所在的类需要被注册到AiServices里,别写完注解就以为万事大吉,注册这一步漏了,模型永远看不到你的工具。
3. RAG 接入:知识库不是“塞进去就行”
3.1 为什么单纯加长上下文解决不了知识问题
很多人第一反应是:我把文档全拼进 Prompt 不就行了?我试过,两个问题。一是成本,每次请求都带上几万字,token 费用扛不住。二是效果,模型在超长上下文里会“迷失”,你问它文档第 37 页的一个细节,它可能给你编一个。RAG 的思路是:先检索,只把最相关的片段塞进去,既省 token 又准。
LangChain4j 的 RAG 链路很清晰:文档加载、切分、向量化、存储、检索、注入。每一步都有坑,我逐个说。
3.2 文档切分:切错了后面全白搭
切分是 RAG 里最容易被忽视、但影响最大的一步。我一开始按固定字数切,每 500 字一刀。结果一个完整的表格被切成两半,检索出来的片段前言不搭后语,模型回答自然离谱。
后来我改成按语义切分,优先在段落、标题、列表项这些自然边界处断开。LangChain4j 提供了DocumentSplitter,可以配置最大片段大小和重叠长度。我的经验值是:片段大小 300 到 500 字,重叠 50 到 80 字。重叠是为了防止关键信息刚好落在切口上,被切没了。
还有一个细节:元数据要保留。每个片段最好带上来源文件名、章节标题、页码。这样检索出来之后,你可以把来源一起给模型,让它回答时能引用出处,用户也更信任。
3.3 多路召回:单路检索的天花板很低
热词里有个“langchain4j 多路召回”,这个我深有体会。单一向量检索有个天然缺陷:它擅长语义相似,但不擅长精确匹配。用户问“错误码 E1024 怎么解决”,向量检索可能给你返回一堆“错误处理”的通用文档,就是找不到那个具体的码。
我的做法是混合检索:向量检索一路,关键词检索一路,两路结果合并去重,再按分数排序。LangChain4j 里可以自己实现ContentRetriever接口,把两路结果融合。关键词那一路我用的是简单的倒排索引,对错误码、产品型号、人名这类精确词效果很好。
还有一个进阶玩法是查询改写。用户的问题往往口语化,直接拿去检索效果差。我加了一个前置步骤:让模型把用户问题改写成两三个更适合检索的查询,分别去检索,结果合并。这一步对召回率的提升非常明显,代价是多一次模型调用,延迟增加几百毫秒,看你能不能接受。
3.4 知识库到底能不能存图片
热词里有人问“rag知识库能存储图片嘛”,答案是能,但要看你怎么用。纯文本 RAG 存不了图片的语义,但你可以做多模态 RAG:图片先用多模态模型生成描述文本,把描述文本向量化存进去,检索时命中描述,再把原图一起返回。LangChain4j 对多模态的支持在逐步完善,但这条路我目前只在实验环境跑过,生产环境还是以文本为主。
4. Agent 流水线:从“单次问答”到“多步执行”
4.1 Agent 和普通工具调用的本质区别
普通工具调用是“一问一答一工具”,模型调一次工具,拿到结果,生成回答,结束。Agent 是“规划-执行-观察-再规划”的循环。模型可以连续调多个工具,根据上一步的结果决定下一步做什么,直到任务完成。
举个例子。用户说“帮我查一下订单 12345,如果还没发货就取消,然后发邮件通知我”。这个任务里包含条件判断和多个动作。普通工具调用做不到,Agent 可以:先调查询工具,看到状态是“待发货”,决定调取消工具,再调邮件工具,最后汇总结果。
LangChain4j 的 Agent 能力体现在AiServices配合工具和记忆的组合上。你不需要自己写循环,框架会处理模型返回的工具调用请求,执行后把结果回填,再次请求模型,直到模型不再请求工具、直接给出最终回答。
4.2 流水线的分层设计
我在项目里把 Agent 流水线分成三层,这个分层是我反复调整后定下来的,分享给你参考。
第一层是工具层。就是前面说的@Tool方法,纯业务逻辑,不涉及任何 AI 概念。这一层要保证每个方法独立、可测试、无副作用或副作用可控。
第二层是编排层。这一层定义 Agent 的行为边界:它能用哪些工具、记忆保留多少轮、系统提示词怎么写、遇到工具调用失败怎么办。我通常一个业务场景对应一个AiServices实例,比如“订单助手”“客服助手”“数据分析助手”,各自有各自的工具集。
第三层是接入层。处理并发、限流、超时、日志、监控。这一层和 AI 无关,但决定了你的 Agent 能不能扛住真实流量。
4.3 系统提示词是 Agent 的灵魂
工具决定 Agent 能做什么,提示词决定 Agent 怎么做。我写提示词有几个固定套路。
开头明确角色和边界:“你是一个订单处理助手,只能处理订单相关的查询和操作,其他问题礼貌拒绝。”
中间列出工具使用规则:“查询订单前必须先确认订单号格式;取消订单前必须确认用户意图,不能自行决定取消。”
结尾规定输出格式:“最终回答用简洁的中文,涉及金额和时间的字段要准确,不确定的信息要说明不确定。”
这套结构看起来简单,但比那种一大段散文式的提示词稳定得多。我做过对比测试,结构化提示词的工具调用准确率比散文式高出两成左右。
4.4 记忆管理:别让上下文无限膨胀
ChatMemory是 LangChain4j 里管理多轮对话的组件。默认的MessageWindowChatMemory保留最近 N 条消息,超出就丢弃最早的。这个策略简单有效,但有个问题:如果早期对话里有重要信息,比如用户一开始说的订单号,被丢弃后模型就忘了。
我的做法是双轨制:窗口记忆保留最近 10 到 20 轮,同时把关键实体(订单号、用户 ID、产品名)抽取出来,单独存一份,每次请求时作为“已知信息”注入。这样即使窗口滑走了,关键信息还在。
注意:记忆不是越多越好。我试过保留 50 轮,结果模型开始“翻旧账”,把很久之前的话题扯进来,回答变得发散。10 到 20 轮对大多数客服场景够用了。
5. 并发与性能:Agent 上线后真正的考验
5.1 AI Agent 怎么扛并发
热词里“ai agent 怎么扛并发”这个问题,我踩过的坑最多。Agent 的一次完整执行可能包含多次模型调用和多次工具调用,耗时是普通接口的好几倍。如果每个请求都同步阻塞,线程池很快就被打满。
我的方案是异步化加超时控制。LangChain4j 支持返回CompletableFuture或响应式流,我把 Agent 调用包在异步任务里,前端用轮询或 SSE 拿结果。同时给整个 Agent 执行设一个总超时,比如 30 秒,超时就返回“处理中,请稍后查询”,避免请求堆积。
另一个关键是模型调用的并发限制。云端 API 通常有 QPS 限制,本地 Ollama 的并发能力也有限。我用信号量控制同时进行的模型调用数量,超出的请求排队等待。这个信号量的值需要根据你的模型服务能力实测确定,我本地 Ollama 跑 7B 模型时,并发设 4 比较稳,再高延迟就明显上升。
5.2 缓存能省掉一半的调用
很多用户问题其实是重复的,或者高度相似。我在 Agent 前面加了一层语义缓存:把用户问题向量化,和缓存里的历史问题比对,相似度超过阈值就直接返回缓存答案。这一层对客服场景特别有效,能挡掉三成左右的重复请求。
工具调用结果也可以缓存。比如查询订单状态,同一个订单号在短时间内多次查询,结果是一样的,没必要每次都打数据库。我给工具方法加了简单的本地缓存,TTL 设 30 秒,既保证数据不太旧,又减少了下游压力。
5.3 监控指标:没有度量就没有优化
Agent 上线后我盯的几个核心指标:单次执行的平均模型调用次数、工具调用成功率、端到端延迟的 P95 和 P99、缓存命中率、超时率。这些指标能快速定位问题。比如模型调用次数突然上升,可能是提示词被改坏了,模型开始反复调工具;工具调用成功率下降,可能是下游服务出问题了。
6. 常见问题与排查技巧实录
6.1 工具调用不触发或触发错误
这是最高频的问题。模型该调工具时不调,或者调了错误的工具。排查顺序我固定为三步。
第一步,检查工具描述。描述是否清晰说明了使用场景?参数描述是否完整?我遇到过参数描述写“订单号”但没写格式,模型传了一个带字母的字符串,导致查询失败。
第二步,检查系统提示词。提示词里有没有明确告诉模型“你有这些工具可用”?有些模型需要显式提示才会调用工具。
第三步,检查模型本身。不同模型对工具调用的支持程度差异很大。我实测下来,同一条提示词,有的模型调用准确率九成,有的只有六成。如果前两步都没问题,换个模型试试。
6.2 RAG 检索结果不相关
检索不相关,八成是切分或向量化的问题。我的排查清单:片段是不是太大或太小?重叠够不够?向量模型和检索时的查询向量是不是同一个模型?元数据有没有丢?还有一个容易忽略的点:查询本身可能就有问题。用户问“那个东西怎么弄”,这种问题检索什么都白搭,需要先做查询改写。
6.3 Agent 陷入死循环
Agent 反复调用同一个工具,或者在不同工具之间来回跳,停不下来。这是提示词和工具设计共同导致的。我的解法是加一个最大迭代次数限制,比如 10 次,超过就强制终止并返回当前结果。同时在提示词里明确“如果工具返回结果已经足够回答用户问题,不要再调用工具”。
6.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 工具不触发 | 描述不清、提示词缺失、模型不支持 | 检查描述和提示词,换模型测试 |
| 工具传参错误 | 参数描述不完整、参数类型模糊 | 补全参数描述,明确格式要求 |
| 检索不相关 | 切分不当、向量模型不一致、查询口语化 | 调整切分参数,统一向量模型,加查询改写 |
| Agent 死循环 | 提示词未设终止条件、工具返回值有歧义 | 加迭代上限,优化工具返回格式 |
| 并发上不去 | 同步阻塞、模型 QPS 限制 | 异步化,加信号量限流,加缓存 |
| 回答编造信息 | 检索没命中、提示词未约束 | 检查检索链路,提示词加“不确定就说不知道” |
6.5 几个我踩过的坑
第一个坑:工具方法里用了事务。Agent 调用工具时,如果方法上有@Transactional,而整个 Agent 执行是异步的,事务上下文会丢。我的做法是工具方法本身不开启事务,事务逻辑下沉到更底层的服务方法里。
第二个坑:向量库选型随意。我一开始用内存向量库,开发阶段没问题,一上生产数据量大了就崩。后来换成支持持久化和索引的向量库,稳定多了。选型时重点看:是否支持增量写入、是否支持元数据过滤、检索延迟如何。
第三个坑:忽略 token 消耗。Agent 多轮调用,每轮都带完整上下文,token 消耗是普通问答的好几倍。我上线第一个月账单超预算不少。后来加了 token 计数和预算告警,才控制住。
7. 我对这套技术栈的真实体会
从@Tool到 Agent 流水线,LangChain4j 给我的最大感受是“够用且不重”。它没有试图做一个大而全的平台,而是把模型接入、工具、记忆、检索、编排这几个核心能力做扎实,剩下的交给你用 Java 的方式去组织。这对 Java 团队特别友好,因为不需要引入新的语言和运行时,现有的工程实践、监控、部署流程都能复用。
我现在的新项目基本是这个套路:先用@Tool把业务能力暴露出来,再根据场景决定要不要上 Agent。简单问答加单工具,AiServices直接搞定;多步任务和条件分支,才上 Agent 流水线。RAG 作为独立模块接入,和 Agent 解耦,方便单独调优。这套组合跑下来,开发效率和线上稳定性都比我早期自己造轮子好太多。
如果你刚开始,我的建议是先跑通一个@Tool的 demo,感受一下模型调用工具的过程,再逐步加记忆、加检索、加 Agent。每一步都单独验证,别一上来就搭全套,出了问题你根本不知道是哪一层的事。