springboot+langchain4j 实战Day2:Function Calling + 简易 RAG

🚀 Day 3:Function Calling + 简易 RAG

Day 2 的 AI 能聊天但能力有限。Day 3 让 AI 能:

  1. 调用外部工具(Function Calling)--- 查天气、算数学、调接口
  2. 查本地文档(RAG)--- 让 AI 回答它"不知道"的知识
  3. 注意 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+ 构建工具

📖 延伸阅读