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

31 KiB
Raw Blame History

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 Windows 版)
Ollama 安装(方案 B Day 6 需要(ollama.com → 下载安装 → ollama pull nomic-embed-text

关于 Embedding 方案Day 6 的 RAG 有两种 Embedding 方案。方案 BOllama 本地) 免费、离线、零成本,是默认选择。方案 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 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 数组:

{
  "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_reasontool_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非常适合户外活动。"

动手 → 理解

  1. 用 Postman给 DeepSeek API 发送一个带 tools 定义的请求(复制上面的 JSON替换 API Key
  2. 观察返回的 finish_reasontool_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 定义工具:

@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

@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 的智能所在。

动手 → 理解

  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

@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 配置不同)

动手 → 理解

  1. 阅读 tool/CalculatorTool.javatool/SearchTool.java
  2. 阅读 service/AgentService.javaexecute() —— 注意到没有?代码和 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 使用 InMemoryChatMemoryRepositoryJVM 内存)存储对话历史。一旦重启应用,所有对话记忆丢失。

Spring AI 的 ChatMemory 双层架构

MessageWindowChatMemory策略层滑动窗口保留最近 30 条)
    ↓ 委托存储
ChatMemoryRepository存储层接口
    ├─ InMemoryChatMemoryRepository默认内存 → 重启丢失)
    ├─ JdbcChatMemoryRepositoryJDBC 数据库)
    ├─ CassandraChatMemoryRepositoryCassandra
    └─ 自定义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 10Redis 持久化版,重启保留):
@Bean
public ChatMemory chatMemory(ChatMemoryRepository repository) {  // 注入我们的 Redis 实现
    return MessageWindowChatMemory.builder()
            .chatMemoryRepository(repository)  // 换成 Redis
            .maxMessages(30).build();
}

其他代码完全不用改——这就是面向接口编程的威力。

Jackson 序列化的 Message 格式

Message 接口的常见实现是 AssistantMessageUserMessage。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);
    // ...
}

动手 → 理解

  1. 安装 RedisWindows 版从 GitHub Release 下载,或使用 WSL/Docker
  2. 启动 Redisredis-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 本地(默认,免费)

# 安装 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
  2. 开通模型服务 → 获取 API Key
  3. 修改 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);
    }
}

动手 → 理解

  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

@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.javaGET /api/promptsPOST /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 记忆管理等进阶内容。