package com.learn.config; import com.learn.tool.CalculatorTool; import com.learn.tool.SearchTool; import com.learn.tool.WeatherTool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.ChatMemoryRepository; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.openai.OpenAiEmbeddingModel; import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.vectorstore.SimpleVectorStore; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; /** * Week 10 核心配置 —— Agent 能力装配 * * 新增(对比 Week 9): * 1. EmbeddingModel(手动创建,用 Ollama 本地) → RAG * 2. VectorStore(SimpleVectorStore,内存) → RAG * 3. ChatMemory 持久化(RedisChatMemoryRepository) → 记忆不丢失 * 4. ChatClient 注册 Tools + QuestionAnswerAdvisor → Agent */ @Configuration public class AIConfig { private static final Logger log = LoggerFactory.getLogger(AIConfig.class); // ==================== Embedding(用于 RAG) ==================== /** * EmbeddingModel —— 把文本转成向量。 * * 这里手动创建 OpenAiEmbeddingModel,指向 Ollama 本地服务, * 因为 Chat 用的是 DeepSeek(不支持 Embedding),两者 base-url 不同。 * * 前置条件:ollama pull nomic-embed-text */ @Bean @Lazy public EmbeddingModel embeddingModel( @Value("${spring.ai.openai.embedding.base-url:http://localhost:11434}") String baseUrl, @Value("${spring.ai.openai.embedding.api-key:ollama}") String apiKey) { var api = OpenAiApi.builder() .baseUrl(baseUrl.endsWith("/v1") ? baseUrl : baseUrl + "/v1") .apiKey(apiKey) .build(); log.info("EmbeddingModel created with base-url: {}", baseUrl); return new OpenAiEmbeddingModel(api); } // ==================== VectorStore(用于 RAG) ==================== /** * SimpleVectorStore —— 纯内存向量存储,免外部服务。 * 生产环境可替换为 RedisVectorStore / PgVectorStore 等。 */ @Bean @Lazy public VectorStore vectorStore(EmbeddingModel embeddingModel) { return SimpleVectorStore.builder(embeddingModel).build(); } // ==================== ChatMemory(持久化) ==================== /** * ChatMemory —— 对话记忆,Redis 持久化(Day 5)。 * * 架构: MessageWindowChatMemory (滑动窗口, 最多 30 条) * ↓ 存储 * ChatMemoryRepository → RedisChatMemoryRepository (自定义实现) * * 切换方案:把注入的 ChatMemoryRepository 换成 InMemoryChatMemoryRepository * 就回到 Week 9 的内存模式,其他代码完全不用改。 */ @Bean public ChatMemory chatMemory(ChatMemoryRepository repository) { return MessageWindowChatMemory.builder() .chatMemoryRepository(repository) .maxMessages(30) .build(); } // ==================== ChatClient(Agent 入口) ==================== /** * ChatClient —— Week 10 增强版: * - 复用 Week 9 的 defaultSystem + MessageChatMemoryAdvisor * - 新增 defaultTools:注册所有 @Tool 方法 * - 新增 QuestionAnswerAdvisor:RAG 检索增强 * * 注意:Tool 对象通过 Spring 注入(用 @Component 标注), * .defaultTools() 接收所有带 @Tool 注解的 Bean。 */ @Bean public ChatClient chatClient( ChatModel chatModel, ChatMemory chatMemory, @Lazy VectorStore vectorStore, WeatherTool weatherTool, CalculatorTool calculatorTool, SearchTool searchTool) { return ChatClient.builder(chatModel) .defaultSystem(""" 你是一个 AI Agent 助手,名字叫"小智 Agent"。 你可以调用工具来获取信息或执行计算。 当用户询问天气、计算数学、或搜索信息时,请使用对应的工具。 回答时请用中文,基于工具返回的结果来组织自然流畅的回复。 """) .defaultTools(weatherTool, calculatorTool, searchTool) .defaultAdvisors( MessageChatMemoryAdvisor.builder(chatMemory).build(), new QuestionAnswerAdvisor(vectorStore) ) .build(); } }