第 2 章:LLM 调用与流式输出
本章解决的问题
前端 AI 面试几乎必问流式输出。这一章解释 token、context window、SSE、chunk 和流式文本处理。
交互图解:一次 Agent Turn 完整调用链 · 查看图解导读
大白话理解
模型不是一次生成整段答案,而是一个 token 一个 token 地生成。
流式输出的本质是:
text
服务器生成一个 token
-> 立即发给浏览器
-> 浏览器把它追加到界面
-> 服务器继续生成下一个 token这样用户看到的是逐字出现,而不是长时间白屏后一次性出现全文。
必须掌握的概念
1. Token
Token 是模型处理文本的最小单位。中文、英文单词、标点都可能被拆成一个或多个 token。
2. Context Window
Context Window 是模型一次请求能看到的 token 上限。上下文包括:
- 系统提示词
- 历史消息
- 工具结果
- 当前问题
3. SSE
SSE 是浏览器和服务器之间的一条单向流。服务器可以持续发送文本片段。
SSE 适合:
- 模型到前端的文本流。
- 状态更新。
- 日志推送。
WebSocket 适合需要浏览器频繁上行消息的场景,例如协同编辑或强实时双向通信。很多 AI 对话系统会混合使用:上行用普通 HTTP,下行用 SSE;复杂协议可以用 WebSocket 承载逻辑流。
4. Chunk
Chunk 是流中的一个片段。前端需要处理:
- 普通文本增量。
- JSON 增量。
- 工具调用开始和结束。
- 完成或错误事件。
DeepSeek Harness 对应实现
DeepSeek Harness 定义了 StreamChunk 词汇。模型适配器返回 chunk,BlockAssembler 把它组装成最终 assistant message。
agent-loop 的核心逻辑大致是:
text
for await chunk of stream:
append assistant/chunk
assembler.push(chunk)
finish = assembler.finish这意味着原始 chunk 既用于 UI 展示,也保留在会话日志中用于回放。
进一步阅读:
packages/llm/llm/src/index.tspackages/core/agent-loop/src/agent.ts
代码示例
使用 fetch 读取流
ts
async function streamCompletion(prompt: string, onDelta: (text: string) => void): Promise<void> {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ prompt }),
})
if (!response.body) throw new Error('streaming is not supported')
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const text = decoder.decode(value, { stream: true })
onDelta(text)
}
}将流转换为 AsyncIterable
ts
async function* completionChunks(prompt: string): AsyncIterable<string> {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ prompt }),
})
if (!response.body) return
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) return
yield decoder.decode(value, { stream: true })
}
}面试怎么问
问:SSE 和 WebSocket 怎么选?
回答要点:
- SSE 基于 HTTP,实现简单,适合服务器持续推送。
- WebSocket 是双向全双工,适合浏览器也要高频上行。
- 很多 AI 产品用普通 HTTP 上行、SSE 下行,或者用 WebSocket 承载自定义 RPC。
问:流式 JSON 为什么难处理?
回答要点:
- 网络 chunk 可能在任意位置切断 JSON。
- 不能对半截 JSON 调用
JSON.parse。 - 需要增量解析器或协议设计成多个小事件。
问:如何优化 token 消耗?
回答要点:
- 裁剪历史消息。
- 对工具结果做摘要或截断。
- 使用合适的 system prompt。
- 后端使用 compaction 或投影,避免把无关日志发给模型。
自测题
- Token 和字符有什么区别?
- Context Window 为什么会影响前端产品设计?
fetch读取流式响应的基本步骤是什么?- SSE 和 WebSocket 各自适合什么场景?
本章小结
前端处理 LLM 的第一步不是“把返回文本放到 div 里”,而是理解 token、流式传输、chunk 和错误结束。这是后续所有 UI 状态设计的基础。
进一步阅读:
packages/llm/llm/src/assembler.tsdocs/subsystems/llm-streaming.md