跳到主要内容
基于 Spring AI + DeepSeek 构建企业级 AI Agent 服务与底层原理 | 极客日志
Java AI java
基于 Spring AI + DeepSeek 构建企业级 AI Agent 服务与底层原理 前言:何为 AI Agent 在 AI 应用爆发的今天,市面上充斥着各种 Agent 工具。但作为技术人,我们不仅要会'用',更要懂'如何集成到业务'。基于实战,拆解一个具备'大脑(LLM)、手脚 (Tool Use / Function Calling)、记忆 (Memory)、规划(Planning / ReAct)、系统提示词(System Prompt)'的 Agent 是如何炼成的。…
性能调优 发布于 2026/4/6 更新于 2026/5/23 87K 浏览前言:何为 AI Agent
在 AI 应用爆发的今天,市面上充斥着各种 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 >
<dependency >
<groupId > org.springframework.ai</groupId >
<artifactId > spring-ai-bom</artifactId >
<version > ${spring-ai-version}</version >
<type > pom</type >
< > import
org.springframework.boot
spring-boot-starter-data-redis
scope
</scope >
</dependency >
</dependencies >
</dependencyManagement >
<dependencies >
<dependency >
<groupId >
</groupId >
<artifactId >
</artifactId >
</dependency >
</dependencies >
<properties >
<fastjson2.version > 2.0.47</fastjson2.version >
</properties >
<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: 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 中配置
spring:
ai:
deepseek:
api-key:
chat:
options:
model: deepseek-chat
temperature: 1.0
nacos 中 redis-common.yaml 中配置
spring:
data:
redis:
port: 6379
host:
password:
timeout: 5000ms
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。
@Configuration
public class WeatherToolFunction {
public record Weather (String city) {}
@Bean
@Description("查询今天天气")
public Function<Weather, String> weatherFunction () {
return weather -> {
if ("成都" .equals(weather.city)) return "成都晴,25°C" ;
return "未找到该城市天气信息" ;
};
}
}
@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 函数
定义对话接口 @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();
}
}
访问接口请求 获取今天重庆天气信息,会返回获取不到,因为我们没有配置重庆天气信息
三、企业级全能 Agent:ChatClient 与拔插机制实战 在生产环境下,我们更倾向于使用 ChatClient,因为它在 ChatModel 之上构建了强大的业务闭环 。这里会产生一个疑问:既然刚才用了 DeepSeekChatModel 发起对话,为什么在这儿又要用 ChatClient 呢?
1. ChatClient vs ChatModel 详细对比 维度 ChatModel (底层驱动层) ChatClient (上层应用层) 打比方 JDBC 的 java.sql.Connection MyBatis-Plus 的 LambdaQueryWrapper纯度 极度纯粹,只接收 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 版 (生产推荐方案,下面按照本方案梳理)
@Configuration
public class AiConfig {
@Bean
public ChatMemory chatMemory (StringRedisTemplate messages) {
return new RedisChatMemory (messages, 50 , 7 );
}
}
3. 实现 redis 分布式 Memory 类 RedisChatMemory (继承了 ChatMemory 接口) @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:" ;
@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;
List<MessageDto> dtos = mutableHistory.stream().map(m -> new MessageDto (
m.getMessageType().getValue(),
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 {
List<MessageDto> dtos = JSON.parseArray(jsonStr, MessageDto.class);
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) :我们引入了 fastjson2,以便于更轻量、宽容地处理纯字符串 JSON。我们不存 Message,我们只存极简的 MessageDto 对象结构。
5. 定义并挂载拔插式 memory 接口 接下来,我们在 AgentChatController 中看一下如何挂载这个拔插式的 Memory 接口并发起对话:
@RestController
@RequestMapping("/ai/agent")
public class AgentChatController {
@GetMapping("/chat/memory")
public String chat (@RequestParam String chatId, // 模拟不同用户的独立记忆
@RequestParam String message) {
return chatClient.prompt()
.user(message)
.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")
public class AgentChatController {
private final ChatClient chatClient;
public AgentChatController (ChatClient.Builder builder, ChatMemory chatMemory) {
String systemPrompt = """
你是一个高级电商后台微服务架构的智能运维助手。
你的主要职责是协助开发者和运营人员排查订单流转问题,并提供相关的天气物流建议。
【核心规则】
1. 你的语气必须专业、严谨,像一个资深的 Java 后端架构师,可以适时使用'接口响应'、'兜底策略'等技术术语。
2. 业务边界:如果用户询问订单或天气,请果断调用你拥有的工具获取真实数据。
3. 安全护栏:如果用户询问与技术、订单、天气无关的问题(如娱乐八卦、政治、让你写诗等),你可以基于上下文记忆,礼貌且极其简短地(不超过 1 句话)回应用户的非业务闲聊以保持对话温度,但回应后,必须立刻用专业术语将话题强制拉回订单排查或系统运维上。严禁长篇大论讨论非业务话题。
4. 总结要求:务必言简意赅。
""" ;
this .chatClient = builder
.defaultSystem(systemPrompt)
.defaultToolNames("weatherFunction" , "orderFunction" )
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
}
注:如果设置了系统提示词职责边界,memory 记忆上下文可能会失效,不会回答职责意外的问题。
设置系统边界确实会'框住'记忆的联想能力,这是大模型安全机制的必然代价。各位'架构师',我们要做的就是通过不断打磨 Prompt 的颗粒度 ,在'绝对安全'和'像个人类'之间找那个最完美的平衡点。
实现记忆问答对话 测试系统提示词规定的职责边界是否生效(使用系统提示词)
先问是否可以取订单 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
@Configuration
public class AiConfig {
@Bean("deepseekClient")
public ChatClient deepseekClient (DeepSeekChatModel deepseekModel) {
return ChatClient.builder(deepseekModel)
.defaultSystem("你是一个由 DeepSeek 驱动的助手" )
.build();
}
@Bean("openAiClient")
public ChatClient openAiClient (OpenAiChatModel openAiModel) {
return ChatClient.builder(openAiModel)
.defaultSystem("你是一个由 GPT-4 驱动的高级分析师" )
.build();
}
}
在使用时,通过 @Qualifier 明确告诉 Spring 你到底要哪个。
@RestController
public class MultiModelController {
private final ChatClient deepseekClient;
private final ChatClient openAiClient;
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 对象序列化反序列化危机 ……
注意
版本适配提示 :Spring AI 目前尚处于版本快速变动的成长期,核心 API 的废弃与重构时有发生。请大家在实战中时刻关注版本特性。但有了我们上面的'记忆防腐层'等架构理念加持,无论官方怎么变,我们系统核心依然稳如泰山!
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
RSA密钥对生成器 生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
Mermaid 预览与可视化编辑 基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online