📌 内容摘要

  • max_tokens 是 Claude API 唯一必填的生成参数——不传直接报错,这和 OpenAI 不同,是迁移时最高频的坑。
  • 设太小导致输出截断、设太大浪费成本,本文给出 8 类任务的推荐区间和选值逻辑。
  • 如何判断输出是被截断还是自然结束?stop_reason 字段是唯一可靠的判断依据。
  • 附动态 max_tokens 计算策略:根据输入长度自动调整,在上下文窗口限制内最大化输出空间。

一、max_tokens 是什么,为什么 Claude 必须传它?

max_tokens 控制 Claude 在这次请求中最多生成多少个 token。它是一个”安全上限”而不是”期望长度”——设成 4096 不代表 Claude 一定会生成 4096 个 token,它会在认为回答完整时自然停止;但如果正常回答需要 5000 token 而你只设了 4096,就会在中途被硬性截断。

Claude API 把 max_tokens 设计为必填参数,没有默认值。这和 OpenAI 的 API 不同——OpenAI 不传 max_tokens 会用一个内部默认值。从 OpenAI 迁移到 Claude 时,漏掉这个参数是最常见的报错原因之一:

# 漏掉 max_tokens 的报错
anthropic.BadRequestError: Error code: 400
{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "max_tokens: field required"
  }
}

二、Token 基础:一个 token 等于多少字?

在设 max_tokens 之前,先理解 token 的实际大小——不同语言差异很大:

内容类型 Token / 字符比例 1000 token 约等于
英文文本 约 1 token / 4 字符 约 750 个英文单词
中文文本 约 1.5–2 token / 汉字 约 500–650 个汉字
Python 代码 约 1 token / 3–4 字符 约 40–60 行代码
JSON / 结构化数据 约 1 token / 3 字符 取决于嵌套深度和键名长度

一个实用的心智模型:中文输出每 1000 字约需 1500–2000 token,英文输出每 1000 词约需 1300 token。

三、stop_reason:判断输出是否被截断的唯一方法

很多开发者设完 max_tokens 就不管了,其实应该每次都检查 stop_reason 字段:

stop_reason 值 含义 处理建议
end_turn Claude 自然结束,输出完整 正常,直接使用
max_tokens 达到 max_tokens 上限,输出被硬截断 ⚠️ 需要处理截断!
stop_sequence 命中了自定义停止序列 符合预期,检查停止序列是否正确
tool_use Claude 需要调用工具 执行工具后继续对话
import anthropic

client = anthropic.Anthropic()

def safe_complete(
    messages:    list[dict],
    system:      str   = "",
    max_tokens:  int   = 1024,
    model:       str   = "claude-sonnet-4-6",
) -> dict:
    """
    带截断检测的完整调用封装
    返回包含内容、停止原因和是否截断的字典
    """
    response = client.messages.create(
        model      = model,
        max_tokens = max_tokens,
        system     = system,
        messages   = messages,
    )

    content     = response.content[0].text
    stop_reason = response.stop_reason
    truncated   = stop_reason == "max_tokens"

    if truncated:
        # 生产环境这里应该记录日志、告警或触发续写逻辑
        print(f"⚠️  输出被截断!已生成 {response.usage.output_tokens} tokens,"
              f"请考虑增大 max_tokens(当前设置:{max_tokens})")

    return {
        "content":         content,
        "stop_reason":     stop_reason,
        "truncated":       truncated,
        "input_tokens":    response.usage.input_tokens,
        "output_tokens":   response.usage.output_tokens,
    }


# 使用示例
result = safe_complete(
    messages   = [{"role": "user", "content": "写一篇2000字的文章"}],
    max_tokens = 500,    # 故意设太小,演示截断
)

if result["truncated"]:
    print("输出不完整,建议增大 max_tokens 或启用续写")
print(f"内容:{result['content'][:100]}...")
print(f"停止原因:{result['stop_reason']}")

四、截断后的续写策略

stop_reason == "max_tokens" 时,输出被强制截断了。处理方式取决于业务场景:

async def complete_with_continuation(
    messages:        list[dict],
    system:          str  = "",
    max_tokens:      int  = 2048,
    max_continuations: int = 3,
) -> str:
    """
    自动续写:输出截断时自动继续生成
    最多续写 max_continuations 次

    适用场景:
    - 长文章生成
    - 代码文件生成
    - 报告撰写
    """
    full_content = []
    current_msgs = list(messages)

    for attempt in range(max_continuations + 1):
        response = client.messages.create(
            model      = "claude-sonnet-4-6",
            max_tokens = max_tokens,
            system     = system,
            messages   = current_msgs,
        )

        chunk = response.content[0].text
        full_content.append(chunk)

        if response.stop_reason != "max_tokens":
            # 自然结束,不需要继续
            break

        if attempt < max_continuations:
            # 把已生成内容加入对话历史,让 Claude 继续
            current_msgs.append({"role": "assistant", "content": chunk})
            current_msgs.append({
                "role": "user",
                "content": "请继续,从你停下的地方接着写,不要重复已有内容。"
            })
        else:
            print(f"已达到最大续写次数({max_continuations}),停止")

    return "".join(full_content)


# 测试续写
content = await complete_with_continuation(
    messages=[{"role": "user", "content": "写一篇详细的Python异步编程教程"}],
    max_tokens=1024,      # 每次最多生成 1024 token
    max_continuations=3,  # 最多续写 3 次,共约 4096 token
)
print(f"总字数:{len(content)}")

五、上下文窗口与 max_tokens 的关系

max_tokens 不是孤立的参数,它受限于模型的上下文窗口大小:

# 上下文窗口 = 输入 token + 输出 token
# max_tokens 不能超过:上下文窗口 - 实际输入 token 数

MODEL_CONTEXT_WINDOWS = {
    "claude-haiku-4-5-20251001": 200_000,
    "claude-sonnet-4-6":         1_000_000,
    "claude-opus-4-6":           1_000_000,
}

def calculate_safe_max_tokens(
    messages:     list[dict],
    system:       str  = "",
    model:        str  = "claude-sonnet-4-6",
    safety_buffer:int  = 100,    # 安全边距,防止边界计算偏差
) -> int:
    """
    根据输入内容自动计算安全的 max_tokens 上限

    避免因为 input_tokens + max_tokens > context_window 导致的报错
    """
    # 精确计算输入 token 数(使用 count_tokens API)
    count_response = client.messages.count_tokens(
        model    = model,
        system   = system,
        messages = messages,
    )
    input_tokens = count_response.input_tokens

    context_window  = MODEL_CONTEXT_WINDOWS.get(model, 200_000)
    available       = context_window - input_tokens - safety_buffer

    if available <= 0:
        raise ValueError(
            f"输入已经用了 {input_tokens} tokens,"
            f"超过上下文窗口 {context_window},无法生成输出"
        )

    return available


# 动态计算示例
long_messages = [
    {"role": "user", "content": "非常长的文档内容..." * 1000}
]

safe_max = calculate_safe_max_tokens(long_messages, model="claude-sonnet-4-6")
print(f"安全的 max_tokens 上限:{safe_max:,}")

response = client.messages.create(
    model      = "claude-sonnet-4-6",
    max_tokens = min(safe_max, 4096),   # 取安全上限和业务需求的较小值
    messages   = long_messages,
)
⚠️ 会导致报错的典型情况
当你传入一个非常长的文档(比如10万token),再设 max_tokens=100000,两者相加可能超过模型的上下文窗口,Claude 会返回 400 错误。用 count_tokens API 提前计算、动态设置 max_tokens 是最稳健的方案。

六、8类任务的推荐 max_tokens 值

任务类型 推荐值 选值逻辑
分类 / 情感分析 16 – 64 输出只是一个词或短句,64 已经绰绰有余
结构化提取(JSON) 256 – 1024 取决于 schema 的字段数量和值的长度
简短问答 / FAQ 256 – 512 一般问答不超过300字,512 有足够余量
对话聊天机器人 512 – 1024 每轮回复不宜过长,防止输出冗长
文档摘要 512 – 2048 摘要通常是原文的 10–20%,按需调整
代码生成(函数级) 1024 – 4096 一个功能完整的函数通常在 50–200 行
长文章 / 报告撰写 4096 – 8192 超过 8192 建议启用续写机制分段生成
复杂推理 / 分析报告 4096 – 16384 思维链推理过程长,需要较大空间展开

七、成本视角:max_tokens 设太大会多花钱吗?

这是一个常见误解,需要澄清:

"""
成本计算的关键:你只为实际生成的 token 付费
不是为 max_tokens 付费

例子:
- 设 max_tokens=8192,Claude 自然回答了 500 tokens
- 实际计费:500 output tokens(不是 8192)

所以从成本角度,把 max_tokens 设大一点是安全的。

但有两个间接影响需要注意:
1. max_tokens 太大但输出真的很长 → 成本确实增加(这是因为内容多,不是参数大)
2. max_tokens 影响 Claude 的"生成倾向":
   - 设得很小(如64):Claude 会更倾向于简洁、提前结束
   - 设得很大(如8192):Claude 更倾向于详细展开
   这不是成本问题,但会影响输出长度和质量
"""

# 验证:max_tokens 不影响实际计费(只有 output_tokens 计费)
response = client.messages.create(
    model      = "claude-sonnet-4-6",
    max_tokens = 8192,     # 设得很大
    messages   = [{"role": "user", "content": "用一句话说你好"}],
)

print(f"max_tokens 设置:8192")
print(f"实际生成:{response.usage.output_tokens} tokens")
print(f"内容:{response.content[0].text}")
# 输出类似:实际生成:6 tokens,内容:你好!

八、常见报错与解决方案

报错一:max_tokens 超过模型限制

# 错误信息
anthropic.BadRequestError: max_tokens: must be less than or equal to 200000

# 原因:Haiku 4.5 的上下文窗口是 20万 token,不能设超过
# 解决:检查模型对应的上下文窗口上限
MAX_OUTPUT_LIMITS = {
    "claude-haiku-4-5-20251001": 8_192,     # Haiku 的最大单次输出
    "claude-sonnet-4-6":         64_000,    # Sonnet 的最大单次输出
    "claude-opus-4-6":           32_000,    # Opus 的最大单次输出
}
# 注意:上下文窗口(输入+输出总量)和单次最大输出是两个不同的限制

报错二:input + max_tokens 超过上下文窗口

# 错误信息
anthropic.BadRequestError: prompt is too long: 950000 tokens > 900000 maximum

# 原因:输入 token 太多,加上 max_tokens 超过上下文窗口
# 解决:用 count_tokens 检查,或者截短输入,或者换更大窗口的模型
count = client.messages.count_tokens(
    model    = "claude-sonnet-4-6",
    messages = your_messages,
)
print(f"输入已用:{count.input_tokens:,} tokens")

报错三:从 OpenAI 迁移漏掉 max_tokens

# 报错
anthropic.BadRequestError: max_tokens: field required

# OpenAI 代码(有默认值,不传没问题):
openai.chat.completions.create(model="gpt-4", messages=[...])  # 不传 max_tokens 也行

# Claude 代码(必须传):
client.messages.create(
    model      = "claude-sonnet-4-6",
    max_tokens = 1024,    # 必须!
    messages   = [...],
)

# 迁移检查清单:grep -r "messages.create" . | grep -v "max_tokens"

九、实用工具函数集

import anthropic
from enum import Enum

client = anthropic.Anthropic()

class TaskType(str, Enum):
    CLASSIFY    = "classify"
    EXTRACT     = "extract"
    QA          = "qa"
    CHAT        = "chat"
    SUMMARIZE   = "summarize"
    CODE        = "code"
    ARTICLE     = "article"
    ANALYSIS    = "analysis"

# 各任务类型的推荐 max_tokens
TASK_MAX_TOKENS = {
    TaskType.CLASSIFY:  64,
    TaskType.EXTRACT:   512,
    TaskType.QA:        512,
    TaskType.CHAT:      1024,
    TaskType.SUMMARIZE: 2048,
    TaskType.CODE:      4096,
    TaskType.ARTICLE:   8192,
    TaskType.ANALYSIS:  8192,
}

def recommend_max_tokens(task_type: TaskType, output_lang: str = "zh") -> int:
    """根据任务类型推荐 max_tokens,中文输出适当增大"""
    base = TASK_MAX_TOKENS[task_type]
    # 中文每个字约 1.5–2 token,相同内容需要更多 token
    multiplier = 1.5 if output_lang == "zh" else 1.0
    return int(base * multiplier)


def estimate_output_tokens(expected_chars: int, lang: str = "zh") -> int:
    """根据预期输出字数估算需要的 token 数"""
    if lang == "zh":
        return int(expected_chars * 1.7)   # 中文约 1.7 token/字
    else:
        return int(expected_chars / 4)     # 英文约 4 字符/token


def smart_complete(
    messages:   list[dict],
    task_type:  TaskType,
    system:     str  = "",
    model:      str  = "claude-sonnet-4-6",
    output_lang:str  = "zh",
) -> dict:
    """
    智能完成:自动选择合适的 max_tokens 并检测截断

    Returns:
        {content, stop_reason, truncated, tokens_used, recommended_increase}
    """
    max_tokens = recommend_max_tokens(task_type, output_lang)

    response = client.messages.create(
        model      = model,
        max_tokens = max_tokens,
        system     = system,
        messages   = messages,
    )

    truncated = response.stop_reason == "max_tokens"
    result = {
        "content":    response.content[0].text,
        "stop_reason":response.stop_reason,
        "truncated":  truncated,
        "tokens_used":{
            "input":  response.usage.input_tokens,
            "output": response.usage.output_tokens,
        },
        "max_tokens_used": max_tokens,
    }

    if truncated:
        # 建议增大到实际用量的 1.5 倍
        result["recommended_increase"] = int(response.usage.output_tokens * 1.5)

    return result


# 使用示例
# 分类任务
result = smart_complete(
    messages  = [{"role": "user", "content": "判断这条评论的情感:'商品不错但发货慢'"}],
    task_type = TaskType.CLASSIFY,
)
print(f"分类结果:{result['content']}(max_tokens={result['max_tokens_used']})")

# 文章生成
result = smart_complete(
    messages  = [{"role": "user", "content": "写一篇关于AI发展的文章"}],
    task_type = TaskType.ARTICLE,
)
if result["truncated"]:
    print(f"输出被截断,建议将 max_tokens 增加到 {result['recommended_increase']}")

十、批量请求中的 max_tokens 优化

"""
批量处理中,不同任务有不同的输出需求
用统一的最大值会导致成本浪费(虽然不直接影响计费,
但会影响 Claude 的输出倾向,可能生成过长的回复)

更好的做法:按任务类型分组,每组用对应的 max_tokens
"""
from anthropic import Anthropic
import anthropic

# Batch API 示例(打5折)
def batch_with_smart_max_tokens(tasks: list[dict]) -> list:
    """
    批量处理,每个任务独立设置 max_tokens

    tasks 格式:
    [
        {"messages": [...], "task_type": "classify"},
        {"messages": [...], "task_type": "summarize"},
        ...
    ]
    """
    requests = []
    for i, task in enumerate(tasks):
        task_type  = TaskType(task.get("task_type", "qa"))
        max_tokens = recommend_max_tokens(task_type)
        requests.append(
            anthropic.types.MessageCreateParamsNonStreaming(
                model      = "claude-haiku-4-5-20251001",   # 批量用 Haiku 省成本
                max_tokens = max_tokens,
                messages   = task["messages"],
            )
        )

    # 发起批量请求
    batch = client.messages.batches.create(requests=requests)
    print(f"批量任务已提交:{batch.id}")
    return batch


# 统计批量结果中的截断情况
def analyze_batch_results(batch_id: str):
    truncated_count = 0
    total_count     = 0

    for result in client.messages.batches.results(batch_id):
        total_count += 1
        if result.result.type == "succeeded":
            msg = result.result.message
            if msg.stop_reason == "max_tokens":
                truncated_count += 1
                print(f"任务 {result.custom_id} 被截断,已用 {msg.usage.output_tokens} tokens")

    print(f"\n截断率:{truncated_count}/{total_count} = {truncated_count/total_count:.1%}")

常见问题

Q:设 max_tokens=1 有意义吗?
有意义,但用途很窄——只在你想测试模型能否启动、或者只需要一个字符的输出时有用。设成 1 时 Claude 会只输出第一个 token 就停止,对大多数业务场景没有用,但在调试连通性时很方便。

Q:为什么我设了 max_tokens=4096,但 Claude 经常在 1000 token 左右就停了?
这是正常的——Claude 认为回答已经完整时会自然结束(stop_reason="end_turn"),不会为了”用满” max_tokens 而强行拖长回复。如果你需要更详细的输出,应该在 Prompt 里要求,比如”请详细说明,不少于 2000 字”,而不是依赖调大 max_tokens 来增加输出长度。

Q:流式输出时怎么知道有没有被截断?
流式输出的 stop_reason 在流结束后才能获取。用 Python SDK 的 stream.get_final_message(),或 Node.js 的 stream.finalMessage(),流结束后检查 final_message.stop_reason 即可。本文流式输出章节的代码已经演示了这个模式。

总结

max_tokens 的正确使用可以概括为四条原则:一、必须传,不传直接报错(Claude 的必填参数,不同于 OpenAI);二、按任务设,分类用64、聊天用1024、长文用8192,不要无脑用一个大值;三、检查截断,每次调用后看 stop_reason,max_tokens 说明输出不完整需要处理;四、用动态计算,长输入时先 count_tokens,再计算安全上限,避免超出上下文窗口的报错。把 safe_completesmart_complete 这两个工具函数加入你的工具库,可以覆盖绝大多数场景的 max_tokens 管理需求。