☰
Java开发者AI入门实战:Spring AI与LangChain4j构建RAG知识库
2026/10/1 5:33:21 网站建设 项目流程

1. Java 开发者切入 AI 的真实路径拆解

1.1 为什么 Java 开发者不需要从零学 Python

我做了十多年 Java 后端,这两年身边问得最多的一句话就是:“搞 AI 是不是得先把 Python 学一遍?”我的答案一直很明确:不需要从零开始,但需要理解 AI 应用的运行范式。

原因很简单。绝大多数企业级 AI 落地场景,不是训练大模型,而是把大模型能力集成进已有的业务系统。你手里那套 Spring Boot 微服务、MyBatis 数据访问层、Redis 缓存、MQ 异步链路,才是 AI 真正要嵌入的地方。训练模型是算法团队的事,而推理调用、上下文编排、知识库检索、结果后处理,这些恰恰是 Java 工程师的主场。

我见过太多 Java 同行一上来就扎进 PyTorch 教程,学了两周矩阵运算,回头发现公司要的只是“把客服知识库接进大模型做个问答”。这就是方向跑偏了。正确的切入姿势是:先跑通一条 Java 调用大模型的链路,再逐步往 RAG、Agent 方向延伸。

具体来说,Java 开发者入门 AI 的路线图可以拆成四个阶段:

  • 第一阶段:模型调用打通。用 HTTP 客户端或官方 SDK 调通一个大模型接口,理解 token、temperature、system prompt 这些基础概念。这一步一两天就能完成。
  • 第二阶段:框架化封装。引入 Spring AI 或 LangChain4j,把裸调用升级成可维护的工程结构,学会用 ChatClient、PromptTemplate、Advisor 这些抽象。
  • 第三阶段:RAG 知识库。把企业私有文档向量化,接入向量数据库,实现“基于自有知识的问答”。这是目前企业需求最集中的方向。
  • 第四阶段:Agent 与工作流。让模型能调用工具、编排多步任务,也就是常说的 Agentic 方向。

这四个阶段不是必须严格串行,但顺序乱了会很难受。我试过直接上手 RAG,结果连 embedding 和 completion 的区别都没搞清,调了一整天以为向量库有问题,其实是 prompt 模板写错了。

1.2 工具链选型的核心考量:Spring AI 还是 LangChain4j

这是被问得最多的问题,没有之一。我的判断逻辑是这样的:

Spring AI 的优势在于和 Spring 生态的无缝融合。如果你的项目本身就是 Spring Boot,那引入 Spring AI 几乎是零摩擦——自动配置、依赖注入、Actuator 监控,全都对得上。它的 ChatClient API 设计得很 Spring 味,写起来就像在用 RestTemplate。缺点是生态相对年轻,一些高级的 Agent 编排能力还在演进中。

LangChain4j 的优势在于抽象层次更丰富。它的 AiServices 可以用接口加注解的方式定义 AI 服务,声明式写法很优雅。RAG 相关的组件也更完整,文档加载器、分割器、嵌入存储、检索器一应俱全。缺点是它自成一套体系,和 Spring 的整合需要额外配置。

我的实际选择是:业务系统集成用 Spring AI,独立 AI 应用或复杂 RAG 用 LangChain4j。两者并不冲突,甚至可以在同一个项目里共存——Spring AI 负责对话链路,LangChain4j 负责知识库检索。

对比维度Spring AILangChain4j
生态融合与 Spring Boot 无缝独立体系,需适配
API 风格命令式,Spring 味浓声明式,注解驱动
RAG 组件基础完备更丰富细致
Agent 能力演进中相对成熟
学习曲线低,Spring 开发者友好中等,概念较多
适合场景业务系统集成独立 AI 应用

提示:不要纠结“哪个更好”,先看你的项目底座是什么。Spring Boot 项目硬上 LangChain4j,光是配置整合就能耗掉你半天热情。

1.3 环境准备:从 JDK 版本到依赖管理

动手之前,环境得先理顺。这块看似简单,但踩坑的人不少。

JDK 版本建议 17 或 21。Spring AI 和 LangChain4j 的新版本都要求 JDK 17 起步,21 的虚拟线程在处理大量并发模型调用时优势明显。我实测过,同样的 RAG 检索并发场景,JDK 21 虚拟线程比传统线程池吞吐量高出约 40%。

构建工具用 Maven 或 Gradle 都行,但要注意依赖版本对齐。Spring AI 有 BOM 管理,直接引入spring-ai-bom就能统一版本。LangChain4j 也有类似的 BOM。我见过有人手动指定每个模块版本,结果 core 和 openai 模块版本不一致,启动就报 NoSuchMethodError。

模型服务的选择上,本地开发和测试建议用 Ollama 跑一个小参数模型,比如 qwen2.5:7b 或 llama3.1:8b。好处是免费、离线、数据不出本机。生产环境再切换到云端 API。这样做的理由是:开发阶段频繁调试,用云端 API 会产生大量无效调用成本,而本地模型虽然效果差一些,但足够验证链路是否通畅。

依赖清单大致如下:

<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-ollama-spring-boot-starter</artifactId> </dependency> </dependencies>

配置文件里把模型地址、密钥、模型名配好,Spring Boot 启动时就会自动装配 ChatClient。这一步跑通,你就已经迈过 AI 应用的门槛了。

2. 核心概念与实操要点深度解析

2.1 Prompt、Token 与上下文窗口:三个必须搞懂的基础概念

很多人调模型调不明白,根子在这三个概念上没吃透。

Prompt 就是你给模型的输入,但它不只是“一句话”。一个完整的 prompt 通常包含 system 角色设定、user 用户输入、assistant 历史回复三部分。system prompt 决定了模型的“人设”和行为边界,这部分写得好不好,直接决定输出质量。我一般会把业务规则、输出格式要求、禁止事项全塞进 system prompt,实测下来比在 user 消息里反复强调有效得多。

Token 是模型的计费和处理单位。中文大致一个字对应 1 到 2 个 token,英文一个单词约 1.3 个 token。为什么要关心这个?因为上下文窗口是有上限的。比如 8k 窗口的模型,你的 system prompt 加历史对话加检索到的文档,总共不能超过 8k token。超了怎么办?要么截断,要么换更大窗口的模型,要么做上下文压缩。

我踩过的一个坑:RAG 场景下检索回来 10 个文档片段,每个 500 字,加起来就 5000 多字,再叠加历史对话,直接把窗口撑爆。模型要么报错,要么悄悄截断前面的内容,导致回答驴唇不对马嘴。后来我改成检索 Top 3 片段,每段限制 300 字,并做去重和相关性排序,问题才解决。

上下文窗口这个概念,你可以理解成模型的“短期记忆容量”。它记不住窗口之外的东西。所以多轮对话时,要么把历史消息一起传进去,要么用外部存储做长期记忆。这也是为什么 RAG 这么重要——它相当于给模型外挂了一个“长期记忆库”。

概念通俗理解实操影响
Prompt给模型的指令包决定输出质量和格式
Token计费和容量单位影响成本和窗口是否溢出
上下文窗口模型的短期记忆决定能塞多少历史与文档

2.2 结构化输出:让模型返回可解析的 JSON

模型默认返回的是自然语言,但业务系统需要的是结构化数据。比如你让模型分析一段文本的情感,返回“这段话是积极的”没法直接用,你需要的是{"sentiment": "positive", "confidence": 0.92}。

Spring AI 提供了BeanOutputConverter,可以把模型输出直接映射成 Java 对象。用法是先定义一个 record 或 POJO,然后用转换器生成格式指令,附加到 prompt 里。模型看到格式要求后,会按 JSON 输出,转换器再反序列化成对象。

record SentimentResult(String sentiment, double confidence) {} BeanOutputConverter<SentimentResult> converter = new BeanOutputConverter<>(SentimentResult.class); String prompt = """ 分析以下文本的情感倾向,并按指定格式返回。 {format} 文本:这家餐厅的服务态度太差了,等了一个小时才上菜。 """; PromptTemplate template = new PromptTemplate(prompt); template.add("format", converter.getFormat()); String response = chatClient.call(template.render()); SentimentResult result = converter.convert(response);

这里有个关键经验:不是所有模型都能稳定输出合法 JSON。小参数本地模型经常多输出一句“好的,以下是分析结果”,导致解析失败。解决办法有两个:一是用支持 JSON mode 的模型,二是在 prompt 里明确写“只输出 JSON,不要任何额外文字”,并在解析前做一次清洗,把 JSON 之外的内容剥掉。

我一般会写一个容错解析方法,先用正则提取第一个{到最后一个}之间的内容,再尝试反序列化。这样即使模型多说了废话,也能兜住。

2.3 向量化与 Embedding:RAG 的地基

RAG 的核心思路是:把文档变成向量存起来,用户提问时也转成向量,然后找最相似的文档片段喂给模型。这里的“变成向量”就是 embedding。

Embedding 模型和对话模型是两回事。对话模型负责生成回答,embedding 模型负责把文本转成高维浮点数组。常见的中文 embedding 模型有 bge、m3e、text-embedding 系列。选型时主要看维度、中文效果、推理速度三个指标。

维度越高,表达能力越强,但存储和计算成本也越高。768 维和 1536 维是常见选择。我实测下来,中文场景下 bge-large-zh 的检索命中率明显优于一些通用多语言模型,尤其是在专业术语较多的领域。

向量存到哪里?开发阶段可以用内存向量库,比如 LangChain4j 自带的InMemoryEmbeddingStore,重启数据就没了,但调试方便。生产环境一般用 Milvus、Qdrant、PgVector 这些。如果团队已经有 PostgreSQL,PgVector 是最省事的选择,不用额外维护一套向量数据库。

EmbeddingModel embeddingModel = new BgeSmallZhEmbeddingModel(); EmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>(); Document doc = Document.from("Java 的垃圾回收机制分为新生代和老年代..."); DocumentSplitter splitter = DocumentSplitters.recursive(300, 50); List<TextSegment> segments = splitter.split(doc); for (TextSegment segment : segments) { Embedding embedding = embeddingModel.embed(segment).content(); store.add(embedding, segment); }

这段代码里,recursive(300, 50)表示每段目标 300 字,重叠 50 字。重叠是为了避免语义被切断——如果一句话正好卡在分割点上,两段各拿一半,检索时可能两边都匹配不上。重叠区能让关键语义至少完整出现在一段里。

2.4 文档分割策略:RAG 效果的分水岭

RAG 做得好不好,七成看分割,三成看检索。这话不夸张。

分割太粗,一个片段里混了好几个主题,检索时相关性被稀释;分割太细,语义不完整,模型拿到手也不知道在说什么。我的经验是:按文档结构分割优先,按固定长度分割兜底。

具体来说,Markdown 文档按标题层级分,PDF 按段落分,代码按函数分。LangChain4j 提供了DocumentByParagraphSplitter、DocumentByLineSplitter等多种分割器。如果文档结构混乱,再用递归字符分割器兜底。

还有一个容易被忽略的点:元数据保留。每个片段除了文本内容,还应该带上来源文件名、页码、章节标题等信息。这样检索回来时,你可以告诉用户“这个答案来自《运维手册》第 3 章”,可信度立刻提升。而且元数据还能用于过滤,比如只检索某个产品线的文档。

Document doc = Document.from(text, Metadata.from("source", "运维手册.pdf") .add("chapter", "第三章 故障排查"));

注意:分割长度没有万能值。技术文档 300 到 500 字比较合适,法律合同可能要 800 字以上,聊天记录则适合按对话轮次分割。一定要拿真实文档试,别照搬网上的参数。

3. 完整实操流程:从零搭一个本地 RAG 知识库

3.1 项目初始化与依赖配置

我以 LangChain4j 加 Ollama 加内存向量库为例,走一遍完整流程。这套组合零成本、离线可用、适合学习和原型验证。

第一步,建一个 Maven 项目,引入依赖:

<dependencies> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>0.35.0</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-ollama</artifactId> <version>0.35.0</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-easy-rag</artifactId> <version>0.35.0</version> </dependency> </dependencies>

langchain4j-easy-rag这个模块值得单独说一句。它把文档加载、分割、嵌入、存储、检索这一整套流程封装成了几行代码,非常适合快速验证。但生产环境不建议直接用,因为它的默认参数不一定适合你的文档,而且可定制性差。学习阶段用它跑通链路,理解每个环节在干什么,然后再拆开自己实现。

第二步,确认 Ollama 已经跑起来,并且拉好了模型:

ollama pull qwen2.5:7b ollama pull nomic-embed-text

qwen2.5:7b 负责对话生成,nomic-embed-text 负责向量化。两个模型各司其职,别想着用一个模型干两件事。

3.2 文档加载与向量入库

假设你有一批 Markdown 格式的内部文档,放在docs/目录下。加载和入库的代码如下:

EmbeddingModel embeddingModel = OllamaEmbeddingModel.builder() .baseUrl("http://localhost:11434") .modelName("nomic-embed-text") .build(); EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>(); EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder() .documentSplitter(DocumentSplitters.recursive(300, 50)) .embeddingModel(embeddingModel) .embeddingStore(embeddingStore) .build(); List<Document> documents = FileSystemDocumentLoader.loadDocuments("docs/"); ingestor.ingest(documents);

EmbeddingStoreIngestor把分割、嵌入、存储三步串起来了。loadDocuments会自动识别文件类型,Markdown、txt、PDF 都支持。如果你的文档是 Word 格式,需要额外引入langchain4j-document-parser-apache-poi模块。

入库完成后,可以做个简单验证:拿一个已知问题去检索,看返回的片段是否相关。

Embedding queryEmbedding = embeddingModel.embed("如何排查内存泄漏").content(); List<EmbeddingMatch<TextSegment>> matches = embeddingStore.findRelevant(queryEmbedding, 3); matches.forEach(m -> System.out.println(m.embedded().text()));

如果返回的片段跟内存泄漏无关,说明分割或嵌入环节有问题,先别急着往下走。

3.3 检索增强的对话链路搭建

检索通了,接下来把检索和对话串起来。LangChain4j 的RetrievalAugmentor就是干这个的:

RetrievalAugmentor augmentor = DefaultRetrievalAugmentor.builder() .contentRetriever(EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(3) .minScore(0.7) .build()) .build(); ChatLanguageModel chatModel = OllamaChatModel.builder() .baseUrl("http://localhost:11434") .modelName("qwen2.5:7b") .build(); Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(chatModel) .retrievalAugmentor(augmentor) .build(); String answer = assistant.chat("系统频繁 Full GC 该怎么排查?");

Assistant是一个接口,用@SystemMessage注解定义人设:

interface Assistant { @SystemMessage("你是一个运维助手,只根据提供的参考资料回答问题。" + "如果资料中没有相关内容,直接说不知道,不要编造。") String chat(String userMessage); }

这里有两个参数值得细说。maxResults(3)表示检索最相关的 3 个片段。为什么不取更多?因为片段越多,噪声越大,而且会挤占上下文窗口。实测 3 到 5 个是比较平衡的值。minScore(0.7)是相似度阈值,低于这个分数的片段直接丢弃。这个值设太低会引入无关内容,设太高可能什么都检索不到。建议先用 0.6 起步,根据实际效果调整。

3.4 效果验证与参数调优

链路跑通只是开始,调优才是真正花时间的地方。我一般从三个维度验证:

检索命中率。准备 20 个典型问题,人工标注每个问题的正确答案在哪个文档片段里,然后看检索器 Top 3 里有没有包含这个片段。命中率低于 80% 就要优化分割策略或换 embedding 模型。

回答准确率。看模型回答是否忠实于检索到的资料,有没有编造。这个只能人工评估,但可以抽样做。

响应延迟。本地 7b 模型在普通笔记本上,一次完整问答大概 3 到 8 秒。如果超过 15 秒,检查是不是检索片段太多或者模型参数太大。

调优的常见手段包括:调整分割长度和重叠、更换 embedding 模型、调整 maxResults 和 minScore、优化 system prompt。每次只改一个变量,否则你分不清是哪个改动起了作用。

调优维度常见问题调整方向
检索不准返回片段与问题无关换 embedding 模型,调分割粒度
回答编造模型不看资料瞎说强化 system prompt,降低 temperature
响应慢等待时间过长减少检索片段,换小模型
答非所问理解偏差优化 prompt 模板,增加示例

4. 常见问题与排查技巧实录

4.1 模型调用报错排查速查表

实际开发中,报错是家常便饭。我把踩过的坑整理成一张表,方便对照排查:

报错现象可能原因解决方向
401 UnauthorizedAPI Key 错误或过期检查配置,确认密钥有效
429 Too Many Requests调用频率超限加退避重试,降低并发
400 Context Length Exceeded上下文超窗口截断历史,减少检索片段
连接超时网络或服务地址错误检查 baseUrl,确认服务可达
JSON 解析失败模型输出非纯 JSON加清洗逻辑,用 JSON mode
中文乱码编码不一致统一 UTF-8

其中429 和上下文超限是最常见的两个。429 的解决办法是加指数退避重试,Spring AI 和 LangChain4j 都支持配置重试策略。上下文超限则需要在业务层做控制,比如限制历史对话轮数、对检索片段做长度截断。

4.2 RAG 效果差的五个隐藏原因

RAG 搭起来容易,效果好难。我总结了五个最容易被忽略的原因:

第一,文档质量差。如果原始文档本身就有大量错别字、格式混乱、内容重复,检索效果不可能好。先清洗文档,再谈 RAG。我一般会做一轮预处理:去页眉页脚、合并断行、去除乱码。

第二,分割破坏了语义。前面提过,分割点卡在句子中间是灾难。解决办法是优先按段落、标题分割,固定长度分割作为兜底,并且一定要加重叠。

第三,embedding 模型不匹配。用英文模型处理中文文档,效果必然打折。中文场景一定要选中文优化的 embedding 模型。

第四,检索策略单一。纯向量检索对关键词不敏感。比如用户搜“AQS 原理”,向量检索可能返回一堆并发相关但没提 AQS 的片段。这时候需要混合检索——向量检索加关键词检索,两路结果合并去重。LangChain4j 支持这种组合。

第五,prompt 没有约束。如果 system prompt 不明确要求“只根据资料回答”,模型会自由发挥,把训练时的知识混进来。这在专业领域是致命的。

提示:RAG 调优是个迭代过程,别指望一次到位。我的习惯是建一个测试问题集,每次改动后跑一遍,用数据说话。

4.3 本地模型与云端 API 的取舍经验

开发阶段用本地模型,生产环境用云端 API,这是我一直推荐的组合。但具体怎么切,有几个经验点:

本地模型选 7b 到 14b 参数。再小效果太差,再大普通机器跑不动。qwen2.5:7b 在中文场景下表现均衡,是我常用的选择。

云端 API 选支持流式输出的。流式输出能让用户看到逐字生成的效果,体验好很多。Spring AI 的StreamingChatClient和 LangChain4j 的StreamingChatLanguageModel都支持。

切换时注意 prompt 兼容性。不同模型对 prompt 的敏感度不一样。本地模型能理解的指令,云端模型可能理解得更好,也可能理解偏。切换后一定要重新验证。

成本控制。云端 API 按 token 计费,RAG 场景下每次调用都带着检索片段,token 消耗不小。我的做法是:简单问题走本地模型,复杂问题才走云端;同时对检索片段做长度限制,避免无谓消耗。

4.4 从 RAG 到 Agent 的进阶思路

RAG 跑顺了,下一步自然是 Agent。Agent 的核心是让模型能调用工具。比如用户问“帮我查一下订单 12345 的状态”,模型识别出需要调用订单查询接口,传参、拿结果、组织回答。

LangChain4j 用@Tool注解定义工具:

class OrderService { @Tool("根据订单号查询订单状态") String queryOrderStatus(@P("订单号") String orderId) { return orderRepository.findById(orderId).getStatus(); } } Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(chatModel) .tools(new OrderService()) .build();

模型会自动判断什么时候调用这个工具。但工具描述一定要写清楚,否则模型不知道该在什么场景下用。我见过工具描述写“查询订单”的,模型在用户问“今天天气”时也去调,就是因为描述太模糊。

Agentic RAG 是更进阶的形态:让 Agent 自己决定检索什么、检索几次、要不要换个关键词再检。这比固定流程的 RAG 灵活,但也更难控制。我的建议是先把基础 RAG 做扎实,再考虑 Agent 化,否则问题排查会非常痛苦。

5. 学习资源与进阶方向建议

5.1 官方文档与源码阅读顺序

学 Spring AI 和 LangChain4j,最靠谱的资料就是官方文档加源码。我的阅读顺序建议是:

先看 Spring AI 的 ChatClient 章节,理解最基础的调用模型。然后看 Advisor 机制,这是 Spring AI 的拦截器体系,理解它才能做日志、限流、RAG 这些横切功能。最后看 RAG 章节,把检索增强的完整链路走一遍。

LangChain4j 则建议先看 AiServices,理解声明式定义 AI 服务的方式。然后看 RAG 包下的各个组件,从 DocumentLoader 到 EmbeddingStore 到 ContentRetriever,逐个搞明白职责。最后看 Agent 和 Tool 相关的内容。

源码阅读不要贪多。挑一个核心类,比如DefaultRetrievalAugmentor,跟一遍它的 augment 方法,看它怎么把检索结果拼进 prompt。这一遍下来,比看十篇博客都管用。

5.2 面试与实战中的高频考点

如果你是为了面试准备,这几个点几乎必问:

RAG 的完整流程。从文档加载、分割、嵌入、存储、检索到生成,每一步都要能说清楚。面试官常追问“分割长度怎么定”“检索 Top K 怎么选”,这些要结合具体场景回答,别背固定值。

向量检索的原理。余弦相似度、内积、欧氏距离的区别,什么时候用哪个。HNSW 索引的大致思路。这些不要求推导公式,但要能说清概念。

Prompt 工程的实际经验。system prompt 怎么写、few-shot 怎么加、输出格式怎么约束。最好能举一个你实际调优的例子。

模型调用的工程问题。超时重试、限流降级、成本控制、流式输出。这些是 Java 工程师的强项,一定要结合自己的后端经验来答。

Agent 与工具调用。工具怎么定义、模型怎么决策、多步任务怎么编排。这块是加分项,能聊清楚说明你真的动手做过。

5.3 后续可以深入的方向

基础 RAG 跑通后,有几个方向值得深入:

混合检索与重排序。向量检索加关键词检索,再用一个重排序模型对结果精排。这套组合能把检索命中率提升一大截。重排序模型可以用 bge-reranker 系列。

GraphRAG。把文档里的实体和关系抽出来,构建知识图谱,检索时同时利用图结构和向量。适合实体关系复杂的领域,比如法律、医疗。实现复杂度高,但效果上限也高。

多模态 RAG。文档里不只有文字,还有表格、图片。把表格转成结构化数据,图片做 OCR 或视觉嵌入,一起纳入检索范围。这块目前还在快速演进。

Agentic 工作流。让模型编排多步任务,比如“先查订单,再查物流,最后生成一封道歉邮件”。这需要模型有较强的规划和工具调用能力,目前用大参数模型效果更稳。

评测体系建设。RAG 效果好不好,不能靠感觉。建一套自动化评测流程,用固定问题集跑分,每次改动后对比。这是从“能跑”到“可靠”的关键一步。

我个人在实际操作中的体会是:AI 应用开发,工程能力比算法能力更重要。模型是现成的,但怎么把它稳定、高效、低成本地嵌进业务系统,这才是 Java 工程师的价值所在。别被那些花哨的算法名词吓住,你手里的 Spring Boot 和微服务经验,才是真正的护城河。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询