# 会话标题大模型生成技术方案 ## 1. 文档信息 - 状态:已确认,待实现 - 日期:2026-08-18 - 影响端:TypeScript 后端、桌面/Web/安卓共用前端 - 不影响端:管理后台、Android 原生壳、Electron 壳 ## 2. 需求背景 当前系统使用用户首次发送消息的文本前缀作为会话标题。该方式不能识别消息的核心意图,长消息、带控制标记的消息和包含大量上下文的消息容易形成冗长、辨识度低的标题,不利于用户在任务列表中快速定位会话。 本次升级由大模型理解用户首次发送的有效文本,概括出简洁的主题短语作为会话标题。标题生成属于辅助流程,不得阻塞或影响 Agent 主任务执行。 ## 3. 现状分析 ### 3.1 当前主会话链路 1. 前端创建会话时不传标题,后端使用“未命名会话”作为默认值。 2. 用户首次发送消息前,`desktop/src/composables/useChat.ts` 调用 `desktop/src/utils/sessionTitle.ts`,在前端清理部分控制标记并截取前 50 个字符。 3. 前端立即更新本地 Store,并异步 PATCH `/v1/sessions/:id` 持久化标题。 4. 消息经 WebSocket 到达后端,`backend-ts/src/session/session.service.ts` 的 `saveMessage()` 在标题仍为占位值时,再次使用 `TitleGenerator` 截取文本前缀。 现状存在前后端双重生成、异步 PATCH 与后端保存竞争、不同消息入口规则不一致的问题。 ### 3.2 当前边路任务链路 `backend-ts/src/session/ws/streaming-ws-handler.ts` 在创建 `SIDE_TASK` 会话前同步调用 `generateTitleFromUserMessage()`;该方法当前仍使用 `TitleGenerator` 截取消息前缀。创建完成后通过 `side_session_created` 事件把标题发送给前端。 ### 3.3 可复用能力 - `OpenAiLlmAdapter.chat()`:现有 OpenAI 兼容非流式模型调用能力。 - `HarnessService.resolveModel()`:按会话模型 ID 解析模型,缺省时回退系统默认模型。 - `llmModelToConfig()`:将模型实体转换为 LLM 调用配置。 - `agentExecutor`:提交不阻塞 WebSocket 消息处理的后台任务。 - `StreamingWsRegistry` 与 `wsEvent()`:向当前用户的多个客户端实时推送会话变化。 - `TitleGenerator.preprocessForTitle()`:处理 Skill 和快捷指令标记的已有基础能力。 ## 4. 需求描述 ### 4.1 要做的内容 1. 主会话和边路任务首次收到有效用户文本后,由后端异步调用大模型生成标题。 2. 标题生成使用当前会话选择的模型;会话未指定模型时使用系统默认模型。 3. 模型根据预处理后的首次用户文本理解和概括任务意图。 4. 提示词要求标题为 15 个字符以内的简洁主题短语,保留关键动作和对象。 5. 标题语言跟随用户首次有效文本的主要语言。 6. 后端清理模型输出中的包装格式,再持久化到会话表。 7. 标题生成完成后通过 WebSocket 实时同步到前端会话列表、当前会话及边路任务列表。 8. 模型调用失败时使用现有文本前缀规则生成兜底标题,不重试,不影响 Agent 执行。 9. 异步结果不得覆盖用户在生成期间手动设置的标题。 10. 首条消息只有图片时不调用标题模型,标题固定为“图片消息”。 11. 移除前端主会话和边路任务的本地标题推导及标题 PATCH,消除双重生成。 12. 增加覆盖标题生成、竞态保护、失败兜底和实时同步的测试。 13. 实现时在根目录 `CHANGELOG.md` 当前版本的“后端”和“前端(桌面 / Web / 安卓)”小节记录用户可见变化。 ### 4.2 明确不做的内容 1. 不对模型输出执行 15 字符硬截断;15 字以内仅由提示词约束。 2. 不为标题生成新增独立模型配置,或在管理后台增加标题模型设置。 3. 不对历史会话批量生成或重算标题,不新增数据迁移脚本。 4. 不修改 `SUBAGENT` 子代理标题;子代理继续使用调用方提供的任务标题。 5. 不为微信固定会话按每条消息重新生成标题。 6. 不把纯图片发送给视觉模型生成标题。 7. 不在标题失败后自动重试,不引入消息队列、定时补偿或持久化任务表。 8. 不把标题模型调用写入 `llm_usage` 用量表,只记录服务日志。 9. 不修改用户手动重命名能力。 10. 不修改管理后台、Electron 壳、Android 原生壳和数据库结构。 ## 5. 验收标准 1. 新主会话发送首条文本后,Agent 正常立即开始执行,标题稍后自动从“未命名会话”更新为模型概括结果。 2. 新边路任务创建后先正常进入执行流程,标题稍后从“任务”更新为模型概括结果。 3. 示例消息“请帮我排查登录接口为什么偶发超时”生成类似“排查登录接口超时”的主题短语,不直接截取原消息前缀。 4. 中文输入生成中文标题,英文输入生成英文标题,中英混合输入按主要语义语言生成标题。 5. 最终标题不包含引号、Markdown 标记、“标题:”前缀、换行解释或句末的句号、问号、感叹号。 6. 模型不遵循长度要求时,系统保留清洗后的模型结果,不做 15 字符硬截断。 7. 模型不可用、超时、返回空内容或清洗后为空时,会话使用预处理文本的现有前缀规则作为标题,Agent 执行不受影响。 8. 用户在模型返回前手动重命名后,模型结果和失败兜底结果均不得覆盖手动标题。 9. 首条消息只有图片时,标题为“图片消息”,不产生标题模型调用。 10. 同一用户同时登录多个客户端时,各客户端都能通过 WebSocket 收到新标题。 11. 历史会话标题保持不变。 ## 6. 技术选型 ### 6.1 后端统一生成 标题生成的唯一权威放在后端。所有客户端共享同一套模型选择、输入预处理、输出清洗、失败兜底和并发保护规则。前端只负责展示持久化标题和处理实时更新事件。 不保留前后端同时生成的方案,避免两次模型调用和迟到结果互相覆盖。 ### 6.2 非流式 LLM 调用 使用现有 `OpenAiLlmAdapter.chat()` 发起短文本非流式调用,参数建议如下: - `stream: false` - `tools: []` - `temperature: 0.2` - `reasoning: { effort: 'none' }` 标题结果很短,不需要流式传输;低温度和关闭推理可降低输出波动及额外延迟。 ### 6.3 会话模型解析 使用 `session.modelId` 解析当前模型;为空时回退系统默认模型。复用现有模型实体和 `llmModelToConfig()`,不新增配置项。 如果无法解析出可用模型,直接进入前缀兜底逻辑。 ### 6.4 异步执行 首条用户消息成功落库后,将标题生成提交给现有 `agentExecutor`。消息保存和 Agent 执行不等待标题生成结果。 标题后台任务只负责:读取并确认会话状态、预处理文本、调用模型、清洗或兜底、条件写入标题、发送更新事件、记录日志。任务异常必须在内部捕获,不能转化为主会话错误事件。 ### 6.5 WebSocket 实时更新 新增统一事件: ```json { "type": "session_title_updated", "sessionId": 123, "data": { "title": "排查登录接口超时", "parentSessionId": null, "sessionType": "NORMAL" } } ``` `SIDE_TASK` 事件携带 `parentSessionId` 和 `sessionType: "SIDE_TASK"`。前端据此更新: - 主会话实体及任务列表; - 当前会话标题; - 边路任务缓存; - 已打开的边路任务 Tab。 不复用只包含 `phase` 语义的 `session_list_update`,保持事件职责清晰。 ## 7. 详细设计 ### 7.1 新增标题生成服务 在 `backend-ts/src/session/` 下新增职责单一的 `SessionTitleService`,由依赖装配层注入以下能力: - LLM Adapter; - 模型查询或 `HarnessService.resolveModel()` 等价能力; - `SessionRepository`; - 用户快捷指令查询能力; - `StreamingWsRegistry`; - 后台执行器。 核心接口建议: ```ts scheduleForFirstUserMessage(sessionId: number, content: unknown): void generateAndApply(sessionId: number, rawText: string): Promise ``` `scheduleForFirstUserMessage()` 只做快速资格判断和后台任务提交。`generateAndApply()` 完成具体生成流程。 标题 LLM 调用不放入 `SessionService.saveMessage()` 的同步等待路径,也不把 LLM、模型和 WS 依赖持续堆叠进现有 `SessionService`。 ### 7.2 首条消息判定 只有同时满足以下条件时才调度标题生成: 1. 会话类型为 `NORMAL` 或 `SIDE_TASK`; 2. 本次保存角色为 `USER`; 3. 会话当前标题为对应系统占位值:主会话“未命名会话”,边路任务“任务”或空标题; 4. 本次消息是该会话首条用户消息; 5. 提取出的有效文本非空。 “首条用户消息”必须由数据库查询确认,不能只依赖标题占位值。建议在 `MessageRepository` 增加按会话统计/判断首条用户消息的方法,或利用刚插入消息 ID 查询该会话是否存在更早的 `USER` 消息。 该判断可以避免用户把历史会话手动改回“未命名会话”后,下一条消息再次触发自动生成。 ### 7.3 输入预处理 处理顺序如下: 1. 从字符串或多模态内容中提取文本; 2. 去除首尾空白; 3. 展开 `#{快捷指令}#` 为当前用户可用的指令正文; 4. 移除 `${skill}$` 控制标记; 5. 移除 `@{文件路径}@` 文件引用标记; 6. 再次合并空白并去除首尾空白。 纯 Skill 消息不能只剩空文本。应将 Skill 名转换为自然语义输入,例如 `/skillName`,再交给模型概括。 如果预处理后文本为空但消息含图片,直接使用“图片消息”;如果文本和图片均为空,不调度标题生成。 ### 7.4 提示词 系统提示词必须明确限定输出格式,不要求 JSON,避免额外解析复杂度: ```text 你是会话标题生成器。根据用户首次消息提炼一个简洁、明确的主题短语。 要求: 1. 目标长度不超过15个字符; 2. 保留核心动作和对象,删除寒暄、语气词和背景赘述; 3. 跟随用户消息的主要语言; 4. 不使用“关于”“用户想要”“请求”等空泛前缀; 5. 不使用句号、问号、感叹号; 6. 只输出标题,不要引号、标签、Markdown 或解释。 ``` 用户消息以独立 `user` 消息传入。不得将用户文本拼接进 system prompt,避免用户内容改变系统指令边界。 ### 7.5 输出清洗 后端按以下顺序清洗模型输出: 1. 标准化换行并 `trim()`; 2. 只取第一个非空行; 3. 去除 Markdown 标题、列表等行首标记; 4. 去除“标题:”“Title:”前缀; 5. 去除成对的中英文引号、反引号; 6. 去除末尾句号、问号、感叹号及对应英文符号; 7. 再次 `trim()`。 不执行字符数截断。清洗后为空时进入失败兜底。 ### 7.6 失败与超时 标题调用使用独立的短超时,建议 10 秒。超时后取消当前非流式请求并立即兜底,不复用 Agent 主调用可能较长的整体等待时间。 以下情况统一进入兜底: - 无可用模型; - LLM 请求异常或超时; - 模型没有返回文本; - 清洗后为空。 兜底使用现有 `TitleGenerator.generate(preprocessedText)` 前缀规则。纯图片仍固定为“图片消息”。兜底不自动重试。 服务日志至少包含 `sessionId`、`modelId`、结果类型(`generated` 或 `fallback`)、失败原因和耗时;不得记录模型 API Key。按已确认决策,不调用 `LlmUsageService.record()`。 ### 7.7 并发与覆盖保护 异步任务开始时读取会话,写回前再次读取或执行带条件的数据库更新。 推荐在 `SessionRepository` 增加条件更新: ```sql UPDATE session SET title = ?, updated_at = ? WHERE id = ? AND title IN ('未命名会话', '任务', '') ``` 实际占位值应按会话类型校验,不能让主会话的模型结果误写到已被用户重命名的会话。条件更新影响行数为 0 时视为用户已改名或会话状态已变化,丢弃结果且不发送 WS 标题事件。 同一会话只允许调度一次标题任务。首条用户消息的数据库判定与条件写回共同提供幂等保护,不新增持久化状态字段。 ### 7.8 主会话改造 调整主会话消息保存链路: 1. `SessionService.saveMessage()` 保留消息持久化和 `updated_at` 更新。 2. 删除其中同步调用 `TitleGenerator.generate()` 并写标题的旧逻辑。 3. WebSocket 主消息入口在首条用户消息保存成功后调用标题服务的调度方法。 4. 立即继续现有 `prepareMessage()` 和 Agent 执行流程。 5. 标题完成后由标题服务条件写回并发送 `session_title_updated`。 队列插入或自动消费形成首条用户消息时,也必须经过相同的标题调度入口,不能只覆盖普通发送路径。 ### 7.9 边路任务改造 调整 `handleCreateSideSession()`: 1. 创建 `SIDE_TASK` 时先使用占位标题“任务”。 2. 立即持久化边路会话并保存首条用户消息。 3. `side_session_created` 先携带占位标题,使前端可立即创建 Tab 并进入执行。 4. 保存消息后异步调度标题生成。 5. 标题完成后发送统一 `session_title_updated`,更新边路列表和 Tab。 删除创建边路任务前同步调用 `generateTitleFromUserMessage()` 的逻辑,保证边路任务启动不被标题生成阻塞。 ### 7.10 前端改造 1. 删除 `useChat.ts` 两处首次发送前调用 `deriveSessionTitle()`、更新 Store 和 PATCH 标题的逻辑。 2. 检查 `TaskView.vue`、`SideChatPanel.vue` 等边路任务入口,删除仅用于自动标题的 `deriveSessionTitle()` 调用;用户手动重命名仍继续调用现有 PATCH API。 3. 若 `desktop/src/utils/sessionTitle.ts` 不再有其他有效调用,则删除该文件;若仍服务非会话标题场景,则仅移除会话自动命名职责。 4. `useStreamWS.ts` 增加 `session_title_updated` 分支: - `NORMAL` 调用 `sessionStore.updateSession(sessionId, { title })`; - `SIDE_TASK` 更新 Side Task 缓存与对应 Tab; - 当前会话依赖 Store 的计算属性自动刷新。 5. 不展示标题生成失败提示,不增加加载动画或 Toast;失败已由后端兜底为可用标题。 ## 8. 实现步骤 1. 在后端新增 `SessionTitleService`,实现模型解析、非流式调用、提示词、输出清洗、10 秒超时、日志和前缀兜底。 2. 扩展 `TitleGenerator.preprocessForTitle()`,统一移除文件引用标记并保留纯 Skill 的语义输入。 3. 在消息仓储增加首条用户消息判定能力,在会话仓储增加占位标题条件更新能力。 4. 从 `SessionService.saveMessage()` 删除旧的同步前缀标题写入逻辑。 5. 在主会话普通发送、队列插入和自动消费路径接入异步标题调度。 6. 将边路任务改为先用“任务”占位创建,再异步生成标题。 7. 在依赖装配中注入 LLM Adapter、模型解析、仓储、WS Registry 和后台执行器。 8. 后端生成完成后发送 `session_title_updated` 事件。 9. 前端移除本地自动标题生成和 PATCH,增加 WS 标题事件处理。 10. 补充后端单元测试、WebSocket Handler 测试和前端 Store/事件测试。 11. 运行后端构建与单测、桌面端构建,并按需要执行现有 Playwright 会话流程测试。 12. 更新根 `CHANGELOG.md` 当前版本的后端和共用前端小节。 ## 9. 测试方案 ### 9.1 标题服务单元测试 - 中文长消息生成简洁中文主题短语。 - 英文消息保留英文标题。 - 模型结果带引号、`#`、`标题:`、换行解释时正确清洗。 - 模型输出超过 15 字符时不截断。 - 模型返回空文本时使用前缀兜底。 - 模型异常、超时、无默认模型时使用前缀兜底。 - 纯图片不调用 LLM,返回“图片消息”。 - Skill、快捷指令、文件引用经过预处理后再调用模型。 - 日志不包含 API Key,不写入 `llm_usage`。 ### 9.2 仓储与并发测试 - 标题仍为主会话占位值时条件更新成功。 - 标题仍为边路占位值时条件更新成功。 - 用户已手动改名时条件更新影响行数为 0。 - 条件更新失败时不发送 `session_title_updated`。 - 非首条用户消息不调度标题任务。 - 同一首条消息重复处理不会覆盖已生成或手动设置的标题。 ### 9.3 WebSocket 测试 - 主会话保存首条消息后立即进入 Agent 执行,不等待标题 Promise。 - 标题成功后向用户发送正确的 `session_title_updated`。 - 边路任务先发送带占位标题的 `side_session_created`,随后发送标题更新事件。 - 队列消息成为首条用户消息时也触发标题生成。 - 标题模型失败不会发送主会话 `error` 事件。 ### 9.4 前端测试 - `session_title_updated` 更新主会话 Store 和列表标题。 - 边路标题事件更新 Side Task 缓存和已打开 Tab。 - 首次发送消息不再调用标题 PATCH API。 - 用户手动重命名仍正常调用 PATCH 并更新 Store。 - 纯图片会话最终显示“图片消息”。 ### 9.5 验证命令 ```bash cd backend-ts && npm run build cd backend-ts && npm test cd desktop && npm run build npm test ``` 根目录 Playwright 测试按现有环境可用性执行;CI 固定覆盖后端构建与单测、桌面端构建。 ## 10. 风险与控制 ### 10.1 模型不遵守 15 字要求 已确认不做长度硬截断,因此不能技术性保证最终标题绝对不超过 15 个字符。通过低温度、明确提示词和单一输出格式提高遵循率;验收时按“目标长度约束”判断,不按数据库硬约束判断。 ### 10.2 标题调用增加模型请求 每个符合条件的新主会话和边路任务增加一次短模型调用。通过非流式、无工具、低温度、关闭推理和短超时控制成本与延迟。按已确认决策,该调用只写日志,不计入 `llm_usage`。 ### 10.3 异步写回覆盖手动标题 通过数据库条件更新控制写回,不采用“先读取再无条件更新”的方式。只有标题仍是系统占位值时才能落库。 ### 10.4 服务重启导致标题任务丢失 本次不引入持久化任务或重试机制。进程在异步任务执行期间退出时,该会话可能保留占位标题;这是“不自动重试、不补算历史会话”决策下接受的边界。 ### 10.5 多实例重复调度 WebSocket 首条消息只由接收实例处理;即使发生重复调度,首条消息判定与条件更新可保证最多一个结果成功写入。后写任务因占位条件不成立而放弃。 ## 11. 落地清单 ### 后端 - [ ] 新增 `SessionTitleService`。 - [ ] 定义并固化标题系统提示词。 - [ ] 实现输入预处理和输出格式清洗。 - [ ] 实现 10 秒超时、异常捕获、服务日志和前缀兜底。 - [ ] 复用会话模型和系统默认模型解析。 - [ ] 增加首条用户消息判定查询。 - [ ] 增加占位标题条件更新 SQL。 - [ ] 删除 `saveMessage()` 中旧的同步前缀标题写入。 - [ ] 主会话普通发送路径接入异步调度。 - [ ] 队列插入与自动消费路径接入异步调度。 - [ ] 边路任务改为占位创建后异步调度。 - [ ] 新增 `session_title_updated` WebSocket 事件。 - [ ] 确保标题调用不写 `llm_usage`。 - [ ] 补充标题服务、仓储竞态和 WS 测试。 ### 前端 - [ ] 删除主会话首次发送时的本地标题推导和 PATCH。 - [ ] 删除边路任务自动标题的前端推导逻辑。 - [ ] 清理不再使用的 `deriveSessionTitle` 引用或文件。 - [ ] 处理 `session_title_updated` 事件。 - [ ] 更新主会话 Store、边路任务缓存和边路 Tab。 - [ ] 保留用户手动重命名 PATCH 链路。 - [ ] 补充 Store 和 WS 事件测试。 ### 发布与验证 - [ ] 更新根 `CHANGELOG.md` 的“后端”条目。 - [ ] 更新根 `CHANGELOG.md` 的“前端(桌面 / Web / 安卓)”条目。 - [ ] 后端 TypeScript 构建通过。 - [ ] 后端单元测试通过。 - [ ] 桌面端构建通过。 - [ ] 现有会话 Playwright 流程无回归。 - [ ] 手工验证主会话、边路任务、纯图片、失败兜底和手动改名竞态。 ## 12. 预计修改文件 实现阶段预计涉及以下现有入口,最终以代码依赖收敛结果为准: - `backend-ts/src/create-app.ts` - `backend-ts/src/session/session.service.ts` - `backend-ts/src/session/session.repository.ts` - `backend-ts/src/session/util/title-generator.ts` - `backend-ts/src/session/ws/streaming-ws-handler.ts` - `backend-ts/src/session/ws/streaming-ws-handler.spec.ts` - `backend-ts/src/session/util/session-utils.spec.ts` - `desktop/src/composables/useChat.ts` - `desktop/src/composables/useStreamWS.ts` - `desktop/src/views/task/TaskView.vue` - `desktop/src/components/chat/SideChatPanel.vue` - `desktop/src/stores/session.ts` - `desktop/src/utils/sessionTitle.ts`(无剩余调用时删除) - `CHANGELOG.md` 预计新增后端标题服务及对应单元测试文件;不新增数据库迁移、前端页面或配置文件。