跳转至

第 6 章:一次聊天请求的完整旅程 —— 从 ChatInput 到 SSE

这是整份拆解的主线。读懂它,就能把前面的层次串成一个运行时故事。

6.1 请求总览

sequenceDiagram
  participant User as 用户
  participant Route as threads/$threadId
  participant Chat as useChat / AI SDK
  participant Transport as CustomChatTransport
  participant Factory as ModelFactory
  participant Backend as local router 或 remote API
  participant Store as Zustand + ServiceHub

  User->>Route: 输入文本 / 附件 / 选择模型
  Route->>Store: 乐观写入 user message
  Route->>Chat: sendMessage
  Chat->>Transport: prepareSendMessagesRequest
  Transport->>Transport: 组装 context、tools、samplers
  Transport->>Factory: createModel(provider, model)
  Factory->>Backend: 建立 LanguageModel / fetch
  Backend-->>Factory: stream chunks
  Factory-->>Transport: text / reasoning / tool parts
  Transport-->>Chat: UIMessageChunk
  Chat-->>Route: 更新 assistant UI
  Route->>Store: persist assistant + tool result
  Route->>Backend: tool call follow-up(如有)

6.2 路由页面是编排器

routes/threads/$threadId.tsx 不只是展示消息。它负责:

  • 读取 thread、assistant、model、attachments 和 session data。
  • 处理 abort controller、tool approval promise、自动滚动和 context-limit 恢复。
  • 使用 lastAssistantMessageIsCompleteWithToolCalls 判断是否需要 follow-up。
  • 把 Core ThreadMessage 转为 AI SDK UIMessage,再把结果转回持久化格式。
  • 处理分支消息:parent、siblings、active path、继续生成。

这解释了为什么 Jan 的聊天页面较大:产品语义(thread、assistant、project、attachment)和模型语义(message parts、tool calls、stream status)在这里相交。

6.3 CustomChatTransport 做了哪些“脏活”

AI SDK 提供通用 ChatTransport,但 Jan 需要在发送前处理一长串本地约束:

  1. 把 UI messages 转为 model messages。
  2. 注入 assistant instructions 和 thread context。
  3. 发现 MCP、RAG、web search 工具并生成 JSON Schema。
  4. 按模型设置、assistant 参数、当前请求合并 sampler。
  5. 对 llama.cpp 的 schema 做规范化,修复 shorthand schema 和不兼容的 PCRE pattern。
  6. 处理音频/视频 sentinel,把 data URL 变成目标后端理解的形状。
  7. 估算上下文长度,执行 trim/compact,必要时触发 out-of-context UI。
  8. 创建工具 approval、执行工具、把 tool result 放回自动 follow-up。

因此它是“产品级 transport”,不是简单的 fetch 封装。

6.4 ModelFactory 的 Provider 分流

model-factory.ts 为每个 Provider 返回统一的 Vercel AI SDK LanguageModel。大致分为:

Provider 类别 实现
llama.cpp 找到 session,连接本地 OpenAI-compatible endpoint,保留本地参数
MLX 连接 macOS MLX session,用 metadata extractor 读取 timings
OpenAI/Anthropic/Google/xAI/Mistral 使用官方 AI SDK provider
自定义兼容 Provider createOpenAICompatible,附加 base URL、headers、key chain

Factory 还会根据 resolveProviderCaps 判断哪些 sampling 参数可以发送。陌生 custom provider 更宽松,因为用户明确选择了它;内置 provider 更严格,因为官方接口会拒绝未知字段。

6.5 流式状态为什么不直接写数据库

生成中间态首先属于 UI session:useChatSessionsuseAppState 和页面局部状态负责 streaming、loading model、prompt progress、live token stats。只有 assistant message 完成、停止或错误时,才把稳定实体写回 useMessages/ServiceHub。

chunk × N → UI session state(高频、易变)
message_end → ThreadMessage(稳定、可恢复)

如果每个 token 都写 messages.jsonl,磁盘 IO 和文件锁会放大;如果只写最终结果,崩溃时会丢掉正在生成的内容。Jan 选择两层状态,兼顾即时 UI 与可恢复历史。

6.6 Tool call follow-up 的闭环

AI SDK 在 assistant message 中返回 tool call 后,onToolCall 会先启动审批 promise;真正执行在完成回调/执行循环中进行,使 tool result 能落在一条完成的 assistant message 上。sendAutomaticallyWhen/lastAssistantMessageIsCompleteWithToolCalls 再触发下一次模型请求。

model → assistant(tool_call)
      → approval(可能等待用户)
      → tool execution
      → tool result message
      → automatic follow-up
      → model final answer

这个设计避免了“工具一返回就偷偷执行”的用户体验,也防止工具结果以半完成消息形式污染上下文。

6.7 上下文溢出不是异常终点

当本地模型或 Provider 报上下文超限,页面会把错误 stamp 到最近的 user message metadata,并保留 partial assistant output,提供继续生成/重试路径。context-manager.ts 的 trim/compact 和 pendingContinueMessage 共同组成“可恢复的生成失败”模型。

这是聊天产品和一次性 API client 的区别:错误不是请求失败就结束,而是要转换成用户可以理解和继续操作的状态。