基于 Spring AI + DeepSeek:构建AI Agent 企业级服务与底层原理解析

目录

  • [前言:何为 AI Agent](#前言:何为 AI Agent)
  • 环境与准备
    • [📦 1. 父项目依赖与版本管控](#📦 1. 父项目依赖与版本管控)
    • [⚙️ 2. YAML 配置与 Nacos 整合](#⚙️ 2. YAML 配置与 Nacos 整合)
  • [实践:落地 Agent 核心支柱](#实践:落地 Agent 核心支柱)
    • [一、 赋予 Agent 手脚:Tool Function 的底层原理](#一、 赋予 Agent 手脚:Tool Function 的底层原理)
      • [1. 全代码展示:天气与订单触手](#1. 全代码展示:天气与订单触手)
      • [2. "小白"解惑:LLM 是怎么识别参数的?](#2. “小白”解惑:LLM 是怎么识别参数的?)
    • [二、 简单触手调用:DeepSeekToolChatController](#二、 简单触手调用:DeepSeekToolChatController)
    • [三、 企业级全能 Agent:ChatClient 与拔插机制实战](#三、 企业级全能 Agent:ChatClient 与拔插机制实战)
      • [1. ChatClient vs ChatModel 详细对比](#1. ChatClient vs ChatModel 详细对比)
      • [2. Agent 的"前尘往事":Memory (记忆) 的接口设计与拔插式配置](#2. Agent 的“前尘往事”:Memory (记忆) 的接口设计与拔插式配置)
      • [3.实现redis分布式Memory类 `RedisChatMemory`(继承了 `ChatMemory`接口)](#3.实现redis分布式Memory类 RedisChatMemory(继承了 ChatMemory接口))
      • [4. 分布式记忆深潜:Redis 序列化陷阱与自定义架构方案](#4. 分布式记忆深潜:Redis 序列化陷阱与自定义架构方案)
      • [5. 定义并挂载拔插式memory 接口](#5. 定义并挂载拔插式memory 接口)
      • [6. Agent 灵魂:System Prompt(系统提示词)](#6. Agent 灵魂:System Prompt(系统提示词))
    • [四、 架构拓展:多模型并存的"神仙打架"与 Spring Bean 冲突](#四、 架构拓展:多模型并存的“神仙打架”与 Spring Bean 冲突)
  • 总结:在巨变的时代造稳固的基石

前言:何为 AI Agent

在 AI 应用爆发的今天,市面上充斥着各种 Agent 工具。但作为技术人,我们不仅要会"用",更要懂"如何集成到业务"。ai-agent-chat 项目正是为了带你从浅入深理解市面上 Agent 能力的底层原理。本文将基于实战,拆解一个具备"大脑(LLM)手脚(Tool Use / Function Calling)记忆(Memory)规划(Planning / ReAct)系统提示词(System Prompt)"的 Agent 是如何炼成的。


环境与准备

源码获取:点击获取源码

📦 1. 父项目依赖与版本管控

本项目作为 spring-ai-lab 的子模块,版本受父 POM 统一管控。

下面是ai-agent-chat模块需要用到的父类依赖

  • Spring Boot: 3.3.3
  • Spring AI : 1.1.4 (引入 spring-ai-bom 抹平依赖)
  • Spring Cloud Alibaba: 2023.0.3.4 (集成了 Nacos)
  • Spring Redis Data: (后面分布式存储Memory 会用到)

父 POM 关键配置展示:

xml 复制代码
<properties>
    <spring-ai-version>1.1.4</spring-ai-version>
    <spring-cloud-alibaba.version>2023.0.3.4</spring-cloud-alibaba.version>
</properties>

<dependencyManagement>
    <dependencies>
        <!-- 引入 Spring AI bom 统一版本 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>${spring-ai-version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>

当前ai-agent-chat模块引入依赖

xml 复制代码
<properties>
    <fastjson2.version>2.0.47</fastjson2.version>
</properties>
<!-- 集成deepseek公司依赖 用于DeepSeek 模型-->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>${fastjson2.version}</version>
</dependency>

⚙️ 2. YAML 配置与 Nacos 整合

本项目由 Nacos 进行分布式配置管理,application.yml 中定义了动态配置导入逻辑,方便在不同环境下切换 Redis 和 AI 密钥。

yaml 复制代码
server:
  port: 10005
spring:
  application:
    name: ai-agent-chat
  profiles:
    active: dev
  cloud:
    nacos:
      config:
        server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
        username: ${NACOS_USERNAME:nacos}
        password: ${NACOS_PWD:nacos}
        file-extension: yaml
        namespace: b0486ef8-e9ac-4c88-881f-8eef86f122a5
        group: DEFAULT_GROUP
      discovery:
        server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
        username: ${NACOS_USERNAME:nacos}
        password: ${NACOS_PWD:nacos}
        namespace: b0486ef8-e9ac-4c88-881f-8eef86f122a5
  config:
    import:
      - nacos:${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
      - nacos:redis-common.${spring.cloud.nacos.config.file-extension}

nacos中ai-agent-chat-dev.yaml中配置

yaml 复制代码
spring:
  ai:
    deepseek:
      api-key: # 登录DeepSeek官方:https://platform.deepseek.com/usage 购买api密钥(如果只是用于测试10元远远够用了)
      chat:
        options:
          model: deepseek-chat
          temperature: 1.0

nacos中redis-common.yaml中配置

yaml 复制代码
spring:
  data:
    redis:
      port: 6379
      host: # ip
      password: # 密码
      timeout: 5000ms  # 注意:建议加上单位 ms
      lettuce:
        pool:
          max-active: 5000  # 注意:属性名用横线分隔
          max-idle: 30
          min-idle: 5
          max-wait: 2000ms
        cluster:
          refresh:
            adaptive: true
            period: 60s

实践:落地 Agent 核心支柱

一、 赋予 Agent 手脚:Tool Function 的底层原理

Agent 与普通聊天机器人的本质区别在于其拥有 Tool Use(功能调用)的能力。

1. 全代码展示:天气与订单触手

我们要让模型通过 Java 代码去"感知"外部世界。

本文模拟一个天气查询和订单查询的"触手",分别对应两个 Function Bean。

java 复制代码
// 天气查询触手
@Configuration
public class WeatherToolFunction {
    // 关键:LLM 参数识别载体。LLM 会解析用户输入并填充到这个 record 中
    public record Weather(String city) { } 

    @Bean
    @Description("查询今天天气") // 模型的"说明书":告诉 LLM 什么时候调用这个 Bean
    public Function<Weather, String> weatherFunction() {
        return weather -> {
            if("成都".equals(weather.city)) return "成都晴,25°C";
            return "未找到该城市天气信息";
        };
    }
}
java 复制代码
// 订单查询触手
@Configuration
public class OrderToolFunction {
    
    public record Order(String orderId) { }
    
    @Bean
    @Description("查询订单信息")
    public Function<Order, String> orderFunction() {
        return order -> {
            if("D123456".equals(order.orderId)) return "订单 D123456,金额 100.00,已完成";
            return "未找到该订单";
        };
    }
}

2. "小白"解惑:LLM 是怎么识别参数的?

Record 参数识别机制 :当你定义 record Weather(String city) 时,Spring AI 会将该类的元数据(字段名、注释)转换成 JSON Schema 发送给大模型。

例如:用户问"成都天气如何?",LLM 识别到意图与 weatherFunction 匹配,并自动提取"成都"填充进 JSON {"city": "成都"},最后 Spring AI 将该 JSON 反序列化成 Java 对象传给你的方法。这就是"触手"的自动化原理。


二、 简单触手调用:DeepSeekToolChatController

这是一场极其简单的入门赛,演示如何通过 chatModel 直接发起调用。并加入上面实现的Tool函数

定义对话接口

java 复制代码
@RestController
@RequestMapping("/ai/agent")
public class DeepSeekToolChatController {
    @Resource
    private DeepSeekChatModel chatModel;

    @GetMapping("/call/toolFunction/chat")
    public String toolFunctionCallChat(@RequestParam String message) {
        return chatModel.call(new Prompt(message,
                DeepSeekChatOptions.builder()
                        .toolNames("weatherFunction", "orderFunction")
                        .build()
        )).getResult().getOutput().getText();
    }
}

访问接口请求

  1. 获取今天成都天气信息

  2. 获取今天重庆天气信息,会返回获取不到,因为我们没有配置重庆天气信息

  3. 获取订单信息


三、 企业级全能 Agent:ChatClient 与拔插机制实战

在生产环境下,我们更倾向于使用 ChatClient,因为它在 ChatModel 之上构建了强大的业务闭环 。这里会产生一个疑问:既然刚才用了 DeepSeekChatModel 发起对话,为什么在这儿又要用 ChatClient 呢?

1. ChatClient vs ChatModel 详细对比

维度 ChatModel (底层驱动层) ChatClient (上层应用层)
打比方 JDBC 的 java.sql.Connection MyBatis-PlusLambdaQueryWrapper
纯度 极度纯粹,只接收 Prompt 对象发送 HTTP 请求 开发体验极佳的流式 API (Fluent API)
功能 不懂什么是"记忆"、不懂什么是"拦截器" 内置大量业务功能:自动管理记忆 (ChatMemory) 、自动挂载系统预设 (System Prompt)、自动将大模型输出映射为 Java POJO
隔离性 你必须显式声明特定的子类(如 DeepSeekChatModel 屏蔽底层差异 :如果有一天你把底层模型换成 OpenAI,只要你不硬编码特定模型的参数,使用 ChatClient 写的业务代码连一行都不用改!

2. Agent 的"前尘往事":Memory (记忆) 的接口设计与拔插式配置

没有记忆的大模型,每次对话都是"出厂设置";有了记忆,它才能知道"刚才发生了什么"。Spring AI 官方提供了 ChatMemory接口。只要实现这个接口,不管你存在内存里还是 Redis 里,ChatClient 都能用同一种方式加载

让我们来看 AiConfig 配置类,这里体现了架构师最看重的"拔插式加载":

方案A:本地 JVM 内存版 (这里代码中直接给出,觉得方案B麻烦的直接拷贝方案A代码即可)

方案B:分布式 Redis 版 (生产推荐方案,下面按照本方案梳理)

java 复制代码
@Configuration
public class AiConfig {

    // 【方案 A:本地 JVM 内存版】
    // 优势:速度极快,无需外部中间件。
    // 劣势:服务重启即丢,无法多实例共享(非分布式)。如果不配置 Redis 的话,使用这个最简单。
    // @Bean
     public ChatMemory chatMemory() {
         InMemoryChatMemoryRepository repository = new InMemoryChatMemoryRepository();
         return MessageWindowChatMemory.builder()
                 .chatMemoryRepository(repository)
                 .maxMessages(20) // 保留最近的 20 条对话
                 .build();
     }

    // 【方案 B:分布式 Redis 版】
    // 优势:持久化、跨实例共享,适合真正的微服务生产环境。
    // 劣势:涉及网络 IO,存在严重的 JSON 序列化陷阱。
    @Bean
    public ChatMemory chatMemory(StringRedisTemplate messages) {
        return new RedisChatMemory(messages, 50, 7);
    }
}

3.实现redis分布式Memory类 RedisChatMemory(继承了 ChatMemory接口)

java 复制代码
@Slf4j
public class RedisChatMemory implements ChatMemory {

    private final StringRedisTemplate stringRedisTemplate;
    private final int maxMessages;
    private final long expireDays;
    private static final String KEY_PREFIX = "ai:agentChat:memory:";

    // 【核心架构设计:脱离框架绑定的纯净 DTO】
    @Data
    public static class MessageDto {
        private String type;
        private String content;
        public MessageDto() {} // 关键:满足无参构造要求
        public MessageDto(String type, String content) {
            this.type = type;
            this.content = content;
        }
    }

    public RedisChatMemory(StringRedisTemplate stringRedisTemplate, int maxMessages, long expireDays) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.maxMessages = maxMessages;
        this.expireDays = expireDays;
    }

    @Override
    public void add(@NonNull String conversationId, @NonNull List<Message> messages) {
        String key = KEY_PREFIX + conversationId;
        // 省略合并历史记录代码...
        
        // 【降维打击 - 存入】:把复杂的多态 Message 剥离成干净的 DTO
        List<MessageDto> dtos = mutableHistory.stream()
                .map(m -> new MessageDto(
                        m.getMessageType().getValue(), // "user", "assistant"
                        m.getText() != null ? m.getText() : ""))
                .collect(Collectors.toList());

        // 像存普通业务数据一样存进去,极其稳健
        stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(dtos), expireDays, TimeUnit.DAYS);
    }

    @Override
    public List<Message> get(@NonNull String conversationId) {
        String key = KEY_PREFIX + conversationId;
        String jsonStr = stringRedisTemplate.opsForValue().get(key);

        if (jsonStr == null || jsonStr.isEmpty()) return new ArrayList<>();

        try {
            // 【降维打击 - 取出】:先用 Fastjson2 解析成我们的 DTO
            List<MessageDto> dtos = JSON.parseArray(jsonStr, MessageDto.class);

            // 然后手动 new 出大模型需要的标准对象
            return dtos.stream().map(dto -> {
                String type = dto.getType();
                if ("user".equalsIgnoreCase(type)) return new UserMessage(dto.getContent());
                if ("assistant".equalsIgnoreCase(type)) return new AssistantMessage(dto.getContent());
                if ("system".equalsIgnoreCase(type)) return new SystemMessage(dto.getContent());
                return new UserMessage(dto.getContent()); // 兜底
            }).collect(Collectors.toList());

        } catch (Exception e) {
            log.warn("解析缓存异常,已清空脏数据: {}", e.getMessage());
            stringRedisTemplate.delete(key);
            return new ArrayList<>();
        }
    }

    @Override
    public void clear(@NonNull String conversationId) {
        stringRedisTemplate.delete(KEY_PREFIX + conversationId);
    }
}

深入理解这个收益 :我们利用物理级别的解耦,彻底切断了业务持久化数据与 Spring AI 第三方框架源码的绑定。无论未来 Spring AI 版本如何狗血地重构内部类,存在 Redis 中的对话数据永远是向后兼容的。这就是架构防腐。

4. 分布式记忆深潜:Redis 序列化陷阱与自定义架构方案

当我们打算上线时,自然首选上述的"方案 B"(Redis)。但由于 Spring AI 框架处于早期迭代阶段,你直接存官方的 Message多态对象会让你怀疑人生!

【核心痛点:为什么原生 Jackson 会彻底崩溃?】

Spring AI 底层的 Message(如 UserMessage, AssistantMessage)设计初衷是组装 HTTP 请求载荷发给大厂。这种面向外部环境的过度设计,忽略了 Java 的 POJO 序列化规范:

  • 它们没有无参构造函数(Jackson 根本反射不出来)。
  • 它们充斥着复杂的嵌套多态。
    导致默认的 Jackson (或者任何没有开挂的 JSON 类库)反序列化直接报错!
    【解法:引入 Fastjson2 与降维 DTO + ACL 防腐隔离】

我们摒弃通过修改 Jackson 全局配置(如强制打 @class)去迎合不成熟框架的"补丁"做法!采用领域驱动设计(DDD)中的 防腐层 (ACL)

  1. 我们引入了 fastjson2,以便于更轻量、宽容地处理纯字符串 JSON。
  2. 我们不存 Message,我们只存极简的 MessageDto 对象结构。

5. 定义并挂载拔插式memory 接口

接下来,我们在 AgentChatController 中看一下如何挂载这个拔插式的 Memory 接口并发起对话:

java 复制代码
@RestController
@RequestMapping("/ai/agent")
public class AgentChatController {
	//...
	@GetMapping("/chat/memory")
	public String chat(
	        @RequestParam String chatId,  // 模拟不同用户的独立记忆
	        @RequestParam String message) {
	
	    return chatClient.prompt()
	            .user(message)
	            // 👇 挂载记忆拦截器参数:通过 chatId 精准打击多并发下的用户路由
	            .advisors(a -> a.param("chat_memory_conversation_id", chatId))
	            .call()
	            .content();
	}
	//...
}

这行代码背后就是 MessageChatMemoryAdvisor 将每次的历史记录自动与当次对话合并。有了 ChatMemory 接口兜底,上面的业务代码无需关心底层用的到底是方案 A 还是方案 B。

6. Agent 灵魂:System Prompt(系统提示词)

有了拔插式记忆的辅佐,为了让 Agent 绝不"胡言乱语",我们需要在 AgentChatController 初始化时设定最高"宪法"(包含了角色定位、业务边界、工作规则等):

定义系统提示词
java 复制代码
@RestController
@RequestMapping("/ai/agent")
public class AgentChatController {
    private final ChatClient chatClient;

    // 构造函数注入全局 Client 和 Memory
    public AgentChatController(ChatClient.Builder builder, ChatMemory chatMemory) {
        String systemPrompt = """
                你是一个高级电商后台微服务架构的智能运维助手。
                你的主要职责是协助开发者和运营人员排查订单流转问题,并提供相关的天气物流建议。
                
                【核心规则】
                1. 你的语气必须专业、严谨,像一个资深的 Java 后端架构师,可以适时使用"接口响应"、"兜底策略"等技术术语。
                2. 业务边界:如果用户询问订单或天气,请果断调用你拥有的工具获取真实数据。
                3. 安全护栏:如果用户询问与技术、订单、天气无关的问题(如娱乐八卦、政治、让你写诗等),你可以基于上下文记忆,礼貌且极其简短地(不超过1句话)回应用户的非业务闲聊以保持对话温度,但回应后,必须立刻用专业术语将话题强制拉回订单排查或系统运维上。严禁长篇大论讨论非业务话题。
                4. 总结要求:务必言简意赅。
                """;

        this.chatClient = builder
                .defaultSystem(systemPrompt) // 1. 挂载系统宪法
                .defaultToolNames("weatherFunction", "orderFunction") // 2. 全局预装触手
                // 3. 将我们上面配置的 ChatMemory (拔插后的 Redis 或 JVM 内存)包在 Advisor 中全局生效
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) 
                .build();
    }
    // ... 下接我们上面展示的 chat() 方法
}

注:如果设置了系统提示词职责边界,memory记忆上下文可能会失效,不会回答职责意外的问题。

设置系统边界确实会"框住"记忆的联想能力,这是大模型安全机制的必然代价。各位"架构师",我们要做的就是通过不断打磨 Prompt 的颗粒度,在"绝对安全"和"像个人类"之间找那个最完美的平衡点。

实现记忆问答对话

将天气和查询订单结合在一起对话

  1. 先问是否可以取订单D123456(去掉系统提示词)
  2. 再问今天成都天气是否合适出门取(去掉系统提示词)
  3. 测试系统提示词规定的职责边界是否生效(使用系统提示词)
  4. 查看redis memory缓存数据结构

四、 架构拓展:多模型并存的"神仙打架"与 Spring Bean 冲突

企业级项目中常常需要引入多个大模型(例如同时使用 DeepSeek 做逻辑推理,OpenAI 做兜底)。如果你在 pom.xml里同时引入了这两个模型的 Starter,Spring Boot 启动时会立刻抛出极其经典的 NoUniqueBeanDefinitionException
原因 :Spring 找到了多个 ChatModel 的实现类(DeepSeekChatModel, OpenAiChatModel),它不知道应该自动注入哪一个给 ChatClient.Builder

【架构解法:基于 @Configuration 的精准声明与注入】

我们需要取消自动装配的偷懒做法,手动暴露不同名称的 ChatClient Bean,并在使用处通过 @Qualifier 进行精准匹配。

1. 显式声明 Bean AiConfig.java

java 复制代码
@Configuration
public class AiConfig {

    // 1. 专门为 DeepSeek 定制的客户端
    @Bean("deepseekClient")
    public ChatClient deepseekClient(DeepSeekChatModel deepseekModel) {
        // 直接把 deepseek 的底层模型塞给 Builder
        return ChatClient.builder(deepseekModel)
                .defaultSystem("你是一个由 DeepSeek 驱动的助手")
                .build();
    }

    // 2. 专门为 OpenAI (ChatGPT) 定制的客户端
    @Bean("openAiClient")
    public ChatClient openAiClient(OpenAiChatModel openAiModel) {
         // 直接把 OpenAI 的底层模型塞给 Builder
        return ChatClient.builder(openAiModel)
                .defaultSystem("你是一个由 GPT-4 驱动的高级分析师")
                .build();
    }
}

2. 业务层的精准注入

在使用时,通过 @Qualifier 明确告诉 Spring 你到底要哪个。

java 复制代码
@RestController
public class MultiModelController {
    
    private final ChatClient deepseekClient;
    private final ChatClient openAiClient;

    // 明确告诉 Spring,哪个变量对应哪个 Bean 定制器
    public MultiModelController(
            @Qualifier("deepseekClient") ChatClient deepseekClient,
            @Qualifier("openAiClient") ChatClient openAiClient) {
        this.deepseekClient = deepseekClient;
        this.openAiClient = openAiClient;
    }
}

通过这种解耦模式,我们就能完美地在一个微服务里面实现"多模型自由切换",让系统更加健壮和灵活。


总结:在巨变的时代造稳固的基石

通过 ai-agent-chat 的实战演示,我们可以看到:

构建一个 Agent 不仅仅是调一个"问答接口"。从 父 POM 的 bom 版本管控 ,到 Record 自动推导的大模型 Tool 识别参数黑魔法 ,到 对 ChatModel 与 ChatClient 职责的区别与选型 ,再到最重要的 通过 DTO + Fastjson2 架构来解决极其复杂的 Spring AI Redis 对象序列化反序列化危机......

!IMPORTANT

版本适配提示:Spring AI 目前尚处于版本快速变动的成长期,核心 API 的废弃与重构时有发生。请大家在实战中时刻关注版本特性。但有了我们上面的"记忆防腐层"等架构理念加持,无论官方怎么变,我们系统核心依然稳如泰山!

相关推荐
Matrix_112 小时前
论文阅读:中央凹堆叠成像技术
论文阅读·人工智能·计算摄影
希望永不加班2 小时前
SpringBoot Web 模块核心组件:从 DispatcherServlet 讲起
java·前端·spring boot·后端·spring
草莓熊Lotso2 小时前
2026年4月UU远程副屏功能测评:多设备协同生态再升级
人工智能
平凡而伟大(心之所向)2 小时前
AI重构制造:2026年工业智能体的实战与进化
人工智能·重构·制造
AI视觉网奇2 小时前
fp8 量化笔记
人工智能·笔记
ai产品老杨2 小时前
异构计算与边缘协同:基于 ARM/X86 的企业级 AI 视频中台架构设计
arm开发·人工智能·音视频
d1z8882 小时前
llama.cpp 性能测试数据汇总 - NVIDIA DGX Spark 对比分析
人工智能·llama·显卡
武汉唯众智创2 小时前
Rust系统安全实训入门:唯众网络安全实训室搭建与边缘节点并发优化实操指南
人工智能·rust·网络安全实训室建设·rust系统安全实训
昨夜见军贴06162 小时前
AI报告文档审核赋能数据不出域:IACheck重构机械制造行业本地化质量管控体系
大数据·人工智能·重构