Spring Boot + LangChain4j 实战:从 0 到 1 打造能调工具、查文档的 AI Agent
🧠 读完本文你将收获:理解 Agent 的核心原理(Function Calling + RAG),掌握 LangChain4j AiServices 的实战用法,跑通一个能自主决策"该调哪个工具、该查哪份文档"的智能体。
一、前言:为什么你需要 Agent?
前面两天,我们分别实现了:
- Day 1:对接硅基流动大模型,跑通 Spring Boot + LangChain4j
- Day 2:多轮对话记忆 + SSE 流式输出
但这里的 AI 只是一个"问答机器"------你问什么,它凭训练数据回答。它不知道今天的天气,查不到你的订单,不认识你公司的内部文档。
Agent 要解决的,就是这个 gap。
普通 ChatModel: Agent:
用户 → AI → 回答 用户 → AI 分析意图
↓
该调工具? 该查文档?
↓ ↓
真实执行方法 真实搜向量库
↓ ↓
拿到结果 ←──────┘
↓
AI 综合 → 回答
今天我们就用 Spring Boot 4.1.0 + LangChain4j 0.36.2 + 硅基流动(DeepSeek-V3)来实现这个 Agent。
完整代码已开源:https://gitee.com/jackXUYY/java-ai-learn/tree/master/day3
二、环境准备
2.1 技术栈
| 组件 | 版本/选型 | 作用 |
|---|---|---|
| Spring Boot | 4.1.0 | 应用框架 |
| Java | 17 | LTS 版本 |
| LangChain4j | 0.36.2 | AI 编排框架 |
| 对话模型 | deepseek-ai/DeepSeek-V3 |
硅基流动免费,支持 Function Calling |
| 嵌入模型 | BAAI/bge-large-zh-v1.5 |
中文嵌入模型,1024 维向量 |
| 向量库 | InMemoryEmbeddingStore |
内存版(开发演示用) |
| 配置加密 | Jasypt 3.0.5 | API Key 加密 |
2.2 为什么选 DeepSeek-V3?
不是所有模型都支持 Function Calling!我们先用 Qwen/Qwen2.5-7B-Instruct 测试,发现它遇到 tool_call 时会直接把指令当作文本输出,而不是返回结构化的工具调用指令。
⚠️ 踩坑记录:Qwen2.5-7B 不支持 Function Calling → 切到 DeepSeek-V3 后一切正常。
判断一个模型是否支持,看文档里有没有 "tool use" 或 "function calling" 字样。最直接的方法:跑起来试。
2.3 Maven 依赖
xml
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- LangChain4j 核心 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>0.36.2</version>
</dependency>
<!-- LangChain4j Spring Boot Starter -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>0.36.2</version>
</dependency>
<!-- LangChain4j OpenAI 集成(硅基流动兼容) -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>0.36.2</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>0.36.2</version>
</dependency>
<!-- 嵌入模型 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-embeddings</artifactId>
<version>0.36.2</version>
</dependency>
<!-- Jasypt 配置加密 -->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
</dependencies>
三、项目架构
先上一张全景图,帮你理解各个组件怎么协作:
┌──────────────────────────────────────────────────┐
│ AgentController │
│ GET /agent/chat?message=... │
└────────────────────┬─────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────┐
│ Agent (AiServices 动态代理) │
│ │
│ ┌─────────────┐ ┌──────────┐ ┌─────────────┐ │
│ │ DeepSeek-V3 │ │ 工具集 │ │ 知识库(RAG) │ │
│ │ (大脑) │ │ │ │ │ │
│ └─────────────┘ └──────────┘ └─────────────┘ │
│ │ │ │
│ ┌─────┴──────┐ ContentRetriever│
│ │ WeatherTool│ (自动检索注入) │
│ │ SearchTool │ │ │
│ └────────────┘ EmbeddingStore │
│ (内存向量库) │
│ ┌─────────────────────────────┐ │ │
│ │ ChatMemory (滑动窗口20条) │ RagService │
│ │ 按 userId 隔离多用户记忆 │ (启动时加载文档) │
│ └─────────────────────────────┘ │
└──────────────────────────────────────────────────┘
- Agent 只是一个 interface,不需要你写实现类------LangChain4j 用动态代理在运行时生成
- AiServices 自动编排"思考→调工具→拿结果→再思考→回答"的完整循环
- ContentRetriever 让 Agent 每次对话自动检索知识库并注入上下文
项目结构
day3/
├── pom.xml
└── src/main/
├── java/com/day3/demo/
│ ├── Day3Application.java # 启动类
│ ├── config/
│ │ ├── ChatModelConfig.java # 模型 Bean(对话/流式/嵌入/向量库)
│ │ ├── LangChain4jConfig.java # 全局记忆配置
│ │ └── AgentConfig.java # ★ Agent 装配(核心)
│ ├── controller/
│ │ └── AgentController.java # REST 入口
│ ├── service/
│ │ └── RagService.java # 知识库加载
│ └── tool/
│ ├── WeatherTool.java # 工具:查天气 + 四则运算
│ └── SearchTool.java # 工具:查时间 + 查订单
└── resources/
├── application.yml # 公开配置
└── docs/
└── 码哥科技.txt # 知识库文档
四、核心实现
4.1 模型配置 --- ChatModelConfig
这是最基础的 Bean 工厂,负责创建大模型相关的 4 个 Bean:
java
@Configuration
public class ChatModelConfig {
@Value("${langchain4j.open-ai.api-key}")
private String apiKey;
@Value("${langchain4j.open-ai.base-url}")
private String baseUrl;
@Value("${langchain4j.open-ai.model-name:deepseek-ai/DeepSeek-V3}")
private String modelName;
@Value("${langchain4j.open-ai.embedding-model-name:BAAI/bge-large-zh-v1.5}")
private String embeddingModelName;
// 1. 普通对话模型(Agent 用)
@Bean
public ChatLanguageModel chatLanguageModel() {
return OpenAiChatModel.builder()
.apiKey(apiKey)
.baseUrl(baseUrl)
.modelName(modelName)
.timeout(Duration.ofSeconds(60))
.build();
}
// 2. 流式对话模型(SSE 用)
@Bean
public StreamingChatLanguageModel streamingChatLanguageModel() {
return OpenAiStreamingChatModel.builder()
.apiKey(apiKey)
.baseUrl(baseUrl)
.modelName(modelName)
.timeout(Duration.ofSeconds(120))
.build();
}
// 3. 嵌入模型(文本 → 向量)
@Bean
public EmbeddingModel embeddingModel() {
return OpenAiEmbeddingModel.builder()
.apiKey(apiKey)
.baseUrl(baseUrl)
.modelName(embeddingModelName)
.timeout(Duration.ofSeconds(60))
.build();
}
// 4. 向量存储(内存版,开发用)
@Bean
public EmbeddingStore<TextSegment> embeddingStore() {
return new InMemoryEmbeddingStore<>();
}
}
💡 嵌入模型 ≠ 对话模型。嵌入模型只负责"理解语义 → 转成数字向量",不负责回答。它通常是更小、更专用的模型。
4.2 工具类 --- 给 AI "手"
用 @Tool 注解标记方法,大模型就能"看到"并调用它们:
java
@Slf4j
@Component
public class WeatherTool {
@Tool("查询指定城市的天气")
public String getWeather(String city) {
log.info(">>> Agent 调用了 getWeather: {}", city);
Map<String, String> mockWeather = Map.of(
"北京", "晴,25°C,湿度 40%,北风 3 级",
"上海", "多云,28°C,湿度 65%,东南风 2 级",
"深圳", "阵雨,30°C,湿度 80%,西南风 4 级",
"杭州", "多云转晴,26°C,湿度 55%",
"成都", "阴,22°C,湿度 70%"
);
return mockWeather.getOrDefault(city, "暂无" + city + "的天气数据");
}
@Tool("数学四则运算")
public double calculate(
@P("第一个操作数") double a,
@P("第二个操作数") double b,
@P("运算类型:add/subtract/multiply/divide") String operation) {
log.info(">>> Agent 调用了 calculate: {} {} {}", a, operation, b);
return switch (operation) {
case "add" -> a + b;
case "subtract" -> a - b;
case "multiply" -> a * b;
case "divide" -> a / b;
default -> throw new IllegalArgumentException("不支持的运算: " + operation);
};
}
}
java
@Slf4j
@Component
public class SearchTool {
@Tool("获取当前日期和时间")
public String getCurrentDateTime() {
log.info(">>> Agent 调用了 getCurrentDateTime");
return LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
@Tool("根据订单号查询订单状态(模拟)")
public String queryOrder(@P("订单编号") String orderId) {
log.info(">>> Agent 调用了 queryOrder: {}", orderId);
return "订单 " + orderId + ":已发货,预计 "
+ LocalDateTime.now().plusDays(2)
.format(DateTimeFormatter.ofPattern("MM-dd")) + " 到达";
}
}
关键机制:
框架会把 @Tool 的 description 和参数信息发给大模型:
"你有以下工具可用:
- getWeather(city) --- 查询指定城市的天气
- calculate(a, b, operation) --- 数学四则运算
- ..."
大模型根据这些描述自主判断该调哪个、传什么参数。大模型不执行工具------它只是"说"要调什么,框架真正执行并把结果回传。
4.3 RAG 知识库 --- 给 AI "参考书"
java
@Slf4j
@Service
@RequiredArgsConstructor
public class RagService {
private final EmbeddingModel embeddingModel;
private final EmbeddingStore<TextSegment> embeddingStore;
private final ResourcePatternResolver resourceResolver;
/**
* 启动时自动加载 docs/ 下的所有 .txt
*/
@PostConstruct
public void initDocuments() {
try {
Resource[] resources = resourceResolver.getResources("classpath*:docs/*.txt");
if (resources.length == 0) {
log.warn("⚠️ docs/ 目录下没有 txt 文件");
return;
}
int totalChunks = 0;
for (Resource resource : resources) {
String content = resource.getContentAsString(StandardCharsets.UTF_8);
List<TextSegment> chunks = splitIntoChunks(resource.getFilename(), content);
embedAndStore(chunks);
totalChunks += chunks.size();
log.info("✅ 已加载: {}, 切片: {}", resource.getFilename(), chunks.size());
}
log.info("🎉 RAG 知识库就绪,共 {} 个文档、{} 个切片",
resources.length, totalChunks);
} catch (Exception e) {
log.error("❌ 知识库初始化失败", e);
// 不抛异常:知识库加载失败不影响应用正常运行
}
}
/**
* 文档切片:按 ## 标题切,无标题则按段落切
*/
private List<TextSegment> splitIntoChunks(String filename, String content) {
String[] parts = content.split("(?=\\n## )");
if (parts.length <= 1) {
parts = content.split("\\n\\n");
}
return Arrays.stream(parts)
.map(String::trim)
.filter(s -> s.length() > 20) // 过滤太短的
.map(s -> s.length() > 2000
? s.substring(0, 2000) : s) // 太长截断
.map(chunk -> TextSegment.from("【" + filename + "】\n" + chunk))
.collect(Collectors.toList());
}
/**
* 批量向量化 + 入库
*/
private void embedAndStore(List<TextSegment> chunks) {
var response = embeddingModel.embedAll(chunks); // 一次 HTTP 请求批量向量化
embeddingStore.addAll(response.content(), chunks);
}
}
RAG 的数据流:
启动时(只执行一次):
码哥科技.txt → 切片 → 向量化 → 存入 InMemoryEmbeddingStore
每次请求时(ContentRetriever 自动执行):
用户问题 → 向量化 → 余弦相似度检索 → TopK 相关片段 → 自动注入 prompt
⚠️ 注意:embedAll() 是一次性批量向量化,10 个切片 = 1 次 HTTP 调用。逐条调用 embed() 需要 10 次 HTTP 调用,效率差 5-10 倍。
4.4 ★ Agent 装配 --- 核心中的核心
java
@Configuration
public class AgentConfig {
@Value("${langchain4j.open-ai.api-key}")
private String apiKey;
@Value("${langchain4j.open-ai.base-url}")
private String baseUrl;
@Value("${langchain4j.open-ai.model-name:deepseek-ai/DeepSeek-V3}")
private String modelName;
@Value("${rag.top-k:3}")
private int topK;
/**
* ContentRetriever --- RAG 自动检索器
* 每次 Agent 调用,自动检索知识库并注入
*/
@Bean
public ContentRetriever contentRetriever(
EmbeddingStore<TextSegment> embeddingStore,
EmbeddingModel embeddingModel) {
return EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(topK) // 返回 topK 个最相关片段
.minScore(0.5) // 相似度 ≥ 0.5 才返回
.build();
}
/**
* Agent Bean --- 三合一智能体
*/
@Bean
public Agent assistant(
WeatherTool weatherTool,
SearchTool searchTool,
ContentRetriever contentRetriever) {
return AiServices.builder(Agent.class)
.chatLanguageModel(OpenAiChatModel.builder()
.apiKey(apiKey)
.baseUrl(baseUrl)
.modelName(modelName)
.timeout(Duration.ofSeconds(90))
.build())
.tools(weatherTool, searchTool) // 工具集
.contentRetriever(contentRetriever) // RAG 自动检索
.chatMemoryProvider(memoryId ->
MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(20) // 只保留最近 20 条
.build())
.build();
}
/**
* Agent 接口 --- 你只需定义,实现由框架生成
*/
public interface Agent {
@SystemMessage("""
你是一个智能助手,名字叫"码哥"。你可以:
1. 调用工具获取实时信息(天气、数学计算等)
2. 从知识库检索文档内容回答专业问题
规则:
- 先判断用户意图,适合用工具就调工具,适合查知识库就查知识库
- 知识库里没有的内容如实告知,不要编造
- 回答简洁、专业
""")
String chat(@MemoryId String userId, @UserMessage String message);
}
}
这里有三件事非常关键:
① AiServices 自动编排
AiServices.builder(Agent.class) 是 LangChain4j 最强大的功能。你只定义一个 interface,框架在运行时通过动态代理生成实现,自动编排:
用户问题
→ 构建 messages(system + history + user)
→ 调大模型
→ AI 说"我需要调 getWeather("北京")"
→ 框架拦截指令,真实执行 WeatherTool.getWeather("北京")
→ 把结果"晴,25°C"回传给大模型
→ 大模型再想 → "信息够了,可以回答了"
→ 返回最终回答
整个过程对 Controller 完全透明 ------你调用 agent.chat() 就拿到结果了。
② ContentRetriever 自动注入
配置了这个 Bean 之后,每次 Agent 调用都自动走一遍 RAG 检索,把相关文档片段拼进 prompt。你不需要写一行检索代码。
③ 工具 + RAG 可以共存
AI 会根据用户意图自己判断:
- "北京天气怎么样" → 调 WeatherTool(实时数据,知识库没有)
- "码哥科技有什么产品" → 走 ContentRetriever(查知识库)
- 两者都可能相关时 → AI 自己决定顺序
4.5 Controller --- 极简入口
java
@RestController
@RequestMapping("/agent")
@RequiredArgsConstructor
public class AgentController {
private final AgentConfig.Agent agent;
@GetMapping("/chat")
public Map<String, String> chat(
@RequestParam(defaultValue = "user1") String userId,
@RequestParam String message) {
String answer = agent.chat(userId, message);
return Map.of("userId", userId, "answer", answer);
}
}
五、测试验证
5.1 启动
bash
cd day3
mvn spring-boot:run \
-Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev -Djasypt.encryptor.password=你的密码"
5.2 测试用例
bash
# 查天气 → 自动调用 WeatherTool
curl "http://localhost:8080/agent/chat?message=北京天气怎么样"
# → {"userId":"user1","answer":"北京今天晴,25°C,湿度40%,北风3级..."}
# 查知识库 → 自动 RAG 检索
curl "http://localhost:8080/agent/chat?message=码哥科技有什么产品"
# → {"userId":"user1","answer":"根据知识库,码哥科技有三款核心产品:码哥AI中台..."}
# 做运算 → 自动调用 calculate
curl "http://localhost:8080/agent/chat?message=帮我算一下 3.14 乘以 2.5"
# → {"userId":"user1","answer":"3.14 × 2.5 = 7.85"}
# 查时间 → 自动调用 getCurrentDateTime
curl "http://localhost:8080/agent/chat?message=现在几点了"
# → {"userId":"user1","answer":"现在是2026年6月23日14:30:15"}
# 查订单 → 自动调用 queryOrder
curl "http://localhost:8080/agent/chat?message=帮我查一下订单 SO001"
# → {"userId":"user1","answer":"订单SO001已发货,预计06-25到达"}
5.3 验证 Agent 真的调用了工具
看控制台日志:
>>> Agent 调用了 getWeather: 北京
>>> Agent 调用了 calculate: 3.14 multiply 2.5
>>> Agent 调用了 getCurrentDateTime
>>> Agent 调用了 queryOrder: SO001
出现这些日志,说明 Function Calling 真的跑通了------大模型发出指令,框架真实执行了你的 Java 方法。
六、踩坑实录
坑 1:模型不支持 Function Calling
现象 :用 Qwen/Qwen2.5-7B-Instruct 测试 /agent/chat?message=北京天气,AI 直接回复文字说"我帮你查一下",但控制台没有 >>> Agent 调用了 日志。
原因:Qwen2.5-7B 不支持 Function Calling(tool use),它把 tool_call 当作文本输出而非结构化指令。
解决 :切到 deepseek-ai/DeepSeek-V3。选模型前务必确认文档是否标注 "tool use" 或 "function calling"。
坑 2:Lombok @RequiredArgsConstructor 的 private final 陷阱
现象:Controller 注入 Agent 时报 NPE。
原因 :字段缺少 private final,Lombok 不会为普通字段生成构造参数 → Spring 无法注入。
正确写法:
java
// ❌ 错误
private AgentConfig.Agent agent;
// ✅ 正确
private final AgentConfig.Agent agent;
坑 3:API Key 硬编码
危险:明文 Key 提交 Git 后无法彻底删除(历史记录里一直有)。
解决方案(Day 3 采用):
- 用 Jasypt 加密:
ENC(密文) application.yml不含真实 Key,application-dev.yml已 gitignore- 启动时传密码:
-Djasypt.encryptor.password=xxx
七、总结与展望
今天的收获
| 组件 | 解决了什么问题 |
|---|---|
| Function Calling | AI 能真的"动手"------调方法、查数据、做计算,不再是光说不练 |
| RAG 知识库 | AI 能回答私有知识,不再瞎编 |
| ContentRetriever | 零代码实现 RAG 自动注入------你只需配 Bean |
| AiServices | 声明式 Agent 接口,动态代理生成实现,编排整个 ReAct 循环 |
| ChatMemory | 多用户记忆隔离,滑动窗口控制 token 消耗 |
Day 4 可以做什么
- PGVector 持久化:替换 InMemoryEmbeddingStore,重启不丢数据
- 多文档格式:支持 PDF / Markdown / 网页
- 工作流编排:多步骤 Agent(查订单 → 查物流 → 生成报告)
- 流式 Agent:SSE 实时推送 AI 的思考过程
- Human-in-the-loop:关键操作需人工确认
📦 完整代码:https://gitee.com/jackXUYY/java-ai-learn/tree/master/day3
🔑 硅基流动免费注册:https://cloud.siliconflow.cn
📚 LangChain4j 文档:https://docs.langchain4j.dev
如果这篇文章对你有帮助,欢迎点赞收藏,也欢迎在评论区交流踩坑经验 🚀