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

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

目录

前言:何为 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 关键配置展示:

<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模块引入依赖

<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 密钥。

server:port:10005spring: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中配置

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中配置

spring:data:redis:port:6379host:# ippassword:# 密码timeout: 5000ms # 注意:建议加上单位 mslettuce:pool:max-active:5000# 注意:属性名用横线分隔max-idle:30min-idle:5max-wait: 2000ms cluster:refresh:adaptive:trueperiod: 60s 

实践:落地 Agent 核心支柱

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

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

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

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

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

// 天气查询触手@ConfigurationpublicclassWeatherToolFunction{// 关键:LLM 参数识别载体。LLM 会解析用户输入并填充到这个 record 中publicrecordWeather(String city){}@Bean@Description("查询今天天气")// 模型的“说明书”:告诉 LLM 什么时候调用这个 BeanpublicFunction<Weather,String>weatherFunction(){return weather ->{if("成都".equals(weather.city))return"成都晴,25°C";return"未找到该城市天气信息";};}}
// 订单查询触手@ConfigurationpublicclassOrderToolFunction{publicrecordOrder(String orderId){}@Bean@Description("查询订单信息")publicFunction<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函数

定义对话接口

@RestController@RequestMapping("/ai/agent")publicclassDeepSeekToolChatController{@ResourceprivateDeepSeekChatModel chatModel;@GetMapping("/call/toolFunction/chat")publicStringtoolFunctionCallChat(@RequestParamString message){return chatModel.call(newPrompt(message,DeepSeekChatOptions.builder().toolNames("weatherFunction","orderFunction").build())).getResult().getOutput().getText();}}

访问接口请求

获取订单信息

在这里插入图片描述

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

在这里插入图片描述

获取今天成都天气信息

在这里插入图片描述

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

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

1. ChatClient vs ChatModel 详细对比

维度ChatModel (底层驱动层)ChatClient (上层应用层)
打比方JDBC 的 java.sql.ConnectionMyBatis-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 版 (生产推荐方案,下面按照本方案梳理)
@ConfigurationpublicclassAiConfig{// 【方案 A:本地 JVM 内存版】// 优势:速度极快,无需外部中间件。// 劣势:服务重启即丢,无法多实例共享(非分布式)。如果不配置 Redis 的话,使用这个最简单。// @BeanpublicChatMemorychatMemory(){InMemoryChatMemoryRepository repository =newInMemoryChatMemoryRepository();returnMessageWindowChatMemory.builder().chatMemoryRepository(repository).maxMessages(20)// 保留最近的 20 条对话.build();}// 【方案 B:分布式 Redis 版】// 优势:持久化、跨实例共享,适合真正的微服务生产环境。// 劣势:涉及网络 IO,存在严重的 JSON 序列化陷阱。@BeanpublicChatMemorychatMemory(StringRedisTemplate messages){returnnewRedisChatMemory(messages,50,7);}}

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

@Slf4jpublicclassRedisChatMemoryimplementsChatMemory{privatefinalStringRedisTemplate stringRedisTemplate;privatefinalint maxMessages;privatefinallong expireDays;privatestaticfinalStringKEY_PREFIX="ai:agentChat:memory:";// 【核心架构设计:脱离框架绑定的纯净 DTO】@DatapublicstaticclassMessageDto{privateString type;privateString content;publicMessageDto(){}// 关键:满足无参构造要求publicMessageDto(String type,String content){this.type = type;this.content = content;}}publicRedisChatMemory(StringRedisTemplate stringRedisTemplate,int maxMessages,long expireDays){this.stringRedisTemplate = stringRedisTemplate;this.maxMessages = maxMessages;this.expireDays = expireDays;}@Overridepublicvoidadd(@NonNullString conversationId,@NonNullList<Message> messages){String key =KEY_PREFIX+ conversationId;// 省略合并历史记录代码...// 【降维打击 - 存入】:把复杂的多态 Message 剥离成干净的 DTOList<MessageDto> dtos = mutableHistory.stream().map(m ->newMessageDto( m.getMessageType().getValue(),// "user", "assistant" m.getText()!=null? m.getText():"")).collect(Collectors.toList());// 像存普通业务数据一样存进去,极其稳健 stringRedisTemplate.opsForValue().set(key,JSON.toJSONString(dtos), expireDays,TimeUnit.DAYS);}@OverridepublicList<Message>get(@NonNullString conversationId){String key =KEY_PREFIX+ conversationId;String jsonStr = stringRedisTemplate.opsForValue().get(key);if(jsonStr ==null|| jsonStr.isEmpty())returnnewArrayList<>();try{// 【降维打击 - 取出】:先用 Fastjson2 解析成我们的 DTOList<MessageDto> dtos =JSON.parseArray(jsonStr,MessageDto.class);// 然后手动 new 出大模型需要的标准对象return dtos.stream().map(dto ->{String type = dto.getType();if("user".equalsIgnoreCase(type))returnnewUserMessage(dto.getContent());if("assistant".equalsIgnoreCase(type))returnnewAssistantMessage(dto.getContent());if("system".equalsIgnoreCase(type))returnnewSystemMessage(dto.getContent());returnnewUserMessage(dto.getContent());// 兜底}).collect(Collectors.toList());}catch(Exception e){ log.warn("解析缓存异常,已清空脏数据: {}", e.getMessage()); stringRedisTemplate.delete(key);returnnewArrayList<>();}}@Overridepublicvoidclear(@NonNullString 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):我们引入了 fastjson2,以便于更轻量、宽容地处理纯字符串 JSON。我们不存 Message,我们只存极简的 MessageDto 对象结构。

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

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

@RestController@RequestMapping("/ai/agent")publicclassAgentChatController{//...@GetMapping("/chat/memory")publicStringchat(@RequestParamString chatId,// 模拟不同用户的独立记忆@RequestParamString 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 初始化时设定最高“宪法”(包含了角色定位、业务边界、工作规则等):

定义系统提示词
@RestController@RequestMapping("/ai/agent")publicclassAgentChatController{privatefinalChatClient chatClient;// 构造函数注入全局 Client 和 MemorypublicAgentChatController(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 的颗粒度,在“绝对安全”和“像个人类”之间找那个最完美的平衡点。
实现记忆问答对话

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

查看redis memory缓存数据结构

在这里插入图片描述

测试系统提示词规定的职责边界是否生效(使用系统提示词)

在这里插入图片描述

再问今天成都天气是否合适出门取(去掉系统提示词)

在这里插入图片描述

先问是否可以取订单D123456(去掉系统提示词)

在这里插入图片描述

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

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

【架构解法:基于 @Configuration 的精准声明与注入】
我们需要取消自动装配的偷懒做法,手动暴露不同名称的 ChatClient Bean,并在使用处通过 @Qualifier 进行精准匹配。

1. 显式声明 Bean AiConfig.java

@ConfigurationpublicclassAiConfig{// 1. 专门为 DeepSeek 定制的客户端@Bean("deepseekClient")publicChatClientdeepseekClient(DeepSeekChatModel deepseekModel){// 直接把 deepseek 的底层模型塞给 BuilderreturnChatClient.builder(deepseekModel).defaultSystem("你是一个由 DeepSeek 驱动的助手").build();}// 2. 专门为 OpenAI (ChatGPT) 定制的客户端@Bean("openAiClient")publicChatClientopenAiClient(OpenAiChatModel openAiModel){// 直接把 OpenAI 的底层模型塞给 BuilderreturnChatClient.builder(openAiModel).defaultSystem("你是一个由 GPT-4 驱动的高级分析师").build();}}

2. 业务层的精准注入
在使用时,通过 @Qualifier 明确告诉 Spring 你到底要哪个。

@RestControllerpublicclassMultiModelController{privatefinalChatClient deepseekClient;privatefinalChatClient openAiClient;// 明确告诉 Spring,哪个变量对应哪个 Bean 定制器publicMultiModelController(@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 的废弃与重构时有发生。请大家在实战中时刻关注版本特性。但有了我们上面的“记忆防腐层”等架构理念加持,无论官方怎么变,我们系统核心依然稳如泰山!

Read more

CPU/MCU/SOC/FPGA概念对比

这是一个关于CPU、MCU、SoC和FPGA的详细对比。我们将沿用“引擎到整车”的比喻,并新增“可重构的积木”来帮助您直观理解它们的本质区别、设计哲学和应用场景。 🎯 核心概念比喻 一、核心概念比喻 * CPU:相当于汽车的发动机。它是计算核心,性能强大,但无法独立工作,需要额外配齐主板、内存、硬盘、电源等所有部件才能运行。 * MCU:相当于一辆完整的微型车。它在“发动机”的基础上,集成了小容量的内存、油箱、基础仪表盘和方向盘。你给它接上电池,它就能独立完成简单的驾驶任务,是嵌入式控制的核心。 * SoC:相当于一辆为特定任务设计的特种车辆。它在“微型车”的基础上,还集成了专用设备,如消防车的水泵、救护车的医疗舱。它针对复杂功能(如手机、智能家居)进行深度优化,追求高性能、高集成度和低功耗。 * FPGA:一套“乐高”

无人机电机与电子调速器模块详解

无人机电机与电子调速器模块详解

一、 无刷电机 无人机主要使用无刷直流电机,因为它具有效率高、寿命长、功率密度大、维护简单的优点。 1. 关键参数: 尺寸: 通常以4位数字表示,如 `2207`、`2306`。 前两位:定子( stator )的直径(单位:毫米),如 22mm。 后两位:定子的高度(单位:毫米),如 07mm。 简单理解:尺寸越大,通常扭矩和功率潜力越大,但也更重。 KV值: 最重要的参数之一。指在空载、1伏特电压下,电机每分钟的转速(RPM)。 低KV电机(如 800KV-1500KV):在给定电压下转速较低,但扭矩更大。通常搭配大尺寸螺旋桨,用于大型机架、长途巡航、载重无人机。 高KV电机(如 2000KV-3000KV+

Ubuntu搭建PX4无人机仿真环境(5) —— 仿真环境搭建(以Ubuntu 22.04,ROS2 Humble,Micro XRCE-DDS Agent为例)

Ubuntu搭建PX4无人机仿真环境(5) —— 仿真环境搭建(以Ubuntu 22.04,ROS2 Humble,Micro XRCE-DDS Agent为例)

目录 * 前言 * 1. 准备 * 1.1 下载 PX4 源码 * 方式一: * 方式二: * 1.2 安装仿真依赖 * 1.3 安装 Gazebo * 2. 安装 Micro XRCE-DDS Agent * 3. 编译 PX4 * 4. 通信测试 * 5. 官方 offboard 程序 * 6. offboard 测试 * 参考 前言 本教程基于 ROS2 ,在搭建之前,需要把 ROS2、QGC 等基础环境安装配置完成。但是这块的资料相比较于 ROS1 下的少很多,不利于快速上手和后期开发,小白慎选! 小白必看:

7天精通AI绘画模型训练:Kohya_SS从零到实战全攻略

7天精通AI绘画模型训练:Kohya_SS从零到实战全攻略 【免费下载链接】kohya_ss 项目地址: https://gitcode.com/GitHub_Trending/ko/kohya_ss 还在为AI模型训练的各种复杂参数头疼吗?想不想用最简单的方式定制专属的AI绘画模型?今天我要为你揭秘Kohya_SS这个神器,让你从AI小白秒变训练达人! 为什么说Kohya_SS是AI训练的最佳选择? 想象一下,你只需要点点鼠标,就能完成从数据准备到模型训练的全过程。Kohya_SS就像一个贴心的训练助手,把复杂的命令行操作变成了直观的图形界面。这不仅仅是一个工具,更是通往AI创作自由的钥匙。 三大核心优势让你爱不释手 一键启动的智能界面:告别繁琐的Python命令,双击gui.bat或运行bash gui.sh,浏览器就会自动打开训练控制台。所有参数都有详细的说明和推荐值,新手也能轻松上手。 全流程自动化支持:从图片预处理到模型输出,Kohya_SS提供了完整的工具链。比如,你可以使用dreambooth_folder_creation_gui.py自动整理数据集,