在深入理解 Token(令牌)与 Context Window(上下文窗口)之后,你自然会想:如果上下文窗口不够用了会怎么办?这正是本文要探讨的三个核心概念——Truncation(截断)、Latency(延迟)和 Streaming(流式输出)。它们直接影响你使用 AI 时的体验,从“它记住了多少”到“它回复得有多快”,都与此息息相关。

三、Truncation(截断):当窗口容量不足时
3.1 截断的含义
结合前文介绍的 Token 和 Context Window,截断的本质可以一句话概括:当输入内容的 Token 总数超过模型预设的上下文窗口上限时,系统会按照既定规则“砍掉”一部分内容。
关键在于,这并非模型“记性不好”那么简单,而是一个非常实在的工程机制——砍哪里、怎么砍,都是写死的规则,冷酷且毫无通融余地。
3.2 两种截断场景
① Input Truncation(输入截断)
当你一次性输入的内容——System Prompt、历史对话、上传的文档、当前提问——转成 Token 后,总和超过了模型的上下文窗口上限。此时,系统会选择“砍掉”哪部分?
| 截断策略 | 具体操作 | 后果 |
|---|---|---|
| 从头截断(Drop-Start) | 丢弃最早的那部分 Token | 模型“失忆”,忘了开头聊过什么,但保留了当前问题和系统提示 |
| 从尾截断(Drop-End) | 只保留末尾 N 个 Token | 较为少见,它会把你刚发出的提问也一并砍掉 |
| 智能截断 | 强制保留 System Prompt + 当前输入,优先砍掉中间的对话历史 | 体验最好,很多产品级的聊天界面都在用这个策略 |
② Output Truncation(输出截断)
即便你的输入成功挤进了窗口,模型的输出本身也有上限,也就是 max_tokens。触发原因可能是:你或 API 参数显式设定了一个较小的最大值;平台默认限制了单次回复长度;模型触发了停止符之外的长度天花板。
结果就是:回复说到一半突然断掉——最后一句没说完、代码写到一半戛然而止,留下一个悬在半空中的句子。
3.3 识别截断信号
如何分辨系统的“裁切”和模型本身的“失误”?以下信号可供参考:
| 现象 | 更像截断 | 更像模型问题 |
|---|---|---|
| 回复末尾突然中断,最后一句不完整 | ✅ | |
| 模型突然“忘了”你 5 轮前交代的重要约束 | ✅ | |
| 上传的文档前半部分完全没被引用 | ✅ | |
| 回复内容完整,但事实错误、逻辑跳步 | ✅ | |
API 返回中有 truncated: true 标志 | ✅ |
3.4 实用规避策略
- 别把上下文窗口当仓库用——对话历史越长,被截断的风险越高。
- System Prompt 里放“不可丢失”的全局规则——多数产品会保护 System Prompt 不被砍掉。
- 长文档别硬塞——改用摘要、RAG 或分段处理的方式。
- 输出被截断时——直接说“继续”续写,或显式要求控制长度。
四、Latency(延迟):你等待 AI 回复的时间
4.1 延迟的定义
延续上面的讨论:输入和输出的规模,不仅影响记忆,还决定等待时长。
一句话定义:从你按下回车,到你看到完整回复,这段时间就是延迟。
4.2 两个关键延迟指标
| 指标 | 全称 | 你感受到的是什么 |
|---|---|---|
| TTFT | Time To First Token | 回车后多久出现第一个字——它在“思考”吗? |
| E2E Latency | End-to-End Latency | 完整回答全部生成完,总共需要多长时间 |
如果你用过流式输出(字一个个蹦出来),你会发现,你直觉里感知的其实是这两件事的叠加。
4.3 LLM 延迟的两阶段结构
传统 Web API 的延迟大致是一次性的,但 LLM 是自回归生成的,延迟结构完全不同:
你的请求进来
├─ [1] Prefill(预填充)阶段
│ 把你的全部输入 Token 序列做一次前向传播
│ → 计算出 KV Cache,准备好“下一个 Token 的预测起点”
│ → 这部分决定了 TTFT(首字延迟)
├─ [2] Decode(解码/生成)阶段
│ 逐个 Token 生成:生成一个 → 拼回去 → 再预测下一个 → 循环
│ → 这部分决定了“后面字蹦多快”(TPS / 吞吐)
└─ 输出完成
4.4 反直觉的结论
输入越长 ≠ 仅仅多传点数据,而是直接推高 TTFT
- 你塞进 Context Window 的每个 Token 都要参与 Prefill 的矩阵运算,输入越多,首字出现越慢。
输出越长 = 线性拉长 E2E 延迟
- 每个输出 Token 本质上是一次小步推理,生成得越多,总耗时自然越长。
4.5 影响延迟的因素
| 因素 | 推高哪段延迟 | 你能不能控 |
|---|---|---|
| 输入 Token 数 | ↑ TTFT | ✅ 能:精简 Prompt、清理历史 |
| 输出 Token 数 | ↑ E2E | ✅ 能:要求“控制在 X 句” |
| 模型尺寸/参数量 | 两段都 ↑ | ❌ 选模型时已定 |
| 并发/负载 | 两段都 ↑ | ❌ 平台侧控制 |
| 是否走思维链/工具调用 | E2E 暴增 | ✅ 能:不是所有问题都需要 |
| 网络往返 / 流式 vs 非流式 | 感知延迟 | ✅ 能:开启流式 |
五、Streaming(流式输出):把等待变成陪伴
5.1 流式输出的定义
顺着前面的概念链:延迟是客观存在的,但我们可以改变用户对延迟的感受——这就是流式输出的价值。
5.2 非流式 vs 流式对比
| 特性 | 非流式 | 流式 |
|---|---|---|
| 模型侧行为 | 照常逐 Token 生成,但憋着不发 | 照常逐 Token 生成,但每生成一个就发一个 |
| 网络传输 | 等全部生成完,一次性返回 | 用 chunked 分块传输,一边生成一边传输 |
| 用户感受 | 长时间空白 → 啪一下全出来 | 很快出现第一个字,逐字蹦出来 |
| E2E 总耗时 | 基本相同 | 基本相同 |
| 感知延迟 | 很差——等待是“死”的 | 好很多——等待变成“活的” |
5.3 技术实现
常见的传输机制:
| 协议/机制 | 备注 |
|---|---|
| SSE (Server-Sent Events) | LLM API 最常用:Content-Type: text/event-stream |
| HTTP chunked transfer | 底层分块发送 |
| WebSocket | 双向更灵活,但多数场景用 SSE 更简单 |
5.4 API 示例对比
关闭流式(stream: false):
{
"choices": [
{
"message": {
"role": "assistant",
"content": "人工智能是一门研究如何让机器具备智能行为的学科……"
}
}
],
"usage": { "prompt_tokens": 120, "completion_tokens": 85 }
}
开启流式(stream: true):
data: {"choices":[{"delta":{"role":"assistant"}}]}
data: {"choices":[{"delta":{"content":"人"}}]}
data: {"choices":[{"delta":{"content":"工"}}]}
data: {"choices":[{"delta":{"content":"智"}}]}
...
data: [DONE]
5.5 为什么要流式
- 把 TTFT 变成“可用时间”——第一个结论片段就有决策价值,不必等到全部生成完。
- 超时与失败更可控——只要有 chunk 在推,就知道它还没“死”。
- 成本/中止机会——看到模型跑偏,可以中途 Abort 连接,避免无谓的生成浪费。
5.6 流式的“坑位清单”
| 坑 | 现象 | 解法思路 |
|---|---|---|
| JSON 解析难 | 流式给的是碎片,不能直接 JSON.parse | 先拼完整字符串再解析 |
| 函数调用也是增量 | arguments 一截一截到达 | 按 index 拼接后再解析 |
| 截断发生在中途 | 尾部可能断在不完整句子 | 检查 finish_reason 标志 |
| 前端闪屏/布局抖动 | 字一蹦出来就触发重排 | 用等宽容器或固定高度 |
完整概念链总结
Token(计量单位)→ Context Window(容量上限,用 Token 计)→ Truncation(塞超了就砍,砍的位置影响结果质量)→ Latency(塞得越多、生成越长,等待时间越久)→ Streaming(不改推理速度,但把“已完成 Token”提前交付)
实用建议汇总
- 成本优化:看 Token 数而非字符数,两者差别很大。
- 体验优化:开启流式输出,把死等变成活等。
- 内容管理:别把上下文窗口当仓库,长文档用 RAG 拆分。
- 调试技巧:遇到 AI“失忆”先查 Token 用量,遇到卡顿先查 TTFT。
