七语言 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
实用经验值:
- 英文:约 4 字符 = 1 token,或约 0.75 个单词 = 1 token
- 中文:约 1.5-2 个汉字 = 1 token(受分词策略影响)
- 代码:约 3-4 字符 = 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
# )
缓存适用场景:
- 大型系统 prompt(>1000 tokens)被多个请求共享
- RAG 系统中的参考文档被多轮对话使用
- 多轮对话中的早期历史部分
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 成本和设计有效系统的基础:
- Token 计量:英文约 4 字符/token,中文约 1.5-2 字符/token;使用
count_tokensAPI 精确测量 - 计费模型:输出 token 是输入的 5 倍;Prompt Cache 对重复 system prompt 提供 90% 折扣
- 200K 窗口的实际限制:技术上限不等于质量保证;中间段信息检索准确率随长度下降
- 长文本策略:
- 分块处理(Chunking):通用,适合摘要任务
- RAG:适合精确检索场景
- 滑动窗口:适合连续分析
- 层级摘要(Map-Reduce):适合超长文档的高质量摘要
- 成本优化:精简 system prompt、压缩对话历史、精确设置 max_tokens
下一章将转向响应格式控制:如何让 Claude 可靠地输出结构化 JSON、使用 XML 标签组织内容,以及处理结构化输出的最佳实践。