Files
mai-bot/计划.md
2026-05-11 23:20:19 +08:00

373 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 私聊定时跟进 V1 计划
## 当前结论
- 本方案只支持私聊,不支持群聊。
- 旧草案中的“到点直接发送预先写死的 `message_text`”不再作为 V1 主方案。
- V1 改为“定时器负责唤醒当前私聊会话的正常思考流程”,由 LLM 基于到点时的当前上下文决定是否主动发言。
- 为了支持“主动发言但不强行回复某条旧消息”,需要新增一个与 `reply` 平行的主动私聊发送工具。
- 平台出口、消息存储、Maisaka 上下文写回、Napcat 适配等链路应尽量复用现有实现,不新造一条发送通道。
## 目标
- 让 LLM 可以在当前私聊里创建一条未来触发的“定时跟进任务”。
- 到点后不直接发固定文本,而是重新进入该私聊的正常 planner 思考流程。
- 到点思考时直接复用现有上下文窗口、原有 planner prompt、原有工具机制。
- 如果 planner 判断现在适合主动发言,则调用新的主动私聊发送工具发出消息。
- 如果 planner 判断现在不适合打扰,则可以不发言并直接结束本轮。
## 明确不做
- 不做群聊版定时跟进。
- 不做跨会话发送;当前私聊中创建的任务,只能唤醒当前私聊,也只能向当前私聊主动发言。
- 不做“任意指定目标用户/目标 session 的消息发送器”。
- 不做复杂多意图分类,不引入冗余的 `intent_key`
- 不伪造一条新的用户入站消息来驱动到点逻辑。
- 不把主动 follow-up 强行伪装成 `reply(msg_id=...)`
## 核心方案
### 1. 定时器从“文本发送器”改为“流程进入器”
- 当前用户在私聊中表达未来某个时间点需要 bot 主动跟进。
- Planner 调用新的定时跟进工具,写入一条未来任务。
- 到点后,后台调度器不直接发消息,而是唤醒对应私聊会话的 Maisaka planner。
- planner 结合当前上下文重新判断:
- 是否应该主动发言
- 现在该说什么
- 是否应该放弃本次跟进
- 是否应该重新约下一个时间点
### 2. 新增主动私聊发送工具
- 保留现有 `reply`
- 语义仍然是“针对某条已有消息进行回复”
- 仍然依赖 `msg_id`
- 新增 `send_private_message`
- 语义是“在当前私聊里主动发出一条可见消息”
- 不依赖 `msg_id`
- 不默认引用旧消息
- 这样可以避免 QQ/Napcat 侧展示成“永远在回复某条旧消息”,更符合真实私聊中的自然主动聊天。
### 3. 发送出口继续复用现有通道
- `send_private_message` 不新造平台出口。
- 它和 `reply` 一样,最终复用现有:
- `send_service`
- `Platform IO`
- 现有 driver / adapter
- Napcat 出口
- 这样发送成功后,仍可复用现有:
- 消息写库
- memory automation
- Maisaka 历史同步
## 端到端流转链路
### 1. 用户触发
- 用户在私聊中表达未来某个时间点需要 bot 主动跟进。
- 当前消息照常进入现有链路:
- 消息接收
- HeartFlow runtime
- Timing Gate
- Planner
### 2. Planner 决策
- Planner 分析当前局势后,可直接调用定时跟进工具。
- 工具应为 planner 直接可见的 action tool不走 deferred tools。
- 同一轮里planner 仍可继续:
- 调用 `reply`
- 正常回复用户
- 告知用户已经记下这次未来跟进
### 3. 工具执行
- 暂定工具名:`schedule_private_followup`
- 工具接收参数后,直接写入任务存储。
- 工具只允许操作当前私聊会话,不允许 LLM 指定别的 session。
- tool result 与其他工具一样,复用现有 tool call 记录与上下文链路。
### 4. 后台调度器轮询
- 独立后台调度器定期扫描到期任务。
- 扫描条件:
- `status = pending`
- `send_at_ts <= now`
- 任务取出后,不直接发送,而是进入“会话唤醒”流程。
### 5. 会话唤醒
- 调度器根据 `session_id` 定位或恢复对应私聊会话。
- 如果对应 runtime 当前不在内存中,应先恢复/创建该会话的 Maisaka runtime。
- 调度器调用一个新的运行时入口,例如:
- `trigger_scheduled_followup(task)`
- 或同等语义的方法
- 这个入口负责以“额外注入消息”的形式进入 planner而不是伪造用户消息。
### 6. 到点后的 planner 行为
- 唤醒后的 planner 继续使用项目原有的主 prompt。
- 同时在请求尾部追加一条 `injected_user_messages`,内容使用项目已有的 `<system-reminder>` 风格。
- planner 基于当前上下文重新分析后,可以:
- 调用 `send_private_message`
- 调用 `reply`(仅当它明确需要围绕某条旧消息回复时)
- 调用 `finish`
- 再次调用 `schedule_private_followup`
### 7. 主动发送
- 当 planner 判断应主动发言时,调用 `send_private_message`
- `send_private_message` 直接复用现有 `send_service.text_to_stream_with_message(...)`
- 发送参数应满足:
- `stream_id = 当前 ToolExecutionContext.session_id`
- `storage_message = True`
- `sync_to_maisaka_history = True`
- `maisaka_source_kind` 建议使用新的主动发送来源标记,例如:
- `proactive_send`
- 若后续需要区分定时唤醒来源,可再在 metadata 中补充 `trigger_source=scheduled_followup`
### 8. 任务完成
- 本次唤醒流程成功结束后,原任务应被标记为完成。
- “完成”不等于“一定发了消息”。
- 只要本次唤醒和 planner 处理已成功结束,即可完成任务,并记录最终动作结果。
## 工具设计
### 工具 1`schedule_private_followup`
#### 工具职责
- 为当前私聊创建一条未来触发的定时跟进任务。
- 任务到点后唤醒当前私聊的 planner而不是直接发送固定文本。
#### 工具参数
- `send_at`
- 类型:`string`
- 含义:触发时间
- 建议:由 LLM 提供标准化后的绝对时间字符串
- `followup_reason`
- 类型:`string`
- 含义:这次未来跟进的原因/事项摘要
- 作用:到点时注入 reminder帮助 planner 理解为什么会被唤醒
- `assistant_commitment_text`
- 类型:`string`
- 含义当前这轮里bot 对用户作出的那句“未来会来找你/提醒你/跟进你”的承诺话术
- 作用:到点时也注入给 planner帮助它和之前的承诺保持一致
- `replace_existing`
- 类型:`boolean`
- 含义:是否覆盖当前私聊中尚未执行的旧跟进任务
- 默认建议:`false`
#### 工具隐含上下文
- 不要求 LLM 传 `session_id`
- 不要求 LLM 传 `user_id`
- 后端直接从当前 `ToolExecutionContext.session_id` 取目标私聊
- 如果当前不是私聊,工具直接失败
#### 工具返回
- 成功时至少返回:
- `task_id`
- `session_id`
- `send_at`
- `followup_reason`
- `assistant_commitment_text`
- `replace_existing`
- 若发生覆盖,返回被取消的旧任务 ID 列表
- 失败时返回明确原因:
- 非私聊
- 时间非法
- 跟进原因为空
- 承诺话术为空
- 存储失败
### 工具 2`send_private_message`
#### 工具职责
- 在当前私聊会话中主动发送一条可见消息。
- 不依赖 `msg_id`
- 不默认带引用回复。
#### 工具参数
- `message_text`
- 类型:`string`
- 含义:要主动发送给当前私聊对象的文本内容
#### 工具隐含上下文
- 不要求 LLM 传 `session_id`
- 不允许 LLM 指定别的目标会话
- 后端直接使用当前 `ToolExecutionContext.session_id`
#### 工具返回
- 成功时至少返回:
- `session_id`
- `message_text`
- `sent_message_id`
- 失败时返回明确原因:
- 非私聊
- 文本为空
- 发送失败
### 工具关系
- `reply`:回应某条已有消息
- `send_private_message`:主动发言,不依赖某条已有消息
- `schedule_private_followup`:创建未来跟进任务,负责到点后唤醒 planner
## 任务数据模型
### 建议字段
- `id`
- `session_id`
- `send_at_ts`
- `status`
- `followup_reason`
- `assistant_commitment_text`
- `created_at_ts`
- `updated_at_ts`
- `created_by_tool_call_id`
- `cancelled_by_tool_call_id`
- `triggered_at_ts`
- `completed_at_ts`
- `completion_action`
- `sent_message_id`
- `last_error`
- `replace_existing`
### 状态
- `pending`
- `running`
- `completed`
- `cancelled`
- `failed`
### 状态语义
- `pending`:已创建,等待到点唤醒
- `running`:调度器已取出,正在执行本次唤醒
- `completed`:本次唤醒流程已成功结束,无论是否真的发出消息
- `cancelled`:被显式取消或被新任务覆盖
- `failed`:本次唤醒流程执行失败
### 完成动作建议
- `sent_private_message`
- `skipped`
- `rescheduled`
- `unknown`
## 覆盖规则
### V1 定义
- `replace_existing = true` 时:
- 将当前 `session_id` 下所有 `pending` 任务置为 `cancelled`
- 再创建当前新任务
- `replace_existing = false` 时:
- 不取消旧任务
- 直接新增当前任务
### 这样做的原因
- 不引入模糊分类
- 不猜“哪个旧任务和哪个新任务算同类”
- 规则简单、确定、易解释
## 到点注入给 planner 的提示方式
### 复用原则
- 继续复用原有 [prompts/zh-CN/maisaka_chat.prompt](/D:/mai-bot-align-pre17/prompts/zh-CN/maisaka_chat.prompt)
- 不复制一整份新 prompt
- 不把 reminder 伪装成真实聊天历史消息
- 复用现有 `injected_user_messages` 机制
### 风格要求
- 注入内容应沿用项目现有 `<system-reminder>...</system-reminder>` 风格
- 它在请求里以 `user` 身份追加,但语义上明确说明“不是用户刚刚发来的新消息”
- 应提醒 planner
- 这是一次定时跟进触发
- 当前为什么会被唤醒
- 你之前对用户说过什么
- 可以主动发言,也可以不发言
- 除非明确需要回应某条旧消息,否则不要默认调用 `reply`
### 提示文案打样
```text
<system-reminder>
以下内容不是用户刚刚发来的新消息,而是当前私聊的一次定时跟进触发。
触发时间:{trigger_at}
你之前在这个私聊中设置过一次定时跟进。
当时记录的跟进原因:
{followup_reason}
当时你已经对用户说过:
{assistant_commitment_text}
请结合当前上下文重新分析现在是否适合主动发言:
- 如果仍然适合,就继续正常分析,并在需要时调用 send_private_message。
- 如果当前话题已经变化、用户已经提到过同类内容、约定已经失效,或者现在不适合打扰,就不要强行发送,直接 finish。
- 不要把这段提醒当成用户的新发言。
- 除非你明确是在回应历史中的某条具体消息,否则不要默认调用 reply。
</system-reminder>
```
## 与现有链路的复用点
### Prompt 与上下文
- 复用 Maisaka planner 主 prompt
- 复用现有上下文窗口选择逻辑
- 复用 `injected_user_messages` 追加尾部提醒
### 发送
- `send_private_message` 复用 `send_service.text_to_stream_with_message(...)`
- 继续复用现有 `Platform IO` 和 Napcat 出口
### 存储
- 复用现有消息发送成功后的消息存储链路
### 上下文写回
- 复用 `sync_to_maisaka_history=True`
- 复用 runtime 的 `append_sent_message_to_chat_history(...)`
### Tool call 记录
- 创建任务时,复用现有 Planner tool 执行记录链路
- 到点唤醒不伪造一条新的 LLM tool call
- 到点执行本质上属于调度器唤醒会话,而不是伪造用户发言
## 需要补的实现模块
### 1. 新内置工具
- 新增 `schedule_private_followup` builtin tool
- 新增 `send_private_message` builtin tool
### 2. 任务存储
- 新增一张私聊定时跟进任务表
- 提供最小操作:
- create
- cancel pending by session
- list due pending
- mark running
- mark completed
- mark failed
### 3. 后台调度器
- 独立异步循环
- 固定间隔扫描 due tasks
- 负责 claim 任务、唤醒会话、更新状态
### 4. Maisaka 运行时入口
- 为运行时新增一个“带 reminder 进入 planner 一轮”的入口
- 该入口负责把 `<system-reminder>` 注入到 planner 请求尾部
### 5. 主程序启动
- 在现有后台任务体系中注册该调度器
## 最小验证路径
### 验证目标
- 确认创建任务、到点唤醒、重新思考、主动发送、写库、上下文同步都能走通
### 建议路径
1. 在私聊中让 bot 创建一条几分钟后的 `schedule_private_followup`
2. 当轮正常 `reply` 给用户,说明之后会再来跟进
3. 到点后由调度器唤醒当前私聊的 planner
4. planner 收到 `<system-reminder>` 后重新分析
5. 若判断合适,调用 `send_private_message`
6. 发送应复用现有 Napcat / Platform IO 出口成功发出
7. 发出的消息应进入:
- 现有消息存储
- Maisaka 上下文历史
- 现有 memory automation
## 当前建议的落地顺序
- 第一步:先把工具边界定下来:`schedule_private_followup``send_private_message`
- 第二步:把任务表与任务存储定下来
- 第三步:补运行时“带 reminder 唤醒 planner”入口
- 第四步:补后台调度器并接入主程序
- 第五步:打通主动发送、写库、上下文同步
- 第六步:再考虑取消/查看任务工具