13 · Java 生态落地:LangChain4j 与 Spring AI
本章目标:把前 12 章的 Python 原型,翻译成你能直接搬进 Maven 工程的 Java 代码。概念你已经全部会了,这一章只是"换一门语言"。
本章你将学到
- LangChain4j 与 Spring AI 的选型对比,以及它们和 LangChain 的概念映射;
- 用 LangChain4j 实现"知识库 RAG + 工具调用"的完整客服助手;
- 用 Spring AI 实现 RAG 与 Function Calling 的开箱即用方案;
- 国产模型接入、流式、向量库选型的落地坑。
前置知识
13.1 为什么 Java 工程值得用 Java 生态
- 团队:全员 Java,维护成本最低;
- 基础设施:现有的配置中心、监控、部署链路都能复用;
- 数据与交易:工具调用要打内部业务接口(订单、航班),Java 服务直连最顺;
- 不需要额外部署 Python 服务:简单场景省一个服务。
边界(诚实说):复杂编排(Multi-Agent、复杂人工介入、图式控制流)在 Python/LangGraph 生态更成熟;Java 生态适合"单模型 + 工具 + RAG + 对话记忆"这类 80% 的业务场景。复杂到 Python 都吃力时,Java 也不该硬扛。
13.1.1 两个库怎么选
| 维度 | LangChain4j | Spring AI |
|---|---|---|
| 出身 | 开源社区(对应 LangChain 思想) | Spring 官方团队 |
| 集成 | 轻量、API 直观 | 与 Spring Boot / Autoconfigure 深度绑定 |
| 适合 | 想要"即插即用、少样板"的组件库 | 已经用 Spring Boot 的团队,要声明式接入 |
| 生态 | 模型/向量库适配较全 | 官方适配,跟随 Spring 版本节奏 |
| 一句话 | "Java 界的 LangChain" | "Spring 官方的 AI 支持" |
如果你团队已重度使用 Spring Boot,优先 Spring AI(注册 Bean 即用);否则 LangChain4j 更灵活。下文两个都演示,你按团队现状挑一个。
13.2 概念映射:你已经在 Python 学过了
| Python 概念(前 12 章) | LangChain4j | Spring AI |
|---|---|---|
ChatOpenAI | OpenAiChatModel | ChatModel(Bean 自动注入) |
| Prompt 模板 | PromptTemplate / 接口方法 + @UserMessage | Prompt / ChatClient.prompt() |
@tool + bind_tools | 普通方法 + @Tool,AiServices 自动装配 | @Bean + @Description + Function |
RAG:Retriever | EmbeddingStoreContentRetriever | QuestionAnswerAdvisor |
记忆:messages / Checkpointer | ChatMemory(MessageWindowChatMemory) | ChatMemory + MessageChatMemoryAdvisor |
| LCEL 链 | AiServices 接口编排 | ChatClient + Advisor |
13.3 LangChain4j:完整 RAG 示例
13.3.1 Maven 依赖
<dependencies>
<!-- LangChain4j 核心 + OpenAI 兼容适配 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>1.0.0</version>
</dependency>
<!-- 本地 Embedding 模型(离线、免费,中文够用) -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId>
<version>1.0.0</version>
</dependency>
<!-- 可选:PDF/Word 文档加载 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-pdfbox</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>版本以官方最新为准;本示例按 1.0.x 写作。
13.3.2 RAG 问答代码
package com.lantian.rag;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.DocumentSplitter;
import dev.langchain4j.data.document.DocumentSplitters;
import dev.langchain4j.data.document.parser.TextDocumentParser;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.embedding.AllMiniLmL6V2EmbeddingModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
import java.nio.file.Paths;
import java.time.Duration;
public class RagExample {
/** 客服助手接口:方法签名就是"提示词契约" */
interface FlightAssistant {
String chat(String userMessage);
}
public static void main(String[] args) {
// 1. 聊天模型:兼容国产端点(DeepSeek/Qwen/GLM,改 baseUrl 与模型名即可)
ChatLanguageModel chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.baseUrl(System.getenv("OPENAI_BASE_URL")) // 如 https://api.deepseek.com/v1
.modelName(System.getenv("MODEL_NAME", "gpt-4o-mini"))
.timeout(Duration.ofSeconds(60))
.build();
// 2. Embedding 模型(本地离线,注意:入库与检索必须用同一个)
EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel();
// 3. 加载文档 → 切分 → 向量化 → 入库(对应第 5 章的步骤)
Document document = FileSystemDocumentLoader.loadDocument(
Paths.get("data/rules.txt"), new TextDocumentParser());
DocumentSplitter splitter = DocumentSplitters.recursive(300, 50); // chunk_size, overlap
var segments = splitter.split(document);
EmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>();
store.addAll(embeddingModel.embedAll(segments).content(), segments);
// 4. 检索器(对应 Python 的 as_retriever)
ContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(store)
.embeddingModel(embeddingModel)
.maxResults(3) // 相当于 k=3
.build();
// 5. 用 AiServices 装配:模型 + 检索 + 记忆(LangChain4j 的"LCEL")
FlightAssistant assistant = AiServices.builder(FlightAssistant.class)
.chatLanguageModel(chatModel)
.contentRetriever(retriever)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.build();
// 6. 问答(模型会先检索再回答,自动拼上下文)
System.out.println(assistant.chat("航班起飞前4小时内退票手续费多少?"));
}
}FileSystemDocumentLoader= 文档加载;DocumentSplitters.recursive= 切分;EmbeddingStore= 向量库;EmbeddingStoreContentRetriever= 检索器;AiServices.builder(...)= 把模型、检索、记忆"接成一条链"——这就是 LCEL 在 Java 里的等价物;- 记忆
MessageWindowChatMemory= 短期记忆(第 11.2.1 节)。
13.4 LangChain4j:带工具的客服助手
LangChain4j 的 @Tool 更贴近 Java 习惯:普通类的方法加注解,AiServices 自动把工具 schema 交给模型并在模型请求时调用。
package com.lantian.agent;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.UserMessage;
import java.time.Duration;
import java.util.Map;
/** 工具类:方法 + @Tool 注解,就是 Python 里的 @tool */
public class FlightTools {
@Tool("查询航班实时状态")
public String getFlightStatus(@P("航班号,如 CA1501") String flightNo) {
Map<String, String> status = Map.of(
"CA1501", "已起飞,预计 11:35 抵达",
"MU5102", "延误约 40 分钟");
return status.getOrDefault(flightNo.toUpperCase(), "未查询到航班 " + flightNo);
}
@Tool("查询经济舱票价")
public String getFlightPrice(@P("航班号") String flightNo,
@P("出行日期,YYYY-MM-DD") String travelDate) {
return flightNo + " 在 " + travelDate + " 的经济舱票价约 780 元";
}
}
/** 客服助手接口:AiServices 会生成实现,自动完成"决策-调工具-回填-再回答"循环 */
interface CustomerService {
String chat(@UserMessage String userMessage);
}
public class AgentExample {
public static void main(String[] args) {
ChatLanguageModel chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.baseUrl(System.getenv("OPENAI_BASE_URL"))
.modelName(System.getenv("MODEL_NAME", "gpt-4o-mini"))
.timeout(Duration.ofSeconds(60))
.build();
CustomerService assistant = AiServices.builder(CustomerService.class)
.chatLanguageModel(chatModel)
.tools(new FlightTools()) // 注册工具
.chatMemory(MessageWindowChatMemory.withMaxMessages(20)) // 短期记忆
.build();
// 连续两轮:第二轮不用重复报航班号(记忆生效)
System.out.println(assistant.chat("帮我查 CA1501 现在的状态"));
System.out.println(assistant.chat("它的票价呢?"));
}
}要点:
@Tool+@P注解的参数描述会自动生成工具 schema(等价于 Python 的@tool函数签名);- 执行权在你手里:模型只输出"想调哪个工具、传什么参数",
AiServices会调用你的FlightTools方法——查订单/改状态这类操作的安全边界就在这些方法里; - 多轮对话由
MessageWindowChatMemory自动维护(等价于第 9 章的 Checkpointer 短期记忆)。
13.5 Spring AI:RAG 开箱即用
如果团队在用 Spring Boot,Spring AI 的 RAG 一条 Advisor 就搞定。
13.5.1 Maven 依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
<version>1.0.0</version>
</dependency>application.yml(兼容国产端点):
spring:
ai:
openai:
base-url: ${OPENAI_BASE_URL:https://api.deepseek.com/v1}
api-key: ${OPENAI_API_KEY}
chat:
options:
model: ${MODEL_NAME:deepseek-chat}
temperature: 013.5.2 RAG 配置:向量库 + Advisor
package com.lantian.aimall;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RagConfig {
/** 关键 Bean:把检索器挂到所有 ChatClient 上 —— RAG 开箱即用 */
@Bean
public QuestionAnswerAdvisor questionAnswerAdvisor(VectorStore vectorStore) {
return new QuestionAnswerAdvisor(
vectorStore,
SearchRequest.builder().topK(3).build());
}
/** 带 RAG 的 ChatClient:先检索知识库,再把结果拼进上下文回答 */
@Bean
public ChatClient ragChatClient(ChatClient.Builder builder, QuestionAnswerAdvisor advisor) {
return builder.defaultAdvisors(advisor).build();
}
}13.5.3 使用
package com.lantian.aimall;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient ragChatClient) {
this.chatClient = ragChatClient;
}
@PostMapping("/chat")
public String chat(@RequestBody String question) {
// 检索器自动生效:先查向量库,再回答(对应 Python 的 RAG chain)
return chatClient.prompt()
.user(question)
.call()
.content();
}
}知识库如何灌进去?写一个启动时初始化 Bean:把文档切分 → Embedding → 写入 VectorStore(PgVectorStore 表 / SimpleVectorStore 内存),逻辑与 13.3 相同,只是换用 Spring AI 的 Document 与 TokenTextSplitter。
13.6 Spring AI:Function Calling
package com.lantian.aimall;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Description;
import org.springframework.context.annotation.Configuration;
import java.util.function.Function;
@Configuration
public class ToolConfig {
/** 输入 POJO:模型按这个 schema 填参数 */
public record FlightQuery(String flightNo) {}
public record FlightStatus(String state) {}
/** 工具定义:@Description 会进入模型看到的 schema */
@Bean
@Description("查询航班实时状态")
public Function<FlightQuery, FlightStatus> getFlightStatus() {
return (query) -> {
// 这里调用你的内部接口/服务(安全边界)
String s = "CA1501".equalsIgnoreCase(query.flightNo()) ? "已起飞" : "延误";
return new FlightStatus(s);
};
}
}// 使用:把 FunctionCallback 注册进 ChatClient,模型即可自动调用
@Bean
public ChatClient toolChatClient(ChatClient.Builder builder,
FunctionCallback getFlightStatus) {
return builder.defaultFunctions("getFlightStatus").build();
}Spring AI 的 Function Calling 相当于 Python 的
bind_tools+ 工具注册:模型"看到"工具 schema 并决定调用,你的Function实现真正执行。参数 POJO(record)就是 schema 的来源。
13.7 落地必踩的坑
| 坑 | 表现 | 对策 |
|---|---|---|
| 国产模型端点差异 | 有的端点不支持 response_format / 部分 embedding 模型 | baseUrl 与模型名以各家官方文档为准;先写一个"冒烟测试"验证 chat / embedding 都能通 |
| Embedding 模型不一致 | 入库用 A 模型、检索用 B 模型,相似度全乱 | 全链路固定同一个 Embedding 模型(Python 第 5 章也强调过) |
| 流式 | 不流式体感太慢 | LangChain4j stream()、Spring AI .stream() 返回 Flux<String>,SSE 推给前端 |
| 向量库选型 | 上了分布式向量库运维成本高 | Java 团队优先 pgvector(直接用现有 PG);数据量 > 千万级再考虑 Milvus/ES |
| 会话状态 | 多副本下记忆串了 | thread_id/会话与存储绑定(第 12.5.3 节);Spring AI 用 ChatMemory + 会话维度隔离 |
| 并发 | 大量并发导致 LLM 限流 | 给 LLM 调用做限流/重试(第 12.4 节),Spring AI 可配 RetryTemplate |
13.8 选型建议与结论
- 概念已经 100% 通用:你在 Python 学的 RAG 切分、检索、工具循环、记忆、评估,在 Java 里一一对应,只是 API 名字不同;
- 简单场景(单模型 + RAG + 工具 + 记忆):用 Spring AI(已在 Spring Boot)或 LangChain4j,一个服务搞定;
- 复杂编排(Multi-Agent、人工审批图、复杂控制流):仍是 Python/LangGraph 更成熟,Java 侧通过 HTTP 调 Agent 服务(第 12.5.1 节方案 A);
- 混排也常见:检索在 Java 服务(贴近数据),复杂 Agent 在 Python 服务(贴近模型),中间用消息队列/HTTP 连接。
一句话:Python 教会你原理,Java 帮你上线。两手都会,你才是团队里那个"能落地 AI 的 Java 工程师"。
练习
- 把 13.3 的 RAG 示例改成读你团队的一份真实文档(如《业务流程与数据流转说明.md》),跑通并问一个业务问题。
- 给 13.4 的
FlightTools加一个calcFee工具(算退票手续费),让模型完成"查价格→算手续费"的连续工具调用。 - 把 13.5 的
QuestionAnswerAdvisor的topK改成 1 和 5,各问一次,对比回答完整性。 - 调研你们现有数据库:如果上 pgvector,哪个库、哪张表最合适存知识库向量?
参考答案
<details>
<summary>练习1(用真实文档跑通 RAG)</summary>
把 13.3 的 data/rules.txt 换成团队真实文档,三步走:
- 拷文档:把《业务流程与数据流转说明.md》复制到
data/下(编码务必 UTF-8,GBK 会被解析成乱码); - 换加载器:Markdown/纯文本用
TextDocumentParser即可;若是 PDF 换ApachePdfBoxDocumentParser(依赖已加),Word 需另加 docx 解析器(langchain4j-document-parser-apache-poi); - 问业务问题:
assistant.chat("订单数据从哪个系统流转到哪个系统?"),验证:回答是否基于文档(而非模型臆造)、是否带检索痕迹(切分不合理时答非所问)。
常见问题排查:切分粒度不对(.recursive(300, 50) 对表格密集的文档要把 chunk 调小或按段落切);Encoding 乱码(Paths.get 读文件时确认 UTF-8);Embedding 模型与检索不一致(统一用同一个 AllMiniLmL6V2EmbeddingModel)。
</details>
<details>
<summary>练习2(FlightTools 加 calcFee,连续工具调用)</summary>
在 FlightTools 里加工具:
@Tool("计算退票手续费")
public String calcFee(@P("票价(元)") double fare,
@P("手续费比例,0~1,如 0.4 表示 40%") double ratio) {
double fee = fare * ratio;
return String.format("退票手续费 %.0f 元(票价 %.0f × %.0f%%)", fee, fare, ratio * 100);
}问"CA1501 票价 780,起飞前 4 小时退票手续费多少"时,AiServices 生成的实现会自动完成连续调用:
- 先调
getFlightPrice("CA1501", "...")(若票价没给,模型会先查); - 再调
calcFee(780, 0.4); - 汇总两轮结果输出最终答案。
验证点:连续调用是自动完成的(你不用写循环,AiServices 内部就是这个循环——对照第 2 章手写版);两轮之间对话记忆自动保持。若模型算出的 ratio 不对,说明比例不该由模型"猜"——把分档比例表做成 getRefundPolicy(flightNo) 工具返回,让模型查了再算(与第 8 章练习 1 同理)。
</details>
<details>
<summary>练习3(topK 1 vs 5 对比)</summary>
把 13.5.2 的 SearchRequest.builder().topK(3) 改成 1 和 5,各问同一批问题,对比:
| topK | 现象 |
|---|---|
| 1 | 回答可能不完整:只命中一块,政策条目的"例外条款/分档表"常被切在不同块,答一半;偶尔答不出来 |
| 3(基准) | 完整度、噪音平衡 |
| 5 | 回答更全,但无关内容增多,模型可能被带偏(引用到不相干段落),token 也涨 |
观察重点:不是"哪个分高用哪个",而是"回答完整率"——问 10 个问题,数"答对/答全/答偏"。结论通常落在 topK=3 附近(文档块小、规则独立时 2-3 足够;长段落合并成块的文档可能需要 5)。配合 Python 第 5 章练习 2,两边的调参逻辑完全一致。
</details>
<details>
<summary>练习4(pgvector 落库调研)</summary>
调研四件事,对着你的数据库逐项回答:
- 哪个库:知识库是"读多写少、量可控"的数据,优先放已有 PG 实例(不用新引存储);若现有 PG 太忙/版本低于 13(pgvector 要求 PG >= 13),单独建一个轻量 PG 实例;
哪张表:新建专用表
knowledge_chunks,不要复用业务表——字段设计参考:CREATE TABLE knowledge_chunks ( id bigserial PRIMARY KEY, doc_id text NOT NULL, -- 稳定文档标识(增量更新用) chunk_seq int NOT NULL, -- 块序号 tenant_id text, -- 权限过滤用(第 6 章) content text, metadata jsonb, -- source/category/updated_at embedding vector(768), -- 维度必须与你的 Embedding 模型一致 created_at timestamptz DEFAULT now() ); CREATE INDEX ON knowledge_chunks USING hnsw (embedding vector_cosine_ops);- 索引类型:HNSW(快、占用内存)vs IVFFlat(构建快、精度略降),数据 < 百万级 HNSW 无脑选;
- 运维成本:pgvector 只是 PG 的扩展(
CREATE EXTENSION vector),不需要额外部署组件——这就是 Java 团队推荐它第一的原因。
红线:embedding 维度要和你用的 Embedding 模型对齐(
AllMiniLmL6V2是 384 维、bge-m3 是 1024 维),建表前先确认,维度不匹配插入就报错。
</details>

0 条评论