基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

如何在发送请求前精准估算(或计算)组装好的上下文Token数量?如果超限,兜底截断策略是什么?

知识点图片

在调用大语言模型(LLM)API时,精准计算 Token 数量并设计健壮的截断策略是保证系统稳定性、控制成本和避免 400 Bad Request(超出上下文窗口)错误的核心能力。

以下是完整的解决方案,分为精准计算 Token超限兜底截断策略 两部分。


第一部分:如何在发送请求前精准计算 Token 数量?

Token 的计算并不是简单的字符数或单词数统计,不同的模型使用了不同的分词器(Tokenizer)。

1. 精准计算(以 OpenAI / GPT 系列为例)

对于 OpenAI 模型,最精准的方式是使用官方提供的 tiktoken 库。注意:不能仅仅计算文本的 Token,必须加上对话格式(ChatML)带来的额外开销。

python
import tiktoken

def num_tokens_from_messages(messages, model="gpt-4"):
    """返回一组 messages 所需的精确 Token 数"""
    try:
        encoding = tiktoken.encoding_for_model(model)
    except KeyError:
        print("Warning: model not found. Using cl100k_base encoding.")
        encoding = tiktoken.get_encoding("cl100k_base") # GPT-3.5 / GPT-4 默认

    # 不同的模型格式开销不同,GPT-3.5 和 GPT-4 通常适用以下规则
    tokens_per_message = 3 # 每条消息的固定开销:<|im_start|>role\n...<|im_end|>\n
    tokens_per_name = 1    # 如果有 name 字段,额外 +1

    num_tokens = 0
    for message in messages:
        num_tokens += tokens_per_message
        for key, value in message.items():
            num_tokens += len(encoding.encode(value))
            if key == "name":
                num_tokens += tokens_per_name
                
    # 模型生成回复时的固定引导 Token(<|im_start|>assistant\n)
    num_tokens += 3 
    return num_tokens

# 测试
messages = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "你好,今天天气怎么样?"}
]
print(f"Total Tokens: {num_tokens_from_messages(messages)}")

2. 其他模型的计算方式

  • 开源模型 (Llama, Qwen, ChatGLM 等)
    使用 HuggingFace 的 transformers 库加载对应的 tokenizer
    python
    from transformers import AutoTokenizer
    tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen1.5-7B-Chat")
    # apply_chat_template 会自动拼接角色标签,计算出最真实的 Token 数
    tokens = tokenizer.apply_chat_template(messages, add_generation_prompt=True)
    print(len(tokens))
  • Anthropic (Claude)
    使用官方的 SDK(如 TypeScript 的 @anthropic-ai/tokenizer)进行离线计算。

3. 粗略估算法(前端或轻量级场景)

如果不方便引入分词器库,可以使用经验公式进行估算(建议计算结果乘以 1.2 作为安全冗余):

  • 纯英文:1 Token ≈ 4 个英文字符 ≈ 0.75 个单词。
  • 纯中文:GPT-4 / Qwen 等现代模型,1 Token ≈ 1 ~ 1.5 个汉字;较老的模型(如 GPT-3)可能 1 个汉字占 2~3 个 Token。

第二部分:超限兜底截断策略 (Fallback Strategies)

如果你计算出组装好的 Context Tokens + Max Expected Output Tokens > Model Window Size,就必须执行截断。

一个成熟的系统通常采用多级降级策略(Graceful Degradation)。以下策略按推荐优先级排序:

策略一:按重要性分级,剔除低优消息(最常用:滑动窗口)

在多轮对话中,最老的聊天记录价值最低。

  1. 绝对保留:System Prompt(系统设定)、最新的 User Query(用户当前问题)。
  2. 截断目标:中间的历史对话(History)。
  3. 做法:从最旧的历史对话开始,一对一对地(User + Assistant)删除,直到总 Token 数降至安全阈值以下。
python
def truncate_chat_history(messages, max_tokens, model="gpt-4"):
    """滑动窗口策略:保留System和最新消息,剔除最老的多轮对话"""
    # 假设 messages[0] 是 system, messages[-1] 是最新的 user prompt
    system_msg = [m for m in messages if m["role"] == "system"]
    current_query = [messages[-1]]
    history = messages[len(system_msg):-1] # 中间的历史记录
    
    while num_tokens_from_messages(system_msg + history + current_query, model) > max_tokens:
        if len(history) >= 2:
            # 每次删掉最老的一轮对话(一问一答,共两条)
            history = history[2:] 
        elif len(history) == 1:
            history = []
        else:
            break # 历史已经被清空了
            
    return system_msg + history + current_query

策略二:RAG(检索增强)场景的 Chunk 截断

如果上下文是由向量数据库检索出的文档块(Chunks)组装的:

  1. 按向量相似度(Relevance Score)对 chunks 排序。
  2. 从评分最低的 chunk 开始剔除,直到满足 Token 限制。

策略三:历史记录动态摘要(Summarization)

这是一种高级策略,不直接丢弃老消息,而是将其压缩。

  • 触发条件:发现历史消息超出阈值。
  • 动作:异步或同步调用一个轻量级模型(如 GPT-3.5/Qwen-Turbo),将老的 10 轮对话总结为一段 200 Tokens 的摘要文本。
  • 替换:将老的 [{"role":"user"}, {"role":"assistant"}...] 替换为 {"role": "system", "content": "Previous Context: [摘要]"}

策略四:终极兜底 —— 强制 Token 级安全截断 (Hard Cut)

如果剔除了所有历史记录和次要文档,单条用户的输入或核心 System Prompt 依然超限(例如用户粘贴了一本几万字的小说),就必须进行强制截断。

⚠️ 危险做法:直接 text[:2000] 切割字符串。在多字节字符(如中文、Emoji)或 JSON 结构中,这样切极易切碎字符,导致模型乱码或报错。

✅ 正确做法:使用 Tokenizer 编码成数组 -> 截取数组 -> 解码回文本。

python
def safe_hard_truncate(text, max_tokens, model="gpt-4"):
    """Token 级别的安全强制截断"""
    encoding = tiktoken.encoding_for_model(model)
    tokens = encoding.encode(text)
    
    if len(tokens) <= max_tokens:
        return text
        
    # 截取允许的最大 Token,然后安全解码回字符串
    truncated_tokens = tokens[:max_tokens]
    return encoding.decode(truncated_tokens)

# 使用场景:
# current_query["content"] = safe_hard_truncate(current_query["content"], 4000)

💡 工程实践的最佳建议 (Best Practices)

  1. 预留输出空间 (Buffer)
    模型窗口 = 输入 (Context) + 输出 (Generation)。如果你使用的模型窗口是 8192,不要把输入塞到 8000。建议预留至少 1000 - 2000 Tokens 给模型的回答
    公式:Max Input Tokens = Model_Context_Window - Max_Tokens_Param - Buffer(约200)
  2. 避免破坏 JSON/Markdown 结构
    如果强行截断了一段代码块或 JSON,模型可能会补全错误的格式。如果截断涉及结构化数据,应当按结构节点(如 JSON Array 的元素)进行剔除,而不是按字符切断。
  3. 多模态图片的 Token 计算
    GPT-4-Vision 等多模态模型中,图片的 Token 取决于分辨率。OpenAI 规则大约是:低分辨率固定 85 Tokens;高分辨率计算瓦片(Tiles),每 512x512 算 170 Tokens 再加上 85 基础值。在处理图文混排时需格外注意累加此项。
00:00
00:00