springboot+langchain4j 实战day3 — Agent 智能体:让 AI 拥有“手“和“脑“

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 和参数信息发给大模型:

"你有以下工具可用:

  1. getWeather(city) --- 查询指定城市的天气
  2. calculate(a, b, operation) --- 数学四则运算
  3. ..."

大模型根据这些描述自主判断该调哪个、传什么参数。大模型不执行工具------它只是"说"要调什么,框架真正执行并把结果回传。

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

如果这篇文章对你有帮助,欢迎点赞收藏,也欢迎在评论区交流踩坑经验 🚀