docs/technical/chat-work-mode-split.md
Last updated: 2026-09
本文档记录聊天模式与工作模式在消息结构操作自由度上的分化设计:聊天模式放开结构自由(平铺追加回答、生成中切换分支、自由编辑删除),工作模式收紧结构手术(禁止平铺追加、编辑助手消息、删除分支等),同时保留单条消息删除、消息队列与 steering。涵盖动机、分阶段设计、性能与兼容性决策。
相关文档:会话管理系统(fork 数据模型)、状态管理(生成锁与并发控制)。
「在下方回答」(Reply Again Below)最初的行为是在消息序列中平铺插入一条新回复。PR 948 将其改为创建 inactive fork 分支(折叠卡片展示),动机是保护后续轮次的 prompt cache 前缀、并让活跃路径保持"一问一答"的干净不变量。
上线后收到明确的用户反馈(主要来自文游 / 角色扮演 / 小说创作群体):
重新评估后的结论:prompt cache 与上下文卫生的收益只在工作模式下真正重要(agent 上下文大、工具历史重、cache 经济性敏感);聊天模式本来就允许随意编辑删除消息,从来不是 cache 稳定场景,结构自由是该用户群体的核心诉求。因此不做全局折中,而是按模式分化。
| 维度 | 聊天模式(Chat Mode) | 工作模式(Work Mode) |
|---|---|---|
| 在下方回答 | 平铺插入活跃路径,可多条并发生成 | 入口移除 |
| 重新生成(Reply Again) | 创建 fork 分支(维持现状) | 创建 fork 分支(维持现状) |
| 生成中切换分支 | 放开 | 维持禁止 |
| 删除分支(delete-fork) | 允许(生成中仍禁止) | 入口移除 |
| 编辑助手消息 | 允许 | 禁止 |
| 删除消息 | 允许 | 允许;对话已有可缓存前缀时,删除历史消息需二次确认(可按场景勾选不再提示) |
| 编辑压缩摘要 | 允许 | 允许 |
| 编辑用户消息 | 允许(可只存不发) | 仅 Save & Resend,无"仅保存",禁改角色 |
| 会话系统提示词 | 消息列表展示,会话设置可编辑(受"隐藏系统提示词"开关控制) | 两处入口都隐藏(身份由 Soul 表达;Copilot 人设冻进 Soul 段,无 Copilot 的会话 prompt 不进入请求) |
| 新话题 / 话题历史 | 允许 | 不允许创建新话题;无历史时隐藏话题历史,已有归档话题时保留分隔与历史入口 |
| 切换模型 | 允许 | 对话已有可缓存前缀时需二次确认(可勾选不再提示) |
| 消息队列(message queue) | 禁用(生成中阻断发送) | 保留 |
| Steering(立即发送插队) | 禁用 | 保留 |
| 全局停止 | 停止一切可达的生成(含 fork 分支内) | 同左(现状) |
| 模式切换 | 仅新对话可选;会话中仅智能切换可升级 | 首条消息后锁死(已有 AgentModeEntry.locked) |
设计原则:每个模式内部自洽,跨机制的交叉项由模式边界消掉。队列×分支投递歧义、steering 锚点×分支切换、审批流×fork 移动、沙箱副作用×并行分支——这些难缠的交互全部只存在于"队列/steering/审批/沙箱"与"结构手术"同时开启的世界;分化后两组能力不再共存于同一个模式。
会话模式由 session.settings.agentMode.value 决定:'on' 为工作模式,'off' / 'auto' / 未设置为聊天模式('auto' 只控制首轮智能切换分类器,能力上等同聊天模式,见 agentModeState.ts)。
策略解析使用 @chatbox/core 的 resolveSessionMode()(纯函数),renderer 侧响应式场景经 useSessionAgentMode 取 entry 后映射。模式策略跟随会话数据而非平台能力:移动端查看桌面创建的工作模式会话时,虽然 agent 不能运行,消息限制仍然生效,保证跨设备行为一致。
静态能力限制收敛在 packages/chatbox-core/src/session/mode-policy.ts:isActionAvailableInMode(action, mode),动作词表包括 reply-below、edit-assistant-message、delete-message、delete-fork、save-message-edit(仅保存不重发)、session-system-prompt(会话自带的系统提示词)、queue-message、steer-queued-message、create-thread(新话题)、thread-history(话题历史列表 / 抽屉 / 消息流标签)。工作模式在对话已有可缓存前缀时,删除历史消息与切换模型的二次确认由 packages/chatbox-core/src/session/prompt-cache-policy.ts 判定。
与 action-gates.ts 的分工刻意区分两种语义:
唯一需要同时看两者的动作是"生成中切换分支":它是临时锁的模式化例外,因此实现为 getSessionActionGate('switch-fork', locks, { sessionMode }) 的 context 参数——sessionMode: 'chat' 时不再被 generating 锁阻挡(compaction 锁不变),'work' 或未传时保持旧行为。未传参保守化的目的是让尚未适配的宿主(mobile-native)自动维持旧语义。
策略执行采用与 guardSessionAction 相同的双层防御:UI 按策略隐藏入口;store 动作入口(如 generateMore)内做后备检查,拦下陈旧界面/新增调用面的漏网请求。模式策略是客户端 UX 约束,不是数据不变量——旧版本客户端与同步端不强制执行,任何数据层逻辑不得假设"工作模式会话的消息不可变"。
generateMore 从"创建 inactive fork + 隔离上下文生成"回退为 insertMessageAfter + 直接生成(即原 fallback 路径转正)。要点:
shouldShowConcurrentReplyStop,单条流式时维持"输入框停止"的无按钮设计)。sequenceMessages 合并(平铺时代既有管线,未拆除)。接受的代价:未删除的候选回答会进入后续轮次上下文(token 开销 + 插入点一次性 cache 失效)。这是目标用户群体主动管理的行为(对比后删除多余候选),且被限定在聊天模式内。
存量数据:fork 时代产生的候选分支继续由 ForkGroup 渲染,无迁移。shared 的 buildCreateInactiveForkPatch 保留(mobile-native 仍在引用,等 native 同步回退后再删)。
setSessionAgentMode 增加 source 参数;会话产生首条用户消息后,'user' 来源的跨模式切换('on' ↔ 非 'on')被拒绝。智能切换建议的 accept(lockSessionAgentMode)与 decline('auto'→'off',source 'suggestion')不受影响;'auto' ↔ 'off' 属聊天模式内部偏好,本就由智能切换开关的过期逻辑(首条消息后禁用)覆盖。entry.locked;聊天→工作在会话开始后禁用按钮,tooltip 复用既有文案("Locked after the chat starts… start a new chat to change")。PROMPT_CACHE_CONFIRM_MIN_CHARS)时,删除历史消息与切换模型会弹出说明缓存失效的二次确认;判定看当前选中的 provider 上下文(压缩点、上下文条数、进行中请求的前缀),删掉不在该上下文里或删完上下文不变的消息不弹。不再提示按场景记在 uiStore(删除历史消息 / 切换模型 / 删除摘要),互不影响。新话题入口隐藏(store 侧 refreshContextAndCreateNewThread 做后备拦截并覆盖快捷键路径,模式经 getSessionAgentModeEntry 规范解析以兼容 legacy map 会话);话题历史在会话没有归档话题时隐藏,已有归档话题时保留消息流分隔与历史入口,避免把多个话题误呈现成一段连续对话;上下文压缩归档不受影响。getSubmitAction / getSubmitControl 增加 queueEnabled 参数:聊天模式下生成中提交返回 'block'(弹既有 'generating' 锁提示),控件保持 Stop 不再切换为入队。MessageAnchorNotFoundError(applyMessageInsert 的 requireAnchor 退化为纯防御断言)。action-gates 中 switch-fork 的 generating 锁按 sessionMode 分化(chat 放开,work / 未知保守维持)。delete-fork 与 compaction 锁在两个模式下均不变。
数据层无需改动,依据是既有机制已经容器无关:
applyMessageUpdate 按 id 寻址(root → threads → fork lists 全搜)。preserveCachedGeneratingMessages,mergeCachedGeneratingMessages 两侧扫描全部三种容器。getGenerationControlMessages),流式消息被切进分支后 session 仍保持 generating 锁——不会误解锁导致跨分支并发新生成。stopAllMessageGenerations)= runtime 表全停 + 可达图扫尾(占位窗口墓碑),天然覆盖被切走的后台流。generating:true 由 recoverSessionOnLoad 在加载时对全部容器修复。UI 补丁:ForkGroup 对包含生成中消息的非活跃分支强制 reveal(切换保存回 slot 时 list id 不变,原有"新 id 才展开"的逻辑覆盖不到)。分支卡片除首条回复外还渲染 follow-up 尾部的流式候选(先平铺多条候选、再切走更早的分支时,live 流在 firstReply 之后)——否则卡片看起来已完成且没有单条停止入口;候选完成后保持可见(与分支级 sticky reveal 一致),折叠或切换分支时回到仅计数摘要,follow-up 计数只统计未展示的消息。
编辑用户消息后重发采用消息版本化,两个模式共用(工作模式下它还是唯一的编辑路径):buildSaveAndResendForkPatch(shared)把 [原消息, ...旧尾巴] 整段存为前驱消息下的分支(pivot 跳过 summary,与 regenerateInNewFork 的既有约定一致),编辑后的内容以新 id 作为新活跃尾巴的开头再生成回复——每条时间线各自携带自己的 prompt,切回旧分支看到的仍是它当初回应的那句提问。fork + 替换在一次 session 写内完成,避免 prompt 在两次写之间闪断;fork 切换器因此落在前驱消息上(通常是上一条助手回复)。
pivot 必须是前驱而非被编辑消息自身,这是数据模型强制的:分支只保存 pivot 之后的尾巴,pivot 本身是各分支共享的主线,以被编辑消息为 pivot 会让改写后的 prompt 成为所有分支的共同前缀。同类产品的选型一致——pi 的会话树、ChatGPT / Open WebUI / LibreChat 的消息版本化、Claude Code / Codex CLI 的 rewind、deepseek-harness 的纯 append-only,没有一家把 prompt 覆写与"旧回复留作可切分支"并存。
边界与回退(口径统一为用户文本永不丢):
createSaveAndResendFork 因此只在什么都没落盘时才 reject。共享 RAG 附件所有权:版本化让原消息与副本引用同一条 session_attachment 索引行,而该行按 message_id 单一归属。删消息 / 删分支时先按 planAttachmentOwnershipTransfers 把幸存者仍引用的行 rebindAttachment 过去,再执行按 owner 的删除;孤儿维护则以"是否仍被活跃消息引用"而非"owner 是否可达"判定(planOrphanCleanup),被引用的行改为修复归属而不是删除。因此转移失败只影响时效,不会让幸存分支丢掉索引文件。
聊天模式重度用户的会话可达数十 MB 文本、大量分支与 threads,以下是各改动的性能账:
useSessionLockState 在每个流式 chunk 后对新 session 快照重新推导。可达遍历为 O(全部消息数) 的 Set 操作,已由 PR 1011 的两项机制兜底——per-snapshot WeakMap 缓存(一个渲染周期所有消费者共享一次遍历)与 sessionLockStatesEqual 引用稳定(值不变时 memo 组件零重渲)。本设计没有改变推导逻辑(遍历本来就含 fork 分支),边际成本为零。sessionMode 在 MessageList / InputBox 层解析一次后以 prop 下发(字符串,引用稳定)。数千行消息的会话里,任何加在 Message 行内的 store 订阅都会在每个 chunk 触发全行选择器求值,明确禁止。isActionAvailableInMode 无状态无分配,可在渲染路径任意调用。updateSessionWithMessages 批量写。messageForksHash / threads / queue 持久化)不变;新旧两种候选形态(fork 分支 / 平铺)长期共存渲染。contextMessages 生成选项保留不删;SessionActionContext.sessionMode 未传时 switch-fork 维持旧锁语义,native 未适配前行为不变。native 需跟进的两项:在下方回答回退、mode-policy 接入(跟踪于 native 复用审计)。