# 私聊定时跟进 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`,内容使用项目已有的 `` 风格。 - 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` 机制 ### 风格要求 - 注入内容应沿用项目现有 `...` 风格 - 它在请求里以 `user` 身份追加,但语义上明确说明“不是用户刚刚发来的新消息” - 应提醒 planner: - 这是一次定时跟进触发 - 当前为什么会被唤醒 - 你之前对用户说过什么 - 可以主动发言,也可以不发言 - 除非明确需要回应某条旧消息,否则不要默认调用 `reply` ### 提示文案打样 ```text 以下内容不是用户刚刚发来的新消息,而是当前私聊的一次定时跟进触发。 触发时间:{trigger_at} 你之前在这个私聊中设置过一次定时跟进。 当时记录的跟进原因: {followup_reason} 当时你已经对用户说过: {assistant_commitment_text} 请结合当前上下文重新分析现在是否适合主动发言: - 如果仍然适合,就继续正常分析,并在需要时调用 send_private_message。 - 如果当前话题已经变化、用户已经提到过同类内容、约定已经失效,或者现在不适合打扰,就不要强行发送,直接 finish。 - 不要把这段提醒当成用户的新发言。 - 除非你明确是在回应历史中的某条具体消息,否则不要默认调用 reply。 ``` ## 与现有链路的复用点 ### 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 一轮”的入口 - 该入口负责把 `` 注入到 planner 请求尾部 ### 5. 主程序启动 - 在现有后台任务体系中注册该调度器 ## 最小验证路径 ### 验证目标 - 确认创建任务、到点唤醒、重新思考、主动发送、写库、上下文同步都能走通 ### 建议路径 1. 在私聊中让 bot 创建一条几分钟后的 `schedule_private_followup` 2. 当轮正常 `reply` 给用户,说明之后会再来跟进 3. 到点后由调度器唤醒当前私聊的 planner 4. planner 收到 `` 后重新分析 5. 若判断合适,调用 `send_private_message` 6. 发送应复用现有 Napcat / Platform IO 出口成功发出 7. 发出的消息应进入: - 现有消息存储 - Maisaka 上下文历史 - 现有 memory automation ## 当前建议的落地顺序 - 第一步:先把工具边界定下来:`schedule_private_followup` 与 `send_private_message` - 第二步:把任务表与任务存储定下来 - 第三步:补运行时“带 reminder 唤醒 planner”入口 - 第四步:补后台调度器并接入主程序 - 第五步:打通主动发送、写库、上下文同步 - 第六步:再考虑取消/查看任务工具