ChatOllama 自动会话标题生成系统开发指南:从触发策略到源码级实现
【免费下载链接】chat-ollamaChatOllama is an open source agentic app for running AI agents across local and hosted models.项目地址: https://gitcode.com/GitHub_Trending/ch/chat-ollama
本文面向 ChatOllama 的开发者与贡献者,系统讲解会话标题自动生成功能的完整技术实现。内容以 docs/guide/README.md 开发者文档为骨架,完整覆盖 会话标题生成完整指南 与 快速参考卡 中的全部架构、API、触发策略与集成示例,并结合仓库内
utils/autoTitleGeneration.ts、composables/useSessionTitle.ts、server/api/sessions/[id]/title.post.ts等源码进行深入剖析,帮助读者理解其模块化设计、调用链与底层原理,从而能够独立使用、集成和扩展这一能力。
一、系统定位与架构总览
ChatOllama 的会话标题生成系统(Session Title Generation)会在用户发出第一条消息后自动为会话生成有意义的标题,替代“新对话”这类无信息量的默认占位。整套系统在设计上强调三个关键词:模块化(modular)、可配置(configurable)、可复用(reusable),既能服务于聊天主流程,也为文档摘要、批量处理、会话设置等未来场景预留了接入点。
1.1 分层调用架构
根据 docs/guide/session-title-generation.md 的架构说明,系统分为四层,数据流自顶向下:
┌─────────────────────────┐ ┌─────────────────────────┐ │ Chat Component │ │ Other Components │ │ │ │ (Future integrations) │ └───────────┬─────────────┘ └───────────┬─────────────┘ │ │ └──────────────┬───────────────┘ │ v ┌─────────────────────────┐ │ AutoTitleGenerator │ │ (utils/autoTitle...) │ └───────────┬─────────────┘ │ v ┌─────────────────────────┐ │ useSessionTitle() │ │ (composables/) │ └───────────┬─────────────┘ │ v ┌─────────────────────────┐ │ API Endpoint │ │ /api/sessions/*/title │ └─────────────────────────┘对应到仓库的实际文件,这条链路是:
- 组件层:
components/Chat.vue在发送首条用户消息后调用标题生成; - 工具层:
utils/autoTitleGeneration.ts中的AutoTitleGenerator类与createAutoTitleGenerator工厂,负责封装触发条件(Trigger)与回调策略; - Composable 层:
composables/useSessionTitle.ts中的useSessionTitle(),承载核心逻辑(API 调用、数据库更新、触发判断); - API 层:
server/api/sessions/[id]/title.post.ts,Nuxt Nitro 服务端端点,真正与 LLM 交互。
1.2 快速参考中的流水线视角
快速参考卡 docs/guide/session-title-quick-reference.md 用一条流水线浓缩了同一架构:
Component → AutoTitleGenerator → useSessionTitle() → API → LLM → Response ↓ ↓ ↓ ↓ ↓ ↓ UI Update Trigger Logic DB Update HTTP Request Title Generation可以看出:组件只负责触发与 UI 更新,触发判断在AutoTitleGenerator(或 Trigger),数据库写入与 HTTP 请求在 composable,真正的标题生成发生在服务端 API。职责分离是这套设计的核心,下面逐层展开。
二、快速开始:首条消息自动生成标题
2.1 工厂函数createAutoTitleGenerator.forFirstMessage
这是文档标注的“最常见用法”。在任意聊天组件中,通过 utils/autoTitleGeneration.ts 暴露的工厂函数创建生成器:
<!-- components/YourChatComponent.vue --> <script setup> import { createAutoTitleGenerator } from '~/utils/autoTitleGeneration' // Setup auto title generation const autoTitleGenerator = createAutoTitleGenerator.forFirstMessage((title) => { // Update your UI when title is generated if (sessionInfo.value) { sessionInfo.value.title = title emit('title-updated', title) } }) // In your message handler const onSendMessage = async (messageContent) => { // ... your existing message logic ... // Auto-generate title if conditions are met if (sessionInfo.value && models.value.length > 0) { const { family, name: model } = parseModelValue(models.value[0]) autoTitleGenerator.attemptTitleGeneration( { messages: messages.value, sessionTitle: sessionInfo.value.title, messageContent: messageContent }, sessionInfo.value.id, model, family ) } } </script>源码印证:components/Chat.vue中正是这样接入的——它在 第 56-61 行 用createAutoTitleGenerator.forFirstMessage初始化生成器,并在发送用户消息后(第 224-238 行)从targetModels[0]中通过parseModelValue解析出family与model,再调用attemptTitleGeneration。
2.2 工厂的触发策略实现
forFirstMessage的内置 Trigger 在源码中如下(与 composables/useSessionTitle.ts 中titleTriggers.firstUserMessage完全一致):
shouldGenerate: (context: { messages: any[], sessionTitle?: string }) => { const hasNoTitle = !context.sessionTitle || context.sessionTitle.trim() === '' const userMessageCount = context.messages.filter(m => m.role === 'user').length return hasNoTitle && userMessageCount === 1 }即两个条件同时满足才触发:
- 会话当前没有标题(
sessionTitle为空或全空白); - 消息列表中恰好只有一条用户消息(首条消息刚发出)。
extractMessage则负责从上下文中提取用于生成标题的内容,且兼容多模态消息:
extractMessage: (context: { messageContent: any }) => { const content = context.messageContent if (Array.isArray(content)) { // 多模态内容:只提取 text 类型的文本片段并拼接 return content .filter(item => item.type === 'text' && item.text) .map(item => item.text) .join(' ') } return content // 纯文本内容直接返回 }2.3attemptTitleGeneration的执行流程
AutoTitleGenerator.attemptTitleGeneration(utils/autoTitleGeneration.ts)是入口方法:
- 开关检查:若
config.enabled === false,直接返回,不做任何请求; - 懒加载初始化:
await this.init()内部通过import('~/composables/useSessionTitle')动态导入 composable(避免增加初始包体积); - 委托给 composable:调用
useSessionTitle().generateTitleWithTrigger(trigger, context, model, family, sessionId, { onSuccess, onError }),把标题成功回调映射到配置里的onTitleGenerated,失败回调映射到onError。
另外,createAutoTitleGenerator.disabled()工厂返回一个enabled: false且shouldGenerate恒为false的生成器,用于需要显式关闭标题生成的场景。
三、高级用法:自定义触发与直接 API 调用
3.1 自定义触发器(Custom Trigger)
当内置的“首条消息”触发条件不满足业务需要时,可实现SessionTitleTrigger接口自定义触发逻辑:
import { useSessionTitle, type SessionTitleTrigger } from '~/composables/useSessionTitle' // Custom trigger for specific scenarios const customTrigger: SessionTitleTrigger = { shouldGenerate: (context) => { // Your custom logic return context.messageCount > 3 && !context.hasTitle }, extractMessage: (context) => { // Extract relevant content for title generation return context.lastUserMessage } } const { generateTitleWithTrigger } = useSessionTitle() await generateTitleWithTrigger( customTrigger, context, model, family, sessionId, { onSuccess: (title) => console.log('Generated:', title), onError: (error) => console.error('Failed:', error) } )接口定义在 composables/useSessionTitle.ts 中:
export interface SessionTitleTrigger { shouldGenerate: (context: any) => boolean // 是否应该生成标题 extractMessage: (context: any) => string | null // 提取用于生成标题的消息内容 }generateTitleWithTrigger的源码逻辑保证了安全性:shouldGenerate返回false或extractMessage提取结果为空字符串时,都会提前返回null,不发任何请求:
const generateTitleWithTrigger = async (trigger, context, model, family, sessionId, options?) => { if (!trigger.shouldGenerate(context)) return null const messageContent = trigger.extractMessage(context) if (!messageContent?.trim()) return null return generateSessionTitle({ sessionId, model, family, userMessage: messageContent, ...options }) }3.2 直接 API 调用(手动生成)
当需要完全掌控生成过程(例如用户手动“重新生成标题”)时,直接使用generateSessionTitle:
import { useSessionTitle } from '~/composables/useSessionTitle' const { generateSessionTitle } = useSessionTitle() const title = await generateSessionTitle({ sessionId: 123, model: 'gpt-4', family: 'OpenAI', userMessage: 'Tell me about quantum computing', autoUpdate: true, // Auto-save to database style: 'technical', // Use technical prompt style maxWords: 8, onSuccess: (title) => { console.log('Title generated:', title) }, onError: (error) => { console.error('Generation failed:', error) } })3.3AutoTitleGenerator的完整配置
除了工厂函数,也可以直接实例化AutoTitleGenerator,并在运行时通过updateConfig热更新配置(例如跟随用户偏好开关):
const generator = new AutoTitleGenerator({ enabled: true, // Enable/disable generation trigger: customTrigger, // When to generate onTitleGenerated: (title, sessionId) => { // Called when title is successfully generated }, onError: (error, sessionId) => { // Called when generation fails } }) // Update configuration later generator.updateConfig({ enabled: userPreferences.autoTitleGeneration })四、预置触发器(Pre-built Triggers)
useSessionTitle()返回对象中携带titleTriggers,提供两个开箱即用的触发器。
4.1titleTriggers.firstUserMessage
触发条件(源码实现见上文 2.2):会话无标题 且 恰好只有 1 条用户消息。
import { titleTriggers } from '~/composables/useSessionTitle' const shouldGenerate = titleTriggers.firstUserMessage.shouldGenerate({ messages: chatMessages, sessionTitle: currentTitle })4.2titleTriggers.onDemand
无条件触发(shouldGenerate恒返回true),适合“用户手动点击刷新标题”的场景:
import { titleTriggers } from '~/composables/useSessionTitle' // Always returns true const shouldGenerate = titleTriggers.onDemand.shouldGenerate(context)其extractMessage对非字符串内容做了兜底:若messageContent不是字符串,则用JSON.stringify序列化,确保总能提取出可用文本:
onDemand: { shouldGenerate: () => true, extractMessage: (context: { messageContent: any }) => { return typeof context.messageContent === 'string' ? context.messageContent : JSON.stringify(context.messageContent) } }五、API 参考:客户端 Composable 与服务端端点
5.1useSessionTitle()方法清单
| 方法 | 职责 |
|---|---|
generateSessionTitle(options) | 主方法:调用 API 并(默认)写入数据库,支持完整回调 |
generateTitleWithTrigger(trigger, context, model, family, sessionId, options?) | 智能生成:仅当 Trigger 条件满足时才生成 |
generateTitleAPI(model, family, userMessage, sessionId) | 底层方法:只发 HTTP 请求,不做数据库操作 |
updateSessionInDB(sessionId, title) | 将新标题写入本地数据库(IndexedDB) |
generateSessionTitle的 Options 定义(composables/useSessionTitle.ts):
interface SessionTitleOptions { sessionId: number model: string family: string userMessage: string autoUpdate?: boolean // 是否自动写库,默认 true onSuccess?: (title: string) => void onError?: (error: any) => void style?: 'concise' | 'descriptive' | 'technical' | 'casual' // 提示词风格 maxWords?: number // 默认 6 systemPrompt?: string // 自定义提示词覆盖 }注:
maxWords、style、systemPrompt会被透传给服务端端点;composable 本身主要消费sessionId/model/family/userMessage/autoUpdate/onSuccess/onError。
generateSessionTitle的实现逻辑(源码要点):
const title = await generateTitleAPI(model, family, userMessage, sessionId) if (title) { if (autoUpdate) await updateSessionInDB(sessionId, title) // 写库 onSuccess?.(title) return title } return null // catch 分支:console.warn 记录 + onError?.(error) + return null5.2 数据库更新updateSessionInDB
标题生成后写回本地客户端数据库(clientDB.chatSessions),并同步刷新updateTime:
const updateSessionInDB = async (sessionId: number, title: string) => { const { clientDB } = await import('~/composables/clientDB') await clientDB.chatSessions.update(sessionId, { title, updateTime: Date.now() }) }这也是“实时更新”这一用户体验要求的落点:服务端返回标题后,前端立即写入本地库并触发 UI 回调。
5.3 服务端 API 端点
POST/api/sessions/:sessionId/title(实现见 server/api/sessions/[id]/title.post.ts)
Request Body:
{ model: string family: string userMessage: string systemPrompt?: string // 自定义提示词,优先级高于 style maxWords?: number // 默认 6 style?: 'concise' | 'descriptive' | 'technical' | 'casual' // 默认 'concise' }Response:
{ title: string }服务端实现要点:
- 提示词模板表
TITLE_PROMPTS,每种 style 一个函数式模板,均以Respond with only the title.约束模型只输出标题本身:
const TITLE_PROMPTS = { concise: (maxWords) => `Generate a ${maxWords}-word title for this chat. Respond with only the title.`, descriptive: (maxWords) => `Generate a descriptive ${maxWords}-word title that captures the main topic of this chat. Respond with only the title.`, technical: (maxWords) => `Generate a technical ${maxWords}-word title focusing on the specific subject matter. Respond with only the title.`, casual: (maxWords) => `Generate a casual, friendly ${maxWords}-word title for this chat. Respond with only the title.` }与聊天相同的模型工厂:调用
createChatModel(model, family, event)(定义在 server/utils/models.ts),确保标题生成使用与当前对话完全一致的模型与提供商配置——这正是博客 blogs/2025-09-09-improving-ai-chat-experience-with-smart-title-generation.md 中记录的“第二版”关键修复:早期版本因未传递x-chat-ollama-keys请求头,导致标题生成回退到本地 Ollama 而非用户正在使用的 Moonshot Kimi。组合消息:
llm.invoke([['system', prompt], ['user', userMessage]]),返回{ title },其中title经.trim()处理。
5.4 模型一致性:为什么标题 API 需要getKeysHeader
客户端的generateTitleAPI在请求头中拼接了...getKeysHeader()(utils/settings.ts 中定义为{ 'x-chat-ollama-keys': encodeURIComponent(JSON.stringify(keysStore.value)) })。该头携带用户在设置中配置的各模型提供商密钥与端点,服务端通过server/middleware/keys.ts解析后注入event.context.keys,最终由createChatModel按family从MODEL_FAMILIES映射或自定义模型列表中选取正确的 LLM 实例(OpenAI / Azure OpenAI / Anthropic / Moonshot / Gemini / Groq / Ollama 等,见 server/utils/models.ts 的createChatModel分支逻辑)。
这就是模型一致性的根本保障:新功能必须复用基础设施(认证、配置管理),而不是重复造轮子。
六、标题风格(Title Styles)
style参数决定服务端使用哪一套提示词,仓库文档给出了四档风格及其示例输出:
| Style | 说明 | 示例输出 |
|---|---|---|
concise | 简短直接 | "Quantum Computing Basics" / "Quantum Computing" |
descriptive | 更详细的描述 | "Understanding Quantum Computing Principles" / "Introduction to Quantum Computing Principles" |
technical | 聚焦技术术语 | "Quantum Superposition and Entanglement" / "Quantum Superposition and Entanglement Theory" |
casual | 友好口语化 | "Learning About Quantum Stuff" |
服务端默认style = 'concise'、maxWords = 6,通过systemPrompt传入自定义提示词时可以完全覆盖 style 模板(const prompt = systemPrompt || TITLE_PROMPTSstyle)。
七、集成场景示例
文档提供了三类典型集成场景,均可在真实业务中直接套用。
7.1 文档摘要(不写库)
利用autoUpdate: false让系统只返回标题,不触碰会话数据库:
const { generateSessionTitle } = useSessionTitle() const summarizeDocument = async (docId: number, content: string) => { const title = await generateSessionTitle({ sessionId: docId, model: 'gpt-4', family: 'OpenAI', userMessage: content, style: 'descriptive', maxWords: 10, autoUpdate: false // Don't auto-save for docs }) await updateDocumentTitle(docId, title) // Handle the title manually }7.2 会话设置:允许用户重新生成标题
const regenerateTitle = async () => { const lastUserMessage = messages.value .filter(m => m.role === 'user') .pop()?.content if (lastUserMessage) { const { generateSessionTitle } = useSessionTitle() const newTitle = await generateSessionTitle({ sessionId: currentSessionId, model: selectedModel, family: selectedFamily, userMessage: lastUserMessage, style: userPreferences.titleStyle }) if (newTitle) updateUI(newTitle) } }7.3 批量处理(并行调用底层 API)
利用generateTitleAPI只做 HTTP 请求的特性,配合Promise.allSettled对多个会话并行生成,单个失败不影响整体:
const { generateTitleAPI } = useSessionTitle() const processSessions = async (sessions: Session[]) => { const results = await Promise.allSettled( sessions.map(session => generateTitleAPI( session.model, session.family, session.firstMessage, session.id ) ) ) // Handle results... }八、最佳实践
8.1 错误处理:永不打断用户主流程
标题生成是增强功能,失败必须静默降级:
const generator = createAutoTitleGenerator.forFirstMessage( (title) => updateUI(title), (error) => { console.warn('Title generation failed:', error) // Don't break the user experience } )源码层面generateSessionTitle的 try/catch 已保证:任何异常都只console.warn+ 触发onError,并返回null,不会向上抛出。
8.2 性能
- 标题生成完全异步,不阻塞 UI(
attemptTitleGeneration未 await 时即触发,标题就绪后通过回调更新); - 生成失败只记录日志,不影响聊天功能;
- 高频率发消息场景建议防抖(debounce),避免重复请求;
AutoTitleGenerator.init()使用动态import()懒加载 composable,避免增加初始包体积。
8.3 用户体验
生成期间展示加载状态,完成后通过回调清除:
const [isGeneratingTitle, setIsGeneratingTitle] = useState(false) const generator = createAutoTitleGenerator.forFirstMessage( (title) => { updateUI(title) setIsGeneratingTitle(false) } ) // Before generation setIsGeneratingTitle(true)8.4 配置化
将标题生成做成用户可配置项,并用updateConfig热更新:
const titleSettings = { enabled: true, style: 'descriptive', maxWords: 8, autoGenerate: true } generator.updateConfig({ enabled: titleSettings.enabled })九、测试策略
9.1 单元测试:触发器逻辑
import { titleTriggers } from '~/composables/useSessionTitle' describe('Title Triggers', () => { it('should generate on first user message', () => { const context = { messages: [{ role: 'user', content: 'Hello' }], sessionTitle: '' } const shouldGenerate = titleTriggers.firstUserMessage.shouldGenerate(context) expect(shouldGenerate).toBe(true) }) })9.2 集成测试:完整生成链路
import { useSessionTitle } from '~/composables/useSessionTitle' describe('Session Title Generation', () => { it('should generate and save title', async () => { const { generateSessionTitle } = useSessionTitle() const title = await generateSessionTitle({ sessionId: 1, model: 'test-model', family: 'OpenAI', userMessage: 'Test message', autoUpdate: false }) expect(title).toBeTruthy() }) })可测试性设计贯穿源码:纯逻辑(Trigger 判断)与副作用(API 调用、写库)分离,错误边界清晰,便于注入 mock。
十、故障排查(Troubleshooting)
10.1 常见问题清单
标题没有生成
- 检查触发条件是否满足(首条消息且无标题);
- 确认
model与family传参正确; - 查看浏览器控制台是否有报错。
API 报错
- 确保模型提供商配置正确;
- 检查 API Key 与端点(
x-chat-ollama-keys头是否携带); - 确认请求头包含认证信息。
UI 未更新
- 确认回调已正确连接;
- 检查
sessionInfo是否为响应式(reactive); - 验证
title-updated事件是否被父组件监听处理。
10.2 调试清单(来自快速参考卡)
- ✅ 模型(model)与家族(family)正确
- ✅ 会话 ID 有效
- ✅ 用户消息非空
- ✅ 触发条件满足
- ✅ API Key 已配置
- ✅ 回调已连接
- ✅ 浏览器控制台无错误
10.3 调试模式
通过回调打印日志快速定位:
const generator = createAutoTitleGenerator.forFirstMessage( (title) => { console.log('Title generated:', title) updateUI(title) }, (error) => { console.error('Title generation error:', error) } )十一、迁移指南:从旧系统升级
若项目此前使用旧的标题生成逻辑(例如composables/useGenerateSessionTitle.ts),按以下三步迁移到当前 API:
1. 替换导入:
// Old import { generateSessionTitle } from '~/composables/useGenerateSessionTitle' // New import { createAutoTitleGenerator } from '~/utils/autoTitleGeneration'2. 更新组件逻辑(从命令式到生成器模式):
// Old if (firstMessage) { generateSessionTitle(sessionId, model, family, message) } // New const generator = createAutoTitleGenerator.forFirstMessage(onTitleGenerated) generator.attemptTitleGeneration(context, sessionId, model, family)3. 配置方式从位置参数改为选项对象:
// Old const title = await generateSessionTitle(sessionId, model, family, message) // New const { generateSessionTitle } = useSessionTitle() const title = await generateSessionTitle({ sessionId, model, family, userMessage: message, style: 'concise' })十二、扩展系统:添加 Trigger 与 Style
根据 docs/guide/session-title-generation.md 的 Contributing 章节,扩展方向包括:
- 新增触发器:扩展
titleTriggers对象(composables/useSessionTitle.ts),实现SessionTitleTrigger接口; - 新增风格:在服务端 server/api/sessions/[id]/title.post.ts 的
TITLE_PROMPTS中追加条目; - 新增功能:遵循关注点分离(Component → Utility → Composable → API);
- 测试与文档:为新功能补充测试用例,并同步更新本指南。
十三、文档导航与后续学习
本指南所属的开发者文档体系位于 docs/guide/README.md,包含:
- 会话标题生成完整指南:架构总览、API 参考、集成示例、测试与迁移;
- 会话标题生成快速参考:复制即用的代码片段、用例速查表与调试清单;
- 同目录下还有 知识库配置指南 等其他开发者文档。
建议的开发学习路径:
- 通读主指南,建立整体架构认知;
- 日常开发携带快速参考卡,复制常用模式;
- 对照现有组件实现(如
components/Chat.vue中的接入方式)理解落地细节; - 运行测试确保改动不破坏现有行为;
- 结合 会话标题生成功能开发手记 了解该功能从“直接复制聊天逻辑”到“模块化重构”的三版演进过程,以及模型一致性、提示工程、非阻塞设计等背后的工程取舍。
本文内容基于 ChatOllama 仓库 docs/guide/README.md 开发者文档及其关联源码整理而成。
【免费下载链接】chat-ollamaChatOllama is an open source agentic app for running AI agents across local and hosted models.项目地址: https://gitcode.com/GitHub_Trending/ch/chat-ollama
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考