Chat Title Generation Flow

AI 聊天应用中会话标题的异步生成流程设计:时机选择、数据库字段、SSE 事件、前端渲染链路。

#status / evergreen #type / concept #tech / dev / architecture #domain / ai-chat

[!info] related notes

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标题正在生成
generatedAI 已生成标题
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()

标题重新生成策略

  • 只在第一轮回答完成后自动生成一次
  • 会话标题应稳定,用户已在侧边栏认识该会话
  • 可支持手动”重新生成标题”按钮
  • 例外:第一轮消息太泛(如”你好”),可在第二轮后低频更新
创建于 2026/6/27 更新于 2026/6/27