第 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 SDKUIMessage,再把结果转回持久化格式。 - 处理分支消息:parent、siblings、active path、继续生成。
这解释了为什么 Jan 的聊天页面较大:产品语义(thread、assistant、project、attachment)和模型语义(message parts、tool calls、stream status)在这里相交。
6.3 CustomChatTransport 做了哪些“脏活”¶
AI SDK 提供通用 ChatTransport,但 Jan 需要在发送前处理一长串本地约束:
- 把 UI messages 转为 model messages。
- 注入 assistant instructions 和 thread context。
- 发现 MCP、RAG、web search 工具并生成 JSON Schema。
- 按模型设置、assistant 参数、当前请求合并 sampler。
- 对 llama.cpp 的 schema 做规范化,修复 shorthand schema 和不兼容的 PCRE pattern。
- 处理音频/视频 sentinel,把 data URL 变成目标后端理解的形状。
- 估算上下文长度,执行 trim/compact,必要时触发 out-of-context UI。
- 创建工具 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:useChatSessions、useAppState 和页面局部状态负责 streaming、loading model、prompt progress、live token stats。只有 assistant message 完成、停止或错误时,才把稳定实体写回 useMessages/ServiceHub。
如果每个 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 的区别:错误不是请求失败就结束,而是要转换成用户可以理解和继续操作的状态。