31 KiB
Week 10:AI Agent 核心概念 —— 从聊天到干活
学习周期:7 天 每日用时:2-3 小时 最终产出:一个具备工具调用、RAG 检索、持久化记忆、Prompt 模板管理的 AI Agent 系统
前置准备
| 事项 | 说明 |
|---|---|
| Week 9 代码可用 | 确保 week9 项目能正常启动和对话 |
| DeepSeek API Key | Week 9 已在用,继续使用 |
| Redis 安装 | Day 5 需要(github.com/tporadowski/redis Windows 版) |
| Ollama 安装(方案 B) | Day 6 需要(ollama.com → 下载安装 → ollama pull nomic-embed-text) |
关于 Embedding 方案:Day 6 的 RAG 有两种 Embedding 方案。方案 B(Ollama 本地) 免费、离线、零成本,是默认选择。方案 A(阿里云百炼) 需要注册获取 Key,但有免费额度,适合网络受限无法下载 Ollama 的情况。教案中同时覆盖两种方案。
启动方式
# 1. 确保 Redis 已启动(Day 5 需要,其他 Day 可先注释掉 Redis 依赖)
# 2. 确保 Ollama 已启动 + nomic-embed-text 已下载(Day 6 需要)
# 3. 修改 application.yml 中的 api-key (DeepSeek)
# 4. IDEA 中运行 Week10Application.main()
# 5. 浏览器访问 http://localhost:8080/agent.html
Week 9 → Week 10:升级了什么?
Week 9: 聊天机器人
POST /api/chat → 同步/流式对话 → AI 陪你聊天
Week 10: AI Agent
POST /api/agent → 工具调用 → AI 能查天气、算数学、搜信息
POST /api/rag/query → 知识库检索 → AI 能基于你的文档回答问题
Redis 持久化 → 记忆不丢 → 重启后对话历史还在
Prompt 模板管理 → 角色预设 → 一键切换 AI 人设
Day 1:Agent 核心概念(ReAct / Plan-Execute)
什么是 AI Agent?
AI Agent(智能体)= 大模型 + 工具 + 决策循环。和普通聊天机器人的区别:
| 聊天机器人 | AI Agent | |
|---|---|---|
| 能力 | 只回答已知知识 | 能调用外部工具 |
| 记忆 | 单次对话 | 持久化 + 多轮 |
| 行为 | 被动回复 | 主动规划 + 执行 |
| 边界 | 模型训练数据 | 训练数据 + 实时信息 |
Agent 的两个核心范式
1. ReAct(Reasoning + Acting,推理-行动循环)
用户: "北京今天天气如何?"
┌─ Thought(思考): 用户想知道北京的天气,我需要调用天气工具
├─ Action(行动): getWeather("北京")
├─ Observation(观察): {"city":"北京","weather":"晴","temp":25}
├─ Thought(思考): 获得了天气信息,可以回答了
└─ Answer(回答): "北京今天晴天,气温 25°C..."
ReAct 是当前最主流的 Agent 范式,Spring AI 的 @Tool 机制本质上就是 ReAct。
2. Plan-Execute(规划-执行)
用户: "帮我做一个技术调研:比较 Spring AI 和 LangChain4j"
┌─ Plan(规划):
│ 1. 搜索 Spring AI 最新文档
│ 2. 搜索 LangChain4j 最新文档
│ 3. 对比两者的 API 设计
│ 4. 写总结报告
├─ Execute Step 1 → 获取结果
├─ Execute Step 2 → 获取结果
├─ Execute Step 3 → 对比分析
└─ Execute Step 4 → 输出报告
Plan-Execute 适合复杂多步任务,需要更强的规划能力。Spring AI 暂未内置此模式,但可以通过多轮 Agent 调用手动实现。
动手 → 理解
- 用 ChatGPT / DeepSeek 网页版,尝试问一个需要"外部信息"的问题(如"今天天气"),观察它如何回答——要么说不知道,要么编造答案
- 再问一个需要"实时信息"的问题:对比 ChatGPT 网页版(能搜索)和 API 版(无工具)的回答差异
- 在纸上画出 ReAct 循环图,尝试把"帮我查北京天气,如果下雨就提醒带伞,如果晴天就推荐去公园"这个任务分解为 ReAct 步骤
思考题
如果模型没有工具调用能力,用户问"今天天气如何?"——模型会怎么回答?为什么会有"幻觉"?工具调用如何解决幻觉问题?
Day 2:Function Calling 原理 —— 模型如何"调用"工具?
核心问题
模型不能真的"调用"你的 Java 方法。整个过程是:
你告诉模型"有哪些工具可用"(JSON Schema)
→ 模型决定"需要调用哪个工具"(返回 JSON)
→ 你的代码解析 JSON → 执行对应方法
→ 把结果返回给模型
→ 模型基于结果生成自然语言回答
工具定义(Tools Definition)—— 告诉模型你能做什么
发送给模型的请求中包含一个 tools 数组:
{
"model": "deepseek-chat",
"messages": [{"role": "user", "content": "北京今天天气如何?"}],
"tools": [
{
"type": "function",
"function": {
"name": "getWeather",
"description": "获取指定城市的当前天气信息",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如'北京'、'上海'"
}
},
"required": ["city"]
}
}
}
]
}
模型的响应 —— 不直接回答,而是说"请调这个工具"
{
"choices": [{
"finish_reason": "tool_calls",
"message": {
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": {
"name": "getWeather",
"arguments": "{\"city\": \"北京\"}"
}
}]
}
}]
}
关键:finish_reason 是 tool_calls 而不是 stop——模型没有生成文本,而是请求调用工具。
第二轮:把工具结果喂回去
{
"model": "deepseek-chat",
"messages": [
{"role": "user", "content": "北京今天天气如何?"},
{
"role": "assistant",
"content": null,
"tool_calls": [{"id": "call_abc123", "type": "function", "function": {"name": "getWeather", "arguments": "{\"city\": \"北京\"}"}}]
},
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "{\"city\": \"北京\", \"weather\": \"晴\", \"temperature\": 25}"
}
]
}
这一次 finish_reason = stop,模型返回自然语言:"北京今天晴天,气温 25°C,非常适合户外活动。"
动手 → 理解
- 用 Postman,给 DeepSeek API 发送一个带
tools定义的请求(复制上面的 JSON,替换 API Key) - 观察返回的
finish_reason是tool_calls不是stop - 手动构造"第二轮请求":把 tool 执行结果放到 messages 里,再次调用,观察模型如何基于结果生成回答
- 试试发送两个工具定义(weather + calculator),同一个用户问题,观察模型如何选择
思考题
如果模型同时定义了 WeatherTool 和 CalculatorTool,用户问"1+1等于几"——模型凭什么知道该选 CalculatorTool 而不是 WeatherTool?(答案:
description字段是唯一的"说明书")
Day 3:Spring AI Function Calling —— 第一个 Tool
Spring AI 的 @Tool 注解
Spring AI 1.0.6 提供了 @Tool 注解,让你用纯 Java 定义工具:
@Component
public class WeatherTool {
@Tool(description = "获取指定城市的当前天气信息,包括天气状况和温度")
public String getWeather(@ToolParam(description = "城市名称,如'北京'、'上海'") String city) {
// 模拟天气数据(生产环境替换为真实 API 调用)
var weatherData = Map.of(
"北京", "晴,25°C",
"上海", "多云,28°C",
"深圳", "阵雨,30°C"
);
return weatherData.getOrDefault(city, "未找到该城市天气信息");
}
}
Spring AI 会自动:
- 从
@Tool.description生成工具定义(JSON Schema) - 在 ChatClient 发送请求时附上工具列表
- 解析模型的
tool_calls响应 - 执行对应的方法
- 把结果自动返回给模型
- 拿到最终的自然语言回答
你只需要:用 @Tool 写方法 + 注册到 ChatClient。
注册工具到 ChatClient
@Bean
public ChatClient chatClient(ChatModel chatModel, Object weatherTool, ...) {
return ChatClient.builder(chatModel)
.defaultSystem("你是一个 AI Agent 助手...")
.defaultTools(weatherTool, calculatorTool, searchTool) // 注册所有工具
.defaultAdvisors(...)
.build();
}
AgentService:让模型自动选择工具
@Service
public class AgentService {
private final ChatClient chatClient;
public Map<String, Object> execute(String task, String conversationId) {
String reply = chatClient.prompt()
.user(task)
.advisors(a -> a.param(CONVERSATION_ID, convId))
.call()
.content(); // 模型自动判断是否调用工具、调用哪个工具
return Map.of("reply", reply, "conversationId", convId);
}
}
你没写任何"if 用户说天气 then 调 WeatherTool"的逻辑。模型自己决定——这就是 Agent 的智能所在。
动手 → 理解
- 阅读
tool/WeatherTool.java——理解@Tool和@ToolParam的用法 - 阅读
AIConfig.java中的defaultTools(weatherTool, calculatorTool, searchTool)注册 - 阅读
service/AgentService.java——对比ChatService.java,有什么区别? - 启动项目,浏览器访问
http://localhost:8080/agent.html - 勾选"Agent 模式",发送:"北京今天天气如何?"
- 观察日志中是否有工具调用记录
- 用 Postman 测试:
POST /api/agentBody:{"message": "深圳天气怎么样?"} - 故意问一个 WeatherTool 不支持的城市(如"拉萨"),观察返回
核心文件
| 文件 | 作用 |
|---|---|
tool/WeatherTool.java |
@Tool 定义:天气查询 |
config/AIConfig.java |
defaultTools 注册 |
service/AgentService.java |
Agent 入口:execute() / executeStream() |
思考题
如果你把 WeatherTool 的
@Tool(description = "...")改成description = "获取当前时间",模型调用这个工具时会发生什么?提示:description 是模型唯一的判断依据。
Day 4:多工具协作 —— 让 Agent 更强大
再加两个工具
CalculatorTool
@Component
public class CalculatorTool {
@Tool(description = "执行数学计算,支持加减乘除和括号。示例: '123 * 456'")
public double calculate(@ToolParam(description = "数学表达式") String expression) {
// 使用 javax.script.ScriptEngine 安全求值
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
return ((Number) engine.eval(expression)).doubleValue();
}
}
SearchTool
@Component
public class SearchTool {
@Tool(description = "联网搜索技术信息,返回相关结果")
public String search(@ToolParam(description = "搜索关键词") String query) {
// 模拟搜索结果(生产环境替换为真实搜索 API)
var results = Map.of(
"spring ai", "Spring AI 1.0.6 是面向 Java 的 AI 框架...",
"java 21", "Java 21 是 LTS 版本,引入虚拟线程...",
"deepseek", "DeepSeek 是深度求索开发的大语言模型..."
);
return results.getOrDefault(query.toLowerCase(), "未找到相关信息");
}
}
多工具协作的关键:让模型自己选
你没有写规则说"当用户问数学时用 CalculatorTool"。模型通过 tools 的 description 自行判断。核心优势:
用户: "北京天气怎么样?顺便帮我算一下 999 * 888"
模型思考过程(内部):
→ 用户问了两个问题:天气 + 计算
→ 查天气 → 需要调用 WeatherTool
→ 数学运算 → 需要调用 CalculatorTool
→ 先取天气结果,再取计算结果,合并回答
Agent 模式 vs 普通聊天模式
| 普通聊天(/api/chat) | Agent 模式(/api/agent) | |
|---|---|---|
| 工具 | 不注册工具 | 注册所有 @Tool |
| 能力 | 只用训练数据 | 可获取外部信息 |
| 适用 | 闲聊、解释概念 | 查天气、计算、搜索 |
| 实现 | ChatService | AgentService(实际上底层一样,只是 ChatClient 配置不同) |
动手 → 理解
- 阅读
tool/CalculatorTool.java和tool/SearchTool.java - 阅读
service/AgentService.java的execute()—— 注意到没有?代码和 Day 3 一模一样!因为模型自己决定调哪个工具 - 启动项目,在 agent.html 中测试多工具协作:
- 发送:"北京天气怎么样?顺便算 123 * 456"
- 发送:"帮我搜索 Spring AI 是什么,再总结成一句话"
- 在日志中观察:是否同时触发了两个工具?
- 流式模式测试:勾选"流式输出",再次发送多工具请求,观察打字机效果
- 对比
/api/agent和/api/chat的响应差异
核心文件
| 文件 | 作用 |
|---|---|
tool/CalculatorTool.java |
@Tool:数学计算 |
tool/SearchTool.java |
@Tool:搜索(模拟) |
service/AgentService.java |
executeStream() 流式 Agent |
思考题
如果问"东京天气如何?帮我换算一下 32°C 是多少华氏度",模型会调用哪些工具?调用顺序如何?描述完整的 ReAct 循环。
Day 5:Redis 记忆持久化 —— 重启对话不丢失
Week 9 的局限
Week 9 使用 InMemoryChatMemoryRepository(JVM 内存)存储对话历史。一旦重启应用,所有对话记忆丢失。
Spring AI 的 ChatMemory 双层架构
MessageWindowChatMemory(策略层:滑动窗口,保留最近 30 条)
↓ 委托存储
ChatMemoryRepository(存储层:接口)
├─ InMemoryChatMemoryRepository(默认,内存 → 重启丢失)
├─ JdbcChatMemoryRepository(JDBC 数据库)
├─ CassandraChatMemoryRepository(Cassandra)
└─ 自定义:RedisChatMemoryRepository ← 本周实现
自定义 RedisChatMemoryRepository
Spring AI 1.0.6 没有内置 Redis 实现,这反而是好的——你可以学习如何扩展框架:
@Component
public class RedisChatMemoryRepository implements ChatMemoryRepository {
private final RedisTemplate<String, Object> redisTemplate;
private static final String KEY_PREFIX = "chat:memory:";
private static final String ID_SET_KEY = "chat:conversation:ids";
@Override
public List<Message> findByConversationId(String id) {
// LRANGE 读取整个 List → JSON 反序列化 → List<Message>
var jsonList = redisTemplate.opsForList().range(KEY_PREFIX + id, 0, -1);
// ... Jackson 反序列化
}
@Override
public void saveAll(String id, List<Message> messages) {
// DEL 清空旧数据 → RPUSH 逐条写入 JSON → SADD 记录 ID
redisTemplate.delete(KEY_PREFIX + id);
messages.forEach(msg -> redisTemplate.opsForList().rightPush(KEY_PREFIX + id, toJson(msg)));
redisTemplate.opsForSet().add(ID_SET_KEY, id);
}
@Override
public void deleteByConversationId(String id) { /* DEL + SREM */ }
@Override
public List<String> findConversationIds() { /* SMEMBERS */ }
}
AIConfig 中的无缝切换
// Week 9(内存版,重启丢失):
@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(new InMemoryChatMemoryRepository())
.maxMessages(20).build();
}
// Week 10(Redis 持久化版,重启保留):
@Bean
public ChatMemory chatMemory(ChatMemoryRepository repository) { // 注入我们的 Redis 实现
return MessageWindowChatMemory.builder()
.chatMemoryRepository(repository) // 换成 Redis
.maxMessages(30).build();
}
其他代码完全不用改——这就是面向接口编程的威力。
Jackson 序列化的 Message 格式
Message 接口的常见实现是 AssistantMessage、UserMessage。Jackson 需要类型信息来正确反序列化。在 RedisConfig 中配置:
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
// 配置 ObjectMapper 以支持多态反序列化
ObjectMapper mapper = new ObjectMapper();
mapper.activateDefaultTyping(BasicPolymorphicTypeValidator.builder().build(),
ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
template.setValueSerializer(serializer);
// ...
}
动手 → 理解
- 安装 Redis(Windows 版从 GitHub Release 下载,或使用 WSL/Docker)
- 启动 Redis:
redis-server.exe(或redis-server) - 修改
application.yml中的 Redis 密码(如果有的话) - 阅读
memory/RedisChatMemoryRepository.java——理解 4 个接口方法的实现 - 阅读
config/RedisConfig.java——理解 JSON 序列化配置 - 阅读
config/AIConfig.java中的chatMemory()Bean——注意ChatMemoryRepository是怎么注入的 - 启动项目,发送几轮对话
- 用
redis-cli查看数据:redis-cli SMEMBERS chat:conversation:ids → 查看所有会话 ID LRANGE chat:memory:default 0 -1 → 查看 default 会话的所有消息 - 重启项目 → 访问
GET /api/chat/history→ 确认记忆还在! - 删除某个会话 → 观察 Redis 中对应的 Key 被删除
核心文件
| 文件 | 作用 |
|---|---|
memory/RedisChatMemoryRepository.java |
实现 ChatMemoryRepository 接口 |
config/RedisConfig.java |
RedisTemplate 序列化配置 |
config/AIConfig.java |
ChatMemory Bean 改造(注入 Repository) |
思考题
如果把
maxMessages(30)改成maxMessages(5),再发 10 轮对话,Redis 里会存几条消息?滑动窗口是怎么删除旧消息的?验证方法:用LLEN chat:memory:xxx检查 List 长度。
Day 6:RAG(检索增强生成)—— 让 AI 基于你的知识库回答
什么是 RAG?
RAG = Retrieval-Augmented Generation(检索增强生成)。核心思想:
用户问题 → 向量检索(从知识库找相关文档)→ 把文档拼接到 Prompt → 模型基于文档生成回答
为什么需要 RAG?
- LLM 训练数据有截止日期,不知道新信息
- LLM 不知道你公司的内部文档
- RAG 可以有效减少幻觉(模型瞎编)
RAG 核心流程
┌─────────────────────────────────────────────────────┐
│ 1. 文档入库(离线) │
│ 文档 → 切分 → Embedding(转向量)→ 存入 VectorStore │
│ │
│ 2. 问答(在线) │
│ 用户问题 → Embedding(转向量) │
│ → VectorStore.similaritySearch(找最相似的) │
│ → 拼接 Context + 问题 → ChatModel 生成回答 │
└─────────────────────────────────────────────────────┘
Embedding(文本转向量)是什么?
"今天天气真好" → Embedding 模型 → [0.12, -0.34, 0.78, ..., 0.05](一个 768 维的浮点数数组)
相似的文本,向量距离近。例如:
- "今天天气真好" 和 "今天阳光明媚" → 余弦相似度 0.92(很近)
- "今天天气真好" 和 "Spring AI 是什么" → 余弦相似度 0.15(很远)
Embedding 方案选择
方案 B:Ollama 本地(默认,免费)
# 安装 Ollama
# Mac: brew install ollama
# Windows: 从 ollama.com 下载安装包
# 拉取嵌入模型(~270MB)
ollama pull nomic-embed-text
# 验证
ollama list
# NAME ID SIZE MODIFIED
# nomic-embed-text:latest xxxxxxxx 274 MB ...
Ollama 默认监听 http://localhost:11434。
方案 A:阿里云百炼(备选,云端)
如果你无法下载 Ollama(网络受限),用阿里云百炼的 text-embedding-v2:
- 注册 bailian.console.aliyun.com
- 开通模型服务 → 获取 API Key
- 修改
application.yml:
spring.ai.openai.embedding:
api-key: ${EMBEDDING_API_KEY:sk-your-bailian-key}
base-url: https://dashscope.aliyuncs.com/compatible-mode
options:
model: text-embedding-v2
AIConfig 中的双 Provider 设计
Chat 和 Embedding 用不同的 API Provider:
// Chat → DeepSeek(由 spring.ai.openai.* 自动配置 ChatModel)
// Embedding → Ollama 本地(手动创建 EmbeddingModel)
@Bean
public EmbeddingModel embeddingModel(
@Value("${spring.ai.openai.embedding.base-url}") String baseUrl,
@Value("${spring.ai.openai.embedding.api-key}") String apiKey) {
var api = OpenAiApi.builder()
.baseUrl(baseUrl.endsWith("/v1") ? baseUrl : baseUrl + "/v1")
.apiKey(apiKey)
.build();
return new OpenAiEmbeddingModel(api);
}
SimpleVectorStore —— 零依赖的向量存储
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
return SimpleVectorStore.builder(embeddingModel).build();
}
SimpleVectorStore 纯内存存储,免外部服务。生产环境可替换为 PgVectorStore、RedisVectorStore 等。
QuestionAnswerAdvisor —— 自动 RAG
在 ChatClient 的 Advisor 链中添加 QuestionAnswerAdvisor:
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(),
new QuestionAnswerAdvisor(vectorStore) // 自动检索 + 增强
)
效果:每次对话时,Advisor 自动从 VectorStore 检索相关文档,拼接到 Prompt 中。你的代码不需要手动调用 vectorStore.similaritySearch()——Advisor 帮你做了。
KnowledgeDataLoader:启动时加载知识库
@Component
public class KnowledgeDataLoader {
@PostConstruct
public void load() {
var docs = List.of(
new Document("Spring AI 是一个面向 AI 工程的 Spring 框架...",
Map.of("source", "spring-ai-overview")),
new Document("RAG(检索增强生成)是结合检索和生成的技术...",
Map.of("source", "rag-concept"))
);
vectorStore.add(docs);
}
}
动手 → 理解
- 安装 Ollama →
ollama pull nomic-embed-text(如果不方便下载,切换到方案 A) - 阅读
config/AIConfig.java:embeddingModel()— 手动创建,指向 OllamavectorStore()— SimpleVectorStorechatClient()— 新增QuestionAnswerAdvisor
- 阅读
rag/KnowledgeDataLoader.java——8 条示例知识库文档 - 阅读
service/RAGService.java——search()方法(纯检索)+addDocuments()方法 - 启动项目,观察日志:
EmbeddingModel created with base-url: http://localhost:11434Loaded 8 knowledge documents into VectorStore
- 在 agent.html 中测试 RAG:
- 发送:"什么是 Spring AI?"
- 发送:"RAG 是什么?"
- 对比:勾掉"Agent 模式"用普通聊天问同样的问题,回答有什么不同?
- 用 Postman 测试纯检索(不生成):
POST /api/rag/searchBody:{"question": "什么是 Spring AI?", "topK": 3}- 观察返回的文档片段和相似度分数
核心文件
| 文件 | 作用 |
|---|---|
config/AIConfig.java |
EmbeddingModel + VectorStore + QuestionAnswerAdvisor |
rag/KnowledgeDataLoader.java |
启动时加载知识库文档 |
service/RAGService.java |
RAG 检索 + 问答 |
application.yml |
embedding 配置(方案 B 默认,方案 A 注释) |
思考题
如果 QuestionAnswerAdvisor 从 VectorStore 检索到的文档和用户问题完全不相关(相似度很低),会发生什么?如何设置
similarityThreshold来过滤低质量结果?
Day 7:Prompt 模板管理 —— 一键切换 AI 人设
为什么需要 Prompt 模板?
System Prompt 写死在 AIConfig 中 → 每次换角色要改代码 + 重启 → 不灵活
Prompt 模板化 → 从文件加载 → 前端选择 → 一键切换角色
模板管理设计
resources/prompts/
├── java-expert.txt → 资深 Java 技术专家
├── code-reviewer.txt → 严格的代码审查专家
└── translator.txt → 专业中英文翻译
模板中使用 {{key}} 占位符:
你是一位资深 Java 技术专家,拥有 15 年以上的企业级开发经验。
请遵循以下原则回答用户的问题:
1. 代码示例: 使用 Java 21+ 语法
2. 最佳实践: 强调 SOLID 原则、设计模式
...
PromptTemplateService
@Service
public class PromptTemplateService {
@PostConstruct
public void loadTemplates() throws IOException {
// 扫描 classpath:/prompts/*.txt
// 文件名 = 模板名,文件内容 = 模板内容
}
public List<Map<String, String>> list() { ... } // 列出所有模板
public String getTemplate(String name) { ... } // 获取模板内容
public String apply(String name, Map<String, String> params) { ... } // 替换占位符
}
使用模板的对话
POST /api/chat/with-template
Body: {
"message": "请写一个 Spring Boot 的全局异常处理类",
"templateName": "java-expert",
"conversationId": "default"
}
后端处理:
- 从文件加载模板内容
- 替换
{{key}}占位符 - 拼接:
模板内容 + "---\n用户消息: " + message - 作为
user()参数传给 ChatClient
前端:agent.html 控制台
增强功能(对比 Week 9 的 chat.html):
| 功能 | Week 9 | Week 10 |
|---|---|---|
| 工具可视化 | — | 侧边栏显示可用工具列表 |
| RAG 来源 | — | 可显示检索来源(预留) |
| 模板选择 | — | 下拉选择 + 内容预览 |
| Agent/聊天切换 | — | 侧边栏开关 |
| 流式/非流式切换 | 有 | 有 |
| 会话管理 | 有 | 有 |
动手 → 理解
- 阅读
resources/prompts/下的 3 个模板文件 - 阅读
service/PromptTemplateService.java——理解@PostConstruct加载和占位符替换 - 阅读
controller/ChatController.java的GET /api/prompts、POST /api/chat/with-template - 启动项目,浏览器访问
http://localhost:8080/agent.html - 在侧边栏下拉选择"java-expert",观察预览内容
- 发送:"请写一个 Spring Boot 的全局异常处理"——观察回复是否体现出"Java 专家"风格
- 切换到"code-reviewer",发送一段代码,观察审查风格
- 切换到"translator",观察翻译风格
- 关掉模板(选择"— 不使用模板 —"),同样的问题再问一次——感受差异
核心文件
| 文件 | 作用 |
|---|---|
resources/prompts/java-expert.txt |
模板:Java 专家 |
resources/prompts/code-reviewer.txt |
模板:代码审查 |
resources/prompts/translator.txt |
模板:翻译 |
service/PromptTemplateService.java |
模板加载 + 替换 |
思考题
现在的模板替换是最简单的字符串替换。如果模板中需要支持条件分支(如
{{#if userLevel}}...{{/if}}),你会怎么设计?这引出了真正的模板引擎(如 Mustache、Handlebars)——Spring AI 也有内置的 PromptTemplate 类,去看看它的源码。
Week 10 总结
技术栈全景
浏览器 (agent.html)
│ fetch + SSE
▼
ChatController
├─ POST /api/chat ──────────→ ChatService ──→ ChatClient (普通聊天)
├─ POST /api/agent ─────────→ AgentService ──→ ChatClient + Tools (Agent)
├─ POST /api/rag/search ────→ RAGService ────→ VectorStore (检索)
├─ POST /api/chat/with-template → PromptTemplateService (模板)
└─ GET/DELETE /api/chat/history → ChatService ──→ Redis (持久化)
│
▼
ChatClient (Spring AI)
├─ ChatModel ────→ DeepSeek API (对话)
├─ Tools ────────→ Weather / Calculator / Search
├─ Advisors ────→ ChatMemory (Redis) + QuestionAnswerAdvisor (RAG)
└─ EmbeddingModel → Ollama / 百炼 (向量化)
能力清单
| 维度 | 掌握内容 |
|---|---|
| Agent 概念 | ReAct 范式、Plan-Execute 范式、Tool Calling 流程 |
| Function Calling | @Tool 注解、工具定义 JSON Schema、多工具协作 |
| RAG | EmbeddingModel、VectorStore、QuestionAnswerAdvisor、相似度检索 |
| 向量存储 | SimpleVectorStore、相似度阈值、手动创建 EmbeddingModel |
| 记忆持久化 | ChatMemoryRepository 接口、Redis 自定义实现、双层架构 |
| Prompt 模板 | 文件加载、占位符替换、多角色切换 |
| 架构设计 | 双 Provider(Chat + Embedding 分离)、接口抽象、插件扩展 |
vs Week 9
| Week 9 | Week 10 | |
|---|---|---|
| AI 角色 | 聊天机器人 | AI Agent |
| 核心接口 | ChatClient.call() | ChatClient.call() + Tools |
| 外部能力 | 无 | 天气、计算、搜索 |
| 记忆 | 内存(重启丢失) | Redis(持久化) |
| 知识 | 训练数据 | 训练数据 + 自定义知识库 |
| 角色切换 | 改代码 | 前端动态切换模板 |
| Embedding | 不需要 | Ollama / 百炼 |
常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
| Agent 不调用工具 | 工具未注册到 defaultTools | 检查 AIConfig 中 .defaultTools(...) 是否包含该工具 |
| Agent 调错工具 | @Tool description 描述不清晰 | 改写 description,让模型更能区分 |
| RAG 检索结果为空 | 1. Ollama 未安装 2. 未 pull 模型 | ollama list 检查是否有 nomic-embed-text |
| Redis 连接失败 | Redis 未启动 | 启动 redis-server,检查端口 6379 |
| 启动报 Redis 错误 | 未安装 Redis | Day 5 前可临时注释 Redis 依赖 |
| RAG 检索结果不相关 | 知识库文档太少 | 增加更多覆盖不同主题的文档 |
| Embedding 返回 404 | base-url 不对 | 确认 Ollama URL 为 http://localhost:11434/v1 |
| 模板选择后没效果 | 前端只是拼接,ChatClient 没用到模板 | 检查 /api/chat/with-template 是否正常拼接 |
| mvn compile 失败 | BOM 或依赖版本冲突 | 运行 mvn clean compile,确认 pom.xml 中无冲突 |
本周核心:你把一个"只会聊天"的 AI 升级成了"能干活"的 Agent。从 Function Calling 让模型自主调用工具,到 RAG 让模型基于你的知识库回答问题,再到 Redis 持久化让记忆永不丢失,最后用 Prompt 模板实现一键切换角色。你掌握了 AI Agent 的四大核心能力——工具、检索、记忆、模板。这四块积木可以组合出无数种 Agent 应用。
下一阶段:Week 11 — 高级 RAG + Agent 实战。在 Agent 基础上,加入多模态(图片识别)、高级 RAG 策略(Query 重写、混合检索、结果重排序)、Agent 记忆管理等进阶内容。