🚀 Day 3:Function Calling + 简易 RAG
Day 2 的 AI 能聊天但能力有限。Day 3 让 AI 能:
- 调用外部工具(Function Calling)--- 查天气、算数学、调接口
- 查本地文档(RAG)--- 让 AI 回答它"不知道"的知识
- 注意 Function Calling 请使用 deepseek-ai/DeepSeek-V3
源码地址:java ai learn
yaml
# ========== 硅基流动 AI 配置(OpenAI 兼容接口)==========
langchain4j:
open-ai:
# 从 https://cloud.siliconflow.cn/account/ak 获取
api-key: ${SILICONFLOW_API_KEY:你的api key}
# 硅基流动 API 地址
base-url: https://api.siliconflow.cn/v1
# 对话模型:
# Qwen/Qwen2.5-7B-Instruct(通用对话,不支持 Function Calling)
# deepseek-ai/DeepSeek-V3(支持 Function Calling)
model-name: deepseek-ai/DeepSeek-V3
3.1 Function Calling 是什么?
让大模型"学会调用函数"。比如问"北京今天天气怎么样?",AI 会自动调用 getWeather("北京") 拿到真实数据,再生成回复。
text
用户:北京今天天气怎么样?
↓
AI 识别意图 → 调用 getWeather("北京") → 拿到 {"temp": 25, "desc": "晴"}
↓
AI 生成回复:北京今天晴,气温 25°C
3.2 定义工具
java
package com.day1.demo.tool;
import dev.langchain4j.agent.tool.Tool;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class WeatherTool {
@Tool("查询指定城市的天气")
public String getWeather(String city) {
// 模拟数据,实际应调用天气 API
Map<String, String> mockWeather = Map.of(
"北京", "晴,25°C,湿度 40%",
"上海", "多云,28°C,湿度 65%",
"深圳", "阵雨,30°C,湿度 80%"
);
return mockWeather.getOrDefault(city, "暂无" + city + "的天气数据");
}
@Tool("简单的数学四则运算")
public double calculate(
@dev.langchain4j.agent.tool.P("第一个操作数") double a,
@dev.langchain4j.agent.tool.P("第二个操作数") double b,
@dev.langchain4j.agent.tool.P("运算类型:add/subtract/multiply/divide") String operation) {
return switch (operation) {
case "add" -> a + b;
case "subtract" -> a - b;
case "multiply" -> a * b;
case "divide" -> a / b;
default -> throw new IllegalArgumentException("不支持的运算: " + operation);
};
}
}
3.3 配置 Function Calling 大模型
java
package com.day1.demo.config;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.UserMessage;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Configuration
public class ToolAssistantConfig {
@Value("${langchain4j.open-ai.api-key}")
private String apiKey;
@Value("${langchain4j.open-ai.base-url}")
private String baseUrl;
@Value("${langchain4j.open-ai.model-name:Qwen/Qwen2.5-7B-Instruct}")
private String modelName;
/**
* 带工具的 AI 助手(代码组装方式)
*/
@Bean
public ToolAssistant toolAssistant(WeatherTool weatherTool) {
return AiServices.builder(ToolAssistant.class)
.chatLanguageModel(OpenAiChatModel.builder()
.apiKey(apiKey)
.baseUrl(baseUrl)
.modelName(modelName)
.timeout(Duration.ofSeconds(60))
.build())
.tools(weatherTool) // 注入工具
.chatMemoryProvider(memoryId ->
MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(20)
.build())
.build();
}
/**
* 工具助手接口
*/
public interface ToolAssistant {
String chat(@MemoryId String userId, @UserMessage String message);
}
}
3.4 工具助手接口
java
@RestController
@RequestMapping("/tool")
@RequiredArgsConstructor
public class ToolAssistantController {
private final ToolAssistantConfig.ToolAssistant toolAssistant;
@GetMapping("/chat")
public String chat(
@RequestParam(defaultValue = "user1") String userId,
@RequestParam String message) {
return toolAssistant.chat(userId, message);
}
}
bash
# AI 会自动调用 getWeather 工具
curl "http://localhost:8080/tool/chat?userId=user1&message=北京今天天气怎么样"
# AI 会自动调用 calculate 工具
curl "http://localhost:8080/tool/chat?userId=user1&message=计算 3.14 乘以 2.5"
3.5 RAG --- 让 AI 回答私有知识
大模型的训练数据有截止日期,也不知道你公司的内部文档。RAG(Retrieval-Augmented Generation)解决这个问题:
text
1. 把你的文档切成小块(chunk)
2. 每块用嵌入模型转成向量
3. 存入向量库(本示例用内存存储,无需 Docker)
4. 用户提问 → 向量搜索找到最相关的文档块
5. 把文档块拼进 prompt 发给大模型 → 生成答案
Day 3 用内存向量库,零依赖、开箱即用。生产环境可换成 PGVector / Redis / Milvus 等。
3.5.1 准备知识文档
在 src/main/resources/docs/ 下放 .txt 文件,比如 码哥科技.txt:
text
# 码哥科技 · 公司知识库
## 公司概况
码哥科技成立于2023年,总部位于杭州未来科技城...
## 核心产品
### 码哥AI中台 (v3.2)
一站式AI能力接入平台,支持LLM调度、Prompt管理...
### 码哥智能客服 (v2.1)
基于RAG技术的企业智能客服系统...
## 联系方式
- 技术支持:support@mage-tech.cn
- 商务合作:sales@mage-tech.cn
启动时 RagService 会自动加载该目录下所有 .txt 文件。
3.5.2 添加嵌入模型 Bean
在 ChatModelConfig 中新增两个 Bean:
java
@Configuration
public class ChatModelConfig {
@Value("${langchain4j.open-ai.embedding-model-name:BAAI/bge-large-zh-v1.5}")
private String embeddingModelName;
// ... 原有的 chatLanguageModel()、streamingChatLanguageModel() ...
/**
* 嵌入模型 --- 把文本转成向量(RAG 的核心)
* 硅基流动免费:BAAI/bge-large-zh-v1.5(1024维,中文)
*/
@Bean
public EmbeddingModel embeddingModel() {
return OpenAiEmbeddingModel.builder()
.apiKey(apiKey)
.baseUrl(baseUrl)
.modelName(embeddingModelName)
.timeout(Duration.ofSeconds(60))
.build();
}
/**
* 内存向量存储 --- 存文档的向量 + 原文
*/
@Bean
public EmbeddingStore<TextSegment> embeddingStore() {
return new InMemoryEmbeddingStore<>();
}
}
.yml 中增加嵌入模型配置:
yaml
langchain4j:
open-ai:
# 嵌入模型(硅基流动免费)
# BAAI/bge-large-zh-v1.5(中文,1024维)
# BAAI/bge-large-en-v1.5(英文)
# Pro/BAAI/bge-m3(多语言)
embedding-model-name: BAAI/bge-large-zh-v1.5
3.5.3 RagService 完整实现
java
@Slf4j
@Service
@RequiredArgsConstructor
public class RagService {
private final ChatLanguageModel chatLanguageModel;
private final EmbeddingModel embeddingModel;
private final EmbeddingStore<TextSegment> embeddingStore;
private final ResourcePatternResolver resourceResolver;
@Value("${rag.top-k:3}")
private int topK;
/**
* 启动时自动加载 docs/ 目录下所有 .txt,切片、向量化、入库
*/
@PostConstruct
public void initDocuments() {
try {
Resource[] resources = resourceResolver.getResources("classpath*:docs/*.txt");
for (Resource resource : resources) {
String content = resource.getContentAsString(StandardCharsets.UTF_8);
List<TextSegment> chunks = splitIntoChunks(resource.getFilename(), content);
embedAndStore(chunks);
log.info("已加载文档: {}, 切片数: {}", resource.getFilename(), chunks.size());
}
} 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);
embeddingStore.addAll(response.content(), chunks);
}
/**
* 核心:检索增强生成
* 1. 问题向量化
* 2. 检索 topK 个最相似片段
* 3. 拼入 prompt 发给 LLM 生成
*/
public String ask(String question) {
// 1. 向量检索
var qEmbedding = embeddingModel.embed(question).content();
List<EmbeddingMatch<TextSegment>> matches =
embeddingStore.findRelevant(qEmbedding, topK);
if (matches.isEmpty()) {
return "抱歉,知识库中没有相关信息。";
}
// 2. 构建增强 prompt
String context = matches.stream()
.map(m -> m.embedded().text())
.collect(Collectors.joining("\n\n---\n\n"));
String prompt = """
你是一个企业知识库助手。请严格基于以下知识库内容回答问题,
不要编造知识库中没有的信息。
【知识库内容】
%s
【用户问题】
%s
""".formatted(context, question);
// 3. 大模型生成
return chatLanguageModel.generate(prompt);
}
}
RAG 流程总结:
text
┌───────────┐ ┌───────────┐ ┌──────────┐ ┌───────────┐
│ 知识文档 │ → │ 切片(chunk) │ → │ 向量化 │ → │ 向量库 │
│ .txt/.md │ │ 按标题切 │ │ bge-zh │ │ InMemory │
└───────────┘ └───────────┘ └──────────┘ └─────┬─────┘
│ 启动时完成
┌───────────┐ ┌───────────┐ ┌──────────┐ │
│ 用户提问 │ → │ 向量检索 │ ←── │ 找到 TopK │ ←────────┘
│ "有什么产品"│ │ 余弦相似度 │ │ 相关片段 │
└───────────┘ └───────────┘ └────┬─────┘
│
┌────────────────────┘
▼
┌──────────┐
│ Prompt │ 知识模板 + 检索结果 + 问题
│ 拼装 │ → LLM 生成 → 用户
└──────────┘
3.5.4 RagController 接口
java
@RestController
@RequestMapping("/rag")
@RequiredArgsConstructor
public class RagController {
private final RagService ragService;
/** GET /rag/ask?question=码哥科技有什么产品 */
@GetMapping("/ask")
public Map<String, String> ask(@RequestParam String question) {
String answer = ragService.ask(question);
return Map.of("question", question, "answer", answer);
}
}
3.5.5 测试 RAG
bash
# 问知识库里有答案的问题
curl "http://localhost:8080/rag/ask?question=码哥科技有什么产品"
# 问创始人信息
curl "http://localhost:8080/rag/ask?question=码哥科技的创始人是谁"
# 问联系方式
curl "http://localhost:8080/rag/ask?question=怎么联系码哥科技的技术支持"
# 问知识库没有的内容 → AI 应回答"不知道"
curl "http://localhost:8080/rag/ask?question=今天比特币多少钱"
关键选择:Vector Store 用什么?
方案 特点 适用 InMemoryEmbeddingStore零依赖,重启丢失 Day 3 学习、原型验证 PGVector 持久化,生产可用 小中型项目 Redis Stack 高性能缓存 高并发场景 Milvus / Qdrant 专业向量库 大规模生产 本教程用 InMemory 方式,升级到完整 PGVector 只需换一个 Bean 实现。
📦 完整项目结构
java-ai-learn/
└── day1/
├── pom.xml
└── src/main/java/com/day1/demo/
├── DemoApplication.java # 启动类
├── config/
│ ├── ChatModelConfig.java # 对话模型 Bean(普通 + 流式)
│ ├── LangChain4jConfig.java # ChatMemory 全局配置
│ ├── AssistantService.java # @AiService 声明式助手
│ └── ToolAssistantConfig.java # Function Calling 配置(Day 3)
├── controller/
│ ├── HelloController.java # 健康检查
│ ├── ChatController1.java # 最简对话 + 系统提示词
│ ├── ChatMemoryController1.java # 带记忆多轮(手动)
│ ├── ChatMemoryUseConfigController2.java # 带记忆多轮(AiService)
│ ├── SSEController1.java # 流式 SSE 输出
│ ├── ToolAssistantController.java # Function Calling 接口(Day 3)
│ └── RagController.java # RAG 问答接口(Day 3)
├── service/
│ ├── ChatService1.java # 最简对话
│ ├── ChatMemoryService1.java # 手动记忆管理
│ └── RagService.java # RAG 检索增强(Day 3)
├── tool/
│ └── WeatherTool.java # 工具定义(Day 3)
├── param/
│ └── ChatTestRequest.java # POST 请求体
└── resources/
├── application.yml # 配置文件
└── docs/
└── 码哥科技.txt # RAG 知识库文档
🔧 启动 & 全部接口测试
bash
# 启动
cd day1
mvn spring-boot:run -DskipTests
# === Day 1 ===
curl http://localhost:8080/hello
curl "http://localhost:8080/chat/chatTest?prompt=用一句话介绍Java"
curl -X POST http://localhost:8080/chat/systemPrompt \
-H "Content-Type: application/json" \
-d '{"systemPrompt":"你是一个幽默的程序员", "question":"什么是OOM"}'
# === Day 2 ===
curl "http://localhost:8080/chatMemory/chat1?userId=user1&message=我叫张三"
curl "http://localhost:8080/chatMemory/chat1?userId=user1&message=我叫什么名字"
curl "http://localhost:8080/assistant/assistantTest?userId=user1&message=介绍Spring Boot"
# 流式:浏览器打开 http://localhost:8080/stream/streamChat?message=讲个笑话
# === Day 3 ===
curl "http://localhost:8080/tool/chat?userId=user1&message=北京天气"
curl "http://localhost:8080/tool/chat?userId=user1&message=算一下 3.14 * 2.5"
curl "http://localhost:8080/rag/ask?question=码哥科技有什么产品"
curl "http://localhost:8080/rag/ask?question=码哥科技的创始人是谁"
curl "http://localhost:8080/rag/ask?question=怎么联系技术支持"
📚 API 速查表
| 方法 | 路径 | 功能 | 天数 |
|---|---|---|---|
GET |
/hello |
健康检查 | Day 1 |
GET |
/chat/chatTest?prompt= |
最简对话 | Day 1 |
POST |
/chat/systemPrompt |
带系统提示词 | Day 1 |
GET |
/chatMemory/chat1?userId=&message= |
多轮记忆(手动) | Day 2 |
GET |
/chatMemory/deleteMemory?userId= |
清空记忆 | Day 2 |
GET |
/assistant/assistantTest?userId=&message= |
多轮记忆(AiService) | Day 2 |
GET |
/stream/streamChat?message= |
流式 SSE | Day 2 |
GET |
/tool/chat?userId=&message= |
Function Calling | Day 3 |
GET |
/rag/ask?question= |
RAG 知识库问答 | Day 3 |
🧩 技术栈
| 组件 | 版本 | 说明 |
|---|---|---|
| Spring Boot | 4.1.0 | 应用框架 |
| Java | 17 | LTS 长期支持 |
| LangChain4j | 0.36.2 | LLM 集成框架 |
| 硅基流动 | deepseek-ai/DeepSeek-V3 | 免费大模型(OpenAI 兼容,支持 Function Calling) |
| 硅基流动 | BAAI/bge-large-zh-v1.5 | 免费嵌入模型(1024维中文向量) |
| Lombok | --- | 简化代码 |
| Maven | 3.9+ | 构建工具 |
📖 延伸阅读