更新于

LLM 输出截断省钱实战:max_tokens 精细化控制指南


在 LLM API 的账单构成中,输出 token 的单价通常是输入 token 的 3–5 倍。以 Claude Sonnet 为例,输入约 $3/M tokens,输出则约 $15/M tokens。这意味着控制输出长度是成本优化中回报率最高的手段之一,而 max_tokens 参数是最直接的抓手。

然而,许多开发者要么把 max_tokens 设得过高(“万一不够用呢”),要么完全不设置(用模型默认值),导致大量”无效输出”白白消耗预算。本文通过真实场景测试,给出针对不同任务类型的精细化截断策略。


一、输出 Token 超量的典型场景

在真实项目中,以下几类场景最容易造成输出浪费:

1. Prompt 写得不够收敛

❌ "介绍一下量子计算"(模型会输出 1000+ token 的长篇大论)
✅ "用 3 句话介绍量子计算的基本原理,面向非技术读者"

2. 不必要的 CoT(Chain of Thought)输出

某些任务只需要最终结果,但模型习惯性地输出推理过程:

❌ 模型输出:"首先,我需要分析这道题……其次……因此答案是 42。"(共 300 token)
✅ 期望输出:"42"(1 token)

3. 格式”臃肿”

❌ 模型输出完整 JSON 后又加了 "以上就是结果,如需调整请告诉我……"(浪费 50+ token)
✅ 通过 system prompt 约束:"只输出 JSON,不要附加任何说明文字"

4. max_tokens 设置远超实际需求

如果某个接口 99% 的响应都在 200 token 以内,却把 max_tokens 设成 4096,虽然不直接多收钱(按实际 token 数计费),但意味着 Prompt 设计本身可能存在问题,也会影响后续的批量处理策略。


二、如何科学设置 max_tokens

步骤一:采样分析历史输出长度

用以下脚本分析你的历史 API 响应,找到实际输出长度的分布:

import json
from collections import Counter

# 假设你已经把历史响应存入 JSONL 文件
output_lengths = []
with open("api_responses.jsonl") as f:
    for line in f:
        resp = json.loads(line)
        output_lengths.append(resp["usage"]["completion_tokens"])

output_lengths.sort()
n = len(output_lengths)

print(f"P50(中位数): {output_lengths[n//2]} tokens")
print(f"P90: {output_lengths[int(n*0.9)]} tokens")
print(f"P95: {output_lengths[int(n*0.95)]} tokens")
print(f"P99: {output_lengths[int(n*0.99)]} tokens")
print(f"最大值: {max(output_lengths)} tokens")

典型场景的参考数据(基于实际项目,仅供参考):

任务类型P50P90P99建议 max_tokens
情感分类(单词输出)351020
关键词抽取(5–10 个)3060100150
摘要(3–5 句)80150250300
结构化 JSON 提取100300600800
代码补全(函数级)1504008001000
长文分析报告500120020002500

建议:将 max_tokens 设为 P99 的 1.2 倍,既覆盖绝大多数情况,又避免无上限浪费。

步骤二:按任务类型分组设置

不要对所有接口用同一个 max_tokens。将你的 LLM 调用分组:

MAX_TOKENS_CONFIG = {
    "classify": 20,        # 分类任务
    "extract_keywords": 150,  # 关键词提取
    "summarize": 300,       # 摘要
    "json_extract": 800,    # 结构化提取
    "code_gen": 1000,       # 代码生成
    "report": 2500,         # 长文分析
}

def call_llm(task_type: str, messages: list) -> str:
    max_tokens = MAX_TOKENS_CONFIG.get(task_type, 500)
    resp = client.chat.completions.create(
        model="claude-sonnet-4-5",
        messages=messages,
        max_tokens=max_tokens
    )
    return resp.choices[0].message.content

三、Prompt 工程配合截断

max_tokens 是”硬截断”,但更优雅的方式是通过 Prompt 让模型主动输出更短的内容

3.1 明确字数/句数限制

SYSTEM_PROMPTS = {
    "summarize": "用不超过 3 句话总结以下内容。不要附加任何说明。",
    "classify": "判断文本的情感倾向,只输出:正面 / 负面 / 中性,不要输出其他内容。",
    "json_extract": "从文本中提取信息,只输出 JSON,格式:{\"name\": \"\", \"date\": \"\"},不要输出其他内容。"
}

3.2 禁止模型”自我解释”

模型有时会在答案后面加上”如需进一步帮助,请告诉我”之类的套话。可以在 system prompt 中明确禁止:

你是一个 API 处理器,只输出所请求的内容。
- 不要在答案前后加任何前缀或后缀说明
- 不要输出"当然"、"好的"、"以下是"等引导语
- 如果任务是输出 JSON,只输出有效的 JSON,不要包含代码块标记

3.3 Few-shot 示例控制输出格式

messages = [
    {"role": "system", "content": "提取文本中的产品名称和价格,只输出 JSON。"},
    # Few-shot 示例
    {"role": "user", "content": "iPhone 16 现在售价 5999 元"},
    {"role": "assistant", "content": '{"product": "iPhone 16", "price": 5999}'},
    {"role": "user", "content": "MacBook Air M4 促销价 7999 元起"},
    {"role": "assistant", "content": '{"product": "MacBook Air M4", "price": 7999}'},
    # 真实任务
    {"role": "user", "content": user_input}
]

Few-shot 示例的输出通常比 Zero-shot 短 20–40%,且格式更稳定。


四、流式输出 + 主动中断

对于某些场景,你可以启用流式输出,在检测到满足条件时主动停止接收,节省剩余输出费用(注意:部分提供商按完整 completion 计费,非按实际接收量,需确认服务商策略):

import tiktoken

def stream_until_done(client, messages, stop_condition, max_tokens=1000):
    """
    流式接收输出,满足 stop_condition 时提前停止。
    stop_condition: callable,接收当前累积文本,返回 bool
    """
    enc = tiktoken.get_encoding("cl100k_base")
    accumulated = ""
    total_tokens = 0

    with client.chat.completions.create(
        model="claude-sonnet-4-5",
        messages=messages,
        max_tokens=max_tokens,
        stream=True
    ) as stream:
        for chunk in stream:
            delta = chunk.choices[0].delta.content or ""
            accumulated += delta
            total_tokens += len(enc.encode(delta))

            if stop_condition(accumulated):
                break  # 提前中止流

    return accumulated, total_tokens

# 示例:找到完整 JSON 对象后立即停止
import json

def is_complete_json(text):
    try:
        json.loads(text.strip())
        return True
    except json.JSONDecodeError:
        return False

result, tokens_used = stream_until_done(
    client,
    messages=[{"role": "user", "content": "提取:苹果手机售价4999"}],
    stop_condition=is_complete_json
)

五、真实场景节省数据

以下是某内容处理项目在引入输出截断优化前后的对比(估算数据,仅供参考):

优化措施月均输出 token变化月账单变化(估算)
基线(无优化)4,200 万$630
加 max_tokens 分组设置3,100 万-26%$465
加 Prompt 约束2,400 万-43%$360
加 Few-shot 示例1,800 万-57%$270
综合优化后1,700 万-59%$255

关键结论:Prompt 优化的边际收益往往高于 max_tokens 设置本身,两者配合才能达到最优效果。


六、不同模型的 max_tokens 默认值差异

在切换模型时,需要注意各家的默认输出长度设置不同:

模型默认 max_tokens最大 max_tokens
Claude Sonnet / Haiku81928192(可扩展至 64K)
GPT-4o409616384
Gemini 1.5 Pro81928192
混元-Turbo20488192
GLM-4-Plus4096128K

如果你未显式设置 max_tokens,不同模型的默认行为差异较大,可能导致同样的任务产生截然不同的账单。最佳实践:始终显式设置 max_tokens。


七、与批量 API 配合使用

输出截断与批量 API 是两个互补的省钱手段。批量 API 通常提供 40–50% 的折扣,但延迟较高(通常数小时);输出截断则对延迟没有影响,可以同时应用:

详细的批量 API 使用方法,可参考LLM Batch API 真实省钱效果测试


八、相关阅读

想要一个 Key 管理多家模型、统一账单并享受折扣价,可以试试 YoTradeApi,支持 Claude、GPT、混元、豆包等主流模型,人民币充值即用。