Chat Title Generation Flow
AI 聊天应用中会话标题的异步生成流程设计:时机选择、数据库字段、SSE 事件、前端渲染链路。
#status / evergreen
#type / concept
#tech / dev / architecture
#domain / ai-chat
[!info] related notes
- 所属 MOC: AI MOC
- 相关实践: AI Chat UI Assistant UI
- 相关实践: Consultation Agent Workflow
- 原子概念: CQRS
Chat Title Generation Flow
核心原则
标题生成是 主聊天回复结束后的异步派生任务,不应该阻塞用户看到 AI 回复,也不应该和消息内容强耦合。标题只是 conversation 的一个 metadata 字段。
conversation.title
conversation.titleStatus
conversation.titleSource
conversation.titleGeneratedFromTurnId
conversation.userRenamedAt
标题生成时机
方案 A:第一条消息后立刻生成
用户发出第一条消息后立即用 user message 生成标题。
- 优点:侧边栏很快有标题
- 缺点:只看到用户问题,标题质量一般(如”问题解决咨询”)
方案 B:第一轮回答完成后生成(推荐)
AI 流式回答完成并落库后,异步生成标题。
- 可同时参考 user message + assistant answer + 工具调用结果
- 标题更稳定具体
方案 C:混合方案(体验最好)
发送后立刻显示临时标题(前 20 字)
↓
AI 第一轮回答完成
↓
后台生成正式标题
↓
侧边栏无感替换
完整链路
用户点击新聊天 → /chat/new,不入库
↓
用户输入问题并回车
↓
前端乐观渲染 user message + assistant streaming 占位
↓
POST /api/chat/send,conversationId = null
↓
后端事务:创建 conversation (titleStatus=pending) + user message + assistant placeholder + run
↓
SSE 返回 conversation.created → 前端 router.replace('/chat/:id')
↓
侧边栏显示临时标题:第一条消息截断
↓
AI 主回答流式返回,前端实时渲染 Markdown
↓
回答完成 → 后端保存 assistant 消息 + 更新 run = completed
↓
后端投递 generate-title job(非阻塞)
↓
主 SSE 结束
↓
标题 worker 读取第一轮 user + assistant 内容
↓
调用轻量模型生成短标题 → 清洗校验
↓
只在用户未手动改名时更新 conversation.title
↓
通过 SSE/WebSocket/重新拉取更新前端侧边栏
数据库字段设计
ALTER TABLE conversations ADD COLUMN title TEXT;
ALTER TABLE conversations ADD COLUMN title_status TEXT NOT NULL DEFAULT 'pending';
ALTER TABLE conversations ADD COLUMN title_source TEXT;
ALTER TABLE conversations ADD COLUMN title_generated_from_turn_id UUID;
ALTER TABLE conversations ADD COLUMN title_version INT NOT NULL DEFAULT 0;
ALTER TABLE conversations ADD COLUMN user_renamed_at TIMESTAMPTZ;
title_status 状态机:
| 状态 | 含义 |
|---|---|
none | 新聊天草稿态 |
pending | 已创建会话,未生成标题 |
generating | 标题正在生成 |
generated | AI 已生成标题 |
manual | 用户手动修改过 |
failed | 标题生成失败 |
标题生成 Prompt
你是一个聊天会话标题生成器。
请根据用户问题和助手回答,生成一个适合显示在聊天侧边栏的短标题。
要求:
- 使用简体中文
- 4 到 12 个汉字,最多 16 个汉字
- 不要加引号、句号、问号、感叹号
- 不要使用"关于""讨论""问题""咨询"等空泛词
- 标题要具体,能概括对话真实主题
用户问题:{{user_message}}
助手回答摘要:{{assistant_answer_excerpt}}
只输出标题。
关键防覆盖逻辑
更新标题时必须防止覆盖用户手动改名:
UPDATE conversations
SET title = $title,
title_status = 'generated',
title_source = 'ai',
title_generated_from_turn_id = $turnId,
updated_at = now()
WHERE id = $conversationId
AND title_status IN ('pending', 'generating', 'failed')
AND user_renamed_at IS NULL;
前端 displayTitle 规则
function getDisplayTitle(conversation) {
if (conversation.title) return conversation.title;
if (conversation.firstUserMessageText)
return truncate(conversation.firstUserMessageText, 24);
return "新对话";
}
标题更新推送方式
| 方式 | 适用场景 |
|---|---|
同一 SSE 连接返回 title.updated | 标题生成快,体验实时 |
| 主 SSE 结束后 invalidateQueries | 简单方案 |
| WebSocket/SSE 全局事件 | 多标签页/多设备同步 |
为什么不应该阻塞式生成标题
const answer = await generateAnswer();
const title = await generateTitle(); // ← 不要这样
- 用户要等更久才能看到请求完成
- 标题模型失败会影响主回答
- 主回答 stream 已结束但 HTTP 请求还挂着
- 移动端/弱网更容易断连
- 标题不是核心内容,不值得阻塞主链路
正确做法:onAnswerFinished → enqueueTitleJob()
标题重新生成策略
- 只在第一轮回答完成后自动生成一次
- 会话标题应稳定,用户已在侧边栏认识该会话
- 可支持手动”重新生成标题”按钮
- 例外:第一轮消息太泛(如”你好”),可在第二轮后低频更新