第 5 章

七语言 SDK 完全指南:Python / TypeScript / Java / Go / C# / Ruby / PHP

第五章:理解 Token:计费、窗口大小与长文本策略

5.1 Token 是什么:从字符到语义单元

Token 是大型语言模型处理文本的基本单位,既不是字符,也不是单词,而是介于两者之间的语义片段。理解 token 的概念对于控制成本、优化 prompt 设计、处理长文本都至关重要。

Token 的基本原理

Claude 使用类似 BPE(Byte Pair Encoding)的分词器。基本规律:

英文:
  "the"      → 1 token
  "running"  → 1 token
  "unbelievable" → 3 tokens (un + believ + able)
  " Hello"   → 1 token(注意前面的空格是 token 的一部分)

中文:
  "你好" → 2 tokens(每个汉字约 1-2 tokens)
  "人工智能" → 约 4-6 tokens
  "量子纠缠的基本原理" → 约 8-12 tokens

代码:
  "def" → 1 token
  "class MyClass:" → 约 5 tokens
  "{" → 1 token

实用经验值

用 API 测量实际 Token 数

不要猜测,直接测量:

import anthropic

client = anthropic.Anthropic()

def count_tokens(text: str, model: str = "claude-sonnet-4-6") -> int:
    """
    使用 API 精确计算文本的 token 数
    注意:此调用本身也会消耗少量 token
    """
    response = client.messages.count_tokens(
        model=model,
        messages=[{"role": "user", "content": text}]
    )
    return response.input_tokens

# 示例
texts = [
    "Hello, world!",
    "你好,世界!",
    "def fibonacci(n): return n if n <= 1 else fibonacci(n-1) + fibonacci(n-2)",
    "The quick brown fox jumps over the lazy dog"
]

for text in texts:
    tokens = count_tokens(text)
    chars = len(text)
    ratio = chars / tokens
    print(f"'{text[:40]}...' → {tokens} tokens ({chars} chars, {ratio:.1f} chars/token)")

用 tiktoken 估算 Token(离线方法)

对于不需要精确计数的场景,可以用 OpenAI 的 tiktoken 库(与 Claude 的分词器非常接近)进行快速估算:

# pip install tiktoken
import tiktoken

def estimate_tokens(text: str) -> int:
    """
    快速估算 token 数(误差约 ±10%)
    使用 cl100k_base 编码(GPT-4 使用的,与 Claude 接近)
    """
    enc = tiktoken.get_encoding("cl100k_base")
    return len(enc.encode(text))

5.2 计费模型详解

输入 vs 输出 Token

Claude 的计费分为输入 token输出 token两部分,输出价格通常是输入的 5 倍:

claude-sonnet-4-6:
  输入:$3 / 百万 tokens
  输出:$15 / 百万 tokens

什么算输入 token:
  - system prompt
  - 所有 user 和 assistant 历史消息
  - 当前用户消息
  - 工具定义(tool use)
  - 图片(按分辨率换算)

什么算输出 token:
  - 模型生成的文本
  - 工具调用参数
  - 扩展思考内容(thinking blocks)

图片的 Token 计费

图片按分辨率换算为 token:

def estimate_image_tokens(width: int, height: int) -> int:
    """
    估算图片消耗的输入 token 数
    Claude 将图片分割为 512x512 的 tile
    """
    import math
    
    # 最大边长限制为 1568px(Claude 的默认限制)
    max_size = 1568
    if width > max_size or height > max_size:
        scale = max_size / max(width, height)
        width = int(width * scale)
        height = int(height * scale)
    
    # 计算 tile 数量
    tiles_x = math.ceil(width / 512)
    tiles_y = math.ceil(height / 512)
    num_tiles = tiles_x * tiles_y
    
    # 每个 tile 约 1600 tokens
    return num_tiles * 1600 + 85  # +85 是基础开销

# 示例
print(estimate_image_tokens(800, 600))   # → 约 4885 tokens
print(estimate_image_tokens(1920, 1080)) # → 约 9685 tokens

缓存(Prompt Caching)的成本优化

Anthropic 提供 Prompt Cache 功能。对于重复使用相同 system prompt 或参考文档的请求,被缓存的 token 只需支付 10% 的正常输入价格(但有 5 分钟的缓存窗口)。

import anthropic

client = anthropic.Anthropic()

# 使用 cache_control 标记需要缓存的内容块
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": "你是一个代码助手..." + long_documentation,  # 2000+ tokens
            "cache_control": {"type": "ephemeral"}  # 标记为可缓存
        }
    ],
    messages=[{"role": "user", "content": "帮我解释 async/await"}]
)

# 第一次请求:正常价格(写入缓存)
# 后续 5 分钟内的请求:被缓存的部分只收 10% 价格
print(response.usage)
# Usage(
#   input_tokens=45,
#   output_tokens=312,
#   cache_creation_input_tokens=2058,   # 首次写入缓存
#   cache_read_input_tokens=0
# )

缓存适用场景:

5.3 上下文窗口:200K 的能力与限制

200K Token 的实际含义

Claude 的 200K token 上下文窗口在实践中意味着:

能放入 200K token 的内容:
  - 约 150,000 个英文单词(一本中等长度小说)
  - 约 100,000 个汉字
  - 约 10,000 行代码(取决于代码密度)
  - 约 400-500 页的 PDF 文档
  - 约 150 张普通分辨率图片

但是,能放入并不等于能有效处理。200K 是技术上限,不是质量保证

Lost in the Middle 现象的量化影响

研究表明,Claude 对超长上下文中间段的信息处理准确率低于首尾:

在不同上下文长度下,中间位置信息的检索准确率(近似值):

上下文长度    中间位置准确率
5K tokens     ~95%
20K tokens    ~90%
50K tokens    ~85%
100K tokens   ~80%
200K tokens   ~70%

对于需要精确检索的任务(如"合同第23条款的具体内容是什么"),这个准确率下降不容忽视。

有效窗口 vs 技术窗口

任务类型 建议的有效窗口
单文档摘要 可用全部 200K
多文档问答 建议 < 100K
精确信息检索 建议 < 50K,或使用 RAG
代码分析 < 50K,或分块处理
多轮对话历史 保持 < 20K(定期压缩)

5.4 长文本处理策略

策略一:分块处理(Chunking)

将长文档分割为多个块,分别处理后合并结果:

import anthropic
from typing import Generator

client = anthropic.Anthropic()

def chunk_text(text: str, chunk_size: int = 50000, overlap: int = 500) -> list[str]:
    """
    将长文本分割为有重叠的块
    overlap 确保边界处的语义连续性
    """
    chunks = []
    start = 0
    
    while start < len(text):
        end = start + chunk_size
        
        if end < len(text):
            # 在句子边界处分割(避免截断句子中间)
            boundary = text.rfind('。', start, end)
            if boundary == -1:
                boundary = text.rfind('\n', start, end)
            if boundary == -1:
                boundary = end
            else:
                boundary += 1  # 包含句号
        else:
            boundary = len(text)
        
        chunks.append(text[start:boundary])
        start = boundary - overlap  # 重叠部分
    
    return chunks


def summarize_long_document(
    document: str,
    question: str = None
) -> str:
    """
    处理超长文档的两步策略:
    1. 分块摘要
    2. 合并摘要
    """
    chunks = chunk_text(document)
    
    # 第一步:逐块摘要
    chunk_summaries = []
    for i, chunk in enumerate(chunks):
        prompt = f"摘要以下文档片段(第 {i+1}/{len(chunks)} 部分)"
        if question:
            prompt += f",重点关注与以下问题相关的内容:{question}"
        prompt += f"\n\n{chunk}"
        
        response = client.messages.create(
            model="claude-haiku-4-5-20251001",  # 用 Haiku 做初步摘要,降低成本
            max_tokens=500,
            messages=[{"role": "user", "content": prompt}]
        )
        chunk_summaries.append(response.content[0].text)
    
    # 第二步:合并摘要(用 Sonnet 做最终整合)
    combined_text = "\n\n---\n\n".join(
        f"[第 {i+1} 部分]\n{s}" 
        for i, s in enumerate(chunk_summaries)
    )
    
    final_prompt = f"以下是一份长文档的各部分摘要。请整合成一份连贯的综合摘要"
    if question:
        final_prompt += f",并回答问题:{question}"
    final_prompt += f"\n\n{combined_text}"
    
    final_response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1500,
        messages=[{"role": "user", "content": final_prompt}]
    )
    
    return final_response.content[0].text

策略二:RAG(检索增强生成)

对于需要精确信息检索的场景,RAG 比直接塞入超长上下文更有效:

# 简化的 RAG 实现示意
import anthropic
import numpy as np
from dataclasses import dataclass

@dataclass
class Document:
    content: str
    metadata: dict

class SimpleRAG:
    """
    简化的 RAG 系统(实际生产应使用专业向量数据库如 pgvector、Pinecone)
    """
    
    def __init__(self, chunk_size: int = 1000):
        self.client = anthropic.Anthropic()
        self.chunks: list[Document] = []
        self.embeddings: list = []
        self.chunk_size = chunk_size
    
    def add_document(self, text: str, metadata: dict = None):
        """将文档分块并存储"""
        chunks = self._split(text)
        for chunk in chunks:
            self.chunks.append(Document(content=chunk, metadata=metadata or {}))
    
    def _split(self, text: str) -> list[str]:
        """简单的按字符分割"""
        return [
            text[i:i+self.chunk_size] 
            for i in range(0, len(text), self.chunk_size - 100)  # 100 字符重叠
        ]
    
    def _simple_score(self, query: str, doc: Document) -> float:
        """
        简化的相关性评分(实际应用中应使用向量嵌入)
        这里用关键词重叠作为粗略代理
        """
        query_words = set(query.lower().split())
        doc_words = set(doc.content.lower().split())
        if not query_words:
            return 0
        return len(query_words & doc_words) / len(query_words)
    
    def query(self, question: str, top_k: int = 3) -> str:
        """检索最相关的文档块并生成答案"""
        # 检索
        scored = sorted(
            self.chunks,
            key=lambda d: self._simple_score(question, d),
            reverse=True
        )
        relevant = scored[:top_k]
        
        # 构建上下文
        context = "\n\n---\n\n".join(
            f"[参考 {i+1}]\n{doc.content}"
            for i, doc in enumerate(relevant)
        )
        
        # 生成答案
        prompt = f"""根据以下参考资料回答问题。
如果参考资料中没有足够信息,明确说明。

<references>
{context}
</references>

问题:{question}"""
        
        response = self.client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            messages=[{"role": "user", "content": prompt}]
        )
        
        return response.content[0].text

策略三:滑动窗口(适用于连续处理)

对于需要连续处理的超长内容(如逐步分析一本书),使用滑动窗口保持局部上下文:

def sliding_window_analysis(
    text: str, 
    task: str,
    window_size: int = 30000,
    step_size: int = 20000
) -> list[str]:
    """
    用滑动窗口处理超长文本
    window_size: 每次处理的 token 窗口大小
    step_size: 滑动步长(step < window 保证重叠)
    
    注意:这里用字符近似 token,实际应使用 token 计数
    """
    results = []
    position = 0
    total_len = len(text)
    
    while position < total_len:
        window_end = min(position + window_size * 4, total_len)  # 4 chars ≈ 1 token
        window_text = text[position:window_end]
        
        is_first = (position == 0)
        is_last = (window_end == total_len)
        
        context_note = ""
        if not is_first:
            context_note = "(注意:这是文档的中间部分,前面已经处理过)"
        
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            messages=[{
                "role": "user",
                "content": f"""{task}{context_note}

文本片段:
{window_text}"""
            }]
        )
        
        results.append(response.content[0].text)
        
        if is_last:
            break
        
        position += step_size * 4
    
    return results

策略四:层级摘要(Map-Reduce 模式)

def hierarchical_summarize(document: str, target_tokens: int = 2000) -> str:
    """
    层级摘要:
    Level 1: 分块 → 每块摘要(~500 tokens)
    Level 2: 合并摘要 → 最终摘要(target_tokens)
    
    对于极长文档,可以增加更多层级
    """
    chars_per_token = 4  # 英文近似值;中文约 2
    chunk_char_size = 20000  # 约 5000 tokens
    
    # Level 1: map
    chunks = [
        document[i:i+chunk_char_size] 
        for i in range(0, len(document), chunk_char_size)
    ]
    
    l1_summaries = []
    for chunk in chunks:
        resp = client.messages.create(
            model="claude-haiku-4-5-20251001",  # 便宜的模型做 map 阶段
            max_tokens=500,
            messages=[{
                "role": "user",
                "content": f"用 3-5 句话概括以下内容的要点:\n\n{chunk}"
            }]
        )
        l1_summaries.append(resp.content[0].text)
    
    # Level 2: reduce
    combined = "\n\n".join(f"• {s}" for s in l1_summaries)
    
    resp = client.messages.create(
        model="claude-sonnet-4-6",  # 质量更好的模型做 reduce 阶段
        max_tokens=target_tokens,
        messages=[{
            "role": "user",
            "content": f"""以下是一份长文档各部分的摘要。
请整合成一份全面、连贯的综合摘要:

{combined}"""
        }]
    )
    
    return resp.content[0].text

5.5 Token 成本优化技巧

优化一:压缩 System Prompt

避免冗余表达:

❌ 冗长版(约 80 tokens):
"你是一个非常有帮助的、友善的、专业的 AI 助手,你的工作是帮助用户解决各种各样的问题,
你总是提供准确、详细、有用的信息,并以礼貌和尊重的方式与用户交流。"

✅ 精简版(约 20 tokens):
"你是专业的技术助手。提供准确、简洁的答案。"

优化二:减少重复的历史消息

长对话中,早期消息的完整保留会线性增加每次请求的 token 消耗:

def compress_history(
    messages: list[dict], 
    max_history_tokens: int = 8000
) -> list[dict]:
    """
    当历史消息超过限制时,压缩早期对话
    保留最近的 N 条,将更早的摘要化
    """
    # 估算当前历史大小(简化:字符数 / 4)
    history_size = sum(len(m["content"]) for m in messages) // 4
    
    if history_size <= max_history_tokens:
        return messages
    
    # 保留最后 6 条消息(3 轮对话)
    recent = messages[-6:]
    old = messages[:-6]
    
    if not old:
        return recent
    
    # 摘要化早期对话
    old_text = "\n".join(
        f"{'用户' if m['role'] == 'user' else 'Claude'}: {m['content'][:200]}..."
        for m in old
    )
    
    summary_response = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=300,
        messages=[{
            "role": "user",
            "content": f"用 3-4 句话概括以下对话的要点:\n\n{old_text}"
        }]
    )
    
    summary = summary_response.content[0].text
    
    # 将摘要作为上下文插入
    return [
        {
            "role": "user",
            "content": f"[之前对话摘要:{summary}]\n\n请继续我们的对话。"
        },
        {
            "role": "assistant",
            "content": "好的,我了解之前的对话背景,请继续。"
        },
        *recent
    ]

优化三:输出 Token 控制

输出 token 比输入贵 5 倍,精确控制输出长度很重要:

def estimate_output_tokens(task_type: str, content_length: int) -> int:
    """
    按任务类型估算合适的 max_tokens 设置
    避免设置过高的值(即使未用完,也不收费,但设太高会影响流式体验)
    """
    estimates = {
        "classification": 20,      # 分类任务:极短输出
        "extraction_json": content_length // 3,  # JSON 提取:约输入的 1/3
        "summary_short": 200,      # 短摘要
        "summary_long": 800,       # 长摘要
        "code_review": 600,        # 代码审查
        "code_generation": 2000,   # 代码生成
        "explanation": 500,        # 概念解释
        "qa_simple": 150,          # 简单问答
        "qa_detailed": 600,        # 详细问答
    }
    
    return estimates.get(task_type, 1024)  # 默认 1024

小结

Token 的深度理解是控制 API 成本和设计有效系统的基础:

  1. Token 计量:英文约 4 字符/token,中文约 1.5-2 字符/token;使用 count_tokens API 精确测量
  2. 计费模型:输出 token 是输入的 5 倍;Prompt Cache 对重复 system prompt 提供 90% 折扣
  3. 200K 窗口的实际限制:技术上限不等于质量保证;中间段信息检索准确率随长度下降
  4. 长文本策略
    • 分块处理(Chunking):通用,适合摘要任务
    • RAG:适合精确检索场景
    • 滑动窗口:适合连续分析
    • 层级摘要(Map-Reduce):适合超长文档的高质量摘要
  5. 成本优化:精简 system prompt、压缩对话历史、精确设置 max_tokens

下一章将转向响应格式控制:如何让 Claude 可靠地输出结构化 JSON、使用 XML 标签组织内容,以及处理结构化输出的最佳实践。

本章评分
4.9  / 5  (95 评分)

💬 留言讨论