引言
在上一篇文章中,我们讲解了 Token 和 Context Window 两个核心概念。今天我们继续深入,探讨当上下文窗口不够用时会发生什么——Truncation(截断),以及这一切会对你的使用体验产生什么影响——Latency(延迟) 和 Streaming(流式输出)。
三、Truncation(截断):当窗口装不下时
3.1 什么是截断
结合前面的 Token → Context Window,截断的本质一句话就能说清:
Truncation = 输入/输出的文本总长度超出了模型的硬性限制(主要是上下文窗口),系统按照既定规则强行"砍掉"一部分,让剩下的内容刚好能塞进去继续跑。
它不是模型"想不起来"这么文艺,而是一个非常冷酷的预处理/后处理工程机制——而且砍哪里、怎么砍,都是提前写死的规则。
3.2 两种截断场景
① Input Truncation(输入截断)
你一次性塞进去的内容(System Prompt + 历史对话 + 附件/文档 + 当前提问)转成 token 后 > 模型上下文窗口上限。
| 截断策略 | 怎么砍 | 后果 |
|---|---|---|
| 从头截断 Drop-Start | 丢掉最早期的那部分 token | 模型"失忆"了开头聊过什么,但保留当前问题和系统提示 |
| 从尾截断 Drop-End | 只保留末尾 N 个 token | 少见,会把刚发的提问也砍掉 |
| 智能截断 | 强制保留 System Prompt + 当前输入,优先砍中间历史 | 体验最好,很多产品级聊天界面在用 |
你可能经历过:"明明刚才还在聊那个方案,怎么突然 AI 像失忆了一样?"——大概率就是从头截断把早期历史切掉了。
② Output Truncation(输出截断)
就算输入顺利塞进去了,模型的输出本身也有上限(如 max_tokens),可能是:
- 你或 API 参数显式设了一个较小的最大值
- 平台默认限制了单次回复长度
- 模型触发了停止符之外的长度天花板
结果就是:回复在半截突然断掉——最后一句说到一半、代码写到一半就没了。
3.3 识别截断信号
几个很典型的信号:
| 现象 | 更像截断 | 更像模型问题 |
|---|---|---|
| 回复末尾突然中断,最后一句不完整 | ✅ | |
| 模型突然"忘了"你 5 轮前交代的重要约束 | ✅ | |
| 上传的文档前半部分完全没被引用 | ✅ | |
| 回复内容完整,但事实错误、逻辑跳步 | ✅ | |
API 返回里有 truncated: true | ✅ |
3.4 实用规避策略
- 别把上下文窗口当仓库用——对话历史越长越危险
- System Prompt 放"不可丢失"的全局规则——多数产品会保护 System Prompt 不被砍
- 长文档别硬塞——改用摘要 / RAG / 分段处理
- 输出被截断——说"继续"续写,或显式要求控制长度
四、Latency(延迟):你等 AI 的时间
4.1 什么是延迟
延续前面的脉络:
Token 是计量单位 → Context Window 是容量天花板 → Truncation 是超限时的硬砍 → 而 Latency 就是你为这一切付出的时间代价。
一句话定义:
Latency = 从你发出请求那一刻起,到拿到(可用的)模型输出为止,经历的时间。
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 什么是流式输出
顺着前面的概念链:
Token → Context Window → Truncation → Latency
Streaming 就是为了解决"Latency 里那段最折磨人的等待"而生的传输方式:模型每生成一个 Token,不等全篇完工,立刻把它推给你。
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
如果你在实际使用中有任何困惑,欢迎在评论区留言讨论!