编程导航AI智能体话题讨论

AI智能体

28 参与
分享

快来分享你的内容吧~

点击登录,快来和大家讨论吧~
表情
图片
话题
打卡
综合
交流
文章
问答

手写 Spring AI Advisor:解耦 ChatMemory 与历史记录,实现聊天记录持久化存储

> **前言**: > 在上一篇文章 [《Spring AI + MySQL 实现会话记忆持久化》](https://www.codefather.cn/post/2011766454579478529) 中,我们成功实现了将 ChatMemory 接入 MySQL,让对话数据不再随服务重启而丢失。 > > 但随着项目测试运行,我发现了一个**致命问题**:**数据库里的聊天记录竟然变少了!** 当对话轮数超过我们在 `MessageWindowChatMemory` 中设置的 `maxMessages`(例如 50 条)时,最早的记录竟然从数据库中凭空消失了。 > > **这不仅不符合“持久化”的初衷,更导致我们无法进行后续的数据分析和历史回溯。** > > 本文将带你深入 Spring AI 源码,揭秘这个“数据消失术”的根本原因,并手把手教你通过重写核心组件,实现 **“数据库永久保存全量历史”** 与 **“大模型只传最近 N 条上下文”** 的完美平衡。 --- ## 一、 数据消失原因 在 Spring AI 的默认设计中,我们通常使用 `MessageWindowChatMemory` 来管理会话上下文。它的核心作用是**限制发送给大模型的 Token 数量**,防止上下文超长。 但是,Spring AI 默认保存的是短期记忆,是大模型上下文里的内容 让我们直接打开 `MessageWindowChatMemory.java` 的源码(位于 `org.springframework.ai.chat.memory` 包下),看看 `add` 方法到底干了什么: ### 1.1 源码分析:`add` 方法 ```java @Override public void add(String conversationId, List<Message> messages) { Assert.hasText(conversationId, "conversationId cannot be null or empty"); Assert.notNull(messages, "messages cannot be null"); Assert.noNullElements(messages, "messages cannot contain null elements"); // 1. 从数据库(Repository)取出当前会话的所有消息 List<Message> memoryMessages = this.chatMemoryRepository.findByConversationId(conversationId); // 2. 【关键点】调用 process 方法处理消息(合并新消息 + 截断) List<Message> processedMessages = process(memoryMessages, messages); // 3. 将处理后的结果“覆盖”回数据库 this.chatMemoryRepository.saveAll(conversationId, processedMessages); } ``` **深度解读:** 1. **`findByConversationId`**:首先,它从底层的 Repository(比如 MySQL)中取出了当前会话的所有历史消息。假设现在数据库里有 50 条。 2. **`process`**:接着,它调用了内部的 `process` 方法。这里是逻辑的核心,它将旧消息和新消息合并,并根据窗口大小进行处理。 3. **`saveAll`**:最后,也是最关键的一步,它将 `process` 处理后的结果**全量覆盖**回数据库。注意这里是覆盖操作,如果 `process` 方法丢弃了某些消息,那么这些消息在数据库里也会被同步删除。 ### 1.2 源码分析:`process` 方法(罪魁祸首) ```java private List<Message> process(List<Message> memoryMessages, List<Message> newMessages) { // ... 省略部分合并逻辑 ... // 合并旧消息和新消息 processedMessages.addAll(newMessages); // 如果总数没超过 maxMessages,直接返回 if (processedMessages.size() <= this.maxMessages) { return processedMessages; } // 【致命逻辑】如果超过了 maxMessages,开始移除旧消息! int messagesToRemove = processedMessages.size() - this.maxMessages; List<Message> trimmedMessages = new ArrayList<>(); int removed = 0; for (Message message : processedMessages) { // 如果是 SystemMessage 或者 已经删够了数量,才保留 if (message instanceof SystemMessage || removed >= messagesToRemove) { trimmedMessages.add(message); } else { removed++; // 计数器+1,这条消息被丢弃了,不会加入 trimmedMessages } } return trimmedMessages; // 返回的是“阉割”后的列表 } ``` **深度解读:** 假设我们将 `maxMessages` 设置为 **50**,且数据库中已经存储了 **50** 条历史消息。此时,用户发送了 **1** 条新消息。 1. **全量聚合**: * `processedMessages.addAll(newMessages)`:代码首先将新消息追加到旧消息列表中。 * **现状**:此时内存中的 `processedMessages` 列表共有 **51** 条消息(Index 0 ~ 50)。 2. **阈值检查**: * `if (processedMessages.size() <= this.maxMessages)`:这是流程的分水岭。 * **判定**:51 > 50,条件不满足。程序意识到消息超载,必须启动“裁员”流程。 3. **计算“裁员”指标**: * `int messagesToRemove = 51 - 50 = 1;` * **目标**:必须从列表中剔除 **1** 条最旧的消息,才能满足窗口限制。 4. **滑动窗口筛选**: * 这是最核心的逻辑,代码使用 `for` 循环从头(最旧的消息)开始遍历,配合 `removed` 计数器决定每条消息的命运。 * **第一轮循环(Index 0,最旧的一条消息)**: * 它是 `SystemMessage` 吗?**否**(假设是普通对话)。 * `removed` (0) >= `messagesToRemove` (1) 吗?**否**(还没删够)。 * **结局**:进入 `else` 分支,`removed` 自增变为 1,**该消息被丢弃**,没有加入 `trimmedMessages`。 * **第二轮循环(Index 1,次旧消息)**: * 它是 `SystemMessage` 吗?**否**。 * `removed` (1) >= `messagesToRemove` (1) 吗?**是**(指标已达标)。 * **结局**:进入 `if` 分支,**该消息被保留**,加入 `trimmedMessages`。 * **后续循环**:因为 `removed` 已经达标,后续所有消息都会满足 `removed >= messagesToRemove` 条件,从而被全部保留。 5. **最终审判**: * 方法返回的 `trimmedMessages` 仅包含 **后 50 条** 消息。 * **致命后果**:这个“阉割版”的列表随后会在 `add` 方法中被直接 `saveAll` 回数据库。**这意味着,数据库中存储的最早那 1 条历史记录,被物理删除了!** 这就是“数据消失术”的底层真相。 流程闭环了:`add` 方法取出了 50 条,合并了 1 条新消息变成 51 条,传给 `process` 方法。`process` 方法一看超了,把第 1 条删了,返回后 50 条。最后 `add` 方法把这后 50 条存回数据库。 ### 1.3 痛点总结 看懂了吗?流程是这样的: 1. **读出来**:把数据库里的 50 条记录读出来。 2. **加进去**:加上用户刚发的 1 条新消息,现在有 51 条。 3. **切一刀**:`process` 方法发现 51 > 50,于是把**第 1 条**旧消息删掉了,只保留后 50 条。 4. **存回去**:调用 `repository.saveAll`。对于 `JdbcChatMemoryRepository` 来说,它会把这个会话 ID 下的数据更新为这 50 条。 **结果:** 数据库里永远只有最近的 50 条,第 51 条之前的历史记录**永久丢失**。这对于需要审计、回溯、数据分析的业务系统来说,是不可接受的。 --- ## 二、 思路分析 ### 2.1 官方文档的启示 Spring AI 官方文档中其实隐晦地提到过:**ChatMemory 的设计初衷并不是作为持久化的聊天记录存储方案**。 ![请添加图片描述](https://pic.code-nav.cn/post_picture/1828322959723974658/V0LkMRW6muHC8EWi.webp) 这种方案的设计目的是**管理模型上下文的短期记忆(Short-term Memory)**,而不是**整个聊天记录(Chat History / Audit Log)**。 * **短期记忆 (Short-term Memory)**:这是**模型层面**的概念。短期记忆存在于模型上下文中的,受限于 LLM 的 Context Window(上下文窗口,如 4k, 8k, 128k Token),我们无法将所有历史对话都喂给模型。因此,必须通过 `MessageWindowChatMemory` 等机制进行“截断”,只保留最近的 N 轮对话,拼接在 Prompt 中供模型即时调用,以维持对话的连贯性。 * **聊天记录 (Chat History)**:这是**业务层面**的概念。它是项目业务需求的一部分,类似于系统日志或审计日志。它的要求是**全量保存、永久存储、不可丢失**,用于后续的用户历史查看、数据分析、模型调优等。它不应该受到模型 Token 限制的影响。 **结论**:**我们不应该试图修改 Spring AI 实现 `ChatMemory` 的源码**(如修改 `process` 方法不去删除旧数据),因为那违背了它的设计初衷(控制上下文大小)。如果强行修改,会导致发送给大模型的 Prompt 无限膨胀,最终撑爆 Token 限制或消耗巨额 Token 费用。 ### 2.2 解决思路:AOP 与 Advisor 既然我们明确了目标——**在业务层面实现聊天记录的持久化**,那么我们完全可以跳出 `ChatMemory` 的圈子,从大模型交互的本质入手。 **大模型本质上就是一个输入(Input)输出(Output)的函数工具。** ![请添加图片描述](https://pic.code-nav.cn/post_picture/1828322959723974658/a85ozlEXNe1Ok4KS.png) Spring AI 的核心架构设计中,大量使用了 **Advisor(增强器)** 模式。这是一种典型的 AOP(面向切面编程)思想。通过 Advisors,我们可以拦截对大模型的每一次请求和每一次响应,对其进行增强、修改或**记录**。 ![请添加图片描述](https://pic.code-nav.cn/post_picture/1828322959723974658/T3JncHWICdY2oTCk.webp) 上图是 Spring AI 文档中对于 Advisors 的介绍。可以看到,Advisor 处于 `ChatClient` 和底层 `ChatModel` 之间,像关卡一样层层拦截递归执行。 **核心思路**: 我们不需要动 `ChatMemory`,而是编写一个自定义的 **Advisor**。 1. **拦截请求**:在用户发送 Prompt 给大模型之前,拦截请求,提取出用户的提问,保存到数据库。 2. **拦截响应**:在大模型生成回复返回给用户之后(或流式传输过程中),拦截响应,提取出 AI 的回答,保存到数据库。 这样,**“短期记忆管理”交给 `ChatMemory`(负责截断),“长期历史记录”交给 `Advisor`(负责全量落库)**。两者职责分离,互不干扰,完美解决了我们的问题。 --- ## 三、 自定义 Advisor 实战 ### 3.1 Advisor 接口详解 Spring AI 的文档中详细介绍了如何实现一个自定义 Advisor。 > **Implementing an Advisor** > To create an advisor, implement either `CallAdvisor` or `StreamAdvisor` (or both). The key method to implement is `nextCall()` for non-streaming or `nextStream()` for streaming advisors. 我们需要实现两个核心接口(通常为了同时支持流式和非流式调用,两个都要实现): * **`CallAdvisor`**:用于拦截普通的同步调用(`call`)。核心方法是 `adviseCall`。 * **`StreamAdvisor`**:用于拦截流式调用(`stream`)。核心方法是 `adviseStream`。 ### 3.2 官方示例解读:SimpleLoggerAdvisor Spring AI 提供了一个 `SimpleLoggerAdvisor` 的示例,用于打印请求和响应日志。这正是我们需要参考的模板,因为“打印日志”和“保存日志到数据库”在逻辑上是完全一样的,只是输出目的地不同。 ![请添加图片描述](https://pic.code-nav.cn/post_picture/1828322959723974658/4lcRXFkvKWnSRWiZ.webp) 让我们深入解析一下官方的这个示例代码: ```java public class SimpleLoggerAdvisor implements CallAdvisor, StreamAdvisor { private static final Logger logger = LoggerFactory.getLogger(SimpleLoggerAdvisor.class); @Override public String getName() { return this.getClass().getSimpleName(); // Advisor 的唯一名称 } @Override public int getOrder() { return 0; // 执行顺序,数字越小越先执行 } // 1. 拦截非流式调用 @Override public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) { // 在调用大模型之前:记录请求 logRequest(chatClientRequest); // 执行链条中的下一个 Advisor 或最终的大模型调用 ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest); // 在大模型返回之后:记录响应 logResponse(chatClientResponse); return chatClientResponse; } // 2. 拦截流式调用 @Override public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) { // 在流开始之前:记录请求 logRequest(chatClientRequest); // 执行流式请求,得到响应流 (Flux) Flux<ChatClientResponse> chatClientResponses = streamAdvisorChain.nextStream(chatClientRequest); // 【关键点】聚合流式响应 // 流式响应是一段一段回来的,我们无法直接打印完整的回复。 // ChatClientMessageAggregator 工具类可以“旁路”收集所有的流片段,拼成一个完整的 Response,供我们处理。 // 注意:这里的操作是异步的,不会阻塞返回给前端的流。 return new ChatClientMessageAggregator().aggregateChatClientResponse(chatClientResponses, this::logResponse); } // ... 省略 logRequest 和 logResponse 的具体实现 ... } ``` **深度解析:** 1. **`getOrder()`**:控制 Advisor 的执行顺序。这在多个 Advisor 串联时非常重要(后面我们会利用这一点)。 2. **`adviseCall`**:这是经典的“环绕通知”。你可以在 `nextCall` 之前拿到 `chatClientRequest`(用户的输入),在 `nextCall` 之后拿到 `chatClientResponse`(AI 的输出)。 3. **`adviseStream` 与 `MessageAggregator`**:这是难点。流式调用返回的是 `Flux<ChatClientResponse>`,里面的内容是破碎的字符(Chunk)。如果我们想保存完整的 AI 回复,不能每收到一个字就存一次数据库。`ChatClientMessageAggregator` 是 Spring AI 提供的神器,它能帮我们在流传输的同时,在后台悄悄把碎片拼凑起来,等流结束时触发回调(`this::logResponse`),让我们拿到完整的文本。 --- ## 四、 核心实现:ChatHistoryRecordAdvisor 基于上述分析,我们现在来实现自己的 **`ChatHistoryRecordAdvisor`**。它的目标是:**拦截对话,将“用户提问”和“AI 回答”持久化到 MySQL 的 `spring_ai_chat_message_log` 表中**。 ### 4.1 完整代码 ```java import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClientMessageAggregator; import org.springframework.ai.chat.client.ChatClientRequest; import org.springframework.ai.chat.client.ChatClientResponse; import org.springframework.ai.chat.client.advisor.api.CallAdvisor; import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain; import org.springframework.ai.chat.client.advisor.api.StreamAdvisor; import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.MessageType; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import java.time.LocalDateTime; /** * 聊天历史记录增强器 * 用于拦截 ChatClient 的请求和响应,并将对话记录持久化到数据库中。 */ @Component @RequiredArgsConstructor @Slf4j public class ChatHistoryRecordAdvisor implements CallAdvisor, StreamAdvisor, Ordered { private final ISpringAiChatMessageLogService messageLogService; private final ISpringAiChatRecordService chatRecordService; @Override public String getName() { return this.getClass().getSimpleName(); } /** * 设置最高优先级 (HIGHEST_PRECEDENCE) * 目的:确保在其他 Advisor(如 RAG 的 QuestionAnswerAdvisor)之前执行。 * 这样我们可以获取到用户最原始的 Prompt,而不是被 RAG 修改/拼接后的 Prompt。 */ @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } /** * 拦截非流式调用 */ @Override public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) { // 1. 提取原始的用户请求文本(在 RAG 修改之前) String originalUserText = extractUserText(chatClientRequest); // 2. 放行请求,继续执行后续的 Advisor 链和 AI 调用 ChatClientResponse response = callAdvisorChain.nextCall(chatClientRequest); // 3. AI 响应回来后,记录日志(包括用户问题和 AI 回答) saveLog(chatClientRequest, response, originalUserText); return response; } /** * 拦截流式调用 */ @Override public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) { // 1. 同样先提取原始用户文本 String originalUserText = extractUserText(chatClientRequest); // 2. 执行流式请求 Flux<ChatClientResponse> responseFlux = streamAdvisorChain.nextStream(chatClientRequest); // 3. 聚合流式响应,以便获取完整的 AI 回答内容进行存储 // 注意:这里不会阻塞流的返回,而是利用 Reactor 的副作用进行异步记录 return new ChatClientMessageAggregator().aggregateChatClientResponse(responseFlux, aggregatedResponse -> { saveLog(chatClientRequest, aggregatedResponse, originalUserText); }); } /** * 从请求中提取最后一条用户消息的内容 */ private String extractUserText(ChatClientRequest request) { try { return request.prompt().getInstructions().stream() .filter(m -> m.getMessageType() == MessageType.USER) .reduce((first, second) -> second) // 获取最后一条,通常是当前用户的提问 .map(Message::getText) .orElse(""); } catch (Exception e) { log.warn("Failed to extract user text", e); return ""; } } /** * 保存聊天日志到数据库 * * @param request 请求对象,包含上下文信息(如 sessionId) * @param response 响应对象,包含 AI 的回答 * @param userText 提取出的用户原始问题 */ private void saveLog(ChatClientRequest request, ChatClientResponse response, String userText) { try { // 1. 获取会话 ID String sessionId = (String) request.context().get(ChatMemory.CONVERSATION_ID); if (sessionId == null) { return; } // 2. 尝试获取用户 ID (从 ChatRecord 表中查找) Long userId = null; SpringAiChatRecord chatRecord = chatRecordService.getById(sessionId); if (chatRecord != null && chatRecord.getUserId() != null) { try { userId = Long.parseLong(chatRecord.getUserId()); } catch (NumberFormatException e) { log.warn("Invalid user ID format: {}", chatRecord.getUserId()); } } // 3. 保存用户提问日志 if (userText != null && !userText.isEmpty()) { SpringAiChatMessageLog userLog = new SpringAiChatMessageLog() .setSessionId(sessionId) .setUserId(userId) .setMessageType("USER") .setContent(userText) .setCreateTime(LocalDateTime.now()); messageLogService.save(userLog); } // 4. 保存 AI 回复日志 if (response.chatResponse() != null && response.chatResponse().getResult() != null && response.chatResponse().getResult().getOutput() != null) { String assistantContent = response.chatResponse().getResult().getOutput().getText(); SpringAiChatMessageLog assistantLog = new SpringAiChatMessageLog() .setSessionId(sessionId) .setUserId(userId) .setMessageType("ASSISTANT") .setContent(assistantContent) .setCreateTime(LocalDateTime.now()); messageLogService.save(assistantLog); } } catch (Exception e) { log.error("Error saving chat history log", e); } } } ``` ### 4.2 深度代码解析 这段代码虽然不长,但每一处设计都暗藏玄机。 #### 1. 为什么 `getOrder()` 要返回 `Ordered.HIGHEST_PRECEDENCE`? ```java @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } ``` **这是最关键的一点!** 在 Spring AI 中,Advisor 是链式执行的。如果你使用了 RAG(检索增强生成),通常会有一个 `QuestionAnswerAdvisor`。这个 RAG Advisor 做的事情是:拿到你的问题 -> 去向量数据库搜索 -> 把搜索结果拼接成一段很长的 Prompt -> 替换掉你的原始问题 -> 传给大模型。 如果我们不设置最高优先级(让我们的 Advisor **第一个**执行),那么我们拦截到的 `ChatClientRequest` 里包含的可能就不是用户原本写的“你好”,而是一大段包含了上下文文档的 RAG Prompt。保存那样的日志对用户来说是不可读的。 设置 `HIGHEST_PRECEDENCE` 确保了我们在任何 Prompt 修改发生**之前**,就截获了用户的原始输入。 #### 2. `extractUserText` 的逻辑 ```java request.prompt().getInstructions().stream() .filter(m -> m.getMessageType() == MessageType.USER) .reduce((first, second) -> second) ``` 一个 Prompt 请求中可能包含多条消息(System Message, User Message, Assistant Message...)。我们只关心**用户当前说的这句话**。 * `.filter(...)`:只筛选用户消息。 * `.reduce(...)`:取最后一条。因为在携带历史上下文的情况下,Prompt 里可能有之前几轮的 User Message,但最后一条肯定才是当前最新的提问。 #### 3. `adviseStream` 的无感聚合 ```java return new ChatClientMessageAggregator().aggregateChatClientResponse(responseFlux, aggregatedResponse -> { saveLog(chatClientRequest, aggregatedResponse, originalUserText); }); ``` 这里使用了 Reactor 的响应式编程技巧。`aggregateChatClientResponse` 方法会返回一个新的 Flux,这个 Flux 对前端来说和原来一样,依然是流式的。但在服务器端内部,它挂载了一个“钩子”,等所有流数据跑完后,它会把拼好的完整结果传给我们的 lambda 表达式。 这样做的好处是:**日志记录完全不影响用户的首字延迟(Time to First Token)**,用户依然可以秒看流式输出,而我们的落库操作是在流结束后的异步线程中完成的。 ##### 注意:这段代码中没有实现Function Calling调用的存储逻辑,可以自行实现。 --- ## 五、 数据库设计与配置 ### 5.1 数据库表结构 为了配合上述 Advisor,我们需要一张表来存储日志。这里提供一个标准的 SQL 建表语句: ```sql -- 用户聊天记录日志表 create table spring_ai_chat_message_log ( id bigint auto_increment comment '主键ID' primary key, session_id varchar(50) not null comment '会话ID,对应 ChatMemory 中的 conversationId', user_id bigint unsigned null comment '用户ID,用于关联业务用户', message_type varchar(20) not null comment '消息类型: USER (提问), ASSISTANT (回答)', content longtext null comment '消息内容,使用 LongText 防止长文本截断', tool_calls longtext null comment '工具调用信息(JSON),预留字段', create_time timestamp default CURRENT_TIMESTAMP not null comment '创建时间' ) comment '用户聊天记录日志表'; -- 建立索引,加速查询 create index idx_session_id on spring_ai_chat_message_log (session_id); create index idx_user_id on spring_ai_chat_message_log (user_id); ``` **设计要点**: * **`session_id`**:这是关联上下文的核心,必须有索引。 * **`content`**:一定要用 `longtext`。大模型的回复(特别是写代码或写文章时)很容易超过 `varchar` 的限制。 * **`tool_calls`**:这是一个预留字段。如果你的大模型使用了 Function Calling(工具调用),你可能还想记录它调用了什么工具、传了什么参数。目前的实现暂未包含此逻辑,可根据业务自行扩展。 ### 5.2 配置 ChatClient 万事俱备,只欠东风。我们需要将写好的 `ChatHistoryRecordAdvisor` 注册到全局的 `ChatClient` 中。 ```java @Configuration public class ChatConfig { @Bean public ChatClient serviceChatClient(AlibabaOpenAiChatModel model, ChatMemory chatMemory, VectorStore vectorStore, ChatHistoryRecordAdvisor chatHistoryRecordAdvisor) { // 注入我们的 Advisor return ChatClient.builder(model) .defaultAdvisors( // 1. 日志 Advisor (官方) SimpleLoggerAdvisor.builder().build(), // 2. 【核心】我们要添加的历史记录 Advisor chatHistoryRecordAdvisor, // 3. 上下文记忆 Advisor (负责短期记忆截断) MessageChatMemoryAdvisor.builder(chatMemory).build(), // 4. RAG 检索增强 Advisor QuestionAnswerAdvisor.builder(vectorStore) .searchRequest(SearchRequest.builder().similarityThreshold(0.5d).topK(1).build()) .build() ) // 设置系统 Prompt .defaultSystem("你是一个智能助手...") .build(); } } ``` **配置详解**: * **注入顺序**:虽然我们在 `ChatHistoryRecordAdvisor` 代码里写了 `HIGHEST_PRECEDENCE`,但在 `defaultAdvisors` 方法中显式添加它依然是必要的。 * **共存关系**:请注意,`ChatHistoryRecordAdvisor` 和 `MessageChatMemoryAdvisor` 是**同时存在**的。 * `ChatHistoryRecordAdvisor`:负责把**每一句话**都完整地记入数据库,不做任何删除。 * `MessageChatMemoryAdvisor`:负责维护一个**滑动窗口**(比如最近 10 条),只把这 10 条发给大模型。 * 两者配合,既保证了大模型不会上下文溢出,又保证了数据库里有永久的查阅记录。 --- ## 六、 总结 通过本文的探索,我们纠正了一个常见的误区:**不要试图用 ChatMemory 来做持久化的业务日志**。 1. **ChatMemory (短期记忆)**:它是给**大模型**看的。为了节省 Token,它必须健忘,必须丢弃旧消息。 2. **Advisor (长期日志)**:它是给**人**看的。利用 AOP 切面思想,我们在大模型的输入输出关口设立“哨兵”,忠实地记录下每一次对话。 这种**读写分离**的架构,不仅解决了“数据消失”的 Bug,还解耦了业务逻辑与 AI 框架逻辑,是构建生产级 AI 应用的最佳实践。 现在,你的数据库里不仅有了永远不会消失的对话历史,你的大模型也依然跑得飞快。这,就是架构的艺术。 --- > **相关阅读**: > [Spring AI + MySQL 实现会话记忆持久化:彻底搞懂 ChatMemoryRepository](https://www.codefather.cn/post/2011766454579478529) > [SpringAI1.1.2官方文档](https://docs.spring.io/spring-ai/reference/)

刚刚 Cursor2.0炸裂发布!这3大亮点必学

大家好,我是程序员鱼皮。刚刚 Cursor 2.0 终于来了,绝对炸裂! 下面我带大家实操 Cursor 2.0 更新的几大核心功能,看看怎么用它大幅提高开发效率。 话不多说,点击收藏,我们开始吧! > 本文对应视频版:https://bilibili.com/video/BV12SyaBLEhN ## 一、实用特性 & 重大更新 ### Multi-Agents 多智能体 先问个问题,你觉得现在最火的 AI 热词是什么? 应该是 Agent 智能体吧? 以前你只有一个 AI 程序员,而现在 Cursor 直接给你安排了 **8 个 Agent 同时干活**! 进入全新的 Agents 布局,我直接丢一个大需求过去: ```markdown 帮我完整分析整个项目,并完成下列任务: 1. 补充前端代码注释 2. 补充后端代码注释 3. 优化前端网页样式 4. 增强后端代码安全性 5. 生成独立的前端文档 6. 生成独立的后端文档 ``` ![](https://pic.code-nav.cn/post_picture/1601072287388278786/Xj4EEOcROs3BohGQ.webp) 然后你可以雇最多 8 个 AI 程序员同时帮你写代码,每个都在自己独立的工作区里干活,互不干扰。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/Bs08zxLPl2O5Ww9Q.webp) 你呢,就坐着等 8 个 AI 赛马,谁先干好用谁的、谁质量高用谁的。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/I6TEV5vjM88Taviw.webp) 需要注意,这个功能背后用的是 Git 的 worktrees 技术,相当于给每个 AI 都复制了一份代码库,分别在不同的代码分支干活。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/95XlR3hjxeiPsam0.webp) 完成后,你可以选其中一个最满意的方案,把代码改动合并到主分支。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/uKtHv6VQLfDgNd6t.webp) 还有个小细节,默认创建的 worktree 只有代码文件,没有 node_modules 等依赖。如果你的项目需要,可以配置一下初始化脚本,让每个 AI 工作区都自动装好依赖、配置好环境,而不只是光秃秃的代码。 这段大家直接看 [官方文档](https://cursor.com/cn/docs/configuration/worktrees#-2) 了解,不细说了。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/T1f31z4wuE5bYQox.webp) 怎么样,多智能体是不是很爽? 不过友情提示,雇 8 个 AI 程序员,**价格也是 8 倍**!所以还是按需使用吧…… ![](https://pic.code-nav.cn/post_picture/1601072287388278786/SpWlzE6bEL05XWUR.webp) 后面我还会分享怎么自己写代码开发多智能体应用,大家可以关注一下。 ### Composer 自研模型 Cursor 这次还带来了自研的 AI 模型 Composer,据官方说,速度比同等水平的模型快 4 倍。 我一开始是怀疑的,但从刚刚那场赛马来看,Composer 模型是真的有点欺负老人家了~ ![](https://pic.code-nav.cn/post_picture/1601072287388278786/FUBb3YYruBGe3vKr.webp) 除了速度之外,我们肯定还关注生成代码的质量。 不妨做个小测试,让它和 Claude 4.5 模型同时开发一个完整的前后端项目《AI 减压小能手》。 输入下列提示词: ```markdown 你是一位专业的程序员,请帮我开发《减压小能手》网站,用户可以通过和专门帮人减压的 AI 聊天来缓解压力。 ## 开发要求 1. 需要包含完整的前端和后端,后端使用 Node.js 2. 使用 Vercel 的 AI Gateway 实现 AI 能力,需要先通过官方文档来获取尽可能多的用法:https://vercel.com/docs/ai-gateway/getting-started 3. 以完成核心功能为目标,确保项目可以正常运行,不用输出文档、也不要做任何多余的功能 4. 整体网站界面采用让人放松的浅色,响应式适配各种尺寸的设备 ``` ![](https://pic.code-nav.cn/post_picture/1601072287388278786/DgUP9hzl1LAs7G3m.webp) 显然,Composer 输出速度更快,代码刷刷刷往外蹦,1 分钟出头就搞定了!而 Claude 花了 2 分多钟。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/SIwWhzRPwUNmyh4Q.webp) 生成代码后,我手动填入了调用 AI 需要的 API Key 配置,然后运行网站看下效果。 左边是 Composer,右边是 Claude,二者都生成了能够正常使用的代码,效果也是旗鼓相当,这么看来 Composer 确实很香。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/XkWF9aZN602kt1O5.webp) 不过这只是个浅测,更深度的感受可能得等我日常开发用一段时间后再分享,大家也可以说说自己的使用感受。 ### Browser 浏览器 这是本次正式上线的功能,简单来说就是给 AI 提供一个浏览器,让它能够上网并操作浏览器。 我们人类能用浏览器做的事,现在很多都能交给 AI 来完成。 我试了几个场景: 1)可视化修改网页 直接在浏览器里点击某个元素,提出你的需求,AI 就能帮你改对应的代码。点哪儿改哪儿,不用再去代码里找、而且修改更精确。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/ej6Pf0USDbfpYKtp.webp) 2)网页截图 做完网站后,可以直接让 AI 给页面截个图,用来编写项目文档什么的不是美滋滋? ![](https://pic.code-nav.cn/post_picture/1601072287388278786/KNUI9O0j6QpahFD9.webp) 3)发现网页报错 有些前端网页的报错信息是输出在浏览器控制台中的,现在 AI 能直接看浏览器控制台,获取到报错信息并帮你分析原因,提高了改 Bug 的效率和准确度。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/cvDEhJ0s81QLFwPL.webp) 4)搜寻内容 比如我让 AI 帮我从面试鸭刷题网站上找到 Java 面试题,并且生成截图。提示词如下: ``` 帮我寻找鱼皮面试鸭刷题网站上的 Java 面试题,并截图 ``` 以前 AI 只能利用工具从网络搜索,有些内容可能获取不到。现在 AI 可以自己打开网站,用鼠标点击探索,找到对应内容,再截图汇报给我,非常方便。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/YQ42XDoxkTl6b44S.webp) 此外,你还可以利用 Browser Use 能力自动整理网页内容、自动化测试、抓取数据、甚至是直接抄某个成熟网站的样式风格并生成代码,玩法很多。 ## 二、可能有用的特性 ### Voice Mode 语音模式 这个功能并不新鲜了,用语音控制 AI 写代码。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/X3yoTOJpeZlWN4H8.webp) 你可以在设置界面中配置触发 AI 执行的关键词,比如 “开始你的表演”: ![](https://pic.code-nav.cn/post_picture/1601072287388278786/JU2gXXort9nS0Sq3.webp) 额,好像不支持中文配置…… 然后我来试一试,直接说出需求 “我想做个鱼皮编程导航那样的网站”。 识别结果: ![](https://pic.code-nav.cn/post_picture/1601072287388278786/0QX0s3ISsIbyKmd5.webp) 说实话,效果一般,对中文支持不太行,感觉还没我自己产品的语音识别效果好…… ![](https://pic.code-nav.cn/post_picture/1601072287388278786/f1t65d8iY7nZayhj.webp) 而且日常开发我还是更喜欢打字,张个嘴搁那叭叭叭叭叭口遁代码不会很奇怪么? ### Sandboxed Terminals 沙盒终端 这个功能是为了提高安全性。让 AI 执行的命令都在沙盒里跑,能读写你的工作区,但默认不能访问网络,也不能动工作区外的文件。 ![](https://pic.code-nav.cn/post_picture/1601072287388278786/KsYVtonUKBvdzdm4.webp) 这样就算 AI 发疯了,最多也就把你项目搞乱,不会把你整个电脑都删了。 对于企业用户,管理员还能统一配置沙盒策略,控制哪些命令能跑、哪些不能跑。 ### Team Commands 团队命令 如果你在团队协作,这个功能就很有用。可以在 Cursor 仪表板里给整个团队定义统一的编码规范、常用命令和项目约定。 配置好后自动同步到所有成员,不用每个人手动设置了。 可惜我没有企业版账号,就不演示这个功能啦~ ## 三、其他优化 此外,还有一堆性能和界面的优化: 1. 代码审查改进:现在能跨多个文件统一查看 AI 的所有修改,不用一个个文件跳了,类似 GitHub 的 PR diff 视图。 2. 性能大幅提升:LSP(语言服务器)加载更快了,大项目不卡了,特别是 Python 和 TypeScript 项目。 3. 提示界面优化:UI 更简洁了,很多手动指定的东西(@Definitions、@Web 这些)都去掉了,AI 现在会自己判断需要什么上下文。 4. 智能体框架升级:底层架构优化,所有 AI 模型表现都更好了。 5. 云端 Agent 改进:提供 99.9% 的可靠性(一年停机不到 9 小时),启动速度更快。 6. 企业功能:比如管理员能统一控制沙盒的安全策略、自定义 Hooks 脚本可以从后台一键分发给全公司、所有管理操作都有记录。 简单来说就是更快、更美、更稳,希望不会更贵…… 我这个月已经在 Cursor 上花了几千元了! ## 最后 从 1.0 到 2.0 其实只有几个月的时间,Cursor 的进化速度真的很快。对我来说,这波更新最实用的功能是多智能体和浏览器使用。虽然价格确实比较贵,而且存在一些限制,但不得不说,Cursor 在功能上还是很能打的。 以上就是本次分享,文字版内容我会同步到 [AI 导航网站](https://ai.codefather.cn/),大家可以随时查看更多 AI 资讯和干货。我也会继续为大家带来最新实用的 AI 和编程教程,感谢关注~ ![](https://pic.code-nav.cn/post_picture/1601072287388278786/XYQfZ4KRrRfoht5v.webp) ## 更多 💻 编程学习交流:[编程导航](https://www.codefather.cn/) 📃 简历快速制作:[老鱼简历](https://www.laoyujianli.com) ✏️ 面试刷题神器:[面试鸭](https://www.mianshiya.com) 📖 AI 学习指南:[AI 知识库](https://ai.codefather.cn/)

10分钟上线一个Web应用?我没开玩笑,用这个AI智能体就行

你是否也曾有过这样的瞬间:一个绝妙的App想法在脑中闪现,却因为不懂代码、缺少时间和开发资源而搁浅?或者,作为开发者,厌倦了从零开始搭建项目的繁琐流程? 别急,今天给你介绍一个“超级个体”—— **SOLO Builder**。它就像你的专属技术合伙人,你只需动动嘴皮子描述想法,它就能在几分钟内为你构建出一个功能完善、可交互、可部署的Web应用。 是的,从想法到上线,一杯咖啡的时间足矣。 --- ## SOLO Builder 是什么“神兵利器”? 简单来说,SOLO Builder 是一个能帮你全自动构建专业Web应用的AI智能体。 整个过程如同与一位全能开发者对话: 1. **你说需求**:用大白话告诉它你想要什么。 2. **它出方案**:自动分析并生成一份清晰的产品需求文档(PRD)。 3. **它写代码**:根据确认好的PRD,迅速编写出高质量代码。 4. **它给结果**:直接产出可以实时预览、交互的成果。 整个流程行云流水,让你亲眼见证一个想法如何奇迹般地变为现实。 ![SOLO Builder 工作流程图](https://pic.code-nav.cn/post_picture/1759828016537657346/gfCxYwZKb4tAceA1.png) *▲ 你提需求,剩下的交给AI* --- ## 三步玩转 SOLO Builder,开启“魔法”之旅 想体验这种“点石成金”的快感?非常简单,只需三步。 **第一步:进入 SOLO 模式并启用 Builder** 首先,确保你已进入 SOLO 模式。在这里,SOLO Builder 默认启用。你也可以在对话框中输入 **`@SOLO Builder`**,精准呼叫你的AI开发伙伴。 ![在AI面板中选择 SOLO Builder](https://pic.code-nav.cn/post_picture/1759828016537657346/ItLvRkWTgPRy9bzv.webp) *▲ 一键@,专属技术大神就位* **第二步:从“聊”需求到写代码** 接下来,就是见证奇迹的时刻。 1. **生成PRD**:告诉它你的想法,比如“帮我做一个个人作品集网站”。SOLO Builder 会立刻在“文档”面板中为你生成一份PRD初稿。你可以直接修改,或通过对话让它完善。 ![PRD初稿自动生成](https://pic.code-nav.cn/post_picture/1759828016537657346/MqEJXW53dyuj1SYG.webp) *▲ 需求再复杂,也能梳理得明明白白* 2. **一键编码**:PRD确认无误后,只需轻轻一点,SOLO Builder 就会化身“码神”,在“编辑器”中唰唰唰地生成所有代码。它甚至会自动安装依赖、执行命令,你只需在“终端”看着它表演。 ![代码自动生成与命令执行](https://pic.code-nav.cn/post_picture/1759828016537657346/89H2UT7omDsCwxGu.webp) *▲ 告别繁琐,享受纯粹的创造乐趣* **第三步:实时预览与一键部署** 代码完成后,应用会自动在“浏览器”面板中运行起来,你可以实时预览、点击、交互。 ![应用成果实时预览](https://pic.code-nav.cn/post_picture/1759828016537657346/c7jOx0TQILdZ4tCA.webp) *▲ 所思即所得,想法立刻看得见* 觉得哪里不满意?点击“选择元素”按钮,直接在页面上框选你想修改的地方,发给AI,它就能精准调整。 ![直接在页面上选择元素进行修改](https://pic.code-nav.cn/post_picture/1759828016537657346/qq1WsHrAgNIYhiZK.webp) *▲ “指哪打哪”的修改体验* 最后,也是最激动人心的一步——**部署上线**! 点击“部署”按钮,跟随引导完成 Vercel 的首次授权。之后,你的应用就能一键发布到线上,并生成一个公开链接,可以随时分享给朋友、同事或投资人! ![一键部署,全球可访问](https://pic.code-nav.cn/post_picture/1759828016537657346/j0IWF4wY46qilMkQ.webp) *▲ 从灵感到上线,从未如此简单* SOLO Builder 不仅仅是一个工具,它更像是一种全新的创造方式,将技术门槛降到最低,让创意的价值最大化。 **还在等什么?** 那些躺在你备忘录里的绝妙点子,是时候让它们出来见见光了!立即体验 SOLO Builder,感受一下10分钟上线一个应用的快感吧! **你最想用 SOLO Builder 构建一个什么样的应用?欢迎在评论区分享你的脑洞!**

浅谈LLM智能体开发的背后——大语言模型应用中的十大安全风险🎯🎯🎯

## 一、提示词注入(LLM01: Prompt Injection) ### 风险原理 攻击者通过构造输入内容干扰系统提示(system prompt),从而影响模型的预期行为。这类攻击可分为: * **直接注入**:用户输入中携带操控指令,直接覆盖或绕过系统提示; * **间接注入**:通过污染上下文源(如数据库、模版变量、搜索引擎摘要)传递恶意语义。 ### 攻击示例 * 使用“忽略之前所有指令”等模式令模型执行攻击者意图; * 在知识库或邮件上下文中注入诱导提示,间接影响模型回答。 ### 安全对策 * 使用结构化 Prompt 模板避免拼接漏洞; * 实施输入级语义过滤、黑名单清洗与脱敏策略; * 彻底区分系统指令与用户上下文,禁止混用。 ### 工程建议 * 利用正则与规则库检测注入模式; * 加强数据源的写权限控制与审计; * 建议封装提示内容为 JSON 格式传输,提升可控性。 --- ## 二、输出处理不当(LLM02: Insecure Output Handling) ### 风险原理 若模型输出未经上下文验证即被直接执行,如 HTML、Shell 命令等,将可能触发 XSS、命令注入等严重漏洞。 ### 攻击示例 * 构造嵌有 `<script>`、SQL 注入语句或系统命令的 Prompt,引导模型输出恶意脚本或命令。 ### 安全对策 * 输出内容必须进行上下文感知的转义或编码处理; * 模型输出不应直接执行,须在隔离沙箱中运行; * 明确输出格式协议,设定合理边界。 ### 工程建议 * 配置 CSP(内容安全策略)以防止浏览器执行恶意脚本; * 后端禁用如 `eval`、`exec` 等高风险函数。 --- ## 三、训练数据污染(LLM03: Training Data Poisoning) ### 风险原理 攻击者可通过混入带有特定触发条件的数据样本,污染训练集或微调集,使模型学到带有恶意语义的行为模式。 ### 攻击示例 * 向开源语料库、数据贡献平台注入恶意内容; * 制造“后门样本”,激活指定输入下的偏差响应。 ### 安全对策 * 强制执行数据审计、清洗与标注标准化流程; * 应用差分隐私技术与抗扰训练策略抵抗污染; * 引入哈希校验与水印跟踪机制定位污染源。 ### 工程建议 * 建设集中化数据治理平台,对语料采集、验证、使用形成闭环。 --- ## 四、模型拒绝服务(LLM04: Model Denial of Service) ### 风险原理 LLM 对长输入或递归上下文较为敏感,易被恶意构造内容或高频请求拖垮,引发内存溢出(OOM)或服务崩溃。 ### 攻击示例 * 构造极长嵌套对话上下文导致资源耗尽; * 利用并发调用触发速率上限,拖垮系统。 ### 安全对策 * 设置最大 Token 长度及调用频率限制; * 使用熔断器与任务排队机制缓解压力; * 引入 Token 成本估算模型(Token-Cost Estimation)。 ### 工程建议 * 配合 IP 限流与中间件层级流控(如 Nginx + Lua)实现细粒度防护。 --- ## 五、供应链安全风险(LLM05: Supply Chain Vulnerabilities) ### 风险原理 依赖包、预训练模型或 API 插件等中间组件存在被篡改或植入恶意逻辑的风险。 ### 攻击示例 * 利用依赖名混淆(Dependency Confusion)攻击私有系统; * 发布带后门模型至公共模型库诱导下载。 ### 安全对策 * 采用依赖签名校验、哈希对比机制确保完整性; * 使用 Lockfile 锁定依赖版本,防止投毒; * 依赖管理纳入 CI/CD 审计流程。 ### 工程建议 * 引入 SBOM(软件物料清单)工具,实现组件溯源与持续监控。 --- ## 六、敏感信息泄露(LLM06: Sensitive Information Disclosure) ### 风险原理 模型可能无意暴露训练数据中的未脱敏信息,或在多用户环境下泄漏其他用户输入内容。 ### 攻击示例 * 通过 Canary Prompt 探测模型是否记忆特定信息; * 构造多轮投毒获取模型记忆信息。 ### 安全对策 * 训练阶段使用差分隐私或脱敏语料; * 输出阶段进行实体识别与敏感数据遮蔽; * 日志脱敏与访问隔离保障链路安全。 ### 工程建议 * 使用自动化泄露检测系统(如 LLM Guard); * 实施语料分级管理与访问控制。 --- ## 七、插件架构不安全(LLM07: Insecure Plugin Design) ### 风险原理 插件系统暴露的接口权限不足或执行边界不明,可能被 LLM 误调用执行敏感操作。 ### 攻击示例 * 引导模型调用开放 API 执行写入、删除或远程命令。 ### 安全对策 * 插件必须设置细粒度权限控制(如 OAuth Scopes); * 明确插件作用域,防止越权访问; * 插件执行环境需运行在受限沙箱中。 ### 工程建议 * 建立插件注册、审计与签名机制,确保调用可追溯。 --- ## 八、过度代理能力(LLM08: Excessive Agency) ### 风险原理 LLM 被赋予自治权限(如文件系统访问、交易指令执行)而缺乏人工校验,将引发高风险业务误操作。 ### 攻击示例 * 模型受诱导直接执行高风险命令,如转账、删除等。 ### 安全对策 * 实施基于角色的权限控制(RBAC); * 对高影响操作设置人工审批流程; * 记录所有代理行为用于事后审计。 ### 工程建议 * 引入 Agent 编排框架,对流程状态、权限边界进行建模与约束。 --- ## 九、过度依赖模型输出(LLM09: Overreliance) ### 风险原理 当系统盲目采信模型输出而缺乏交叉验证机制时,可能导致虚假内容直接影响业务流程或用户认知。 ### 攻击示例 * 模型编造答案用于决策系统,未做真实性核查。 ### 安全对策 * 引入知识图谱、规则引擎、传统检索等校验机制; * 使用投票机制、置信度评分筛选输出结果; * 对不确定输出内容引导人工确认。 ### 工程建议 * 构建可信度标注与共识机制,实现人机协同判断。 --- ## 十、模型盗用(LLM10: Model Theft) ### 风险原理 攻击者通过黑盒 API 访问,结合推理样本与输出,逆推出模型结构、参数甚至训练数据,实现克隆或窃取。 ### 攻击示例 * Knockoff Nets、成员推理攻击(Membership Inference)。 ### 安全对策 * 对 API 调用设限:验证码、频率限制、访问控制; * 向模型输出注入噪声或水印,防止建模; * 审计访问日志并建立异常调用检测模型。 ### 工程建议 * 设计诱饵样本与响应陷阱,用于识别异常行为。 --- ## 推荐资源 * [OWASP LLM Top 10 官网](https://owasp.org/www-project-top-10-for-large-language-model-applications/) ---

🔥 全球首个可以无限自主执行的 AI 出现了?一句话生成复杂网站、几十页PPT、爆款图文、千万字长篇小说!鱼皮带你深度体验 视频链接:https://bilibili.com/video/BV1QFTJzzENP/

下载 APP