指南/上下文截断
上下文究竟在哪里被截断:从请求体到模型窗口逐层排查
长对话突然忘了前文,最容易被归因成中转站截断。可同一种表现也可能来自客户端自动删历史、官方接口的截断配置、输出上限,或模型在长输入里没有取到那条信息。要定位,必须把「发出去」「收到」「用到」分开检查。
「没记住」不是截断证据
模型没有答出前文的一句话,只能证明这次回答没有正确使用那段信息。输入可能已经被删,也可能完整进入窗口但没有被模型找到。长上下文能力同时受输入位置、任务写法、模型随机性和输出预算影响,靠一次问答无法区分这些原因。
更稳的排查方式是把三个事实分别留证:
- 发出去的内容:客户端在加模板、历史裁剪和序列化后,最终生成了什么请求体。
- 接口接收的规模:官方 Token 计数接口与生成响应中的
usage报了多少输入 Token。 - 回答使用的内容:固定位置探针在多次请求中的召回结果如何分布。
前两项属于协议和请求证据。第三项是行为证据,强度较低。
四个可能改变上下文的位置
客户端组装请求时
聊天界面、agent 框架和 SDK 外层常会管理历史:只保留最近若干轮、把早期对话改成摘要、清掉体积大的工具结果,或者在超过自定预算时换一个小模型整理上下文。这些行为发生在请求离开本机之前。
排查时不要记录界面里看到的聊天记录,要记录序列化后的最终 JSON。至少保存消息数量、各角色内容长度、工具定义数量、文件引用、模型名与截断相关参数。含敏感内容时保存经过规范化的哈希和分段长度,原文放在受控环境;数据边界见中转站能看到哪些数据。
如果发送前的请求体已经没有开头,后续不必再测中转站。
中转层解析与重写时
中转站要做协议转换、模型映射或内容审核时,通常会解析请求体。实现可以删除未知字段、限制消息总长度、压缩历史,也可能因为反向代理的请求体上限直接返回 413。这里说的是技术能力,不代表某个站一定这样处理。
能用官方 Key 时,把同一份固定请求体分别发给官方入口和中转入口。两边模型 ID、参数、工具定义与字节内容要一致。官方成功且中转返回 413、400,或中转响应中的输入 Token 长期明显更少,范围就缩到了中转入口及其后端。
输入 Token 不完全相等仍需谨慎。中转站可能把 OpenAI 格式转换成 Anthropic 格式,系统消息、工具定义和图像的计数规则会改变;不同模型也可能使用不同 tokenizer。先确认两边对应的是同一模型与同一协议,再参考Token 虚标与倍率对账。
官方 API 处理窗口溢出时
不同接口对超长输入的处理并不统一。
OpenAI Responses API 的 truncation 默认为 disabled:输入超过模型上下文窗口时返回 400;设为 auto 后,接口会从对话开头丢弃条目以适配窗口。这个行为写在 Responses API 的 truncation 参数中。排查「开头消失」时,先查请求是否显式或经封装设置了 auto。
Claude API 对「输入本身已经超窗」返回 400 invalid_request_error。在较新的模型上,如果输入加 max_tokens 超过窗口但输入本身仍放得下,请求可以被接受;生成碰到窗口边界时以 stop_reason: "model_context_window_exceeded" 结束。Anthropic 建议发送前调用 Token Counting API,详见上下文窗口文档与Token 计数文档。计数结果是估算值,实际输入可能有小幅差异。
Gemini 提供 countTokens 计算请求输入,生成后可从 usageMetadata.promptTokenCount 读取输入规模;图像、音频等非文本内容也会计入。字段定义见 Gemini Token 计数。具体模型窗口会变化,应从模型页面读取,不要把一个固定数字写死在探针里。
模型生成回答时
请求完整、usage 也符合预期,模型仍可能漏掉某个位置的信息。这属于长输入检索或指令执行失败。尤其是把十几个问题塞进同一条 prompt,再用「少答了一项」推断截断,证据很弱;模型可能遗漏、合并或拒绝其中一项。
还要区分输入和输出。回答在半句处结束,先看 stop_reason、finish_reason 与输出 Token 数。max_tokens、安全拦截、工具调用或流中断都能让输出变短,它们不能证明输入被裁掉。Claude 降智怎么判断里列出的停止原因探针可以配合使用。
一份能定位位置的探针
探针材料应使用无敏感信息的合成文本。把长文分成等长区块,每个区块放一个随机生成、互不相似的位置标记,例如:
[SEGMENT-00] code = K7Q2-M4VX
……固定长度的中性填充文本……
[SEGMENT-01] code = P9DA-8RHC
……固定长度的中性填充文本……
请求只问一个位置的 code,并要求逐字返回。每个位置单独发请求,避免一个回答同时承担十几项任务。模型、温度、系统消息、输出上限和工具定义全部固定;材料里不要放当前时间、随机运行 ID 或每次变化的说明。
按以下顺序记录:
- 在客户端序列化完成后,记录请求体哈希、总字节数、区块数与本地 Token 估算。
- 用供应商的 Token 计数接口读取同一请求的输入规模;再发送生成请求,保存响应中的
usage。 - 从开头、中间、末尾各取若干位置,分别重复探测。单个位置失败不要下结论。
- 如果开头连续失败而末尾稳定成功,用区块做二分搜索,找到行为变化的大致边界。
- 同一请求在官方入口和中转入口交叉测试,并保留请求 ID、错误体与时间。网络错误的记录方法见AI API 错误排查。
位置标记也有边界。模型可能猜错 code,或在完整看到文本时仍未准确召回;短而有规律的标记还可能被 tokenizer 切得不同。探针要重复运行,并用官方直连结果作为同条件基线。
按证据强度判断
| 观察结果 | 可以支持的判断 | 不能推出的结论 |
|---|---|---|
| 最终出站 JSON 已缺少早期消息 | 客户端或框架在发送前改了历史 | 中转站删了内容 |
| 官方 Token 计数大于中转响应报告的输入量,且协议与模型相同 | 中转路径中的内容或计数发生差异 | 一定是故意截断 |
| 返回 400 且错误体明确写输入过长 | 接收该请求的接口拒绝了超窗输入 | 模型已经看过部分输入 |
stop_reason 指向窗口或输出上限 | 生成在对应边界停止 | 输入开头被删除 |
| 位置探针多次呈现固定边界 | 某处存在稳定的位置效应,值得继续查请求与计数 | 单凭行为给某一层定责 |
最强的组合证据是:同一份出站请求、同一模型与协议,官方 Token 计数和生成用量稳定,中转侧输入量在某个长度后突然变小,同时位置探针从同一处开始失败。即便如此,结论也应写成「中转路径中发生了裁剪或计数改写」,不要扩展成模型身份或运营动机判断。
LinkyMonitor 用固定长输入和位置探针记录 Token 用量、停止原因与召回结果,让偶发失忆和持续截断分开呈现。