模型答到一半就停了:输出被截断的最小复现 版权与内容来源声明本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容均在附表 A 中标注来源引用官方原文保持原样不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式也不对任何收益结果作承诺。转载请注明出处。一个做订单摘要的接口上线第二周开始出现一种很别扭的失败HTTP 状态码一直是 200可返回体里最后那个右大括号时不时就没了下游一解析整条链路断掉换个模型、把提示词重写三遍还是隔一阵少一个括号。翻日志翻不出东西因为从头到尾就没有一条报错——模型不是报错它是被砍断了而且是「正常返回」着被砍断的。这篇不复述原理。先用一段能跑的最小代码把「到底有没有被截断」这件事判出来再解释模型为什么不会主动告诉你它没写完最后给一组改了预算口径前后的对照外加一条什么时候该扩预算、什么时候该停手的线。一、先把一段正常回答喂进去看它在哪一步被砍断被砍断的位置不在你的解析层而在服务端的解码循环最后一步——你看到半个 JSON是这一步的结果不是它的原因。所以先别急着改提示词先学会问一句「它是怎么停的」。1.1 现场状态码 200返回体里少一个右大括号把同一段「把这张订单整理成 JSON」的输入反复打过去约十分之一的请求会返回一份体面的半成品字段排得整整齐齐items数组也开好了就是最后一个大括号没合上。注意它的形态——它不是乱码不是空字符串也不是明显的报错而是一段「看起来快写完了」的文本。这种失败最难查因为它长得太像成功。1.2 这一刀砍在生成不在传输第一反应通常是「网络断了」或「网关截断了」。但把同一请求换成长连接、缩短超时、去掉中间代理现象一样。真正的原因在于生成是一步一步往外吐 token 的服务端在吐到某个上限时把循环停掉然后把「已经吐出来的部分」当成一次成功响应返回。传输没丢东西是源头就少了一截。1.3 「回答多长」和「句子通不通」都当不了判据很多人第一反应是看长度短了就是被截断。这条判据立刻就会失效——有些任务本来就只要两行也有些被砍断的回答断在了一个完整的句号后面读起来毫无破绽。句子通顺就更不可靠了模型完全可能在一个漂亮的逗号处被停下。两个直觉判据都不成立所以下面的代码不看它们只看一个字段。二、把「是否被截断」这个判据跑成一段代码判据只有一句话看结束原因字段不看回答本身。OpenAI 的 Chat Completions 把它放在finish_reasonAnthropic 的 Messages 把它放在stop_reason两者语义一一对应。2.1 官方给的取值与语义先钉死OpenAI 官方 API 参考对finish_reason的定义是逐字这样的The reason the model stopped generating tokens. This will be stop if the model hit a natural stop point or a provided stop sequence, length if the maximum number of tokens specified in the request was reached, content_filter if content was omitted due to a flag from our content filters, tool_calls if the model called a tool, or function_call (deprecated) if the model called a function.这句话里最关键的是length那一支它的触发条件不是「回答太长」而是「达到了请求里指定的最大 token 数」。也就是说是不是被截断和你主观觉得长不长无关和你在请求里填了多少有关。2.2 一张表看两边口径 实测环境数据取自官方 API 参考文档截至 2026-10-14为文档核对而非运行时数据无本地依赖协议字段取值官方语义转述本文处置finish_reasonOpenAIstop命中自然停止点或给定的停止串可放行finish_reasonOpenAIlength达到请求里指定的最大 token 数需重试提高上限或分段finish_reasonOpenAItool_calls模型调用了工具需继续执行工具后再发一次finish_reasonOpenAIcontent_filter内容因过滤被省略按内容策略处理finish_reasonOpenAIfunction_call模型调用了函数已弃用需继续stop_reasonAnthropicend_turn模型自然完成响应可放行stop_reasonAnthropicmax_tokens达到你设的max_tokens上限提高上限或继续生成stop_reasonAnthropicstop_sequence命中了自定义停止串按约定处理stop_reasonAnthropictool_use模型正在调用工具需继续stop_reasonAnthropicpause_turn服务端工具循环到达迭代上限把助手内容发回以继续stop_reasonAnthropicrefusal模型拒绝响应读拒绝详情并在回退模型重试stop_reasonAnthropicmodel_context_window_exceeded填满了模型的上下文窗口视为已截断需重试出处OpenAI 开发者文档 Chat 参考与 Create chat completion、Anthropic《Stop reasons and fallback》链接见附表 A。取数日期2026-10-14。口径说明两边字段名不同、取值个数不同但「正常收尾」「触达上限」「要调工具」这三类控制流是齐的——这也是下面这段判定函数能同时吃两种返回体的原因。2.3 十几行代码把判决跑出来判定只做一件事把结束原因映射成一个控制流动作。字段缺失时一律不放过——因为「查不到原因」和「原因是正常」是两码事。DECISION{# 结束原因 → 该做什么length:需重试提高上限或分段禁止把半截结果写进下游,max_tokens:需重试提高上限或继续生成禁止把半截结果写进下游,stop:可放行正常收尾,end_turn:可放行正常收尾,stop_sequence:可放行命中自定义停止串按约定处理,tool_calls:需继续执行工具后把结果回填再发一次请求,tool_use:需继续执行工具后把结果回填再发一次请求,}defjudge(rec):rrec.get(finish_reason)orrec.get(stop_reason)ifnotr:# 字段缺失 ≠ 正常收尾return需重试结束原因字段缺失不能当作正常收尾returnDECISION.get(r,f需人工确认未知结束原因{r!r})forrecin[{id:r1,finish_reason:length},{id:r2,finish_reason:stop},{id:r3,finish_reason:tool_calls},{id:r4,stop_reason:max_tokens},{id:r5}]:print(f记录{rec[id]}结束原因{rec.get(finish_reason)orrec.get(stop_reason)!r}判决{judge(rec)}) 实测环境Python 3.13.12 / macOS / 仅标准库脚本 work/exp1014/exp01b.py真实输出原样粘贴未做任何修饰记录r1 结束原因length 判决需重试提高上限或分段禁止把半截结果写进下游 记录r2 结束原因stop 判决可放行正常收尾 记录r3 结束原因tool_calls 判决需继续执行工具后把结果回填再发一次请求 记录r4 结束原因max_tokens 判决需重试提高上限或继续生成禁止把半截结果写进下游 记录r5 结束原因None 判决需重试结束原因字段缺失不能当作正常收尾r1 和 r4 就是线上那个 bug 的两种拼法一个是 OpenAI 口径的length一个是 Anthropic 口径的max_tokens形态都是「半截 JSON」。判定函数把它们都归到「需重试」于是那半截东西根本进不了下游解析器。三、模型为什么不会告诉你它没写完模型没有「我还没写完」这个内部状态可上报——它每一刻做的事只有一件根据前面的 token 猜下一个 token。「到没到头」是外部循环决定的不是它的自我认知。3.1 停下来的原因是接口层记的账模型自己不记账接口层记。所以「为什么停」这件事被单独抽成一个字段由服务端填。Anthropic 的官方说明里讲得很直白成功的响应都会带stop_reason它和「请求处理失败」的错误不是一回事——错误表示这次请求没处理成而stop_reason是在告诉你「响应是怎么结束的」。3.2 状态码 200 只说明请求被处理了这就是那个接口能一直「绿」着的原因HTTP 200 描述的是请求侧的成败它完全不承诺生成侧是自然收尾还是被砍断。一条被截断的响应仍然是一次成功的调用。把 200 当通关信号等于把「链路上线了」当成「内容写完了」。3.3 更会骗人的是「空回答也可能是正常收尾」Anthropic 文档里还提到一种情况有时会返回一段几乎没有内容的响应文档描述为只有两个到三个 token而它的结束原因仍然是end_turn。反过来说end_turn不等于「一定给了完整内容」它只等于「模型认为自己这一轮说完了」。所以判据要按控制流用别顺势把它当成质量分。四、改掉预算口径前后两组对照实测口径错数字算得再准也没用。旧口径只盯着「正文要多少 token」忘了两件事结构化输出的收尾要单独留量输入会把输出额度一起吃掉。4.1 先算清一件事输入和输出共用同一个窗口上下文窗口是共享的输入多占一点留给输出的就少一点。所以正确的顺序是先量输入再用「窗口减输入」得到剩余额度最后一件事才是定输出预算。用官方分词器把三段不同文本量一遍这个账立刻显形。 实测环境Python 3.13.12 / macOS / tiktoken 0.14.0cl100k_base 编码脚本 work/exp1014/exp01a.py下列可独立运行importtiktoken enctiktoken.get_encoding(cl100k_base)# 官方 BPE 分词器WINDOW,NEED_OUT8192,2048# 上下文窗口 / 本轮需要的输出预算A(把一段中文技术说明喂给模型时字符数和 token 数完全不是一回事。一个汉字往往要占掉一到两个 token标点、换行、空格也都各自记账。如果按字符数去估输出长度预算会算少一大截。)B{name:张三,age:32,skills:[检索,重排,评测],note:这是结构化输出的一段示例}Cdef truncate(records):\n return [r for r in records if r[finish_reason] ! length]\nnlambdas:len(enc.encode(s))fork,sin((A,A),(B,B),(C,C)):print(f编码器cl100k_base 文本{k}字符数{len(s)}tokens{n(s)}剩余输出额度{WINDOW-n(s)})print(-*60)forcopiesin(60,72,73,90):tn(A*copies)print(f编码器cl100k_base 输入文本A×{copies}tokens{t}剩余输出额度{WINDOW-t}f需要输出{NEED_OUT}判定{够用ifWINDOW-tNEED_OUTelse溢出})真实输出原样粘贴未做任何修饰编码器cl100k_base 文本A 字符数92 tokens85 剩余输出额度8107 编码器cl100k_base 文本B 字符数70 tokens36 剩余输出额度8156 编码器cl100k_base 文本C 字符数89 tokens22 剩余输出额度8170 ------------------------------------------------------------ 编码器cl100k_base 输入文本A×60 tokens5100 剩余输出额度3092 需要输出2048 判定够用 编码器cl100k_base 输入文本A×72 tokens6120 剩余输出额度2072 需要输出2048 判定够用 编码器cl100k_base 输入文本A×73 tokens6205 剩余输出额度1987 需要输出2048 判定溢出 编码器cl100k_base 输入文本A×90 tokens7650 剩余输出额度542 需要输出2048 判定溢出三个数要盯住中文段落 92 个字符量出 85 个 tokenJSON 片段 70 个字符只有 36 个 token——字符数不是 token 数两者的比例随文本类型变。更关键的是下半段输出需求一直是 2048输入从 6120 涨到 6205判决就从「够用」翻成了「溢出」。这就是旧口径漏掉的那一刀你什么都没改只是输入长了 85 个 token输出就被挤没了。4.2 换个编码器别拿字符数当刻度官方分词器仓库 README 对这件事说得很朴素tiktoken 是面向 OpenAI 模型的快速 BPE 分词器BPE 是一种把文本转成 token 的方式并且给了一个经验刻度——「On average, in practice, each token corresponds to about 4 bytes.」注意这是平均意义上的约数不是换算公式中文、JSON、代码各走各的比例实测那三行就是证据。所以别再用「1 个字符≈1 个 token」这种刻度去估预算。4.3 两组口径摆在一起看口径max_tokens怎么定漏掉了什么同一段 JSON 任务的结果口径一只按正文估等于正文预期 token 数没给结构收尾留量右大括号被砍得到非法 JSON口径二正文估加固定余量但不管输入正文 200没校验「输入 输出 ≤ 窗口」输入一涨剩余额度不够偶发截断口径三本文建议预期正文上限 × 1.3 结构收尾预留两个约束都覆盖溢出在请求前就算出来不必等线上翻车口径三里的「×1.3」是给正文波动的缓冲「结构收尾预留」是给 JSON 的收尾括号、给 Markdown 表格的最后一行这种必须吐完才合法的尾部留的额度。结构化输出尤其不能省这一块正文少一句还能看JSON 少一个括号直接不可解析。而这一切之前先做一次减法输入 token 数 输出预算 ≤ 上下文窗口不满足就别发请求。这份资料里有什么如果你正在把这类生成接口接进业务链路需要一份讲清上下文预算与失败判定的参考配套资料里有 Agent 与 RAG 的完整梳理。放在资料包里扫码即可获取五、什么时候该扩预算、什么时候该停手结论先给触达上限就重试但重试前必须先算窗口否则你只是把溢出挪到下一次。5.1 判据与动作一张表收口现象判据动作结束原因是触达上限且「输入 新预算 ≤ 窗口」还有空间提高上限重试一次结束原因是触达上限但输入已占掉大半窗口空间不够分段拆成多次生成或先压缩输入反复提高上限结束原因仍是触达上限任务本身超过窗口停手改成流式续写或预处理分段别再加大数字正常收尾但正文比预期短很多不是截断别动预算问题在提示词或采样结束原因字段缺失无法判定按不可放行处理先补齐字段再谈优化这张表里有两条是最容易被写错的。第一条输入占掉大半窗口时继续调大max_tokens是无效动作因为剩余额度已经被输入锁定怎么加都超窗正确动作是分段或压缩输入。第二条反复触达上限说明任务粒度本身就不对这时候该停手改架构而不是把数字往上堆。5.2 一条能直接抄进代码的线把判定放进统一出口结束原因不是「正常收尾」也不是「继续生成」时一律不许把结果写进下游触达上限的记录进重试队列并且重试时按口径三重算一次预算。这条线的好处是它不依赖你对长文/短文的直觉判断只看字段所以不会因为换模型、换任务而失效。5.3 收尾先看字段再谈长度回到开头那个少一个括号的接口。它的问题从来不是提示词而是没人看那个结束原因字段——200 一路放行半截 JSON 一路往下流。给这件事一个可执行的判断只要结束原因是触达上限就必须重试并提高上限或分段只要字段缺失就必须按不可放行处理。至于「扩到多大」先算减法再定输入 token 数 输出预算 ≤ 上下文窗口算得过再发算不过先削输入别削收尾预留。这份资料里有什么本文的截断判定函数、分词预算脚本和两张官方取值表都归到了这套配套里。放在资料包里扫码即可获取附表 A本文引用事实与出处对照表事实出处本文位置finish_reason的取值与语义含length表示达到请求里指定的最大 token 数原文为「The reason the model stopped generating tokens. This will be stop if the model hit a natural stop point or a provided stop sequence, length if the maximum number of tokens specified in the request was reached…」OpenAI 开发者文档 Chat 参考developers.openai.com/api/reference/resources/chat原 platform.openai.com 地址 301 跳转至此第 2.1、2.2 节max_tokens是「chat completion 中可生成的最大 token 数」已弃用、改用max_completion_tokens与 o 系列模型不兼容max_completion_tokens是「一次补全可生成的 token 数上界包含可见输出与推理 token」OpenAI 开发者文档 Create chat completiondevelopers.openai.com/api/reference/resources/chat/subresources/completions/methods/create第 2.2 节、第 4.3 节stop_reason取值与语义end_turn、max_tokens、stop_sequence、tool_use、pause_turn、refusal、model_context_window_exceeded其中model_context_window_exceeded的处置是「将响应视为已截断」Anthropic 官方《Stop reasons and fallback》platform.claude.com/docs/en/build-with-claude/handling-stop-reasons第 2.2 节max_tokens原文为「The maximum number of tokens to generate before stopping. Note that our models may stop before reaching this maximum.」——只规定绝对上限模型可能提前停Anthropic Messages API 参考platform.claude.com/docs/en/api/messages第 2.2 节、第 4.3 节成功响应都带stop_reason它与「请求处理失败」的错误不是一回事Anthropic 官方《Stop reasons and fallback》同链接第 3.1 节有时会返回几乎无内容的响应文档描述为只有两个到三个 token结束原因仍为end_turnAnthropic 官方《Stop reasons and fallback》同链接第 3.3 节tiktoken 是面向 OpenAI 模型的快速 BPE 分词器原文含「On average, in practice, each token corresponds to about 4 bytes.」开源仓库 openai/tiktoken 的 READMEgithub.com/openai/tiktoken第 4.2 节本文实测92 字符中文段落 85 token70 字符 JSON 36 token89 字符代码 22 token窗口 8192 下输入 6120 token 时剩余 2072 尚可、输入 6205 token 时剩余 1987 对 2048 的输出需求即溢出本文实测Python 3.13.12 / macOS / tiktoken 0.14.0 / cl100k_base取数日期 2026-10-14第 4.1 节写在最后这篇用到的资料写这篇文章时把相关的官方文档和源码又翻了一遍顺手也整理了几份配套的东西大模型学习路线图从零基础到能自己动手做 Agent按阶段说明每一步该学什么、哪些可以先跳过《LangChain LangGraph MCP 智能体开发实战》视频课7 个模块从私有化部署、EmbeddingRAG 到 MCPAgent 全流程AI 大模型知识库在线可查Agent Skills 从入门到落地、Claude Skills 完全指南等专题按目录浏览即可640 套 AI 大模型行业报告 经典 PDF 书籍看行业落地案例和别人怎么做的时候用得上大模型零基础到精通教学视频跟着敲一遍比只读文档快得多资料是我自己整理的放在下面这个码上扫码即可获取添加时备注「AI」优先通过。资料按「先路线、再动手、最后查漏」的顺序整理好了建议先看学习路线那一份照着它挑一条适合自己当前基础的路径再往下看。