This commit is contained in:
2026-04-30 16:08:39 +08:00
parent f95aa18724
commit 1503b26959
41 changed files with 4503 additions and 0 deletions

856
week10/教案.md Normal file
View File

@@ -0,0 +1,856 @@
# Week 10AI Agent 核心概念 —— 从聊天到干活
> **学习周期**7 天
> **每日用时**2-3 小时
> **最终产出**一个具备工具调用、RAG 检索、持久化记忆、Prompt 模板管理的 AI Agent 系统
---
## 前置准备
| 事项 | 说明 |
|------|------|
| Week 9 代码可用 | 确保 `week9` 项目能正常启动和对话 |
| DeepSeek API Key | Week 9 已在用,继续使用 |
| Redis 安装 | Day 5 需要([github.com/tporadowski/redis](https://github.com/tporadowski/redis) Windows 版) |
| Ollama 安装(方案 B | Day 6 需要([ollama.com](https://ollama.com) → 下载安装 → `ollama pull nomic-embed-text` |
> **关于 Embedding 方案**Day 6 的 RAG 有两种 Embedding 方案。**方案 BOllama 本地)** 免费、离线、零成本,是默认选择。**方案 A阿里云百炼** 需要注册获取 Key但有免费额度适合网络受限无法下载 Ollama 的情况。教案中同时覆盖两种方案。
---
## 启动方式
```bash
# 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 1Agent 核心概念ReAct / Plan-Execute
### 什么是 AI Agent
AI Agent智能体= **大模型 + 工具 + 决策循环**。和普通聊天机器人的区别:
| | 聊天机器人 | AI Agent |
|------|-----------|----------|
| 能力 | 只回答已知知识 | 能调用外部工具 |
| 记忆 | 单次对话 | 持久化 + 多轮 |
| 行为 | 被动回复 | 主动规划 + 执行 |
| 边界 | 模型训练数据 | 训练数据 + 实时信息 |
### Agent 的两个核心范式
#### 1. ReActReasoning + 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 调用手动实现。
### 动手 → 理解
1. 用 ChatGPT / DeepSeek 网页版,尝试问一个需要"外部信息"的问题(如"今天天气"),观察它如何回答——要么说不知道,要么编造答案
2. 再问一个需要"实时信息"的问题:对比 ChatGPT 网页版(能搜索)和 API 版(无工具)的回答差异
3. 在纸上画出 ReAct 循环图,尝试把"帮我查北京天气,如果下雨就提醒带伞,如果晴天就推荐去公园"这个任务分解为 ReAct 步骤
### 思考题
> 如果模型没有工具调用能力,用户问"今天天气如何?"——模型会怎么回答?为什么会有"幻觉"?工具调用如何解决幻觉问题?
---
## Day 2Function Calling 原理 —— 模型如何"调用"工具?
### 核心问题
模型不能真的"调用"你的 Java 方法。整个过程是:
```
你告诉模型"有哪些工具可用"JSON Schema
→ 模型决定"需要调用哪个工具"(返回 JSON
→ 你的代码解析 JSON → 执行对应方法
→ 把结果返回给模型
→ 模型基于结果生成自然语言回答
```
### 工具定义Tools Definition—— 告诉模型你能做什么
发送给模型的请求中包含一个 `tools` 数组:
```json
{
"model": "deepseek-chat",
"messages": [{"role": "user", "content": "北京今天天气如何?"}],
"tools": [
{
"type": "function",
"function": {
"name": "getWeather",
"description": "获取指定城市的当前天气信息",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如'北京'、'上海'"
}
},
"required": ["city"]
}
}
}
]
}
```
### 模型的响应 —— 不直接回答,而是说"请调这个工具"
```json
{
"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`——模型没有生成文本,而是请求调用工具。
### 第二轮:把工具结果喂回去
```json
{
"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非常适合户外活动。"
### 动手 → 理解
1. 用 Postman给 DeepSeek API 发送一个带 `tools` 定义的请求(复制上面的 JSON替换 API Key
2. 观察返回的 `finish_reason``tool_calls` 不是 `stop`
3. 手动构造"第二轮请求":把 tool 执行结果放到 messages 里,再次调用,观察模型如何基于结果生成回答
4. 试试发送两个工具定义weather + calculator同一个用户问题观察模型如何选择
### 思考题
> 如果模型同时定义了 WeatherTool 和 CalculatorTool用户问"1+1等于几"——模型凭什么知道该选 CalculatorTool 而不是 WeatherTool答案`description` 字段是唯一的"说明书"
---
## Day 3Spring AI Function Calling —— 第一个 Tool
### Spring AI 的 @Tool 注解
Spring AI 1.0.6 提供了 `@Tool` 注解,让你用纯 Java 定义工具:
```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 会自动:
1.`@Tool.description` 生成工具定义JSON Schema
2. 在 ChatClient 发送请求时附上工具列表
3. 解析模型的 `tool_calls` 响应
4. 执行对应的方法
5. 把结果自动返回给模型
6. 拿到最终的自然语言回答
你只需要:用 `@Tool` 写方法 + 注册到 ChatClient。
### 注册工具到 ChatClient
```java
@Bean
public ChatClient chatClient(ChatModel chatModel, Object weatherTool, ...) {
return ChatClient.builder(chatModel)
.defaultSystem("你是一个 AI Agent 助手...")
.defaultTools(weatherTool, calculatorTool, searchTool) // 注册所有工具
.defaultAdvisors(...)
.build();
}
```
### AgentService让模型自动选择工具
```java
@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 的智能所在。
### 动手 → 理解
1. 阅读 `tool/WeatherTool.java`——理解 `@Tool``@ToolParam` 的用法
2. 阅读 `AIConfig.java` 中的 `defaultTools(weatherTool, calculatorTool, searchTool)` 注册
3. 阅读 `service/AgentService.java`——对比 `ChatService.java`,有什么区别?
4. 启动项目,浏览器访问 `http://localhost:8080/agent.html`
5. 勾选"Agent 模式",发送:"北京今天天气如何?"
6. 观察日志中是否有工具调用记录
7. 用 Postman 测试:`POST /api/agent` Body: `{"message": "深圳天气怎么样?"}`
8. 故意问一个 WeatherTool 不支持的城市(如"拉萨"),观察返回
### 核心文件
| 文件 | 作用 |
|------|------|
| `tool/WeatherTool.java` | @Tool 定义:天气查询 |
| `config/AIConfig.java` | defaultTools 注册 |
| `service/AgentService.java` | Agent 入口execute() / executeStream() |
### 思考题
> 如果你把 WeatherTool 的 `@Tool(description = "...")` 改成 `description = "获取当前时间"`模型调用这个工具时会发生什么提示description 是模型**唯一的判断依据**。
---
## Day 4多工具协作 —— 让 Agent 更强大
### 再加两个工具
#### CalculatorTool
```java
@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
```java
@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 配置不同) |
### 动手 → 理解
1. 阅读 `tool/CalculatorTool.java``tool/SearchTool.java`
2. 阅读 `service/AgentService.java``execute()` —— 注意到没有?代码和 Day 3 一模一样!因为模型自己决定调哪个工具
3. 启动项目,在 agent.html 中测试多工具协作:
- 发送:"北京天气怎么样?顺便算 123 * 456"
- 发送:"帮我搜索 Spring AI 是什么,再总结成一句话"
4. 在日志中观察:是否同时触发了两个工具?
5. 流式模式测试:勾选"流式输出",再次发送多工具请求,观察打字机效果
6. 对比 `/api/agent``/api/chat` 的响应差异
### 核心文件
| 文件 | 作用 |
|------|------|
| `tool/CalculatorTool.java` | @Tool:数学计算 |
| `tool/SearchTool.java` | @Tool:搜索(模拟) |
| `service/AgentService.java` | executeStream() 流式 Agent |
### 思考题
> 如果问"东京天气如何?帮我换算一下 32°C 是多少华氏度",模型会调用哪些工具?调用顺序如何?描述完整的 ReAct 循环。
---
## Day 5Redis 记忆持久化 —— 重启对话不丢失
### Week 9 的局限
Week 9 使用 `InMemoryChatMemoryRepository`JVM 内存)存储对话历史。一旦重启应用,所有对话记忆丢失。
### Spring AI 的 ChatMemory 双层架构
```
MessageWindowChatMemory策略层滑动窗口保留最近 30 条)
↓ 委托存储
ChatMemoryRepository存储层接口
├─ InMemoryChatMemoryRepository默认内存 → 重启丢失)
├─ JdbcChatMemoryRepositoryJDBC 数据库)
├─ CassandraChatMemoryRepositoryCassandra
└─ 自定义RedisChatMemoryRepository ← 本周实现
```
### 自定义 RedisChatMemoryRepository
Spring AI 1.0.6 没有内置 Redis 实现,这反而是好的——你可以学习如何扩展框架:
```java
@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 中的无缝切换
```java
// Week 9内存版重启丢失:
@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(new InMemoryChatMemoryRepository())
.maxMessages(20).build();
}
// Week 10Redis 持久化版,重启保留):
@Bean
public ChatMemory chatMemory(ChatMemoryRepository repository) { // 注入我们的 Redis 实现
return MessageWindowChatMemory.builder()
.chatMemoryRepository(repository) // 换成 Redis
.maxMessages(30).build();
}
```
**其他代码完全不用改**——这就是面向接口编程的威力。
### Jackson 序列化的 Message 格式
Message 接口的常见实现是 `AssistantMessage``UserMessage`。Jackson 需要类型信息来正确反序列化。在 RedisConfig 中配置:
```java
@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);
// ...
}
```
### 动手 → 理解
1. 安装 RedisWindows 版从 GitHub Release 下载,或使用 WSL/Docker
2. 启动 Redis`redis-server.exe`(或 `redis-server`
3. 修改 `application.yml` 中的 Redis 密码(如果有的话)
4. 阅读 `memory/RedisChatMemoryRepository.java`——理解 4 个接口方法的实现
5. 阅读 `config/RedisConfig.java`——理解 JSON 序列化配置
6. 阅读 `config/AIConfig.java` 中的 `chatMemory()` Bean——注意 `ChatMemoryRepository` 是怎么注入的
7. 启动项目,发送几轮对话
8.`redis-cli` 查看数据:
```
redis-cli
SMEMBERS chat:conversation:ids → 查看所有会话 ID
LRANGE chat:memory:default 0 -1 → 查看 default 会话的所有消息
```
9. **重启项目** → 访问 `GET /api/chat/history` → 确认记忆还在!
10. 删除某个会话 → 观察 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 6RAG检索增强生成—— 让 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 方案选择
#### 方案 BOllama 本地(默认,免费)
```bash
# 安装 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`
1. 注册 [bailian.console.aliyun.com](https://bailian.console.aliyun.com)
2. 开通模型服务 → 获取 API Key
3. 修改 `application.yml`
```yaml
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
```java
// 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 —— 零依赖的向量存储
```java
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
return SimpleVectorStore.builder(embeddingModel).build();
}
```
SimpleVectorStore 纯内存存储,免外部服务。生产环境可替换为 PgVectorStore、RedisVectorStore 等。
### QuestionAnswerAdvisor —— 自动 RAG
在 ChatClient 的 Advisor 链中添加 `QuestionAnswerAdvisor`
```java
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(),
new QuestionAnswerAdvisor(vectorStore) // 自动检索 + 增强
)
```
效果每次对话时Advisor 自动从 VectorStore 检索相关文档,拼接到 Prompt 中。**你的代码不需要手动调用 `vectorStore.similaritySearch()`**——Advisor 帮你做了。
### KnowledgeDataLoader启动时加载知识库
```java
@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);
}
}
```
### 动手 → 理解
1. 安装 Ollama → `ollama pull nomic-embed-text`(如果不方便下载,切换到方案 A
2. 阅读 `config/AIConfig.java`
- `embeddingModel()` — 手动创建,指向 Ollama
- `vectorStore()` — SimpleVectorStore
- `chatClient()` — 新增 `QuestionAnswerAdvisor`
3. 阅读 `rag/KnowledgeDataLoader.java`——8 条示例知识库文档
4. 阅读 `service/RAGService.java`——`search()` 方法(纯检索)+ `addDocuments()` 方法
5. 启动项目,观察日志:
- `EmbeddingModel created with base-url: http://localhost:11434`
- `Loaded 8 knowledge documents into VectorStore`
6. 在 agent.html 中测试 RAG
- 发送:"什么是 Spring AI"
- 发送:"RAG 是什么?"
- 对比:勾掉"Agent 模式"用普通聊天问同样的问题,回答有什么不同?
7. 用 Postman 测试纯检索(不生成):
- `POST /api/rag/search` Body: `{"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 7Prompt 模板管理 —— 一键切换 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
```java
@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"
}
```
后端处理:
1. 从文件加载模板内容
2. 替换 `{{key}}` 占位符
3. 拼接:`模板内容 + "---\n用户消息: " + message`
4. 作为 `user()` 参数传给 ChatClient
### 前端agent.html 控制台
增强功能(对比 Week 9 的 chat.html
| 功能 | Week 9 | Week 10 |
|------|--------|---------|
| 工具可视化 | — | 侧边栏显示可用工具列表 |
| RAG 来源 | — | 可显示检索来源(预留) |
| 模板选择 | — | 下拉选择 + 内容预览 |
| Agent/聊天切换 | — | 侧边栏开关 |
| 流式/非流式切换 | 有 | 有 |
| 会话管理 | 有 | 有 |
### 动手 → 理解
1. 阅读 `resources/prompts/` 下的 3 个模板文件
2. 阅读 `service/PromptTemplateService.java`——理解 `@PostConstruct` 加载和占位符替换
3. 阅读 `controller/ChatController.java` 的 `GET /api/prompts`、`POST /api/chat/with-template`
4. 启动项目,浏览器访问 `http://localhost:8080/agent.html`
5. 在侧边栏下拉选择"java-expert",观察预览内容
6. 发送:"请写一个 Spring Boot 的全局异常处理"——观察回复是否体现出"Java 专家"风格
7. 切换到"code-reviewer",发送一段代码,观察审查风格
8. 切换到"translator",观察翻译风格
9. 关掉模板(选择"— 不使用模板 —"),同样的问题再问一次——感受差异
### 核心文件
| 文件 | 作用 |
|------|------|
| `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 模板 | 文件加载、占位符替换、多角色切换 |
| 架构设计 | 双 ProviderChat + 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 记忆管理等进阶内容。