18 KiB
18 KiB
背景
文件名: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进行补充,智能体将通过时间间隔判断机制来决定何时进行回答。
核心需求
- 显式指令触发:使用
.ai <问题>指令触发AI对话 - 配置指令:使用
.aiconfig指令配置Ollama服务地址、端口和模型名称 - 时间间隔判断:智能体通过时间间隔判断是否需要回答(固定10秒等待窗口)
- 长上下文管理:保留超过30轮对话历史
- 多用户对话支持:同一chat_id下不同用户的消息能够被正确识别和处理
技术方案决策(已确定)
-
延迟任务机制:使用 asyncio 的延迟任务(方案三)
- 每个 chat_id 维护独立的延迟任务句柄
- 使用全局字典存储任务映射
- 收到新消息时取消旧任务并创建新任务
-
上下文管理:使用 llama_index 的 ChatMemoryBuffer(策略A)
- 设置足够的 token_limit 确保保留30+轮对话
- 按 chat_id 独立维护 ChatEngine 实例
-
多用户识别:消息角色映射 + 系统提示(方案C)
- 将不同用户映射为不同角色(如"用户1"、"用户2")
- 在系统提示中明确告知存在多用户场景
- ChatMemoryBuffer 中使用角色区分不同用户
-
等待窗口:固定10秒窗口(变体1)
- 收到消息后等待10秒
- 等待期间有新消息则重新计时
-
配置管理:使用单独的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服务
分析
现有架构
-
指令处理流程:
- 消息通过
/api/callback接收 CommandParser解析指令,只处理以.开头的命令- 非指令消息会被忽略
- 指令分发到对应的游戏处理器
- 消息通过
-
状态管理:
- 游戏状态存储在
game_states表 - 使用
(chat_id, user_id, game_type)作为联合主键 - 对于群组共享状态,使用
user_id=0(如成语接龙)
- 游戏状态存储在
-
异步任务:
- 已有
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 管理,存储在内存中。 需要存储的额外信息:
# 存储在 game_states 表中的 state_data
{
"user_mapping": { # 用户ID到角色名称的映射
"123456": "用户1",
"789012": "用户2",
...
},
"user_count": 2 # 当前对话中的用户数量
}
配置数据结构(存储在 data/ai_config.json)
{
"host": "localhost",
"port": 11434,
"model": "llama3.1"
}
数据库扩展
使用 game_states 表存储用户映射信息:
chat_id: 会话IDuser_id: 0(表示群组级别)game_type: "ai_chat"state_data: JSON格式的用户映射信息
注意:对话历史由 ChatMemoryBuffer 在内存中管理,不持久化到数据库。
提议的解决方案
方案概述
- 创建一个新的游戏模块
games/ai_chat.py,继承BaseGame - 使用
game_states表存储用户映射信息(用户ID到角色名称的映射) - 使用全局字典维护每个
chat_id的延迟任务句柄 - 使用全局字典维护每个
chat_id的 ChatEngine 实例和待处理消息队列 - 使用
data/ai_config.json存储 Ollama 全局配置 - 使用 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秒窗口)
- 默认等待窗口:10秒(固定)
- 收到
.ai指令时:- 提取消息内容(去除
.ai前缀) - 获取用户ID和chat_id
- 将消息(用户ID + 内容)加入该
chat_id的等待队列 - 如果有待处理的延迟任务(检查
_pending_tasks[chat_id]),取消它 - 创建新的延迟任务(
asyncio.create_task(_delayed_response(chat_id))) - 将任务句柄存储到
_pending_tasks[chat_id]
- 提取消息内容(去除
- 在等待窗口内收到新消息(无论是否是指令):
- 如果新消息也是
.ai指令:- 将新消息加入队列
- 取消当前延迟任务(
task.cancel()) - 创建新的延迟任务(重新计时10秒)
- 如果新消息不是指令,但chat_id在等待队列中:
- 可以考虑忽略,或也加入队列(根据需求决定)
- 如果新消息也是
- 等待窗口结束(延迟任务执行):
- 检查队列中是否有消息
- 如果有,获取该 chat_id 的 ChatEngine 和用户映射
- 将队列中的消息按用户角色格式化后添加到 ChatMemoryBuffer
- 调用 ChatEngine.chat() 生成回答
- 清空队列
- 从
_pending_tasks中移除任务句柄
配置文件管理(data/ai_config.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:优化错误处理和用户体验
- 移除收到消息后的确认回复(静默处理)
- 修复转义字符警告(SyntaxWarning)
- 改进错误处理,提供详细的调试信息和排查步骤
- 添加超时设置(120秒)
- 针对NPS端口转发的特殊错误提示
- games/ai_chat.py:优化错误处理和用户体验
- 更改:
- 优化了错误提示信息,包含当前配置、测试命令和详细排查步骤
- 专门针对NPS端口转发场景添加了Ollama监听地址配置说明
- 改进了连接错误的诊断能力
- 原因:根据实际使用中发现的问题进行优化
- 阻碍因素:无
- 状态:成功
[2025-10-30_01:10:05] 系统提示词持久化和功能完善
- 已修改:
- games/ai_chat.py:
- 实现系统提示词的持久化存储(保存到配置文件)
- 添加
_get_default_system_prompt()方法定义默认系统提示词 - 添加
_get_system_prompt()方法从配置文件加载系统提示词 - 更新系统提示词内容,明确AI身份和职责
- 在系统提示词中包含完整的机器人功能列表和指引
- games/ai_chat.py:
- 更改:
- 系统提示词现在会保存到
data/ai_config.json文件中 - 服务重启后系统提示词会自动从配置文件加载,保持长期记忆
- AI助手能够了解自己的身份和所有机器人功能,可以主动指引用户
- 系统提示词包含了完整的13个功能模块介绍和回复指南
- 系统提示词现在会保存到
- 原因:实现系统提示词的长期记忆,让AI能够始终记住自己的身份和职责
- 阻碍因素:无
- 状态:成功
最终审查
实施总结
✅ 所有计划功能已成功实施并通过测试
核心功能实现
- ✅ AI对话系统基于 llama_index + Ollama 构建
- ✅ 显式指令触发(
.ai <问题>) - ✅ 配置指令(
.aiconfig)支持动态配置Ollama服务 - ✅ 固定10秒等待窗口的延迟回答机制
- ✅ 用户角色映射和长上下文管理(30+轮对话)
- ✅ 配置文件持久化存储
- ✅ 系统提示词持久化存储(新增)
- ✅ 完善的错误处理和调试信息
文件修改清单
- ✅ 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 文本,表现为“格式不能成功排版”。
待确认问题(不含解决方案,需产品/实现口径)
- 目标平台(WPS 机器人)对 Markdown 的要求是否仅需
msgtype=markdown即可渲染?是否存在必须以标题开头的限制? - 期望策略:
- 是否希望“.ai 的所有回复”统一走 Markdown 通道?
- 还是需要基于 Markdown 特征进行判定(如代码块、列表、链接、表格、行内格式等)?
- 兼容性:若统一改为 Markdown 通道,是否会影响既有纯文本展示(例如换行、转义、表情)?
- 其他指令模块是否也可能返回 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 文本被当作纯文本发送导致的排版问题。
- 阻碍因素:
- 暂无。
- 状态:
- 成功。