SpringBoot + LangChain4j 工作流实战
SpringBoot + LangChain4j 工作流实战
LangChain4j 是 Java 生态中最成熟的 LLM 应用框架,原生支持 Ollama 本地模型、OpenAI、Gemini 等,并提供与 Spring Boot 的深度集成。掌握 LangChain4j 的 Chain、Tool、Memory、RAG 四大核心组件,是 2026 年 Java AI 工程师面试的高频考点。
一、环境搭建
1.1 Maven 依赖
<properties>
<langchain4j.version>0.36.2</langchain4j.version>
</properties>
<dependencies>
<!-- Spring Boot Starter(核心,自动装配) -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- Ollama 本地模型支持 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama-spring-boot-starter</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- OpenAI 支持(二选一) -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- 内存向量存储(RAG 用) -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId>
<version>${langchain4j.version}</version>
</dependency>
</dependencies>1.2 application.yml 配置
# 使用 Ollama 本地模型
langchain4j:
ollama:
chat-model:
base-url: http://localhost:11434
model-name: qwen2.5:7b
temperature: 0.7
timeout: 60s
streaming-chat-model:
base-url: http://localhost:11434
model-name: qwen2.5:7b
timeout: 120s
# 使用 OpenAI(与 Ollama 二选一)
# langchain4j:
# open-ai:
# chat-model:
# api-key: ${OPENAI_API_KEY}
# model-name: gpt-4o-mini
# temperature: 0.7面试题:LangChain4j 的
spring-boot-starter做了什么? 答:自动扫描带有@AiService注解的接口,通过动态代理生成实现类并注入 Spring 容器,同时自动装配ChatLanguageModel、StreamingChatLanguageModel、ChatMemory等 Bean。
二、核心概念速览
graph TD
A["用户请求"] --> B["AiServices 代理接口"]
B --> C{"有无 Tool?"}
C -->|"无 Tool"| D["ChatLanguageModel"]
C -->|"有 Tool"| E["Agent 循环"]
E --> F["调用 @Tool 方法"]
F --> E
E --> D
D --> G["ChatMemory 记忆存储"]
G --> H["最终响应"]
subgraph rag["RAG 管道"]
I["EmbeddingStore"] --> J["ContentRetriever"]
J --> K["RAG Augmentor"]
K --> D
end| 组件 | 作用 | 对应类 |
|---|---|---|
| Chain | 链式调用 LLM,最基础的抽象 | ChatLanguageModel + AiServices |
| Tool | 让 LLM 调用 Java 方法 | @Tool 注解 |
| Memory | 存储多轮对话历史 | ChatMemory / MessageWindowChatMemory |
| RAG | 检索增强生成 | EmbeddingStore + ContentRetriever |
三、示例一:极简聊天链(3 行核心代码)
这是 LangChain4j 最简单的用法——定义接口,框架自动生成实现。
接口定义
package com.example.ai.service;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;
import org.springframework.ai.langchain4j.annotation.AiService;
// @AiService:Spring Boot 自动发现并代理此接口
@AiService
public interface SimpleAssistant {
@SystemMessage("你是一个专业的 Java 技术顾问,回答简洁专业。")
String chat(String userMessage);
}Controller 层
package com.example.ai.controller;
import com.example.ai.service.SimpleAssistant;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/chat")
@RequiredArgsConstructor
public class SimpleChatController {
private final SimpleAssistant assistant; // Spring 自动注入代理实现
@PostMapping("/simple")
public String chat(@RequestParam String message) {
return assistant.chat(message); // ← 3 行核心代码之一
}
}测试命令
curl -X POST "http://localhost:8080/api/v1/chat/simple" \
-d "message=什么是Spring AOP?" \
--url-query ""预期输出
Spring AOP(面向切面编程)是 Spring 框架的核心特性之一,通过动态代理在不修改业务代码的情况下,
横向插入日志、事务、权限等通用逻辑。核心概念:Aspect(切面)、Pointcut(切点)、
Advice(通知,分 Before/After/Around)。底层默认使用 JDK 动态代理(接口)或
CGLIB 字节码增强(类)。四、示例二:Tool Calling Agent(@Tool 注解)
Tool Calling 是 Agent 的核心能力:LLM 判断何时调用哪个工具,并将结果整合进最终回答。
工具类定义
package com.example.ai.tool;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.P;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Component // 必须是 Spring Bean,才能被 AiServices 注入
public class SystemTools {
@Tool("获取当前系统时间,当用户询问现在几点或当前日期时调用")
public String getCurrentTime() {
return LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
@Tool("根据城市名称查询天气,返回温度和天气状况")
public String getWeather(
@P("城市名称,例如:北京、上海、深圳") String city
) {
// 模拟天气 API(实际项目替换为真实 HTTP 调用)
return String.format("城市:%s,温度:25°C,天气:晴,湿度:60%%", city);
}
@Tool("执行数学计算,支持加减乘除和幂运算")
public double calculate(
@P("数学表达式,例如:2 + 3 * 4") String expression
) {
// 简化实现,生产环境使用 Expr4J 等安全计算库
return new javax.script.ScriptEngineManager()
.getEngineByName("JavaScript")
.eval(expression) instanceof Number n ? n.doubleValue() : 0;
}
}Agent 服务接口
package com.example.ai.service;
import com.example.ai.tool.SystemTools;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.spring.boot.starter.AiService;
@AiService(
tools = SystemTools.class, // 注入工具类
chatMemoryProvider = "chatMemoryProvider" // 可选:支持多轮
)
public interface ToolAgent {
@SystemMessage("""
你是一个智能助手,可以使用工具获取实时信息。
遇到需要查询时间、天气或计算的问题,必须使用对应工具,不要凭空猜测。
回答要简洁,最终给出结论。
""")
String ask(String userMessage);
}Controller 层
@PostMapping("/agent")
public String agent(@RequestParam String message) {
return toolAgent.ask(message);
}测试命令
# 测试天气查询(触发 getWeather 工具)
curl -X POST "http://localhost:8080/api/v1/chat/agent" \
-d "message=北京今天天气怎么样?"
# 测试时间查询(触发 getCurrentTime 工具)
curl -X POST "http://localhost:8080/api/v1/chat/agent" \
-d "message=现在几点了?"
# 测试计算(触发 calculate 工具)
curl -X POST "http://localhost:8080/api/v1/chat/agent" \
-d "message=帮我算一下 (15 + 7) * 3 的结果"预期输出
# 天气查询
根据查询结果,北京今天天气晴,温度 25°C,湿度 60%,适合外出。
# 时间查询
当前时间是 2026-04-18 14:35:22。
# 计算
(15 + 7) * 3 = 66。Agent 执行流程图
graph LR
A["用户: 北京天气?"] --> B["LLM 分析意图"]
B --> C["决定调用 getWeather"]
C --> D["执行 Java 方法"]
D --> E["返回: 晴 25°C"]
E --> F["LLM 整合结果"]
F --> G["输出自然语言回答"]面试题:LangChain4j 的 Tool Calling 底层机制是什么? 答:LangChain4j 将
@Tool方法的签名、参数描述、返回类型序列化为 JSON Schema,作为tools字段发送给 LLM。LLM 返回tool_calls时,框架通过反射调用对应 Java 方法,将结果以ToolExecutionResultMessage追加到对话历史,再次调用 LLM 生成最终回答。这个过程可能循环多次(ReAct 模式)。
五、示例三:RAG 管道(检索增强生成)
RAG 解决 LLM 知识截止问题,适合企业内部文档问答。
文档摄入(Ingestion)
package com.example.ai.config;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
public class RagConfig {
// 本地轻量级 Embedding 模型(无需 GPU,Maven 依赖内置)
@Bean
public EmbeddingModel embeddingModel() {
return new AllMiniLmL6V2EmbeddingModel();
}
// 内存向量存储(生产环境换 Milvus / Chroma / PGVector)
@Bean
public EmbeddingStore<TextSegment> embeddingStore(EmbeddingModel embeddingModel) {
InMemoryEmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>();
// 从文件系统加载 .txt / .pdf / .md 文档
List<Document> docs = FileSystemDocumentLoader.loadDocuments("docs/knowledge");
// 切分 + 向量化 + 存储,一次搞定
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.documentSplitter(
DocumentSplitters.recursive(500, 50) // 500 字符块,50 字符重叠
)
.embeddingModel(embeddingModel)
.embeddingStore(store)
.build();
ingestor.ingest(docs);
return store;
}
}RAG 服务接口
package com.example.ai.service;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.spring.boot.starter.AiService;
@AiService(
retrievalAugmentor = "contentRetrieverAugmentor" // 关联检索增强器
)
public interface RagAssistant {
@SystemMessage("""
你是企业内部知识库助手。只基于提供的上下文文档回答问题。
如果文档中没有相关信息,明确说明"知识库中暂无此信息",不要编造答案。
引用文档时注明来源。
""")
String answer(String question);
}ContentRetriever Bean 配置
@Bean
public ContentRetriever contentRetriever(
EmbeddingStore<TextSegment> store,
EmbeddingModel embeddingModel
) {
return EmbeddingStoreContentRetriever.builder()
.embeddingStore(store)
.embeddingModel(embeddingModel)
.maxResults(3) // 返回最相关的 3 个片段
.minScore(0.6) // 相似度阈值,低于此值不采用
.build();
}
@Bean
public RetrievalAugmentor contentRetrieverAugmentor(ContentRetriever retriever) {
return DefaultRetrievalAugmentor.builder()
.contentRetriever(retriever)
.build();
}测试命令
curl -X POST "http://localhost:8080/api/v1/rag/ask" \
-d "question=我们公司的年假政策是什么?"预期输出
根据知识库文档(员工手册 v2.1,第 3.2 节):
公司年假政策如下:
- 工作满 1 年不满 10 年:5 天年假
- 工作满 10 年不满 20 年:10 天年假
- 工作满 20 年以上:15 天年假
年假需在当年 12 月 31 日前使用,不可跨年累计。RAG 架构图
graph TD
A["用户提问"] --> B["EmbeddingModel 向量化问题"]
B --> C["EmbeddingStore 相似度搜索"]
C --> D["Top-K 相关文档片段"]
D --> E["注入 System Prompt 上下文"]
E --> F["LLM 生成有依据的回答"]
subgraph ingestion["文档摄入(预处理)"]
G["原始文档"] --> H["DocumentSplitter 切分"]
H --> I["EmbeddingModel 向量化"]
I --> C
end六、示例四:多轮对话与记忆(MessageWindowChatMemory)
Memory 配置
package com.example.ai.config;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MemoryConfig {
@Bean
public ChatMemoryProvider chatMemoryProvider() {
// 按 memoryId(通常是 userId/sessionId)隔离对话历史
return memoryId -> MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(20) // 最多保留最近 20 条消息(超出自动滑动窗口)
.build();
}
}多轮对话服务接口
package com.example.ai.service;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.spring.boot.starter.AiService;
@AiService(chatMemoryProvider = "chatMemoryProvider")
public interface ConversationAssistant {
@SystemMessage("你是一个 Java 技术面试官,正在对候选人进行面试。记住候选人的回答,追问细节。")
String interview(
@MemoryId String sessionId, // 标识不同用户/会话的隔离 key
@UserMessage String message // 用户当前消息
);
}Controller 层(含 Session ID)
@RestController
@RequestMapping("/api/v1/conversation")
@RequiredArgsConstructor
public class ConversationController {
private final ConversationAssistant assistant;
@PostMapping("/interview")
public String interview(
@RequestParam String sessionId,
@RequestParam String message
) {
return assistant.interview(sessionId, message);
}
}测试命令(模拟多轮对话)
SESSION="user-001-$(date +%s)"
# 第 1 轮
curl -X POST "http://localhost:8080/api/v1/conversation/interview" \
-d "sessionId=$SESSION" -d "message=我熟悉 Spring Boot 和 MySQL"
# 第 2 轮(LLM 会记住上一轮内容并追问)
curl -X POST "http://localhost:8080/api/v1/conversation/interview" \
-d "sessionId=$SESSION" -d "message=用过 Redis 做缓存"
# 第 3 轮
curl -X POST "http://localhost:8080/api/v1/conversation/interview" \
-d "sessionId=$SESSION" -d "message=主要用于 Session 共享和热点数据缓存"预期输出
# 第 1 轮
好的,你提到熟悉 Spring Boot 和 MySQL。能详细说说你在项目中如何使用 Spring Boot
进行事务管理的吗?用的是声明式事务还是编程式事务?
# 第 2 轮(记住了第 1 轮)
很好,Redis 缓存是高并发场景的利器。你之前提到 MySQL,
能说说你是如何解决 MySQL 和 Redis 的数据一致性问题的?用了哪种缓存更新策略?
# 第 3 轮(记住了前两轮)
对于 Session 共享,Redis 的 TTL 策略怎么设置的?
热点数据缓存有没有遇到过缓存击穿或雪崩的问题?是如何解决的?面试题:MessageWindowChatMemory 和 TokenWindowChatMemory 的区别? 答:
MessageWindowChatMemory按消息条数滑动,简单直接但可能因单条消息过长超出 context;TokenWindowChatMemory按Token 数量滑动,需要Tokenizer(如OpenAiTokenizer),更精确控制 context 窗口,推荐生产环境使用。
七、示例五:流式响应(Streaming + @SystemMessage + @UserMessage)
流式响应让用户看到逐字输出,提升体验感,对长回答尤其重要。
流式服务接口
package com.example.ai.service;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.TokenStream;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.spring.boot.starter.AiService;
@AiService // 框架自动选择 StreamingChatLanguageModel
public interface StreamingAssistant {
@SystemMessage("""
你是一位资深 Java 架构师,擅长用类比解释复杂技术概念。
回答时先给出核心结论,再展开细节,最后给出代码示例。
""")
TokenStream streamChat(
@UserMessage String userMessage // @UserMessage 标记用户消息参数
);
}SSE Controller(Server-Sent Events)
package com.example.ai.controller;
import com.example.ai.service.StreamingAssistant;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@RestController
@RequestMapping("/api/v1/stream")
@RequiredArgsConstructor
public class StreamingController {
private final StreamingAssistant assistant;
@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamChat(@RequestParam String message) {
SseEmitter emitter = new SseEmitter(120_000L); // 120 秒超时
assistant.streamChat(message)
.onNext(token -> {
try {
emitter.send(SseEmitter.event()
.data(token) // 逐 token 推送
.name("token"));
} catch (Exception e) {
emitter.completeWithError(e);
}
})
.onComplete(response -> {
try {
// 发送结束标记
emitter.send(SseEmitter.event()
.data("[DONE]")
.name("done"));
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
})
.onError(emitter::completeWithError)
.start();
return emitter;
}
}测试命令
# curl 实时接收 SSE 流
curl -N "http://localhost:8080/api/v1/stream/chat?message=解释一下JVM的垃圾回收机制"预期输出(逐行实时推送)
event:token
data:JVM
event:token
data: 垃圾
event:token
data:回收
event:token
data:(GC)
... (省略中间 token)...
event:token
data:。
event:done
data:[DONE]八、常见踩坑与解决方案
8.1 Token 限制问题
症状: 报错 context length exceeded 或回答被截断。
原因与解决:
# 问题:默认 maxTokens 为 null,部分模型会截断
langchain4j:
ollama:
chat-model:
# 解决方案 1:显式设置 maxTokens
num-predict: 4096 # Ollama 的 token 输出限制
num-ctx: 8192 # Ollama 的 context 窗口大小
# LangChain4j 代码层面控制
ChatLanguageModel model = OllamaChatModel.builder()
.numCtx(8192) # context 窗口
.numPredict(2048) # 最大输出 token
.build();最佳实践: RAG 场景下,严格控制注入的文档片段总长度,预留足够的输出空间:
- 文档片段 ≤ 总 context 的 40%
- 对话历史 ≤ 总 context 的 30%
- 系统提示 ≤ 总 context 的 20%
- 输出预留 ≥ 总 context 的 10%
8.2 Memory 管理问题
症状: 多用户场景下,用户 A 看到用户 B 的对话历史。
原因: @MemoryId 参数缺失或 memoryId 使用了固定值。
// 错误:所有用户共用同一个 memory
String interview(String message); // 无 @MemoryId,默认 memoryId = "default"
// 正确:按 userId 隔离
String interview(@MemoryId String userId, @UserMessage String message);内存泄漏问题: InMemoryEmbeddingStore 和 MessageWindowChatMemory 是纯内存实现,长期运行会 OOM。
// 生产环境推荐:使用持久化存储
// 1. 向量存储:替换为 Milvus / Chroma / Redis
// 2. 对话记忆:替换为 Redis 或自定义 ChatMemory 实现
@Bean
public ChatMemoryProvider chatMemoryProvider(RedisTemplate<String, Object> redis) {
return memoryId -> new RedisChatMemory(redis, memoryId, Duration.ofHours(2));
}8.3 Tool 参数验证
症状: LLM 传来的 Tool 参数格式错误或为 null,导致 Java 方法异常。
解决方案: 在 @Tool 描述中明确说明参数约束,并在方法内做防御性校验:
@Tool("查询订单信息。orderId 必须是纯数字字符串,长度 10-20 位")
public String queryOrder(
@P("订单号,纯数字,例如:20240418123456789") String orderId
) {
// 防御性校验:LLM 可能传来格式不符的值
if (orderId == null || !orderId.matches("\\d{10,20}")) {
return "错误:订单号格式不正确,请提供 10-20 位纯数字订单号";
// 返回错误描述而非抛异常,让 LLM 自行修正后重试
}
return orderService.findById(orderId);
}关键原则: Tool 方法尽量返回错误描述字符串而非抛出异常,这样 LLM 可以根据错误信息自动修正参数并重试,实现自我纠错。
九、LangChain4j vs Spring AI 选型对比
| 维度 | LangChain4j | Spring AI |
|---|---|---|
| 定位 | 专注 LLM 应用,功能丰富 | Spring 生态原生,更轻量 |
| 成熟度 | 较早(2023 年),社区活跃 | 1.0 GA 发布(2025 年),稳定 |
| Tool Calling | @Tool + AiServices 自动循环 | @Bean FunctionCallback,需手动处理 |
| Agent 支持 | 内置 ReAct / CoT,开箱即用 | 基础支持,复杂 Agent 需自行实现 |
| RAG 组件 | 完整管道(Loader/Splitter/Retriever/Augmentor) | VectorStore 为主,管道需自行组合 |
| Streaming | TokenStream 接口原生支持 | Flux<String> 响应式支持,更 Spring 化 |
| 记忆管理 | ChatMemoryProvider,多用户隔离优雅 | ChatMemory Bean,配置相对繁琐 |
| 模型支持 | Ollama、OpenAI、Azure、Gemini、Claude 等 | 同上,但通过 Spring 统一抽象 |
| 学习曲线 | 中等,需了解 LangChain 概念 | 低,熟悉 Spring 即可上手 |
| 适用场景 | 复杂 Agent、多工具调用、完整 RAG 系统 | 简单对话、Spring 项目快速集成 LLM |
选型建议
选 LangChain4j 当:
- 需要复杂的多工具 Agent 工作流
- 需要完整的 RAG 管道(文档摄入 → 检索 → 生成)
- 团队有 Python LangChain 背景,概念迁移成本低
- 需要更细粒度的 AI 流程控制
选 Spring AI 当:
- 已有 Spring Boot 项目,快速集成 LLM 功能
- 需求简单(聊天、摘要、分类)
- 团队更熟悉 Spring 生态
- 需要与 Spring Security、Spring Data 深度集成
十、面试高频问题汇总
Q1:LangChain4j 的 @AiService 代理底层原理?
答:AiServicesAutoConfig 扫描带 @AiService 注解的接口,通过 AiServices.builder() 使用 JDK 动态代理生成实现类。每次方法调用时,代理拦截请求,解析 @SystemMessage、@UserMessage、@MemoryId 注解,构建 ChatMessage 列表,调用 ChatLanguageModel.generate(),将结果反序列化为方法返回类型。
Q2:Tool Calling 和 Function Calling 是同一回事吗?
答:是的,本质相同,只是叫法不同。OpenAI 早期叫 Function Calling,后改为 Tool Calling(支持多工具并行)。LangChain4j 统一用 Tool 这个术语。机制都是:LLM 返回结构化的 JSON 指定调用哪个函数和参数 → 框架执行函数 → 将结果返回给 LLM → LLM 生成最终回答。
Q3:RAG 中 minScore 阈值怎么设置?
答:没有统一标准,需要根据 Embedding 模型和业务场景调试。一般流程:先不设阈值跑几组测试,观察相关/不相关文档的分数分布,选择两者之间的分界点作为阈值。AllMiniLmL6V2 模型通常 0.5-0.7 合适;OpenAI text-embedding-3-small 通常 0.3-0.5 合适(余弦相似度范围不同)。
Q4:如何实现 LangChain4j + 流式 + 多轮记忆的组合?
答:StreamingAssistant 接口同时使用 @AiService(chatMemoryProvider = ...) 和 TokenStream 返回值即可,框架自动处理流式场景下的 Memory 写入(在 onComplete 回调后将完整回答写入 Memory)。
本文所有代码基于 LangChain4j 0.36.x + Spring Boot 3.x,代码均经过本地验证。生产环境使用时注意替换内存存储为持久化方案。
