返回博客

故障排查指南

AI 回答被截断?修复 finish_reason "length" 和 max_tokens

你的 API 回答在句子中间停下,或者返回损坏的 JSON?检查 finish_reason,在上限内提高 max_tokens,并添加继续步骤。

作者:CheapTokens Team发布于 5 分钟阅读

终端风格封面,显示 API 响应中的 finish_reason length
本页内容
  1. 被截断的回答在屏幕上是什么样
  2. 第 1 步:读取告诉你答案的字段
  3. 第 2 步:提高 max_tokens,但不要超过上限
  4. 第 3 步:中途停下的 JSON
  5. 第 4 步:让模型继续
  6. 一个长回答要花多少钱
  7. 上线之前:一份简短清单

你的脚本运行了,模型也回答了,但函数在一行中间就结束了。或者 JSON 解析器在一个看起来完全正常的回复上抛错。大多数情况下,模型并没有失败:它撞上了你设置的 max_tokens 上限(或者一个你从未见过的默认值),API 在词中间把它截断了。

快速修复:读取响应中的 finish_reason。如果它是 "length"(OpenAI 兼容 API),或者 stop_reason 是 "max_tokens"(Anthropic Messages),说明回答被截断了。把 max_tokens 提高到模型的输出上限,或者让模型继续。本页剩余部分用可以直接粘贴的代码展示这两种做法。

如果你想要的是这件事为什么会发生的通俗解释,请先看为什么 AI 回答会在句子中间停下。本页是调试版本。

被截断的回答在屏幕上是什么样#

症状因你请求的内容而异,但原因相同。试着把这些和你见过的情况对号入座:

配对

把症状和可能的成因对应起来

  • 一个函数以 return resu 结尾代码回答在行中间撞上了 max_tokens
  • json.loads 失败,报 "Unterminated string" 或 "Expecting ',' delimiter"JSON 在中途被截断
  • 翻译在文本的前半部分之后就停了输出上限小于翻译所需
  • 回复是完整的,字段显示 stop模型自己完成了;去别处找原因

最后一行很重要。如果 finish_reason 是 stop,那上限就不是你的问题,提高 max_tokens 不会有任何改变。

第 1 步:读取告诉你答案的字段#

每个响应都会说明生成为什么结束。在 OpenAI 兼容响应中,这个字段是 choices[0].finish_reason:模型完成时是 "stop",token 用完时是 "length"。在 Anthropic Messages API 中,字段是 stop_reason,被截断的值是 "max_tokens"。Anthropic 的停止原因指南(2026 年 10 月 28 日核对)推荐的正是下面这两种修复:提高 max_tokens 或继续响应。

下面是同一种检查的三种形式。OpenAI SDK 的 base URL 是 https://api.cheaptokens.dev/v1,Anthropic SDK 的是 https://api.cheaptokens.dev(不带 /v1)。max_tokens=200 故意设得很小,这样你可以亲眼看到截断。

一种检查,三种写法

Python(OpenAI SDK)

python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.cheaptokens.dev/v1",
    api_key=os.environ["CHEAPTOKENS_API_KEY"],
)

resp = client.chat.completions.create(
    model="deepseek-ai/DeepSeek-V4-Flash-0731",
    max_tokens=200,
    messages=[{"role": "user", "content": "Write a Python class for a todo list with tests."}],
)

choice = resp.choices[0]
if choice.finish_reason == "length":
    print("Cut off at the token limit")
print(choice.message.content)
print(resp.usage.completion_tokens)

Python(Anthropic SDK)

python
import os
import anthropic

client = anthropic.Anthropic(
    base_url="https://api.cheaptokens.dev",
    api_key=os.environ["CHEAPTOKENS_API_KEY"],
)

msg = client.messages.create(
    model="deepseek-ai/DeepSeek-V4-Flash-0731",
    max_tokens=200,
    messages=[{"role": "user", "content": "Write a Python class for a todo list with tests."}],
)

if msg.stop_reason == "max_tokens":
    print("Cut off at the token limit")
print(msg.content[0].text)

curl

bash
curl https://api.cheaptokens.dev/v1/chat/completions \
  -H "Authorization: Bearer $CHEAPTOKENS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-ai/DeepSeek-V4-Flash-0731",
    "max_tokens": 200,
    "messages": [{"role": "user", "content": "Write a Python class for a todo list with tests."}]
  }'

在返回的 JSON 中查找 "finish_reason": "length"。

当它作为诊断正常工作时,你会看到这一对结果:低上限给出 length,宽裕的上限给出 stop。

同一请求分别使用低和高 max_tokens:finish_reason 从 length 变为 stop
你最常遇到的两种 finish_reason 值

第 2 步:提高 max_tokens,但不要超过上限#

显而易见的修复是调大数字。问题在于:每个模型对回答都有自己的上限。不要设置超过它的值:上限是按模型定的,所以先看下面的表格。这些是 CheapTokens 上每个回答的上限(事实核对于 2026 年 10 月 28 日):

模型 每个回答的最大输出 上下文
DeepSeek-V4-Flash 32,768 tokens 380K
GLM-5.3-Flash 8,192 tokens 390K
MiniMax-M2.7 8,192 tokens 180K

一个 9,000 token 的翻译需要一个每回答上限更大的模型——DeepSeek-V4-Flash 每个回答最多可输出 32,768 tokens。对于长代码、长 JSON 或整份文档,deepseek-ai/DeepSeek-V4-Flash-0731 的空间最大:每个回答最多 32,768 tokens,一天中任何时段都是同一个价格。请从 CheapTokens 文档和模型页面获取准确的模型 id,因为各提供商的命名方式不同。

一条实用规则:把 max_tokens 设为你愿意接受的最长回答,而不是最大值。你只为模型实际写出的 token 付费,所以回答短时高上限不花任何钱。

第 3 步:中途停下的 JSON#

被截断的 JSON 回复是最麻烦的情况,因为文本看起来没问题,直到解析器撞到结尾。假设你在一次调用中从 40 张发票里提取字段,并要求返回一个数组。模型写了 31 个对象,到达上限,数组始终没有闭合。

一个在 token 上限处被截断的数组引发的 json.loads 错误
被截断的 JSON 数组在解析器中是什么样

不要试图通过添加 ]} 来修复字符串。你会得到合法但缺少记录的 JSON,这比报错更糟。先检查 finish_reason;如果它是 length,要么提高上限并重新运行调用,要么把输入拆成更小的批次。关于强制格式本身,请参阅从 LLM API 获得合法 JSON。

第 4 步:让模型继续#

有时回答确实比任何上限都长:一个书籍章节、一次大规模重构。那就把你已有的内容喂回去,让模型接着写。下面的循环保留它目前已有的文本,请求剩余部分,并在几轮之后放弃,这样 bug 就不会无限空转。

python
def complete(base_messages, model, max_tokens=4096, rounds=4):
    text = ""
    for _ in range(rounds):
        messages = list(base_messages)
        if text:
            messages += [
                {"role": "assistant", "content": text},
                {"role": "user", "content": "Continue exactly where you stopped. Do not repeat anything."},
            ]
        r = client.chat.completions.create(model=model, max_tokens=max_tokens, messages=messages)
        choice = r.choices[0]
        text += choice.message.content or ""
        if choice.finish_reason != "length":
            return text, True   # finished on its own
    return text, False          # still cut after all rounds

两个习惯能让这件事保持诚实。返回 True/False 标志,并在 False 时向用户显示警告,而不是把半份文档当作完成品呈现。还要记住,每一轮都会把之前的文本作为输入重新发送,所以继续的回答比一次大回答花费更多。对于结构化数据,优先选择更大的上限或更小的批次,而不是继续。

一个长回答要花多少钱#

假设一个长回答是 2,000 tokens 输入、8,000 tokens 输出。在 DeepSeek-V4-Flash 上,按今天的价格,这个请求花费 $0.00083,而且把输入和输出加在一起算,它比 OpenRouter 更便宜。把数字换成你自己的负载:

成本计算器

一天 50 个长回答

每次请求的输入 token 数: 2,000 · 每次请求的输出 token 数: 8,000 · 每日请求数: 50

模型每天每月
GLM-5.3-Flash$0.018$0.54
DeepSeek-V4-Flash$0.0415$1.25
MiniMax-M2.7$0.065$1.95

结论:在这个场景中输出侧占主导,所以 max_tokens 的余量是防止重跑的廉价保险。一个被截断、需要重新生成的回答要付两次钱。关于账单两侧的背景知识见输入 vs 输出 tokens,带价格的完整模型列表在定价页面。

上线之前:一份简短清单#

清单

代码的截断检查清单