我把一大段资料和指令拼进 prompt 喂给大模型,内容少时一切正常,内容一多模型就开始不按我的要求做、像没看见我的指令一样,排查半天才发现 prompt 超了 token 上限、被默默截断、我放在末尾的关键指令根本没送进去的深度复盘
这是一次让我对"一个有容量上限的容器,装超了不一定会报错,可能默默把装不下的丢掉"有了刻骨认知的事故。我做了个基于大模型的功能:把一段资料(文档、检索到的内容、历史对话)和我的处理指令拼成一个 prompt,喂给模型让它按指令处理资料。我习惯把指令(比如"请用 JSON 格式输出""只总结要点,不要发挥")放在 prompt 的末尾,觉得这样模型"最后看到、印象最深"。资料少的时候,模型乖乖按指令做,一切正常。
可一旦资料变多,怪事就来了:模型开始"不听话"——我明明要求输出 JSON,它却输出大段自然语言;我明明要求只总结要点,它却开始长篇大论地发挥。就像它压根没看见我的指令一样。我一开始以为是模型能力不行、是指令写得不够清楚,反复改 prompt 措辞都没用。直到我把拼好的完整 prompt 的长度(token 数)打出来,和模型的上下文窗口上限一对比,才恍然大悟:资料一多,整个 prompt 的 token 数超过了模型的上下文窗口上限;而模型(或 API)在超限时,往往会静默地把超出的部分截断丢弃——偏偏我把关键指令放在了 prompt 的末尾,于是被截断砍掉的,正好就是我那段最重要的指令!模型根本没收到指令,自然不按要求做;而且整个过程不报错,我还以为模型把指令读了、只是没执行好。
故障现场:prompt 超 token 上限,末尾的指令被静默截断
我把这个"指令凭空消失"的过程还原出来,问题一目了然:
我的 prompt 结构(指令放在末尾):
[一大段资料................................] ← 内容多, 占了很多 token
[我的关键指令: 请用 JSON 输出, 只要要点] ← 放在末尾
模型的上下文窗口有上限(比如 8K / 32K token):
- 资料少时: 资料 + 指令 总 token < 上限 → 全部送入 → 模型看到指令 ✓
- 资料多时: 资料 + 指令 总 token > 上限
→ 超出部分被【截断】(很多实现是从尾部或按策略丢)
→ 偏偏指令在末尾, 正好被砍掉! → 模型根本没收到指令 ✗
→ 模型只看到资料、没看到指令 → 不按要求做
→ 且整个过程【不报错】(静默截断), 我误以为模型读了指令没执行好
# 验证: 打印拼好的 prompt 的 token 数, 和模型上下文上限对比
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4")
print(len(enc.encode(full_prompt))) # 一看: 远超上限!
# 真相: 不是模型不听话, 是它压根没收到我的指令(被截断丢了)
看着"指令因为放在末尾、超限时被砍掉",我才彻底明白:大模型的上下文窗口是一个有容量上限的容器;当我塞进去的内容超过这个上限时,超出的部分不会被"压缩"或"报错",而是被静默地截断丢弃。而"丢掉的是哪一部分",取决于截断策略和内容的位置——我恰好把最关键的指令放在了最容易被砍掉的末尾。于是模型收到的是一个"缺了指令的残缺 prompt",它当然不按我的要求做;而这个截断悄无声息、不报错,让我完全没往"内容被砍了"这个方向想,反而去怪模型、改措辞,南辕北辙。我以为我把指令清清楚楚地交给了模型,其实那段指令在送达之前,就被悄悄丢在了门外。
第一件事:搞懂上下文窗口与静默截断——超限不报错,而是丢内容
冷静下来,我去把"大模型的上下文窗口与 token 限制"这一课认真补了,才明白这个"指令消失"的根源:
【为什么 prompt 超长会让指令"消失"——上下文窗口与截断】
大模型有一个【上下文窗口(context window)】:
- 它能"看到"的输入 + 输出, 总 token 数有一个上限(如 8K/32K/128K)
- token 不等于字数(中文约 1 字 ≈ 1~2 token, 英文约 4 字符 ≈ 1 token)
超过上限会发生什么(关键、且常被忽略):
- 不是报错、不是自动压缩, 很多实现是【静默截断】——把超出的部分丢掉
- 丢哪部分取决于实现/策略: 可能从头丢、从尾丢、或按规则丢
- 一旦你的关键内容(指令/最重要的数据)落在被丢的区域 → 它就没进模型
- 而模型对"它没看到的东西"一无所知, 只会基于"残缺的输入"作答, 且不报错
我的双重错误:
1. 没控制 prompt 总长度, 让它超了上下文窗口上限
2. 把最关键的指令放在最容易被截断的末尾 → 正好被砍
正确的认识与做法:
- 把上下文窗口当成"有限的预算", 主动管理 prompt 的 token 总量
- 估算/统计 token 数(tiktoken 等), 别让它超限; 超了就主动取舍
- 关键指令放在【不会被截断】的位置(很多模型把 system 指令放最前更稳),
别赌它在末尾还能被看到
- 资料太多: 先检索/筛选/摘要, 只放真正相关的(而非全塞)
- 超长输入: 分块处理 + 汇总, 而非一次硬塞
- 显式检查: 拼完 prompt 校验 token 数, 超限就报错/截断有数, 别静默
这一下点醒了我:我把大模型的输入当成了一个"能无限装、装多少它都全看"的口袋,可它其实是一个有严格容量上限的容器;塞超了,多出来的部分不会让它报错、也不会被它压缩,而是被静默地丢弃——而丢的恰好可能是我最在乎的那部分。更隐蔽的是,这个"丢弃"悄无声息、不报错,让我误以为模型"收到了却没做好",从而把排查方向完全带偏。不是模型不听话,是我的指令根本没送进去——它被这个有限容器的静默溢出,无声地吞掉了。
第二件事:正解——主动管理 token 预算,关键指令放安全位置,资料先筛后塞
找到根因,正解就清晰了:把上下文窗口当成有限的预算来主动管理——拼 prompt 前统计/估算 token 数、别让它超限;关键指令放在不会被截断的安全位置(很多模型 system 指令在最前更稳);资料太多就先检索/筛选/摘要,只放真正相关的、或分块处理再汇总,而不是把一切硬塞进去赌它不超。并显式检查,别让截断静默发生。
# 错误: 把全部资料 + 末尾指令直接拼起来塞进去, 超了就被静默截断
prompt = all_documents + "\n请用 JSON 输出, 只要要点" # ✗ 超限时指令被砍
# 正解1: 拼前统计 token, 超限就主动处理(别静默)
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4")
def count(s): return len(enc.encode(s))
BUDGET = 7000 # 给输入留的 token 预算(给输出留余量)
if count(prompt) > BUDGET:
raise ValueError("prompt 超预算, 需筛选/摘要") # 显式暴露, 别让它悄悄截
# 正解2: 关键指令放安全位置(system 在最前), 别赌末尾还在
messages = [
{"role": "system", "content": "用 JSON 输出, 只总结要点, 不要发挥"}, # 指令在最前
{"role": "user", "content": selected_docs}, # 资料随后
]
# 正解3: 资料太多 → 先检索/筛选/摘要, 只放真正相关的(而非全塞)
relevant = retrieve_top_k(query, k=5) # RAG: 只取最相关的几段
context = "\n".join(relevant) # 控制在预算内
# 正解4: 超长输入 → 分块处理 + 汇总, 而非一次硬塞
chunks = split_by_token(long_text, max_tokens=3000)
partials = [llm_summarize(c) for c in chunks] # 分块各自处理
final = llm_summarize("\n".join(partials)) # 再汇总(map-reduce)
# 关键: 永远清楚"我塞进去多少 token、上限多少", 别让它默默溢出
这套做法的精髓,是把"能塞多少内容"从"无意识地一把全塞、赌它不超",变成"有意识地管理一份有限的 token 预算":拼 prompt 前统计 token、超了就主动筛选/摘要而非静默截断;关键指令放在不会被砍的安全位置;资料太多就先检索筛选、或分块处理再汇总。核心是永远清楚自己塞了多少、上限多少,绝不让内容在我不知情的情况下被悄悄丢掉。不是把一切都塞进去指望模型全看,而是认清容量有限、主动决定"什么必须进、什么可以舍"。
【和 token 上限相处, 几条原则】
1. 上下文窗口有上限; 超了不报错、常被静默截断, 丢的可能正是关键内容
2. 拼 prompt 前统计 token(tiktoken 等), 超预算主动处理, 别静默溢出
3. 关键指令放安全位置(system 在最前), 别赌它在末尾还能被看到
4. 资料太多: 先检索/筛选/摘要, 只放真正相关的; 别一把全塞
5. 超长输入: 分块处理 + 汇总(map-reduce), 而非一次硬塞
6. 给输出留够 token 预算(输入+输出共享窗口); 输出也可能被截断
第三件事:其他"超了容量上限、静默丢弃/截断"的同类坑
顺着"有限容量超了会静默丢东西"这条线,我把同类的坑都梳理了一遍,它们都源于"把一个有上限的容器当成无限的、塞超了还不知道":
第一个,数据库字段长度超了被截断。varchar(255) 存了更长的字符串,有的数据库静默截断、只存前 255 个字符,数据悄悄丢了一截还不报错。
第二个,日志/消息体超长被截断。日志单条有长度上限、消息队列消息有大小上限,超了被截断,关键信息(往往在末尾的堆栈/详情)丢失。
第三个,缓冲区/队列满了丢数据。固定大小的缓冲区或队列满了,新数据被丢弃(或覆盖旧的),且常常静默,造成数据丢失。
第四个,整数/数值溢出。数值超过类型上限,静默回绕/溢出成一个错误的值,不报错,后续计算全错。
第四件事:塞满 prompt vs 管理 token 预算,一张表对照
我把"一把全塞、赌它不超"和"主动管理 token 预算"的差别整理成一张表,这是我现在拼 prompt 时的依据:
| 维度 | 一把全塞(不管 token) | 主动管理 token 预算 |
|---|---|---|
| 超上限时 | 静默截断, 内容悄悄丢 | 提前发现, 主动筛选/摘要 |
| 关键指令 | 放末尾, 可能被砍 | 放安全位置(system 最前) |
| 是否报错 | 不报错, 表现为"模型不听话" | 显式校验, 超了暴露出来 |
| 资料处理 | 有多少塞多少 | 检索/筛选/摘要/分块 |
| 排查难度 | 极难(怪模型、改措辞) | 一眼看出 token 超限 |
| 结果 | 数据量一大就异常 | 稳定可控 |
这张表让我看清:"一把全塞"在数据量小时和"管理预算"看不出差别,可一旦内容超过上下文窗口,前者就会静默截断、悄悄丢掉关键内容、表现成"模型不听话"这种最难排查的样子。主动管理 token 预算、把关键指令放安全位置、超量先筛选,才能让内容完整送达、行为稳定可控。把上下文窗口当成有限预算来花,而不是无底洞来填。
第五件事:我对"prompt 想塞多少塞多少"的几个想当然
这次事故,本质是我把"上下文窗口"当成了无限的。把这些想当然列出来,每一条都值得警惕:
| 我曾经的想当然 | 事故教我的真相 |
|---|---|
| "prompt 想塞多少塞多少,模型都能看到" | 上下文窗口有上限,超了的部分被静默截断 |
| "超长了模型会报错提醒我" | 很多实现是静默截断,不报错,内容悄悄丢 |
| "指令放末尾模型印象最深" | 末尾最容易被截断砍掉,反而最危险 |
| "模型不听话是它能力/我措辞的问题" | 可能是指令被截断了,模型根本没收到 |
| "资料越全,模型答得越准" | 超限会丢内容;且塞太多还稀释关键,要筛选 |
| "token 数差不多就行,不用精确算" | 差一点就超限触发截断;关键场景要统计 token |
第六件事:拼 prompt、给模型喂内容时,我现在的自检习惯
现在每当我拼 prompt 喂大模型,或排查"模型像没看见指令一样不照做",我都会先按这张图问自己:
这张图的精髓,是"拼 prompt 前先确认 token 数会不会超上下文窗口、关键指令在不在会被截断的位置;超了主动筛选别静默截断"。写时就统计 token 控制预算、关键指令放 system 最前、资料先检索筛选摘要、排查就看模型不照做是不是 prompt 超 token 上限、指令被截断了。这套习惯,让我从"想塞多少塞多少"变成了"把上下文窗口当有限预算来管理"——核心始终是:大模型的上下文窗口有 token 上限(输入+输出共享)、超过上限时往往不报错而是静默截断丢弃超出部分、丢哪部分取决于位置和策略;我把关键指令放在末尾、资料一多 prompt 超限、末尾的指令正好被砍掉,模型收到残缺输入不照做却不报错,让我误以为模型不听话;正解是主动管理 token 预算——拼前统计 token 别超限、关键指令放安全位置(system 最前)、资料太多先检索筛选摘要或分块处理再汇总、显式校验别让截断静默发生。
我立下的几条规矩
这场"prompt 超 token 上限、末尾指令被截断"的事故,换来了我做大模型应用时,刻进骨子里的几条铁律:
- 大模型上下文窗口有 token 上限(输入+输出共享);别把它当无限的口袋。
- 超过上限往往不报错、而是静默截断丢弃超出部分——丢的可能正是你最关键的内容。
- 关键指令放在不会被截断的安全位置(很多模型 system 指令在最前更稳),别放最容易被砍的末尾。
- 拼 prompt 前统计/估算 token(tiktoken 等),控制在预算内,超了主动处理别静默溢出。
- 资料太多就先检索/筛选/摘要,只放真正相关的;超长输入分块处理再汇总(map-reduce)。
- 给输出留够 token 预算(输入输出共享窗口);输出也可能因为窗口不够而被截断。
- 推而广之:字段长度、日志、缓冲区、数值类型都有上限,超了常静默截断/溢出,要主动管理用量。
附:我现在拼 prompt 固定套的"token 预算守卫"
这是我现在拼任何 prompt 时固定套的一层"token 预算守卫"——把这次踩坑的教训(统计 token、控制预算、关键指令前置、超量主动筛选而非静默截断)固化成了一个函数,让指令再不会被悄悄砍掉:
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4")
def n_tokens(s: str) -> int:
return len(enc.encode(s))
def build_prompt(system_instruction: str, docs: list[str],
model_limit: int = 8192, reserve_for_output: int = 1500):
""" 关键指令前置 + token 预算守卫: 资料超预算就主动筛减, 绝不静默截断 """
# 1) 给输出留余量, 算出输入可用的 token 预算
input_budget = model_limit - reserve_for_output
# 2) 关键指令永远先放(放最前, 不会被截断), 并先扣掉它的预算
used = n_tokens(system_instruction)
budget_for_docs = input_budget - used
if budget_for_docs <= 0:
raise ValueError("仅指令就超预算, 模型/预算选错了")
# 3) 资料按相关性逐条放入, 放到预算用完为止(主动取舍, 不静默截断)
selected, total = [], 0
for d in docs: # docs 应已按相关性排序
t = n_tokens(d)
if total + t > budget_for_docs:
break # 预算到顶, 停止(明确知道丢了后面的)
selected.append(d)
total += t
dropped = len(docs) - len(selected)
if dropped: # 丢了多少, 显式记录, 不让它静默
log.warning(f"token 预算不足, 丢弃了 {dropped} 段较不相关的资料")
return [
{"role": "system", "content": system_instruction}, # 指令在最前, 安全
{"role": "user", "content": "\n\n".join(selected)},
]
这个守卫把我这次的教训钉死在了拼 prompt 的入口:它先给输出留够预算、把关键指令放在最前并优先保住它的 token,再按相关性把资料一条条放进剩余预算、放满即停,而且明确记录丢弃了多少——把"截断"从悄无声息变成了我主动决定、且看得见的取舍。有了它,我的 prompt 永远在上下文窗口之内、关键指令永远在不会被砍的位置、被舍弃的永远是最不相关的那部分而非我最在乎的指令。把"上下文窗口是有限预算、要主动管理、别静默溢出"这个道理,沉淀成一道拼 prompt 时必经的守卫,这是我对这次事故最实在的交代——毕竟,内容会不会被丢、丢的是哪部分,这种事绝不该交给一个不会吭声的静默截断去替我决定。
写在最后
回头看,这场由"prompt 超 token 上限静默截断"引发的"模型像没看见指令"事故,真正教给我的,远不止"统计 token、指令前置"这一个技巧。它让我对"我们很容易把一个'有容量上限'的东西, 当成'无限的、能装下我给的一切'; 而当我们塞超了, 它往往不会大喊'装不下了', 而是悄无声息地把多出来的部分丢掉——更要命的是, 它表现得'好像一切正常', 只是结果不对, 让我们对着'没收到的那部分'毫无察觉、还在别处苦苦找原因",有了一次刻骨的体会。我栽跟头,是因为我把一个'有限的容器'当成了'无限的', 又因为它'静默丢弃'而对'东西已经丢了'毫不知情——我以为我把资料和指令都"交给"了模型, 它该全看到;我没意识到, 模型的输入是有上限的容器, 我塞超了, 它就把超出的(恰好是末尾的指令)默默扔了; 而它不报错——既不告诉我"你塞超了", 也不告诉我"我没看到指令";于是我得到的是一个"看似正常运行、结果却不对"的局面, 我对着"模型为什么不听话"百般尝试, 却从没想到"它根本没收到指令"。这让我领悟到一个关于"容量上限、静默失败与可见性"的深刻认知:许多容器/通道/资源都有一个容量上限; 当输入超过上限时, 最危险的失败方式不是"明确报错", 而是"静默地截断、丢弃、溢出"——它不抗议、不中断, 只是悄悄地少做了一部分, 然后让系统带着"残缺的输入"继续运行;这种"静默失败"之所以格外可怕, 是因为它破坏了"我以为发生的"和"实际发生的"之间的一致性: 我以为我的内容/指令完整地送达了, 实际它被砍了一截; 而由于没有任何报错, 我会把错误归因到完全不相干的地方;所以面对任何"有上限"的东西, 都要主动去管理用量、并让"是否超限、是否被截断"变得可见(统计、校验、显式报错), 而不是默认它能装下一切、并信任一个不会主动告诉你"我丢东西了"的系统。这给了我一种看待"一切'把内容交给一个有容量限制的系统'之事"时的清醒:每当我把数据/内容/指令交给一个系统处理时, 要追问"这个系统有容量上限吗?我交的会不会超?如果超了, 它是会明确报错, 还是会静默地丢掉一部分、却装作一切正常?我怎么才能看见'它到底完整收到了没有'?"——主动管理用量、把"超限/截断"从静默变成可见(统计+显式校验), 把关键内容放在最不会被丢弃的位置, 而不是默认容器无限、并盲信一个会静默吞掉东西的系统;"认清容量上限、警惕静默截断、让用量与丢弃变得可见", 是用对大模型、也是用对一切'有限容器'的关键。认清上下文窗口有上限、超限静默截断丢内容、关键指令要放安全位置并主动管理 token 预算——这,是我用一次模型像没看见指令的事故,换来的、关于 AI、也关于如何看待容量上限与静默失败的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次往 prompt 里塞一大堆资料、又把关键指令放末尾时,先想想"这会不会超 token 上限?超了我的指令会不会正好被砍掉?",并统计一下 token、把指令挪到最前,那我对着那个"模型像没看见我指令"的诡异行为折腾的大半天,就值了。
—— 别看了 · 2026