鱼皮AI导航
鱼皮AI导航
  • 主页
  • 社区
  • AI工具
  • AI知识库
  • AI提示词
  • MCP
  • Skills
  • AI学习资源
  • AI资讯
  • GitHub开源
登录/注册
站长:程序员鱼皮 老鱼简历 商务合作 增值电信业务经营许可证
2026 鱼皮AI导航 | 沪ICP备19026706号-6
10
2
13

AI 超级智能体-基于 MySQL 的 AI 对话持久化

LMQICU
LMQICU
2025-08-08 04:21
阅读 2258
LMQICU
2025-08-08 04:21
阅读 2258

AI 健身教练对话历史模块的实现

库表设计

根据方案设计出对话历史表的结构,为了避免与 AI 库的 ChatMessage 冲突,表名采用 chat_history:

▼
mysql
复制代码
-- 对话历史表 create table if not exists keep_report ( id bigint auto_increment comment 'id' primary key, chatId varchar(255) not null comment '对话id', userId bigint not null comment '创建用户id', messages text not null comment '对话记录(JSON格式存储)', lastMessage text null comment '最后一条消息内容(用于列表展示)', createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间', editTime datetime default CURRENT_TIMESTAMP not null comment '编辑时间', updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', isDelete tinyint default 0 not null comment '是否删除', INDEX idx_user_chat (chatId, userId), -- 新增索引,优化查询效率 INDEX idx_user_time (userId, updateTime) -- 按用户和时间查询的索引 ) comment '上下文对话表' collate = utf8mb4_unicode_ci;

使用代码生成器

image-20250730173306436

后端代码生成 - Vibe Coding

参考 Prompt (AI 的随机性真的“一时生成一时爽,改改 Bug 到半夜”,所以 Prompt 尽量写得详细,不然真的改到崩溃!!!):

▼
markdown
复制代码
系统描述: 现在我的项目里通过实现 Chat⁢Memory 接口自定义 MySQL 的存储‎,使用 MySQL 来保存对话上下文功能,在 messages 字段里记录了对话记录(JSON格式存储)经给JSON工具转换后如下: ```json [ { "messageType": "USER", "message": "我是LMQ,我想通过健身来增肌,请你给我一些健身的建议" }, { "messageType": "ASSISTANT", "message": "嗨 LMQ,很高兴你选择通过健身来增肌,这是一个很棒的目标!在我们开始前,xxxxxxxxx" } ] ``` 需求: 现在我想增加上下文对话表的功能来实现历史对话的功能,因为目前 messages 字段里已经存贮了用户对话内容和AI响应内容,那么是不是可以在对话记录中添加一个用户id,当前端点击对话历史后根据用户id和对话id从数据库查找到对应的用户内容和AI响应的内容,如果我说的不能实现历史对话功能,那请你给我想其他可以实现历史对话功能的办法来实现 分析: 1、存储逻辑调整: 保存对话时,需要同时写入`userId`和`chatId`: - `chatId`:唯一标识一个对话(建议用UUID生成,确保唯一性)。 - 每次用户与AI的新交互(发送消息/接收响应),都需要更新该`chatId`对应的`messages`字段(在JSON数组中追加新消息)。 示例:用户`user123`的一个对话`chat456`,首次交互后`messages`是初始JSON,当用户发送第二条消息后,`messages`会追加新的`USER`和`ASSISTANT`消息: ```json [ {"messageType": "USER", "message": "首次消息"}, {"messageType": "ASSISTANT", "message": "首次响应"}, {"messageType": "USER", "message": "第二条消息"}, {"messageType": "ASSISTANT", "message": "第二条响应"} ] ``` 2、核心接口设计(实现历史对话功能): 需要两个关键接口,配合前端实现“历史对话列表展示”和“对话详情加载”: (1)获取用户的历史对话列表 **用途**:前端展示用户的所有历史对话(类似聊天软件的会话列表)。 **请求参数**:`userId`(当前用户id)。 **返回数据**:包含每个对话的基本信息(不需要完整消息,只需缩略信息): ```json { "code": 200, "data": [ { "chatId": "chat456", "lastMessage": "第二条响应", // 对话最后一条消息(用于列表展示) "createTime": "2023-10-01 09:00:00", // 对话创建时间 "updateTime": "2023-10-01 09:30:00" // 最后更新时间(用于排序) }, // 更多对话... ] } ``` 实现逻辑: - 根据`userId`查询`keep_report`表(过滤`isDelete=0`)。 - 对每个对话,解析`messages`的JSON数组,取最后一条消息作为`lastMessage`(为了效率,可在表中新增`last_message`字段直接存储,避免每次解析JSON)。 (2)获取单个对话的完整消息 用途:用户点击某个历史对话时,加载该对话的完整聊天记录。 请求参数:`userId`、`chatId`。 返回数据:该对话的完整`messages`数组: ```json { "code": 200, "data": { "chatId": "chat456", "messages": [ {"messageType": "USER", "message": "首次消息"}, {"messageType": "ASSISTANT", "message": "首次响应"}, // ... 更多消息 ] } } ``` 实现逻辑: - 根据`userId`和`chatId`查询`keep_report`表,返回`messages`字段的JSON内容。 总结: 核心是通过添加`userId`字段关联用户,再配合两个接口(获取对话列表、获取对话详情),即可实现历史对话功能。现有`messages`字段已存储完整的对话内容(包含用户和 AI 消息),无需修改 JSON 结构,只需确保`chatId`唯一标识一个对话,`userId`正确关联用户即可。 3、优化建议 1. 新增`last_message`字段: 为了避免每次查询历史对话列表时都解析`messages` JSON(影响性能),可以在表中新增`last_message`字段,每次更新对话时同步更新该字段(存储最后一条消息内容)。 2. 分页查询: 当用户历史对话较多时,“获取历史对话列表”接口需要支持分页(传入`pageNum`、`pageSize`参数)。 3. 删除对话功能: 新增“删除历史对话”接口,通过`user_id`和`chatId`将`isDelete`设为1(逻辑删除),前端不再展示已删除的对话。

1)实体类

▼
java
复制代码
/** * 健身报告记录表 * * @TableName keep_report */ @Data @Builder @AllArgsConstructor @NoArgsConstructor @TableName(value = "keep_report") public class KeepReport implements Serializable { /** * id */ @TableId(type = IdType.AUTO) private Long id; /** * 对话id */ @TableField(value = "chatId") private String chatId; /** * 创建用户id */ @TableField(value = "userId") private Long userId; /** * 对话记录(JSON格式存储) */ @TableField(value = "messages") private String messages; /** * 最后一条消息内容(用于列表展示) */ @TableField(value = "lastMessage") private String lastMessage; /** * 创建时间 */ private Date createTime; /** * 编辑时间 */ private Date editTime; /** * 更新时间 */ private Date updateTime; /** * 是否删除 */ private Integer isDelete; @TableField(exist = false) private static final long serialVersionUID = 1L; }

2)聊天历史详情 DTO 类

▼
java
复制代码
/** * 聊天历史详情DTO * * @author MQQQ * @create 2025-08-07 */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public class ChatHistoryDetailDTO { /** * 对话ID */ private String chatId; /** * 消息列表 */ private List<ChatMessageDTO> messages; /** * 单条消息DTO */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public static class ChatMessageDTO { /** * 消息类型:USER/ASSISTANT */ private String messageType; /** * 消息内容 */ private String message; } }

3)聊天历史列表 DTO 类

▼
java
复制代码
/** * 聊天历史列表DTO * * @author MQQQ * @create 2025-08-07 */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public class ChatHistoryListDTO { /** * 对话ID */ private String chatId; /** * 最后一条消息内容 */ private String lastMessage; /** * 对话创建时间 */ private Date createTime; /** * 最后更新时间 */ private Date updateTime; }

4)聊天历史服务接口

▼
java
复制代码
/** * 聊天历史服务接口 * * @author MQQQ * @create 2025-08-07 */ public interface ChatHistoryService { /** * 获取用户的历史对话列表 * * @param userId 用户ID * @return 历史对话列表 */ List<ChatHistoryListDTO> getChatHistoryList(Long userId); /** * 获取单个对话的完整消息 * * @param userId 用户ID * @param chatId 对话ID * @return 对话详情 */ ChatHistoryDetailDTO getChatHistoryDetail(Long userId, String chatId); /** * 删除用户的某个对话 * * @param userId 用户ID * @param chatId 对话ID * @return 是否删除成功 */ boolean deleteChatHistory(Long userId, String chatId); }

5)聊天历史服务实现类

▼
java
复制代码
/** * 聊天历史服务实现类 * * @author MQQQ * @create 2025-08-07 */ @Service @Slf4j public class ChatHistoryServiceImpl implements ChatHistoryService { @Resource private KeepReportMapper keepReportMapper; @Override public List<ChatHistoryListDTO> getChatHistoryList(Long userId) { try { // 构建查询条件:根据用户ID查询,按更新时间倒序排列 LambdaQueryWrapper<KeepReport> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(KeepReport::getUserId, userId) .eq(KeepReport::getIsDelete, 0) .orderByDesc(KeepReport::getUpdateTime); // 查询数据库 List<KeepReport> keepReports = keepReportMapper.selectList(queryWrapper); // 转换为DTO return keepReports.stream() .map(report -> ChatHistoryListDTO.builder() .chatId(report.getChatId()) .lastMessage(report.getLastMessage()) .createTime(report.getCreateTime()) .updateTime(report.getUpdateTime()) .build()) .collect(Collectors.toList()); } catch (Exception e) { log.error("获取用户历史对话列表失败,用户ID: {}, 错误: {}", userId, e.getMessage(), e); return new ArrayList<>(); } } @Override public ChatHistoryDetailDTO getChatHistoryDetail(Long userId, String chatId) { try { // 构建查询条件 LambdaQueryWrapper<KeepReport> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(KeepReport::getUserId, userId) .eq(KeepReport::getChatId, chatId) .eq(KeepReport::getIsDelete, 0); // 查询数据库 KeepReport keepReport = keepReportMapper.selectOne(queryWrapper); if (keepReport == null || keepReport.getMessages() == null || keepReport.getMessages().isBlank()) { log.warn("未找到对话记录或消息为空,用户ID: {}, 对话ID: {}", userId, chatId); return null; } // 解析消息JSON List<ChatHistoryDetailDTO.ChatMessageDTO> messages = parseMessages(keepReport.getMessages()); return ChatHistoryDetailDTO.builder() .chatId(chatId) .messages(messages) .build(); } catch (Exception e) { log.error("获取对话详情失败,用户ID: {}, 对话ID: {}, 错误: {}", userId, chatId, e.getMessage(), e); return null; } } @Override public boolean deleteChatHistory(Long userId, String chatId) { try { // 构建更新条件:逻辑删除 LambdaUpdateWrapper<KeepReport> updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(KeepReport::getUserId, userId) .eq(KeepReport::getChatId, chatId) .set(KeepReport::getIsDelete, 1); // 执行逻辑删除 int updatedRows = keepReportMapper.update(null, updateWrapper); if (updatedRows > 0) { log.info("成功删除对话记录,用户ID: {}, 对话ID: {}", userId, chatId); return true; } else { log.warn("删除对话记录失败,记录不存在,用户ID: {}, 对话ID: {}", userId, chatId); return false; } } catch (Exception e) { log.error("删除对话记录失败,用户ID: {}, 对话ID: {}, 错误: {}", userId, chatId, e.getMessage(), e); return false; } } /** * 解析消息JSON为DTO列表 * * @param messagesJson 消息JSON字符串 * @return 消息DTO列表 */ private List<ChatHistoryDetailDTO.ChatMessageDTO> parseMessages(String messagesJson) { List<ChatHistoryDetailDTO.ChatMessageDTO> messages = new ArrayList<>(); try { JSONArray jsonArray = JSON.parseArray(messagesJson); for (int i = 0; i < jsonArray.size(); i++) { JSONObject jsonObject = jsonArray.getJSONObject(i); String messageType = jsonObject.getString("messageType"); // 处理消息内容 Object messageObj = jsonObject.get("message"); String messageContent; if (messageObj instanceof JSONObject) { // 如果是JSON对象,转换为字符串 messageContent = messageObj.toString(); } else { // 否则直接使用字符串值 messageContent = jsonObject.getString("message"); } messages.add(ChatHistoryDetailDTO.ChatMessageDTO.builder() .messageType(messageType) .message(messageContent) .build()); } } catch (Exception e) { log.error("解析消息JSON失败: {}", e.getMessage(), e); } return messages; } }

6)聊天历史接口

▼
java
复制代码
/** * 聊天历史接口 * * @author MQQQ * @create 2025-08-07 */ @RestController @RequestMapping("/chat/history") @Slf4j public class ChatHistoryController { @Resource private ChatHistoryService chatHistoryService; @Resource private UserService userService; /** * 获取用户的历史对话列表 * * @param request HTTP请求对象 * @return 历史对话列表 */ @GetMapping("/list") public BaseResponse<List<ChatHistoryListDTO>> getChatHistoryList(HttpServletRequest request) { try { // 获取当前登录用户 User currentUser = userService.getLoginUser(request); // 获取历史对话列表 List<ChatHistoryListDTO> historyList = chatHistoryService.getChatHistoryList(currentUser.getId()); return ResultUtils.success(historyList); } catch (BusinessException e) { log.warn("获取历史对话列表失败: {}", e.getMessage()); return ResultUtils.error(e.getCode(), e.getMessage()); } catch (Exception e) { log.error("获取历史对话列表异常: {}", e.getMessage(), e); return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统异常"); } } /** * 获取单个对话的完整消息 * * @param chatId 对话ID * @param request HTTP请求对象 * @return 对话详情 */ @GetMapping("/detail") public BaseResponse<ChatHistoryDetailDTO> getChatHistoryDetail(@RequestParam String chatId, HttpServletRequest request) { try { // 参数校验 if (chatId == null || chatId.trim().isEmpty()) { return ResultUtils.error(ErrorCode.PARAMS_ERROR, "对话ID不能为空"); } // 获取当前登录用户 User currentUser = userService.getLoginUser(request); // 获取对话详情 ChatHistoryDetailDTO historyDetail = chatHistoryService.getChatHistoryDetail(currentUser.getId(), chatId); if (historyDetail == null) { return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, "对话记录不存在"); } return ResultUtils.success(historyDetail); } catch (BusinessException e) { log.warn("获取对话详情失败: {}", e.getMessage()); return ResultUtils.error(e.getCode(), e.getMessage()); } catch (Exception e) { log.error("获取对话详情异常: {}", e.getMessage(), e); return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统异常"); } } /** * 删除用户的某个对话 * * @param chatId 对话ID * @param request HTTP请求对象 * @return 是否删除成功 */ @DeleteMapping("/delete") public BaseResponse<Boolean> deleteChatHistory(@RequestParam String chatId, HttpServletRequest request) { try { // 参数校验 if (chatId == null || chatId.trim().isEmpty()) { return ResultUtils.error(ErrorCode.PARAMS_ERROR, "对话ID不能为空"); } // 获取当前登录用户 User currentUser = userService.getLoginUser(request); // 删除对话 boolean success = chatHistoryService.deleteChatHistory(currentUser.getId(), chatId); if (success) { return ResultUtils.success(true); } else { return ResultUtils.error(ErrorCode.OPERATION_ERROR, "删除失败,对话记录不存在"); } } catch (BusinessException e) { log.warn("删除对话失败: {}", e.getMessage()); return ResultUtils.error(e.getCode(), e.getMessage()); } catch (Exception e) { log.error("删除对话异常: {}", e.getMessage(), e); return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统异常"); } } }

7)DatabaseChatMemory 支持用户ID参数

▼
java
复制代码
/** * ClassName:DatabaseChatMemory * Package:com.mq.mqaiagent.chatmemory * Description: 持久化对话到 MySQL * Author:MQQQ * * @Create:2025/6/19 - 18:51 * @Version:v1.0 */ @Slf4j public class DatabaseChatMemory implements ChatMemory { private final com.mq.mqaiagent.mapper.KeepReportMapper KeepReportMapper; private Long currentUserId; // 当前用户ID /** * 构造函数,初始化 KeepReportMapper。 * * @param KeepReportMapper 健身报告数据库操作 Mapper */ public DatabaseChatMemory(com.mq.mqaiagent.mapper.KeepReportMapper KeepReportMapper) { this.KeepReportMapper = KeepReportMapper; } /** * 设置当前用户ID * * @param userId 用户ID */ public void setCurrentUserId(Long userId) { this.currentUserId = userId; } /** * 向指定对话 ID 的记忆中添加消息列表。 * 会将新消息追加到现有消息列表末尾,并持久化到数据库。 * * @param conversationId 对话的唯一标识符。 * @param messages 要添加的消息列表。 */ @Override public void add(String conversationId, List<Message> messages) { if (currentUserId != null) { // 如果设置了用户ID,使用支持用户ID的方法 add(conversationId, currentUserId, messages); } else { // 获取当前对话的完整历史记录 List<Message> conversationMessages = getOrCreateConversation(conversationId); // 将新消息添加到历史记录末尾 conversationMessages.addAll(messages); // 保存更新后的完整对话历史 saveConversation(conversationId, conversationMessages); } } /** * 向指定对话 ID 的记忆中添加消息列表(支持用户ID)。 * 会将新消息追加到现有消息列表末尾,并持久化到数据库。 * * @param conversationId 对话的唯一标识符。 * @param userId 用户ID。 * @param messages 要添加的消息列表。 */ public void add(String conversationId, Long userId, List<Message> messages) { // 获取当前对话的完整历史记录 List<Message> conversationMessages = getOrCreateConversation(conversationId, userId); // 将新消息添加到历史记录末尾 conversationMessages.addAll(messages); // 保存更新后的完整对话历史 saveConversation(conversationId, userId, conversationMessages); } /** * 获取指定对话 ID 的最近 N 条消息。 * 如果请求的消息数量 N 大于等于实际消息总数,则返回所有消息。 * 如果对话不存在或为空,则返回空列表。 * * @param conversationId 对话的唯一标识符。 * @param lastN 要获取的最近消息的数量。 * @return 包含最近 N 条消息的列表,按时间顺序排列(旧 -> 新)。 */ @Override public List<Message> get(String conversationId, int lastN) { if (currentUserId != null) { // 如果设置了用户ID,使用支持用户ID的方法 return get(conversationId, currentUserId, lastN); } else { // 获取完整的对话历史 List<Message> allMessages = getOrCreateConversation(conversationId); if (allMessages.isEmpty()) { // 对话为空,返回空列表 return allMessages; } // 计算需要跳过的消息数量 int skipCount = Math.max(0, allMessages.size() - lastN); return allMessages.stream() // 跳过前面的消息 .skip(skipCount) // 返回最后 N 条消息 .toList(); } } /** * 获取指定对话 ID 的最近 N 条消息(支持用户ID)。 * 如果请求的消息数量 N 大于等于实际消息总数,则返回所有消息。 * 如果对话不存在或为空,则返回空列表。 * * @param conversationId 对话的唯一标识符。 * @param userId 用户ID。 * @param lastN 要获取的最近消息的数量。 * @return 包含最近 N 条消息的列表,按时间顺序排列(旧 -> 新)。 */ public List<Message> get(String conversationId, Long userId, int lastN) { // 获取完整的对话历史 List<Message> allMessages = getOrCreateConversation(conversationId, userId); if (allMessages.isEmpty()) { // 对话为空,返回空列表 return allMessages; } // 计算需要跳过的消息数量 int skipCount = Math.max(0, allMessages.size() - lastN); return allMessages.stream() // 跳过前面的消息 .skip(skipCount) // 返回最后 N 条消息 .toList(); } /** * 清除指定对话 ID 的所有记忆(删除对应的数据库记录)。 * * @param conversationId 对话的唯一标识符。 */ @Override public void clear(String conversationId) { try { // 构建查询条件 LambdaQueryWrapper<KeepReport> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(KeepReport::getChatId, getSafeConversationId(conversationId)); // 删除对应的记录 int deletedRows = KeepReportMapper.delete(queryWrapper); if (deletedRows > 0) { log.info("成功清除对话记录: {}", conversationId); } else { log.warn("尝试清除不存在的对话记录: {}", conversationId); } } catch (Exception e) { log.error("清除对话记录失败: {}", conversationId, e); } } /** * 获取或创建指定对话 ID 的消息列表。 * 如果对应的记录存在,则从中反序列化消息列表;否则,返回一个新的空列表。 * * @param conversationId 对话的唯一标识符。 * @return 对话的消息列表。如果反序列化失败或记录不存在,则返回空列表。 */ private List<Message> getOrCreateConversation(String conversationId) { String safeConversationId = getSafeConversationId(conversationId); try { // 构建查询条件 LambdaQueryWrapper<KeepReport> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(KeepReport::getChatId, safeConversationId); // 查询数据库 KeepReport loveReport = KeepReportMapper.selectOne(queryWrapper); if (loveReport != null && loveReport.getMessages() != null && !loveReport.getMessages().isBlank()) { // 从数据库记录中反序列化完整的消息列表 try { // 解析存储的简化消息格式 return deserializeMessages(loveReport.getMessages()); } catch (Exception e) { log.error("反序列化对话消息失败,对话ID: {}, 错误: {}", safeConversationId, e.getMessage(), e); // 反序列化失败,可能数据已损坏,返回空列表 return new ArrayList<>(); } } else { log.debug("未找到对话记录或消息为空,对话ID: {}", safeConversationId); } } catch (Exception e) { log.error("获取对话记录失败,对话ID: {}, 错误: {}", safeConversationId, e.getMessage(), e); } // 如果记录不存在或发生异常,返回新的空列表 return new ArrayList<>(); } /** * 获取或创建指定对话 ID 的消息列表(支持用户ID)。 * 如果对应的记录存在,则从中反序列化消息列表;否则,返回一个新的空列表。 * * @param conversationId 对话的唯一标识符。 * @param userId 用户ID。 * @return 对话的消息列表。如果反序列化失败或记录不存在,则返回空列表。 */ private List<Message> getOrCreateConversation(String conversationId, Long userId) { String safeConversationId = getSafeConversationId(conversationId); try { // 构建查询条件 LambdaQueryWrapper<KeepReport> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(KeepReport::getChatId, safeConversationId) .eq(KeepReport::getUserId, userId); // 查询数据库 KeepReport keepReport = KeepReportMapper.selectOne(queryWrapper); if (keepReport != null && keepReport.getMessages() != null && !keepReport.getMessages().isBlank()) { // 从数据库记录中反序列化完整的消息列表 try { // 解析存储的简化消息格式 return deserializeMessages(keepReport.getMessages()); } catch (Exception e) { log.error("反序列化对话消息失败,对话ID: {}, 用户ID: {}, 错误: {}", safeConversationId, userId, e.getMessage(), e); // 反序列化失败,可能数据已损坏,返回空列表 return new ArrayList<>(); } } else { log.debug("未找到对话记录或消息为空,对话ID: {}, 用户ID: {}", safeConversationId, userId); } } catch (Exception e) { log.error("获取对话记录失败,对话ID: {}, 用户ID: {}, 错误: {}", safeConversationId, userId, e.getMessage(), e); } // 如果记录不存在或发生异常,返回新的空列表 return new ArrayList<>(); } /** * 将指定对话的完整消息列表序列化并保存到数据库。 * 会覆盖之前的记录。 * * @param conversationId 对话的唯一标识符。 * @param messages 要保存的完整消息列表。 */ private void saveConversation(String conversationId, List<Message> messages) { String safeConversationId = getSafeConversationId(conversationId); try { // 将消息列表转换为简化的JSON格式 String messagesJson = serializeMessages(messages); // 构建查询条件 LambdaQueryWrapper<KeepReport> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(KeepReport::getChatId, safeConversationId); // 查询数据库中是否已存在该对话的记录 KeepReport existingReport = KeepReportMapper.selectOne(queryWrapper); if (existingReport != null) { // 更新现有记录的 messages 字段 existingReport.setMessages(messagesJson); int updatedRows = KeepReportMapper.updateById(existingReport); if (updatedRows > 0) { log.debug("成功更新对话记录: {}", safeConversationId); } else { log.warn("更新对话记录失败(可能已被删除): {}", safeConversationId); } } else { // 创建新记录 KeepReport newReport = KeepReport.builder() .chatId(safeConversationId) .createTime(new Date()) .updateTime(new Date()) // 存储序列化后的完整消息列表 .messages(messagesJson) .build(); int insertedRows = KeepReportMapper.insert(newReport); if (insertedRows > 0) { log.debug("成功插入新对话记录: {}", safeConversationId); } else { log.error("插入新对话记录失败: {}", safeConversationId); } } } catch (Exception e) { log.error("保存对话记录失败,对话ID: {}, 错误: {}", safeConversationId, e.getMessage(), e); } } /** * 将指定对话的完整消息列表序列化并保存到数据库(支持用户ID)。 * 会覆盖之前的记录。 * * @param conversationId 对话的唯一标识符。 * @param userId 用户ID。 * @param messages 要保存的完整消息列表。 */ private void saveConversation(String conversationId, Long userId, List<Message> messages) { String safeConversationId = getSafeConversationId(conversationId); try { // 将消息列表转换为简化的JSON格式 String messagesJson = serializeMessages(messages); // 获取最后一条消息内容用于列表展示 String lastMessage = getLastMessageContent(messages); // 构建查询条件 LambdaQueryWrapper<KeepReport> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(KeepReport::getChatId, safeConversationId) .eq(KeepReport::getUserId, userId); // 查询数据库中是否已存在该对话的记录 KeepReport existingReport = KeepReportMapper.selectOne(queryWrapper); if (existingReport != null) { // 更新现有记录的 messages 和 lastMessage 字段 existingReport.setMessages(messagesJson); existingReport.setLastMessage(lastMessage); int updatedRows = KeepReportMapper.updateById(existingReport); if (updatedRows > 0) { log.debug("成功更新对话记录: {}, 用户ID: {}", safeConversationId, userId); } else { log.warn("更新对话记录失败(可能已被删除): {}, 用户ID: {}", safeConversationId, userId); } } else { // 创建新记录 KeepReport newReport = KeepReport.builder() .chatId(safeConversationId) .userId(userId) .createTime(new Date()) .updateTime(new Date()) // 存储序列化后的完整消息列表 .messages(messagesJson) .lastMessage(lastMessage) .build(); int insertedRows = KeepReportMapper.insert(newReport); if (insertedRows > 0) { log.debug("成功插入新对话记录: {}, 用户ID: {}", safeConversationId, userId); } else { log.error("插入新对话记录失败: {}, 用户ID: {}", safeConversationId, userId); } } } catch (Exception e) { log.error("保存对话记录失败,对话ID: {}, 用户ID: {}, 错误: {}", safeConversationId, userId, e.getMessage(), e); } } /** * 获取最后一条消息的内容用于列表展示 * * @param messages 消息列表 * @return 最后一条消息的内容,如果为空则返回默认文本 */ private String getLastMessageContent(List<Message> messages) { if (messages == null || messages.isEmpty()) { return "暂无消息"; } Message lastMessage = messages.get(messages.size() - 1); String content = lastMessage.getText(); // 如果内容过长,截取前200个字符 if (content != null && content.length() > 200) { return content.substring(0, 200) + "..."; } return content != null ? content : "暂无消息"; } /** * 处理对话 ID,确保其可以安全地用作数据库记录的聊天ID。 * * @param conversationId 原始对话的唯一标识符。 * @return 处理后的安全对话 ID,例如 "chat_your_conversation_id"。 */ private String getSafeConversationId(String conversationId) { // 确保 conversationId 不为 null 或空 if (conversationId == null || conversationId.isBlank()) { log.warn("接收到空的 conversationId,将使用默认值 'default_chat'"); conversationId = "default_chat"; } // 替换掉不安全的字符,保留字母、数字、下划线和连字符 String sanitizedId = conversationId.replaceAll("[^a-zA-Z0-9\\-_]", "_"); // 检查是否已经以 "chat_" 开头,如果是则直接返回,避免重复添加前缀 if (sanitizedId.startsWith("chat_")) { return sanitizedId; } // 添加前缀以避免潜在的冲突或保留字问题 return "chat_" + sanitizedId; } /** * 将消息列表序列化为简化的JSON格式。 * 格式为:[ * { * "messageType": "USER", * "message": "用户消息内容" * }, * { * "messageType": "ASSISTANT", * "message": { ... JSON对象 ... } * } * ] * * @param messages 要序列化的消息列表 * @return 序列化后的JSON字符串 */ private String serializeMessages(List<Message> messages) { JSONArray jsonArray = new JSONArray(); for (Message message : messages) { JSONObject jsonObject = new JSONObject(); jsonObject.put("messageType", message.getMessageType().toString()); if (message instanceof UserMessage) { // 用户消息直接存储文本内容 jsonObject.put("message", message.getText()); } else if (message instanceof AssistantMessage) { // 助手消息可能包含JSON格式的内容,尝试解析 String content = message.getText(); try { // 尝试将内容解析为JSON对象 JSONObject contentJson = JSON.parseObject(content); jsonObject.put("message", contentJson); } catch (Exception e) { // 如果解析失败,则直接存储文本内容 jsonObject.put("message", content); } } else { // 其他类型的消息直接存储文本内容 jsonObject.put("message", message.getText()); } jsonArray.add(jsonObject); } return jsonArray.toString(); } /** * 将简化的JSON格式反序列化为消息列表。 * * @param json 要反序列化的JSON字符串 * @return 反序列化后的消息列表 */ private List<Message> deserializeMessages(String json) { List<Message> messages = new ArrayList<>(); JSONArray jsonArray = JSON.parseArray(json); for (int i = 0; i < jsonArray.size(); i++) { JSONObject jsonObject = jsonArray.getJSONObject(i); String messageTypeStr = jsonObject.getString("messageType"); MessageType messageType = MessageType.valueOf(messageTypeStr); if (messageType == MessageType.USER) { // 用户消息 String content = jsonObject.getString("message"); messages.add(new UserMessage(content)); } else if (messageType == MessageType.ASSISTANT) { // 助手消息 Object messageObj = jsonObject.get("message"); String content; if (messageObj instanceof JSONObject) { // 如果是JSON对象,转换为字符串 content = messageObj.toString(); } else { // 否则直接使用字符串值 content = jsonObject.getString("message"); } // 创建助手消息,使用空的元数据 Map<String, Object> metadata = new HashMap<>(); messages.add(new AssistantMessage(content, metadata)); } else { // 其他类型的消息(暂不支持) log.warn("不支持的消息类型: {}", messageType); } } return messages; } }

8)MqManus 添加对话记忆支持

移除 @Component 注解避免构造函数冲突:

▼
java
复制代码
/** * ClassName:MqManus * Package:com.mq.mqaiagent.agent * Description: MqManus 实例 * Author:MQQQ * * @Create:2025/7/2 - 16:15 * @Version:v1.0 */ public class MqManus extends ToolCallAgent { public MqManus(ToolCallback[] allTools, ChatModel dashscopeChatModel) { super(allTools); this.setName("mqmanus"); String SYSTEM_PROMPT = """ You are MqManus, an all-capable AI assistant, aimed at solving any task presented by the user. You have various tools at your disposal that you can call upon to efficiently complete complex requests. """; this.setSystemPrompt(SYSTEM_PROMPT); String NEXT_STEP_PROMPT = """ Based on user needs, proactively select the most appropriate tool or combination of tools. For complex tasks, you can break down the problem and use different tools step by step to solve it. After using each tool, clearly explain the execution results and suggest the next steps. If you want to stop the interaction at any point, use the `terminate` tool/function call. """; this.setNextStepPrompt(NEXT_STEP_PROMPT); this.setMaxSteps(20); // 初始化客户端 ChatClient chatClient = ChatClient.builder(dashscopeChatModel) .defaultAdvisors(new MyLoggerAdvisor()) .build(); this.setChatClient(chatClient); } /** * 支持对话记忆和用户ID的构造函数 * * @param allTools 工具数组 * @param dashscopeChatModel 聊天模型 * @param chatMemory 对话记忆 */ public MqManus(ToolCallback[] allTools, ChatModel dashscopeChatModel, DatabaseChatMemory chatMemory) { super(allTools); this.setName("mqmanus"); String SYSTEM_PROMPT = """ You are MqManus, an all-capable AI assistant, aimed at solving any task presented by the user. You have various tools at your disposal that you can call upon to efficiently complete complex requests. """; this.setSystemPrompt(SYSTEM_PROMPT); String NEXT_STEP_PROMPT = """ Based on user needs, proactively select the most appropriate tool or combination of tools. For complex tasks, you can break down the problem and use different tools step by step to solve it. After using each tool, clearly explain the execution results and suggest the next steps. If you want to stop the interaction at any point, use the `terminate` tool/function call. """; this.setNextStepPrompt(NEXT_STEP_PROMPT); this.setMaxSteps(20); // 初始化带对话记忆的客户端 ChatClient chatClient = ChatClient.builder(dashscopeChatModel) .defaultAdvisors( new MessageChatMemoryAdvisor(chatMemory), new MyLoggerAdvisor()) .build(); this.setChatClient(chatClient); } }

9)KeepApp 新增支持用户ID的流式对话方法

▼
java
复制代码
private final ChatModel dashscopeChatModel; private final KeepReportMapper keepReportMapper; /** * KeepApp 使用流式对话(支持用户ID) * * @param message 用户消息 * @param chatId 对话ID * @param userId 用户ID * @return 流式响应 */ public Flux<String> doChatByStream(String message, String chatId, Long userId) { // 使用支持userId的DatabaseChatMemory DatabaseChatMemory chatMemory = new DatabaseChatMemory(keepReportMapper); // 设置当前用户ID chatMemory.setCurrentUserId(userId); ChatClient userChatClient = ChatClient.builder(dashscopeChatModel) .defaultSystem(SYSTEM_PROMPT) .defaultAdvisors( new MessageChatMemoryAdvisor(chatMemory), new MyLoggerAdvisor()) .build(); return userChatClient .prompt() .user(message) .advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId) .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)) .stream() .content(); }

10)API接口扩展

  • /ai/keep_app/chat/sse/user: 流式对话(支持用户认证)
  • /ai/manus/chat/user: Manus智能体(支持用户认证和对话记忆)
▼
java
复制代码
@Resource private UserService userService; @Resource private KeepReportMapper keepReportMapper; /** * KeepApp 使用流式对话(支持用户认证) * * @param message * @param chatId * @param request * @return */ @GetMapping(value = "/keep_app/chat/sse/user", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> doChatWithKeepAppSSEUser(String message, String chatId, HttpServletRequest request) { // 获取当前登录用户 User currentUser = userService.getLoginUser(request); return keepApp.doChatByStream(message, chatId, currentUser.getId()); } /** * 流式调用 Manus 超级智能体(支持用户认证和对话记忆) * * @param message * @param chatId * @param request * @return */ @GetMapping("/manus/chat/user") public SseEmitter doChatWithManusUser(String message, String chatId, HttpServletRequest request) { // 获取当前登录用户 User currentUser = userService.getLoginUser(request); // 创建支持用户ID的DatabaseChatMemory DatabaseChatMemory chatMemory = new DatabaseChatMemory(keepReportMapper); chatMemory.setCurrentUserId(currentUser.getId()); // 创建带对话记忆的MqManus实例 MqManus mqManus = new MqManus(allTools, dashscopeChatModel, chatMemory); return mqManus.runStream(message); }

11)commit 信息

▼
markdown
复制代码
feat: 实现用户历史对话功能 ✨ 新增功能: - 添加用户历史对话存储和查询功能 - 支持用户认证的AI对话接口 - 实现对话记录的用户隔离和权限控制 🗄️ 数据库更新: - keep_report表新增userId和lastMessage字段 - 添加复合索引优化查询性能 - 支持用户级别的对话记录管理 🔧 核心组件修改: - DatabaseChatMemory: 支持用户ID参数,修复chatId重复前缀问题 - KeepApp: 新增支持用户ID的流式对话方法 - MqManus: 添加对话记忆支持,移除@Component注解避免构造函数冲突 🌐 API接口扩展: - /ai/keep_app/chat/sse/user: 流式对话(支持用户认证) - /ai/manus/chat/user: Manus智能体(支持用户认证和对话记忆) - /chat/history/list: 获取用户历史对话列表 - /chat/history/detail: 获取单个对话详情 - /chat/history/delete: 删除用户对话记录 📦 新增文件: - ChatHistoryController: 历史对话REST API控制器 - ChatHistoryService: 历史对话业务逻辑接口 - ChatHistoryServiceImpl: 历史对话服务实现 - ChatHistoryListDTO/DetailDTO: 数据传输对象 🐛 问题修复: - 修复chatId存储时重复添加前缀的问题 - 确保前端传递的chatId与数据库存储格式一致feat: 实现用户历史对话功能 ✨ 新增功能: - 添加用户历史对话存储和查询功能 - 支持用户认证的AI对话接口 - 实现对话记录的用户隔离和权限控制 🗄️ 数据库更新: - keep_report表新增userId和lastMessage字段 - 添加复合索引优化查询性能 - 支持用户级别的对话记录管理 🔧 核心组件修改: - DatabaseChatMemory: 支持用户ID参数,修复chatId重复前缀问题 - KeepApp: 新增支持用户ID的流式对话方法 - MqManus: 添加对话记忆支持,移除@Component注解避免构造函数冲突 🌐 API接口扩展: - /ai/keep_app/chat/sse/user: 流式对话(支持用户认证) - /ai/manus/chat/user: Manus智能体(支持用户认证和对话记忆) - /chat/history/list: 获取用户历史对话列表 - /chat/history/detail: 获取单个对话详情 - /chat/history/delete: 删除用户对话记录 📦 新增文件: - ChatHistoryController: 历史对话REST API控制器 - ChatHistoryService: 历史对话业务逻辑接口 - ChatHistoryServiceImpl: 历史对话服务实现 - ChatHistoryListDTO/DetailDTO: 数据传输对象 🐛 问题修复: - 修复chatId存储时重复添加前缀的问题 - 确保前端传递的chatId与数据库存储格式一致

前端功能实现 - Vibe Coding

参考 Prompt :

▼
markdown
复制代码
1、前端交互流程 1. **展示历史对话列表**: 前端页面加载时,调用“获取用户的历史对话列表”接口,展示`chatId`、`lastMessage`、`updateTime`等信息(按`updateTime`倒序排列,最新对话在最前)。 2. **加载对话详情**: 用户点击某个历史对话时,前端携带`userId`和该对话的`chatId`,调用“获取单个对话的完整消息”接口,解析`messages`数组后,按顺序渲染用户和AI的消息(区分`USER`和`ASSISTANT`角色,用不同样式展示)。 3. **继续对话**: 用户在历史对话详情页发送新消息时,前端仍使用该`chatId`,后端将新消息追加到`messages`数组中,实现“续聊”功能。 2、接口调整 AI健身教练调用的后端AI接口改为:/api/ai/keep_app/chat/sse/user AI超级智能体调用的后端AI接口改为:/api/ai/manus/chat/user

后续主包的 Commit 信息如下,看看 Commit 就知道主包调试时的头大了:

▼
markdown
复制代码
feat: 实现历史对话查看和状态恢复功能 🎉 主要功能实现: - ✅ 修复页面刷新后无法查看历史对话的问题 - ✅ 实现对话状态恢复,用户返回主页后重新进入可恢复之前对话 - ✅ 解决页面卡住和EventSource连接泄漏问题 - ✅ 优化对话创建流程,避免不必要的新chatId生成 🔧 技术改进: - 新增ChatManager.vue统一对话管理组件 - 新增LocalStorageService本地存储服务,实现数据持久化 - 修复API调用逻辑,正确处理后端响应数据格式 - 添加组件生命周期管理,防止内存泄漏 - 实现本地缓存降级策略,提升用户体验 🐛 问题修复: - 修复404 API错误(移除不存在的saveAiMessage调用) - 修复前端数据格式转换问题(chatId映射为id) - 修复初始化时自动创建新对话的问题 - 修复SSE连接错误和EventSource资源泄漏 - 修复对话列表获取和显示逻辑 📱 用户体验优化: - 页面刷新后自动恢复到最后活跃的对话 - 网络异常时使用本地缓存保证功能可用 - 优化欢迎消息显示逻辑 - 改进错误处理和用户反馈 - 添加详细的调试日志便于问题排查 🎯 核心解决方案: 1. 统一对话管理:ChatManager组件统一处理所有对话相关逻辑 2. 数据持久化:LocalStorageService确保数据不丢失 3. API修复:正确调用后端接口并处理响应数据 4. 状态恢复:页面刷新后智能恢复用户对话状态 5. 资源管理:正确清理EventSource连接防止内存泄漏 经过多轮调试和优化,终于实现了完整的历史对话查看功能!

最终历史对话效果

image-20250808035418041

TODO

后端:

  1. 实现用户上传头像功能
  2. 实现身体数据的存储
  3. 实现健身数据的存储

前端:

  1. 实现删除用户的某个对话
  2. 对话的标题使用用户提问的前6-12个字符
  3. 输入框的优化
  4. 添加与用户交互的动态效果,提高用户体验
  5. 实现健身计划的推荐
  6. 实现健身知识的展示
AI超级智能体
MySQL
AI
10
13
分享
操作
评论
相关内容
0个评论
全部评论
点击登录,快来和大家讨论吧~
表情
图片
🔥热门工具
朱雀AI检测朱雀AI检测
朱雀AI检测助手朱雀AI检测助手
LovartLovart
老鱼简历老鱼简历
WritePass官网WritePass官网
面多多面多多
CursorCursor
星月写作星月写作
零感AI零感AI
墨得问题墨得问题

目录

🔥 热门文章
1
手把手教你设计一个成熟的 Agent Harness
2530
2
我用 REA 逆向了付费颜色 APP,结果很意外!
570
3
离谱,DeepSeek 崩了 5 小时,大家却喊:梁圣恩情还不完??
420
4
DeepSeek 全新模型发布,性价比最高的一集!
380
5
B 站上 DeepSeek 闹鱼灾了,我发起《大肥鱼清除计划》!
370
6
自己学习ai,直接买cursor 的套餐,还是买gpt 或者claude 的套餐?
360
7
又一个国产版 Codex 神器,也来卷办公了!
330
8
day3-如何写出一个高质量的 Skill(组内分享) 高质量 Skill 不是“一个调得很顺的巨型 Prompt”,而是一个可验证、可回归、可维护的能力包。 先定义“什么算好”,再实现“怎么做好”。评估体系先于 Skill 本身成型。 下面用“故障诊断 Markdown 转 YAML”作为例子,但方法是通用的。 --- 一、先想清楚:Skill 到底是什么 一个 Skill 至少包含四部分: 部分 作用 接口 输入什么、输出什么、边界在哪 流程 分几步做,每步的产物是什么 规则 硬约束、经验规则、失败模式 降级策略 不确定时怎么办,而不是硬编一个看似合理的结果 高质量 Skill 的共同点:职责单一、接口清晰、规则可定位、输出可验证。 反例:把几十条经验全塞进一个主 Prompt,改一条规则要重调整个 Prompt,评估时也不知道是哪条规则退化。 --- 二、核心方法:先建评估,再写 Skill 不要先写转换器再补测试。正确顺序是: 1. 定义输出契约:什么算合法、什么算合格; 2. 建金标准集:一小批人工认可“可放行”的样本; 3. 先写评估 Skill:让它在金标准集上跑出基线; 4. 再迭代生成 Skill:每次修改跑同一套评估,无退化且目标维度提升才算正向。 这就是评估驱动开发:评估是 Skill 的单元测试,金标准集是回归锚点。 --- 三、评估 Skill:三层门禁,逐层递进 前一层不过,后一层不跑。每层留可追溯证据。 第 1 层:确定性硬门禁 用解析器、Schema、校验器完成,不消耗模型成本。 · 输出可解析; · 必填字段齐全,类型/枚举合法; · 引用目标存在,无悬空引用、无不可达节点; · 关键路径至少有一条能到达预期出口。 不通过直接打回。 第 2 层:语义断言 用脚本 + 结构遍历检查“逻辑对不对”,仍然保持确定性。 通用检查项: · 覆盖率:源材料里的关键步骤、分支、条件,输出里是否都有对应? · 完整性:每个判断节点是否都有明确出口?有没有死胡同? · 可达性:每条路径是否都能到达预期终点?有没有无出口循环? · 顺序与依赖:有先后依赖的操作,是否保留了顺序?有没有被拍平成无序列表? 例子:故障诊断转换中,检查“每个判断项是否有是/否/异常出口”“每条路径是否能到根因”。 第 3 层:LLM-as-Judge 只处理解析器表达不了的维度:忠实度、完整性、可执行性。 但 Judge 会漂移,不能单独放行,必须建立在第 1、2 层之上。Judge 的 Prompt、rubric、模型版本都要版本化,换模型必须用金标准集重新校准。 通用 Judge 维度: 维度 问什么 忠实度 输出是否忠实源材料意图?有没有自行发明内容? 完整性 关键步骤、条件、证据是否保留?隐式知识有没有被压成空话? 收敛性 输出是否仍指向原目标,而不是发散? 可执行性 下游不依赖源材料能否直接使用? --- 四、金标准集:回归测试的锚点 不需要一开始就标注几百份。用已认可的样本当种子,扩到 15~30 份,覆盖失败模式而非随机抽样: · 完整正例:典型流程; · 边界/异常分支:异常路径、循环、前置失败回退; · 已知易错模式:实际踩过的坑; · 留出集:不参与调优,只用于最终验收,防过拟合。 金标准不需要完美,只需要当前标准下认可可放行。 回归指标:关键路径用 pass^k(k 次全过),稳定性用 pass@k。 --- 五、工程约束:让批量产出可重放 · 幂等:同输入 + 同 Skill 版本 + 同规则 → 输出一致,否则无法判断变好是 Skill 改了还是随机波动; · 隔离不阻断:单份失败进隔离队列,记录失败层和原因,不阻塞其余; · 版本绑定:规则、rubric、金标准集进 Git;每份报告绑定“源版本 + 生成 Skill 版本 + 评估 Skill 版本 + Judge 模型版本”; · 抽样人工复核:Judge 临界样本 + 易错模式,人工抽检结果反过来校准 rubric。 --- 六、Skill 本身怎么写 · SKILL.md 控制篇幅,写清:输入约定、输出契约、分步流程、硬规则、降级策略; · 经验规则放 references/ 或 rules/,按失败模式分类,不要全塞进主 Prompt; · 不确定时标 needs_review: true,不要硬编看似合理的结果; · 迭代由评估报告驱动:报告告诉你哪层挂了、哪类失败模式,你改对应规则文件,而不是漫无目的调 Prompt。 --- 七、落地顺序(不要反过来) 1. 固化输出契约:Schema + 结构约束; 2. 建金标准集:从已认可的 + 最易出错的样本开始; 3. 先写评估 Skill:跑出基线报告; 4. 再迭代生成 Skill:每次修改对比逐项通过率,无退化且目标提升才算正向; 5. 批量运行 + 隔离回放:失败进队列,结束后人工抽检; 6. 评估 Skill 自身版本化:换 Judge 模型必须重新校准。 --- 八、给组员的三条硬要求 1. 不许凭感觉说“变好了”:每次修改必须附评估报告对比,逐项通过率说话。 2. 不许跳过确定性门禁直接看 Judge 分数:前两层挂了,Judge 分数没有意义。 3. 不许把规则堆进主 Prompt:规则按失败模式分文件,评估报告要能定位到具体文件。 --- 一句话收尾 生成 Skill 决定“能不能做”,评估 Skill 决定“敢不敢用”。 先让评估体系站稳,再让生成器迭代。这样交付的不是一堆“看起来还行”的产出,而是一套可证明、可回归、可交接的 Skill 质量体系。
310
9
AI 越强,被它拒绝的代价就越大 我有时候会想:AI 越来越厉害之后,有些事情,会不会反而越来越难做? 这个想法看起来有点矛盾。AI 明明在降低门槛,让不会写代码的人做出软件,让不会画画的人生成图片,让一个人完成过去需要整支团队才能完成的工作。但今天用 Codex 的时候,AI 拒绝帮我编写 SQL 注入的请求体,而那还是我有源码、在本地跑测试的情况。那么,往后是否只会更难? 我们假设某一天,AI 说:「这个请求违规,我不能帮助你。」到那个时候,你还有多少能力,能绕过它,把事情做完? 这里的「违规」需要打个引号。平台的规则、模型的判断、法律的要求,以及一件事实际有没有造成伤害,彼此之间并不总是画得上等号。一个请求可能确实危险,也可能只是缺少上下文被误判,或者恰好落在某家公司的限制范围里。 无论是哪种情况,随着我们越来越依赖 AI,这句拒绝都可能变得越来越贵。 今天,一个程序员让 AI 帮忙写代码,AI 拒绝了,他大概率还有别的办法:查文档、找开源项目、问同行,或者自己写。麻烦一点,慢一点,但路还在。 可如果十年之后,大部分程序都由 AI 完成,情况就不一样了。 很多人从接触编程的第一天起,就习惯了描述需求、等待生成、运行结果、继续迭代。他们能做出相当复杂的产品,却未必经历过独立处理底层问题的训练。平时,这种能力结构完全够用,效率甚至更高。只有 AI 拒绝的时候,他们才会发现:平常被省掉的那段学习过程,恰好是此刻最需要的东西。 到那时,一次拒绝的成本,可能就从「今晚多花两个小时」,变成「先花半年学会这门技术」。 拿 SQL 注入举个稍显极端的例子:往后,连真正理解这类攻击如何发生、能够独立构造测试的人,会不会也越来越少? 这里也包括那些有正当需求的人。一个开发者想检查自己的旧系统,一个经过授权的安全研究员想复现漏洞,假如 AI 识别到危险模式就拒绝,而使用者又只熟悉「让 AI 帮我检查」,这项原本可以独立完成的工作,就这样卡住了。 但是安全专家不会因此消失,但这类技能可能越来越集中在少数人手里。你仍然可以找专家,只是要付出更多的钱、更多的时间,以及更高的沟通成本。 类似的困境,未必只发生在网络安全领域。 你想从一个停止维护的软件里取回自己的数据,需要解析它的旧文件格式。AI 判断这涉及规避保护措施,拒绝继续。你拥有这些数据,却没有能力把它们读出来。 你买了一台设备,厂商停止提供服务。你想修改程序让它继续运转,过程中需要处理某种访问限制。AI 不愿协助,而你所掌握的维修方式,早已只剩下「把报错发给 AI」。 你想给常用的网站写一个辅助工具,减少重复操作。平台将此归入自动化限制,AI 也据此拒绝。过去会写脚本的人可以自己判断、自己处理;未来的使用者,可能连从哪里开始都不知道。 这些例子有一个共同点:当工具同时承担了「执行工作」和「传授知识」的角色,它的拒绝就不只是挡住了一次任务,也挡住了获得这种能力的机会。 更麻烦的是,这种依赖往往是我们主动选择的,而且每一步都很合理。 既然 AI 几秒钟就能做好,为什么还要花几个月学习?既然公司可以采购成熟的 AI 服务,为什么还要养一支负责基础工作的团队?既然市场上已经很少有人手写这些东西,学校为什么还要花大量时间教? 对个人、公司和学校来说,这些选择都有现实依据。但叠加起来,整个社会保留的独立能力就可能越来越少。等到某项能力突然受到限制,我们才发现,备用方案早已在追求效率的过程中被一点点削掉了。 所以,「AI 越强,做事越便宜」可能只描述了未来的一半。 另一半是:被允许的事情越来越便宜,被拒绝的事情依然需要人工完成,却要面对更少的人才、更高的报价,以及越来越难获得的知识。两边的成本差距,会越拉越大。 有人会说,可以换一个模型,或者用本地模型。这确实是重要的选项。只要工具之间还有充分竞争,开放模型还足够可用,限制就未必能形成真正的门槛。 但备用方案也需要维护。它需要硬件、知识、软件生态,以及一群仍然理解这些系统的人。如果大多数人已经习惯了最方便的商业服务,那么「理论上还有别的办法」和「普通人实际用得起、用得明白」,之间仍然可能隔着很远。 我担心的,正是这段距离。 我不指望 AI 对任何请求都照单全收。对明显会伤害他人的行为设置限制,有充分的理由。但我们也需要关心:限制由谁定义,误判有没有申诉渠道,用户能不能换工具,以及脱离这个工具之后,还剩下多少独立行动的能力。 保留这些能力,平时看起来甚至有点低效,明明可以自动生成,还要理解原理;明明可以一键完成,还要知道手动怎么做。 但这些看似多余的知识,会在工具拒绝、失效、涨价或改变规则的那一刻,成为真正的选择权。 AI 越强,我们可能越需要留意它说「不」的那一刻。 因为那句话的分量,最终取决于:它拒绝之后,我们还能做什么。
310
10
什么是LLM
300