跳到主要内容

03 · 生成主线

这章是全项目的“主算法”:一条用户消息怎么走完 拼上下文 → 建工具 → 调模型 → 边流边落盘 的全程。读完你能在脑子里跑一遍 orchestrateGeneration

3.1 入口分流

用户回车后,sessionActions.generate()(转发自 session/generation.ts:11)先看会话类型:画图会话走 orchestratePictureGeneration,普通聊天走主角 orchestrateGeneration(session/orchestration.ts:98)。本章只讲后者。

3.2 全流程鸟瞰

怎么读这张图:从上到下是 orchestrateGeneration 的时间顺序;右侧标注每步的目的。

orchestrateGeneration(sessionId, targetMsg)

├─ 取 session / settings / 全局配置
├─ initializeTargetMessage ← 给空的 assistant 消息盖上模型名等元数据
├─ persistStreamingMessage ← 先落一条“正在生成”的占位

├─ createModel(settings) ← 注册表造模型 (第2章)
├─ buildContext(历史) ← 拼上下文/附件/压缩 (第5章)
├─ [若模型不支持图] OCR 兜底 ← 把图片转成文字塞进 prompt
├─ applyLegacyToolFallback ← 老模型不支持工具 → prompt 工程降级 (第4章)
├─ buildToolsForSession ← 按能力拼工具 + 收集 instructions (第4章)
├─ injectModelSystemPrompt ← 把工具说明等注入 system 消息
├─ convertToModelMessages ← 转成 AI SDK 的 ModelMessage

├─ model.chatStream(coreMessages) ← 拿到 AsyncGenerator (第1章)
│ for await (chunk):
│ processStreamChunk ← 累积进 contentParts
│ 每 2s persist / 否则更新内存缓存

└─ 收尾: generating=false, 落盘最终消息 (refreshCounting)

3.3 关键步骤拆解

① 先落占位、再找位置。 它先把 targetMsg 落盘(orchestration.ts:121),再在会话里定位它的下标 targetMsgIx,只取它之前的消息作为 prompt 历史(orchestration.ts:137)——因为 targetMsg 是即将被填充的空 assistant 气泡。

② 可中断。 建一个 AbortController,把 targetMsg.cancel = () => controller.abort() 挂上去(orchestration.ts:201-205),UI 的“停止”按钮就调它。signal 一路透传到 chatStream

③ OCR 兜底(巧妙处)。 如果当前模型不支持视觉、但历史里有图片,它不会直接报错,而是找一个 OCR 模型把图转成文字注入 prompt,并加一条 info 提示“当前模型不支持图片,已用 OCR 处理”(orchestration.ts:147-166)。找不到 OCR 模型才抛错。

④ system prompt 注入与角色降级。 injectModelSystemPrompt 把工具说明 instructions 拼进 system 消息;若模型不支持 system role,则把 system 消息降级成 user 消息(orchestration.ts:183-192)。这让“无 system 消息”的小模型也能拿到工具说明。

3.4 精华:边流边写 + 节流落盘

流循环是性能与正确性的平衡点(orchestration.ts:229-269)。每收到一个 chunk:

  1. 交给 processStreamChunk(session/stream-chunk-processor.ts:40)累积进 processorState.contentParts
  2. 状态块特殊处理:status 类型(如“重试中”)只更新消息的 status 字段、不动正文,skipUpdate 跳过(orchestration.ts:233-242)。
  3. 首 token 延迟:第一次出现非空文本时记 firstTokenLatency(orchestration.ts:250-252)。
  4. 节流持久化:不是每个 token 都写盘(会把磁盘 IO 打爆),而是"距上次落盘 ≥ 2 秒"才 persistStreamingMessage,否则只更新内存缓存 updateStreamingCache(orchestration.ts:260-268):
// 示意,非源码:流式更新的节流策略
const shouldPersist = Date.now() - lastPersistTimestamp >= 2000
if (shouldPersist) { void persistStreamingMessage(...); lastPersistTimestamp = Date.now() }
else updateStreamingCache(...) // 仅内存,UI 仍实时刷新

直觉:UI 要实时(内存缓存每块都更新),磁盘不必实时(2 秒一次足够防丢)。 这是流式应用的经典取舍。

3.5 收尾与错误

  • 流正常结束:补齐所有 reasoning 的 duration,标 generating: false、清 cancel,带上 usage/finishReason,最终落盘并触发计数刷新(orchestration.ts:271-289)。
  • 被中断(controller.signal.aborted):静默收尾,不报错(orchestration.ts:291-300)。
  • 其他异常:交给 handleGenerationError 把错误信息写进消息(orchestration.ts:302),用户能看到红色错误气泡。

3.6 stream-chunk-processor:第二份流解析器

渲染进程这份 processStreamChunk(stream-chunk-processor.ts)是纯函数式的——传入 (chunk, state, callbacks) 返回新 state,便于单测(stream-chunk-processor.test.ts)。它处理 7 种 chunk:text-delta / reasoning-delta / tool-call / tool-result / tool-error / file(图片落 blob)/ status / finish。tool 的结果通过 toolCallId 回填到对应的 tool-call 片段上(stream-chunk-processor.ts:91-115)。

如第1章所述,这与基类里那份解析逻辑功能重叠;主线走的是这份纯函数版本。

3.7 代码地图

主题文件符号
入口分流(聊天/画图)src/renderer/stores/session/generation.tsgenerate
生成总指挥src/renderer/stores/session/orchestration.tsorchestrateGeneration
OCR 兜底src/renderer/stores/session/ocr-helper.tsgetOCRModel / ocrImagesInMessages
流块累积(纯函数)src/renderer/stores/session/stream-chunk-processor.tsprocessStreamChunk / createInitialState
节流落盘src/renderer/stores/session/messages.tspersistStreamingMessage / updateStreamingCache
system prompt 注入src/renderer/packages/model-calls/message-utils.tsinjectModelSystemPrompt / convertToModelMessages
错误收尾src/renderer/stores/session/utils.tshandleGenerationError