Files
gc-plan/week10/教案.md
2026-04-30 16:08:39 +08:00

857 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 记忆管理等进阶内容。