# 背景 文件名:2025-10-29_3_ai_chat.md 创建于:2025-10-29_23:32:40 创建者:user 主分支:main 任务分支:task/ai_chat_2025-10-29_3 Yolo模式:Off # 任务描述 在本项目中新增一个AI对话功能,使用llama_index构建,服务商为本地部署的ollama。 这个智能体将拥有足够长的上下文(超过30轮对话历史),能够同时与不同的用户展开交流。例如用户A提问后用户B进行补充,智能体将通过时间间隔判断机制来决定何时进行回答。 ## 核心需求 1. **显式指令触发**:使用 `.ai <问题>` 指令触发AI对话 2. **配置指令**:使用 `.aiconfig` 指令配置Ollama服务地址、端口和模型名称 3. **时间间隔判断**:智能体通过时间间隔判断是否需要回答(固定10秒等待窗口) 4. **长上下文管理**:保留超过30轮对话历史 5. **多用户对话支持**:同一chat_id下不同用户的消息能够被正确识别和处理 ## 技术方案决策(已确定) 1. **延迟任务机制**:使用 asyncio 的延迟任务(方案三) - 每个 chat_id 维护独立的延迟任务句柄 - 使用全局字典存储任务映射 - 收到新消息时取消旧任务并创建新任务 2. **上下文管理**:使用 llama_index 的 ChatMemoryBuffer(策略A) - 设置足够的 token_limit 确保保留30+轮对话 - 按 chat_id 独立维护 ChatEngine 实例 3. **多用户识别**:消息角色映射 + 系统提示(方案C) - 将不同用户映射为不同角色(如"用户1"、"用户2") - 在系统提示中明确告知存在多用户场景 - ChatMemoryBuffer 中使用角色区分不同用户 4. **等待窗口**:固定10秒窗口(变体1) - 收到消息后等待10秒 - 等待期间有新消息则重新计时 5. **配置管理**:使用单独的JSON文件 - 配置存储在 `data/ai_config.json` - 全局单一配置(服务器级别,非chat级别) - 通过 `.aiconfig` 指令修改配置并保存到文件 # 项目概览 ## 项目结构 ``` WPSBot/ ├── app.py # FastAPI主应用 ├── config.py # 配置管理 ├── core/ # 核心模块 │ ├── database.py # 数据库操作 │ ├── models.py # 数据模型 │ └── middleware.py # 中间件 ├── routers/ # 路由模块 │ ├── callback.py # 回调处理 │ └── health.py # 健康检查 ├── games/ # 游戏模块 │ ├── base.py # 游戏基类 │ └── ... # 其他游戏 ├── utils/ # 工具模块 │ ├── message.py # 消息发送 │ ├── parser.py # 指令解析 │ └── rate_limit.py # 限流控制 └── data/ # 数据文件 ``` ## 技术栈 - FastAPI:Web框架 - SQLite:数据存储 - llama-index-core:AI对话框架核心 - llama-index-llms-ollama:Ollama LLM集成 - Ollama:本地LLM服务 # 分析 ## 现有架构 1. **指令处理流程**: - 消息通过 `/api/callback` 接收 - `CommandParser` 解析指令,只处理以 `.` 开头的命令 - 非指令消息会被忽略 - 指令分发到对应的游戏处理器 2. **状态管理**: - 游戏状态存储在 `game_states` 表 - 使用 `(chat_id, user_id, game_type)` 作为联合主键 - 对于群组共享状态,使用 `user_id=0`(如成语接龙) 3. **异步任务**: - 已有 `periodic_cleanup` 后台清理任务示例 - 使用 `asyncio.create_task` 和 `asyncio.sleep` 实现 ## 关键技术挑战 ### 1. 延迟回答机制 需要实现一个基于时间间隔的判断机制: - 收到 `.ai` 指令时,将消息加入等待队列 - 设置一个等待窗口(例如5-10秒) - 如果在等待窗口内有新消息,重新计时 - 等待窗口结束后,如果没有新消息,生成回答 - 需要在 `chat_id` 级别维护等待队列和延迟任务 ### 2. 长上下文管理 - 使用 llama_index 的 `ChatMemoryBuffer` 管理对话历史 - 确保超过30轮对话历史能够被保留 - 对话历史需要按 `chat_id` 独立存储 - 对话历史中需要包含用户ID信息,以便区分不同用户 ### 3. Ollama配置管理 - 使用全局单一配置(服务器级别) - 配置存储在 `data/ai_config.json` 文件中 - 配置包括:服务地址、端口、模型名称 - 通过 `.aiconfig` 指令修改配置并持久化到文件 - 配置需要有默认值(localhost:11434,默认模型需指定) ### 4. 多用户对话识别 - 对话历史中需要记录每条消息的发送者(user_id) - 生成回复时,需要识别上下文中的不同用户 - 回复格式可以考虑使用 @用户 的方式 ### 5. 依赖管理 - 需要添加 llama-index-core 和相关依赖 - 需要确保与现有代码库的兼容性 - 考虑资源占用(内存、CPU) ## 数据结构设计 ### AI对话状态数据结构 对话状态由 llama_index 的 ChatMemoryBuffer 管理,存储在内存中。 需要存储的额外信息: ```python # 存储在 game_states 表中的 state_data { "user_mapping": { # 用户ID到角色名称的映射 "123456": "用户1", "789012": "用户2", ... }, "user_count": 2 # 当前对话中的用户数量 } ``` ### 配置数据结构(存储在 data/ai_config.json) ```json { "host": "localhost", "port": 11434, "model": "llama3.1" } ``` ### 数据库扩展 使用 `game_states` 表存储用户映射信息: - `chat_id`: 会话ID - `user_id`: 0(表示群组级别) - `game_type`: "ai_chat" - `state_data`: JSON格式的用户映射信息 注意:对话历史由 ChatMemoryBuffer 在内存中管理,不持久化到数据库。 # 提议的解决方案 ## 方案概述 1. 创建一个新的游戏模块 `games/ai_chat.py`,继承 `BaseGame` 2. 使用 `game_states` 表存储用户映射信息(用户ID到角色名称的映射) 3. 使用全局字典维护每个 `chat_id` 的延迟任务句柄 4. 使用全局字典维护每个 `chat_id` 的 ChatEngine 实例和待处理消息队列 5. 使用 `data/ai_config.json` 存储 Ollama 全局配置 6. 使用 llama_index 的 ChatMemoryBuffer 管理对话上下文(内存中) ## 实现细节 ### 1. 指令注册 在 `utils/parser.py` 中添加: - `.ai`: 触发AI对话 - `.aiconfig`: 配置Ollama参数 ### 2. AI对话模块 (`games/ai_chat.py`) - `handle()`: 主处理函数,处理 `.ai` 和 `.aiconfig` 指令 - `_handle_ai()`: 处理AI对话请求 - 将消息加入等待队列 - 取消旧的延迟任务(如果存在) - 创建新的延迟任务(10秒后执行) - `_handle_config()`: 处理配置请求 - 解析配置参数(host, port, model) - 更新 `data/ai_config.json` 文件 - 返回配置确认消息 - `_add_to_queue()`: 将消息加入等待队列(按 chat_id 组织) - `_delayed_response()`: 延迟回答任务(内部异步函数) - 等待10秒后执行 - 检查队列并生成回答 - 处理任务取消异常 - `_generate_response()`: 使用LLM生成回答 - 获取或创建 ChatEngine 实例 - 获取用户角色映射 - 将队列中的消息按用户角色格式化 - 调用 ChatEngine.chat() 生成回答 - 更新 ChatMemoryBuffer - `_get_chat_engine()`: 获取或创建ChatEngine实例 - 检查全局字典中是否已存在 - 不存在则创建新的 ChatEngine,配置 ChatMemoryBuffer - 设置系统提示(告知多用户场景) - `_get_user_role()`: 获取用户角色名称(创建或获取映射) - `_load_config()`: 从 JSON 文件加载配置 - `_save_config()`: 保存配置到 JSON 文件 ### 3. 延迟任务管理 - 使用全局字典 `_pending_tasks` 存储每个 `chat_id` 的延迟任务句柄 - 使用全局字典 `_message_queues` 存储每个 `chat_id` 的待处理消息队列 - 使用全局字典 `_chat_engines` 存储每个 `chat_id` 的 ChatEngine 实例 - 新消息到达时,取消旧任务(调用 task.cancel())并创建新任务 - 使用 `asyncio.create_task` 和 `asyncio.sleep(10)` 实现固定10秒延迟 - 处理 `asyncio.CancelledError` 异常,避免任务取消时的错误日志 ### 4. 用户角色映射机制 - 为每个 chat_id 维护用户ID到角色名称的映射(如"用户1"、"用户2") - 映射信息存储在 `game_states` 表中(chat_id, user_id=0, game_type='ai_chat') - 首次出现的用户自动分配角色名称(按出现顺序) - 在将消息添加到 ChatMemoryBuffer 时使用角色名称作为消息角色 - 系统提示中包含:"这是一个多用户对话场景,不同用户的发言会用不同的角色标识。你需要理解不同用户的发言内容,并根据上下文给出合适的回复。" ### 4. 依赖添加 在 `requirements.txt` 中添加: ``` llama-index-core>=0.10.0 llama-index-llms-ollama>=0.1.0 ``` ### 5. 路由注册 在 `routers/callback.py` 的 `handle_command()` 中添加AI对话处理分支 ### 6. 帮助信息更新 在 `games/base.py` 的 `get_help_message()` 中添加AI对话帮助 ## 时间间隔判断逻辑(固定10秒窗口) 1. **默认等待窗口**:10秒(固定) 2. **收到 `.ai` 指令时**: - 提取消息内容(去除 `.ai` 前缀) - 获取用户ID和chat_id - 将消息(用户ID + 内容)加入该 `chat_id` 的等待队列 - 如果有待处理的延迟任务(检查 `_pending_tasks[chat_id]`),取消它 - 创建新的延迟任务(`asyncio.create_task(_delayed_response(chat_id))`) - 将任务句柄存储到 `_pending_tasks[chat_id]` 3. **在等待窗口内收到新消息**(无论是否是指令): - 如果新消息也是 `.ai` 指令: - 将新消息加入队列 - 取消当前延迟任务(`task.cancel()`) - 创建新的延迟任务(重新计时10秒) - 如果新消息不是指令,但chat_id在等待队列中: - 可以考虑忽略,或也加入队列(根据需求决定) 4. **等待窗口结束(延迟任务执行)**: - 检查队列中是否有消息 - 如果有,获取该 chat_id 的 ChatEngine 和用户映射 - 将队列中的消息按用户角色格式化后添加到 ChatMemoryBuffer - 调用 ChatEngine.chat() 生成回答 - 清空队列 - 从 `_pending_tasks` 中移除任务句柄 ## 配置文件管理(data/ai_config.json) - 文件结构: ```json { "host": "localhost", "port": 11434, "model": "llama3.1" } ``` - 首次加载时如果文件不存在,创建默认配置 - 通过 `.aiconfig` 指令修改配置时,实时保存到文件 - ChatEngine 创建时从配置文件加载配置 # 当前执行步骤:"4. 执行模式 - 代码实施完成并测试通过" # 任务进度 ## [2025-10-29_23:55:08] 执行阶段完成 - 已修改: - requirements.txt:添加 llama-index-core 和 llama-index-llms-ollama 依赖 - data/ai_config.json:创建默认配置文件 - utils/parser.py:添加 .ai 和 .aiconfig 指令映射和解析逻辑 - games/ai_chat.py:创建完整的 AI 对话模块实现 - routers/callback.py:添加 ai_chat 处理分支 - games/base.py:添加 AI 对话帮助信息 - 更改: - 实现了基于 llama_index 和 Ollama 的 AI 对话功能 - 实现了固定10秒等待窗口的延迟回答机制 - 实现了用户角色映射和长上下文管理 - 实现了配置文件的 JSON 存储和管理 - 原因:按照计划实施 AI 对话功能的所有核心组件 - 阻碍因素:无 - 状态:成功 ## [2025-10-30_00:56:44] 功能优化和问题修复 - 已修改: - games/ai_chat.py:优化错误处理和用户体验 1. 移除收到消息后的确认回复(静默处理) 2. 修复转义字符警告(SyntaxWarning) 3. 改进错误处理,提供详细的调试信息和排查步骤 4. 添加超时设置(120秒) 5. 针对NPS端口转发的特殊错误提示 - 更改: - 优化了错误提示信息,包含当前配置、测试命令和详细排查步骤 - 专门针对NPS端口转发场景添加了Ollama监听地址配置说明 - 改进了连接错误的诊断能力 - 原因:根据实际使用中发现的问题进行优化 - 阻碍因素:无 - 状态:成功 ## [2025-10-30_01:10:05] 系统提示词持久化和功能完善 - 已修改: - games/ai_chat.py: 1. 实现系统提示词的持久化存储(保存到配置文件) 2. 添加 `_get_default_system_prompt()` 方法定义默认系统提示词 3. 添加 `_get_system_prompt()` 方法从配置文件加载系统提示词 4. 更新系统提示词内容,明确AI身份和职责 5. 在系统提示词中包含完整的机器人功能列表和指引 - 更改: - 系统提示词现在会保存到 `data/ai_config.json` 文件中 - 服务重启后系统提示词会自动从配置文件加载,保持长期记忆 - AI助手能够了解自己的身份和所有机器人功能,可以主动指引用户 - 系统提示词包含了完整的13个功能模块介绍和回复指南 - 原因:实现系统提示词的长期记忆,让AI能够始终记住自己的身份和职责 - 阻碍因素:无 - 状态:成功 # 最终审查 ## 实施总结 ✅ 所有计划功能已成功实施并通过测试 ### 核心功能实现 1. ✅ AI对话系统基于 llama_index + Ollama 构建 2. ✅ 显式指令触发(`.ai <问题>`) 3. ✅ 配置指令(`.aiconfig`)支持动态配置Ollama服务 4. ✅ 固定10秒等待窗口的延迟回答机制 5. ✅ 用户角色映射和长上下文管理(30+轮对话) 6. ✅ 配置文件持久化存储 7. ✅ 系统提示词持久化存储(新增) 8. ✅ 完善的错误处理和调试信息 ### 文件修改清单 - ✅ requirements.txt - 添加依赖 - ✅ data/ai_config.json - 配置文件(包含系统提示词) - ✅ utils/parser.py - 指令解析 - ✅ games/ai_chat.py - AI对话模块完整实现 - ✅ routers/callback.py - 路由注册 - ✅ games/base.py - 帮助信息更新 ### 技术特性 - ✅ 多用户对话支持 - ✅ 延迟任务管理(asyncio) - ✅ ChatMemoryBuffer长上下文管理 - ✅ JSON配置文件管理 - ✅ NPS端口转发支持 - ✅ 详细的错误诊断和排查指南 ### 测试状态 - ✅ 功能测试通过 - ✅ Ollama服务连接测试通过 - ✅ NPS端口转发配置测试通过 - ✅ 系统提示词持久化测试通过 ## 实施与计划匹配度 实施与计划完全匹配 ✅ ## 补充分析:Markdown 渲染与发送通道(2025-10-30) ### 现状观察 - `routers/callback.py` 中仅当 `response_text.startswith('#')` 时才通过 `send_markdown()` 发送,否则使用 `send_text()`。这意味着即使 AI 返回了合法的 Markdown,但不以 `#` 开头(例如代码块、列表、表格、普通段落等),也会被按纯文本通道发送,导致下游(WPS 侧)不进行 Markdown 渲染。 - `games/ai_chat.py` 的 `_generate_response()` 直接返回 `str(response)`,未对内容类型进行标注或判定,上层仅依赖首字符为 `#` 的启发式判断来选择发送通道。 - `utils/message.py` 已具备 `send_markdown()` 与 `send_text()` 两种发送方式,对应 `{"msgtype":"markdown"}` 与 `{"msgtype":"text"}` 消息结构;当前缺少自动识别 Markdown 的逻辑。 ### 影响 - 当 AI 返回包含 Markdown 元素但非标题(不以 `#` 开头)的内容时,用户端看到的是未渲染的原始 Markdown 文本,表现为“格式不能成功排版”。 ### 待确认问题(不含解决方案,需产品/实现口径) 1. 目标平台(WPS 机器人)对 Markdown 的要求是否仅需 `msgtype=markdown` 即可渲染?是否存在必须以标题开头的限制? 2. 期望策略: - 是否希望“.ai 的所有回复”统一走 Markdown 通道? - 还是需要基于 Markdown 特征进行判定(如代码块、列表、链接、表格、行内格式等)? 3. 兼容性:若统一改为 Markdown 通道,是否会影响既有纯文本展示(例如换行、转义、表情)? 4. 其他指令模块是否也可能返回 Markdown?若有,是否一并纳入同一策略? ### 相关代码参照点(路径) - `routers/callback.py`:回复通道选择逻辑(基于 `startswith('#')`) - `games/ai_chat.py`:AI 回复内容生成与返回(直接返回字符串) - `utils/message.py`:`send_markdown()` 与 `send_text()` 的消息结构 ### 决策结论与范围(2025-10-30) - 分支策略:不创建新分支,继续在当前任务上下文内推进。 - 发送策略:`.ai` 产生的回复统一按 Markdown 发送。 - 影响范围:仅限 AI 对话功能(`.ai`/`ai_chat`),不扩展到其他指令模块。 # 任务进度(补充) ## [2025-10-30_??:??:??] 标注 Markdown 渲染问题(记录现状与待确认项) - 已修改: - `.tasks/2025-10-29_3_ai_chat.md`:补充“Markdown 渲染与发送通道”分析与待确认清单(仅问题陈述,无解决方案)。 - 更改: - 明确当前仅以标题开头触发 Markdown 发送的启发式导致部分 Markdown 未被渲染。 - 原因: - 用户反馈“AI 返回内容支持 Markdown,但当前直接当作文本返回导致无法正确排版”。 - 阻碍因素: - 目标平台的 Markdown 渲染细节与统一策略选择待确认。 - 状态: - 未确认(等待策略口径与平台渲染规范确认)。 ## [2025-10-30_11:40:31] 执行:AI 回复统一按 Markdown 发送(仅限 AI) - 已修改: - `routers/callback.py`:在 `callback_receive()` 的发送阶段,当 `game_type == 'ai_chat'` 且存在 `response_text` 时,无条件调用 `send_markdown(response_text)`;若发送异常,记录日志并回退到 `send_text(response_text)`;其他指令模块继续沿用 `startswith('#')` 的启发式逻辑。 - 更改: - 使 `.ai` 产生的回复在 WPS 端稳定触发 Markdown 渲染,不再依赖以 `#` 开头。 - 原因: - 对齐“统一按 Markdown 发送(仅限 AI)”的决策,解决 Markdown 文本被当作纯文本发送导致的排版问题。 - 阻碍因素: - 暂无。 - 状态: - 成功。