2026年开始重新看Java生态的AI应用开发,我最大的体感是:Java开发者已经不能再拿“AI应用是Python专属”当借口。最近我在梳理 SpringAI 2.0、Langchain4j、RAG、Agent 这些技术栈时,发现很多团队的痛点并不在模型能力上,而在“Java服务怎么低摩擦地把模型接进业务流程”。如果你所在的团队已经跑着大量 Spring Boot 服务,却为了一个知识库问答系统另起一套 Python 技术栈,这种割裂感一定不陌生。SpringAI 和 Langchain4j 就是来补这座桥的。这篇文章不打算复述任何教程,而是把一个模拟航空客服智能体作为贯穿案例,拆解 Java + SpringAI 2.0 + Langchain4j + RAG + Agent 这条链路里,哪些事情值得做,哪些地方最容易踩坑。
1. 先用Java生态的视角,重新看一遍AI应用开发
1.1 为什么Java在AI应用层一直有种“慢半拍”的观感
过去几年,AI 领域的很多Demo都是Python写的,因为模型训练、科研代码、Notebook 生态在Python里最成熟。但落到企业级业务系统时,情况不一样:银行、航空、制造、政务这些领域,核心业务系统大量是 Java,尤其是 Spring Boot。AI 只是这些系统中的一个小模块,它要被事务、权限、审计、日志、灰度发布这些约束包裹着。
Java 生态真正的优势,从来不是模型训练,而是服务治理。Spring Boot 里有成熟的路由、拦截、配置中心、链路追踪、消息队列集成方案。如果 AI 能力能作为 Spring Bean 被注入到业务代码里,那么它就不再是一个“外挂”,而是一个普通的外部服务依赖。
这也是我判断 SpringAI 2.0 和 Langchain4j 更有价值的原因:它们不是来抢Python饭碗的,而是让Java团队用熟悉的工程方式来接入大模型。
1.2 SpringAI 和 Langchain4j 不是重复方案,而是两条路线
很多人在刚开始看材料时会困惑:到底选 SpringAI 还是 Langchain4j?它们看起来都在做“Java调大模型”这件事,但侧重点不完全一样。
SpringAI 是 Spring 官方体系里的 AI 抽象层,思路更接近“把模型接入变成一种 Spring 风格的配置”。如果你已经在用 Spring Boot,它的集成体验更顺,很多配置可以走 starter 和环境变量。Langchain4j 则更像 LangChain 思路的 Java 实现,早期在工具调用、Agent 链路的抽象上更灵活,社区里也有不少从 Python LangChain 迁移过来的用户。
下面这张表是我个人理解的综合对比,具体以当前官方文档为准:
| 维度 | SpringAI | Langchain4j |
|---|---|---|
| 定位 | Spring 官方 AI 应用接入框架 | 受 LangChain 启发的 Java AI 框架 |
| 模型接入 | 统一模型抽象,Spring 风格配置 | ChatLanguageModel 等接口,模型来源多 |
| 工具调用 | 支持函数回调、@Tool 等方式 | @Tool + AiServices 装配 |
| Spring Boot 集成 | 原生贴合 | 也有 spring-boot-starter |
| 适合人群 | 深度使用 Spring 生态的团队 | 习惯 LangChain API、需要快速迁移的团队 |
在实际项目里,两支框架都在快速演进,选型时不要只看名气,要看你团队对“哪一种抽象方式更容易接受”。如果原本就是 Spring 开发团队,我建议先顺Spring 的思路跑通一个最小例子;如果想复用 LangChain 里的记忆、Chain 概念,Langchain4j 的手感会更接近。
1.3 它们真正解决的是低摩擦接入,而不是模型能力
很多新手容易把 SpringAI 或 Langchain4j 当成“模型本身”,实际上它们只是封装了模型调用协议。模型还是那个模型,但接入方式从“写HTTP客户端、拼Json”变成了“声明一个接口、注入一个Bean”。
这里有个关键变化:过去Java项目接大模型,最常见方案是写一个 RestTemplate 请求,然后解析返回。单次调用没问题,但一旦涉及工具调用、知识库检索、多轮对话、Agent 循环,自己手写很容易乱。框架要做的是把这些流程标准化,让开发者只需要关心业务逻辑。
建议:先不要纠结“哪个框架更强”,先跑通一个最小调用,再去对比两者的工具调用和RAG体验。
2. 拆解一个智能航空项目的“零件”:Tools、RAG、Agent各自负责什么
2.1 用一个航空客服智能体作为贯穿案例
假设我们要做一个航空客服智能体,用户可能会问三类问题:
- “CA1234航班现在到哪了?”
- “退改签政策是什么?”
- “我行李丢了,应该怎么办?”
第一类问题需要查询航班系统,拿到实时数据;第二类问题需要读取航空公司的内部业务手册;第三类问题可能需要同时读手册、查行李状态、甚至创建一个工单。
这里面就出现了三个核心能力:Tools(工具调用)、RAG(检索增强生成)、Agent(智能编排)。它们缺一不可,也只有三种能力组合起来,这个智能体才像一个能干活的人,而不是一个只会聊天的对话模型。
2.2 Tools:让模型可以查实时数据的“手”
大模型本身不知道某个航班此刻的状态,因为它的训练数据不可能实时更新。Tools 就是给模型外接一个查询能力:模型在回答用户问题时,如果发现需要实时信息,会生成一个调用请求,由Java后端执行这个方法,再把返回结果交回给模型去组织语言。
在航空场景里,常见的 Tools 包括:
- 航班状态查询
- 行李状态查询
- 退改签规则实时查询
- 值机柜台查询
- 投诉工单创建
工具的价值不是“把API包装一下”,而是让模型知道“什么时候该调用哪个工具”。这需要工具命名、描述、参数定义都足够清晰,否则模型很难判断。
2.3 RAG:让模型可以读内部文档的“记忆”
RAG 解决的是“知识新鲜度”和“专业文档动态更新”的问题。航空业务手册可能几百页,且会持续更新。模型不能把这些内容都塞进上下文里,也不该依赖训练语料里的旧知识。
RAG 的一般链路是:把文档切分、向量化、存入向量库,用户提问时先在知识库里检索相关内容,再带着检索结果让大模型生成答案。这样回答的“素材来源”可控,而且可以做到引用溯源。
在航空场景,RAG 尤其重要,因为退改签政策、行李规定、危机处理流程一旦说错,代价很高。单纯靠模型“记忆”回复不可靠,必须有文档依据。
2.4 Agent:把“手”和“记忆”串起来的“调度器”
Agent 是这几年最难被准确定义的概念。在我的理解里,它的核心是“决策”:面对用户问题,决定是先查工具,还是先检索知识库,还是两者都做,还是直接回答。
技术上,Agent 循环大致是:
- 接收用户问题
- 判断是否需要调用工具
- 若需要,则调用工具并拿到结果
- 判断结果是否足够回答用户问题
- 若不够,继续调用下一个工具或检索知识库
- 最终生成回答
如果只做一次工具调用,那还不能叫 Agent,更像“带功能插件的对话”。Agent 的价值在于它可以多次迭代,直到拿到足够信息后再回复。
2.5 三者协作的一个典型流程
当用户问“我行李丢了,怎么办”时,完整链路的理想形态是:
- Agent 判断这个问题需要航空手册中的“行李异常处理流程”;
- 先去 RAG 知识库里检索相关章节;
- 同时调用“行李状态查询”工具,尝试获取用户行李的最近追踪记录;
- 将检索结果和工具结果合并给模型;
- 模型组织一段有依据、有步骤的回答,并给出人工服务入口。
这个流程里,Tools 提供实时数据,RAG 提供静态规则,Agent 负责决定谁先谁后。Java 开发者要做的,就是用 Spring AI 或 Langchain4j 把这些流程串起来,而不是让用户在Prompt里自己拼。
3. 从零跑通一个最小Agent:不要一开始就做知识库
3.1 最小闭环:只接模型加一个航班查询工具
我见过太多团队一上来就准备向量库、切片、调参,结果两三天后才想起来还没验证“模型能不能正确调用一个简单工具”。更稳妥的做法是先跑通最小闭环:一个模型、一个航班查询工具、一个问答入口。
这个闭环一旦跑通,你就理解了链路中最重要的“工具调用”机制:模型怎么识别意图、怎么把用户问题里的参数抽出来、怎么生成方法调用、结果怎么回填给大模型。
3.2 环境准备
常规情况下,你至少需要:
- JDK 17 以上
- 一个 Spring Boot 项目,或者直接使用 Langchain4j / SpringAI 的 starter 依赖
- 一个模型API服务,支持 OpenAI 兼容格式或对应厂商格式
- 基础的内存配置,不要太低
由于这些框架版本迭代很快,我下面只写通用案例,不写死版本号。具体依赖坐标和类名,一定要以你当前引入版本的官方文档为准。
// 常见依赖坐标示例,具体版本号请查官方文档 // implementation 'dev.langchain4j:langchain4j-spring-boot-starter' // implementation 'dev.langchain4j:langchain4j-open-ai'3.3 定义一个航班查询工具
在 Langchain4j 风格的写法里,Tools 通常是一个普通类,方法上用@Tool注解描述这个工具的作用。下面是一个结构化示例:
import dev.langchain4j.agent.tool.Tool; public class FlightTools { @Tool("查询指定航班号在当前日期的实时状态,返回航班状态、起飞时间、到达时间") public String getFlightStatus(String flightNumber, String date) { // 这里调用内部航班系统,通常是连接数据库或外部API // 这里用模拟数据代替,真实项目需要替换 if ("CA1234".equalsIgnoreCase(flightNumber)) { return "航班号: CA1234, 日期: " + date + ", 状态: 准时, 计划起飞: 08:30, 预计到达: 11:40"; } return "未找到航班信息"; } }这里要注意:@Tool注解里的描述不是给用户看的,是给大模型看的。描述写得越清楚,模型越容易在正确的时候调用这个工具。
3.4 把工具接入Agent
在 Langchain4j 里,可以用AiServices把一个接口和工具组合起来:
public interface FlightAssistant { String chat(String userMessage); } FlightAssistant assistant = AiServices.builder(FlightAssistant.class) .chatLanguageModel(chatModel) .tools(new FlightTools()) .build(); String answer = assistant.chat("CA1234航班现在什么状态?"); System.out.println(answer);这段代码看起来简单,但它背后完成了好几件事:接收用户问题、判断需要调用工具、解析参数、调用getFlightStatus、把返回值交给模型、生成最终回答。
如果把这段代码放到 Spring Boot 里,就可以把chatModel和FlightTools配成 Bean,然后通过接口注入到 Controller 或 Service 中。
3.5 验证时要看什么,而不是只看能不能回复
第一次跑通时,很多人只看最终输出是否像话,这是远远不够的。
你需要打开日志,重点确认:
- 模型是否生成了工具调用指令?
- 工具方法是否被正确调用?
- 参数是否被正确抽取?比如用户说“CA1234现在”,模型有没有自动补当天日期?
- 工具返回后,模型是否基于返回结果组织回答,而不是自说自话?
如果发现模型没有调用工具,直接凭“印象”回答了航班状态,那问题往往出在工具描述不够清晰,或者模型服务不支持 function calling 配置。
建议:先别急着做知识库,把你最核心的一到两个工具调通,理解Agent的循环机制,再往后走。
4. RAG才是航空场景里最难啃的部分:文档解析、索引和检索
4.1 航空知识库的典型麻烦
航空业务手册不是一篇干净博客。它们通常是数百页的 PDF,里面既有章节目录,又有表格、流程图、政策版本更新说明,甚至还有扫描件。这种情况下,RAG 的难点往往不在“向量化”这一步,而在“文档加载和解析”。
如果文档解析不到位,后面无论怎么调检索都没有意义。常见问题包括:
- PDF 里表格被拆碎,语义丢失
- 页眉页脚混进正文
- “旧版政策”和“新版政策”同时存在,需要定位版本
- “行李”和“手提行李”是两个概念,必须靠分块和检索区分
4.2 全流程要从加载、清洗、分块到检索闭环
一个可用于航空知识库的 RAG 流程,至少包含以下环节:
- 文档加载:读取 PDF、Word、Markdown 等格式
- 解析清洗:去掉页眉页脚、处理表格、保留章节上下文
- 切分:按章节、段落而不是固定字符切,必要时增加重叠
- 向量化:用 Embedding 模型把文本变成向量
- 存储:写入向量数据库
- 检索:用户提问后,做向量检索
- 重排:在多召回结果里挑出最相关的
- 生成:把检索结果交给大模型,要求它依据材料回答
在 Java 生态里,文档解析、切分、向量化、向量存储都有对应抽象。比如 Langchain4j 提供了Document、DocumentSplitter、EmbeddingStore等概念,SpringAI 也有对应的文档读取器。
这里给一个 Java 环境下常见的处理思路示意:
// 示意代码:具体实现和类名以框架版本为准 Document document = loadDocument("flight-policy.pdf"); List<TextSegment> segments = DocumentSplitter.recursive(500, 50).split(document); List<Embedding> embeddings = embeddingModel.embedAll(segments).content(); embeddingStore.addAll(segments, embeddings);向量数据库可以用 Milvus、Redis 或关系型数据库的向量插件。如果你已经有 Milvus,Langchain4j 和 SpringAI 都有集成方式。关键点在于:不要为了用向量库而用向量库,先想清楚数据量、检索频率和部署成本。
4.3 检索质量差,先别急着调Prompt
一个很常见的错误是:RAG 结果答非所问,于是反复调 Prompt,结果还是没用。实际上,大部分检索问题出在“召回”环节:
- 分块太大,把不相关内容塞进同一块
- 分块太小,一段话被切断,语义不完整
- Embedding 模型没有和语义匹配
- topK 参数太小,漏掉关键文档
- 没有做版本过滤,旧政策和新政策混淆
- 纯向量检索表现一般,缺少关键词混合检索
排查时,建议先打印出实际召回的文档片段,人工判断这些片段是否真的能回答用户问题。如果召回内容本身就不靠谱,Prompt 写得再花哨也没用。
4.4 结合外部Embedding模型和重排的通用策略
在航空场景里,我建议把检索链路做两层:第一层用 Embedding 做语义召回,第二层用重排模型或规则把最相关的结果排到最前面。必要时再叠加关键词匹配,确保专有名词不被漏掉。
例如用户问“手提行李尺寸限制”,如果只做语义召回,可能召回到“行李运输限制”的泛化内容。如果加入关键词和章节检索,就能更精准命中“手提行李”章节。
注意:RAG 不是“向量数据库 + Prompt”就行。它是一条数据处理管线,必须用真实业务文档反复验证。
5. Tools设计得好不好,直接决定Agent像不像“真人”
5.1 工具调用失败,很多不是模型的问题
当 Agent 在航空客服场景里表现不聪明时,我首先怀疑的不是模型,而是工具定义。
模型能不能正确选择工具,主要看三点:
- 工具是谁:方法名或工具名是否直观
- 什么时候用:描述里有没有写清楚触发条件
- 怎么用:参数名和参数说明是否明确
举例来说:
@Tool("查询航班状态,当用户询问航班是否准点、起飞时间、到达时间、航班动态时使用") public String queryFlightStatus( @ToolParam("航班号,例如 CA1234") String flightNumber, @ToolParam("日期,格式 yyyy-MM-dd,默认当天") String date ) { ... }这段描述对模型就友好得多:触发条件清晰,参数含义清楚。很多模型之所以不调用工具,是因为描述里只写了“get status”这种模糊定义。
5.2 工具返回值也要给模型“能看懂的结构”
工具返回的不应该是给人看的UI文案,而应该是结构化数据。模型会把这段文本拼进上下文,如果返回是一大段带样式的 HTML,既浪费 token,又干扰理解。
比较差的返回:
当前航班状态为准时,计划起飞时间为08:30,预计到达时间为11:40,登机口为23号。更好的返回:
{ "flightNumber": "CA1234", "status": "ON_TIME", "scheduledDeparture": "2026-02-01 08:30", "estimatedArrival": "2026-02-01 11:40", "gate": "23" }当然,实际上你给模型的返回不一定是 JSON 字符串,但至少要保证信息边界清楚、无歧义。如果是对象,可以在@Tool方法里序列化为字符串。模型需要的是“能读的信息”,不是“好看的信息”。
5.3 超时、空结果和异常必须显式处理
工具调用不是总能成功。航班查询接口可能超时,行李状态可能查不到,权限校验可能失败。这些都需要在 Tool 方法里显式处理,不能把异常直接抛给 Agent。
如果航班状态接口超时,工具可以返回“查询超时,请稍后重试”,而不是让整个 Agent 循环卡死。如果你看到类似agent execution provider did not respond in time的报错,优先检查你的工具调用耗时、网络超时配置、Agent 的执行超时时间,而不是怀疑模型变笨了。
5.4 一个工具设计检查清单
我通常用下面这个清单检查一个 Tool 是否合格:
- 工具名是否直白,避免“searchFlightInfo”和“getFlightDetails”这种容易混淆的名字
- 描述是否说清了“什么时候该用”
- 参数是否带示例,模型才能正确抽取
- 返回值是否结构化且不含多余格式
- 是否处理了空结果、异常、权限不足
- 是否做了幂等,同一个参数重试不会产生副作用
- 是否有超时时间,避免Agent被拖死
6. 从Demo到生产:航空智能体落地还要补哪些工程能力
6.1 不是能回答问题就够了
很多团队跑通一个智能体之后,会误以为剩下的工作只是上线。但在航空这类对准确性、安全性要求高的行业,能回答问题和能生产使用之间隔着一整层工程能力。
举个简单例子:同样一个航班状态查询接口,Demo 里可以直接调用,生产环境必须有权限校验、限流、日志审计和降级方案。同样一个 RAG 知识库,Demo 里可以用简单的 CSV 文件,生产环境必须考虑文档版本、数据更新、引用溯源。
6.2 需要补的工程清单
下面是我建议的最低生产化清单:
- 日志:记录用户问题、模型输出、工具调用输入输出、检索到的文档ID
- 审计:在大盘上能追踪每一次AI回答的依据和工具动作
- 权限:普通用户只能查自己的订单,内部客服才能查全部航班数据
- 限流:模型API和工具接口都要有配额控制,防止异常流量
- 缓存:高频问题、热点航班查询结果可以缓存,减少模型调用成本
- 幂等:对于“创建工单”这类有副作用的工具,必须防止重复提交
- 可观测性:用链路追踪把“用户问题 -> RAG检索 -> 工具调用 -> 模型生成”串起来
- 灰度发布:先放内部客服使用,再逐步扩大范围
6.3 成本控制:不能把所有流量都直接打到模型API
一个航空客服智能体如果每个问题都走“模型 + RAG + 多轮工具调用”,成本会很高。
常见的降本策略包括:
- 先做意图分类,简单FAQ直接走规则或知识库命中
- 对高频查询结果做缓存
- 限制 Agent 最大循环次数,避免模型反复调用工具
- 使用小的模型做意图分类,用大模型做最终答案生成
- Embedding 向量缓存,避免同一文档反复向量化
6.4 准确性保障:答案必须有出处,不能只靠自觉
在航空场景,RAG 的优势就在于“答案可以有出处”。生产环境应该把模型回答和引用的知识库段落一起返回。前端可以展示“来源: 值机行李规定.pdf 第23页”,后台也能据此定位问题。
如果模型发现检索到的内容互相矛盾,比如新旧版政策冲突,应该让它主动说“存在多种说法,需要人工确认”,而不是硬挑一个答案。这一点可以在 Prompt 或后置校验里做。
6.5 JVM内存问题,不能只靠调大 -Xmx
跑 Agent 和 RAG 应用时,经常遇到java: OutOfMemoryError: insufficient memory或类似问题。很多人第一反应是调大堆内存,但根因往往不是堆太小:
- 向量数据库客户端把大量数据加载到内存
- 文档解析时一次性读取超大 PDF
- LangChain4j 或 SpringAI 在处理流式响应时有对象堆积
- 容器内存限制小于 JVM 堆配置
- 工具并发线程过多
排查时先看是堆内存溢出、元空间溢出,还是容器内存被杀。再结合 dump 文件确认是不是向量索引或文档对象占用了大量内存。不要一上来就-Xmx8g,那可能只是掩盖问题。
建议:在生产前先做一次压力测试,模拟并发用户同时提问,观察内存、CPU、外部API超时情况。
7. 遇到问题怎么排查:按输入、模型、工具、流程逐层收敛
7.1 先看现象,再定位是哪一个环节
智能体应用的问题通常复杂,因为失败可能发生在模型层、工具层、RAG层,也可能发生在 Agent 循环里。我习惯用“三层收敛法”来排查:
- 看输入:用户问题是否清楚?系统提示词是否有冲突?上下文是否被截断?
- 看中间动作:模型有没有正确生成工具调用?工具返回了什么?检索召回了什么?
- 看输出:模型是依据中间结果生成答案,还是自己发挥了?
大部分问题都能在中间动作里找到原因。
7.2 输入层最容易犯的错
模型API返回空content的时候,不要急着怀疑模型。先检查请求参数:
- 是否传了
stream=false却收到了流式空响应? - 是否设置了过高的
temperature导致输出不稳定? - 是否把系统提示词设置成了“你是一个什么都不懂的机器人”?
- 是否用户问题本身就包含太多噪声?
如果日志里能看到原始请求和响应,这一步会省很多时间。很多兼容 OpenAI 协议的服务,在异常时返回的content为空,但reasoning_content或错误信息里有提示。
7.3 工具层:重点看参数和描述
如果模型没有触发工具调用,先检查工具描述是否清晰;如果触发了但报错,看参数是否传对了。工具层常见问题还包括:
- 方法只有一个字符串参数,模型不知道怎么填充
- 参数类型和模型生成类型不匹配
- 工具返回超长文本,导致上下文溢出
- 工具内部抛了异常,模型拿到的是异常信息
排查方式就是把工具调用前后的输入输出完整打日志。
7.4 RAG层:先人工看召回结果
RAG 答非所问时,按这个顺序排查:
- 把用户问题单独拿出来,看向量检索结果的前5条
- 判断召回的片段是否真的能回答问题
- 如果结果不对,看是分块问题、TopK问题,还是 Embedding 模型问题
- 如果召回了但没有引用出处,看 Prompt 里是否要求了引用
- 如果新旧政策冲突,看是否加了版本过滤字段
很多 RAG 问题不是模型回答能力差,而是根本没召回正确的知识点。
7.5 Agent流程层:防止循环和超时
Agent 经常出现的两类流程问题:
- 死循环:模型反复调用同一个工具,拿不到决定性信息
- 超时:工具调用或模型响应太慢,导致 Agent 执行 Provider 超时
针对死循环,设置最大工具调用次数;针对超时,区分是网络原因、工具耗时长,还是模型 API 响应慢。如果报错提示agent execution provider did not respond in time,优先检查执行超时配置,再排查外部依赖。
7.6 一张排查表,放到团队文档里
| 现象 | 优先检查 | 再进一步 |
|---|---|---|
| 模型不调用工具 | 工具描述、工具名、触发场景 | 模型是否开启 function calling |
| 工具报错 | 参数抽取是否正确 | 工具方法内部异常日志 |
| 返回空 content | API原始响应 | 是否流式响应、是否超时 |
| RAG 答非所问 | 召回的前几段片段 | 分块、TopK、版本过滤 |
| Agent 死循环 | 最大循环次数 | 工具返回信息是否足够判定 |
| 内存溢出 | JVM 堆 / 容器限制 | 向量数据、文档对象、线程数 |
| 执行超时 | Agent 超时配置 | 外部API响应时间、网络 |
写在最后:先跑通最小闭环,再谈AI航空梦
如果只记住一句话,我希望是:Java 生态做 AI 应用,已经不是“能不能做”的问题,而是“怎么做成一个可靠业务系统”的问题。SpringAI 和 Langchain4j 提供了接入模型的脚手架,但真正的复杂度在实时工具、知识库检索、Agent 编排和工程化落地里。
航空客服智能体这个案例,其实放大看就是无数企业级 AI 应用的缩影:模型负责语言理解和生成,Tools 负责触达业务系统,RAG 负责引入企业私有知识,Agent 负责判断先后顺序。谁先谁后有千百种可能,但落地方法论是通的:先跑通一个最小闭环,再逐步把工具、知识库、可观测性加进去。
下一次再有人问“Java 能不能做 Agent”,你可以先让他跑一个航班查询工具试试。跑通之后,他会发现真正的困难不在模型,而在“把一个能力稳定地放进业务流程”。这个过程需要耐心,但方向已经很清楚了。