Haystack Token Counters 完全指南:在调用模型前精确估算对话与工具 Schema 的 Token 占用
【免费下载链接】haystackOpen-source AI orchestration framework for building context-engineered, production-ready LLM applications. Design modular pipelines and agent workflows with explicit control over retrieval, routing, memory, and generation. Built for scalable agents, RAG, multimodal applications, semantic search, and conversational systems.项目地址: https://gitcode.com/GitHub_Trending/ha/haystack
本文是一份围绕 Haystacktoken_counters模块的深度技术指南。该模块提供了TokenCounter协议与三种内置实现(ApproximateTokenCounter、TiktokenCounter、OpenAITokenCounter),用于在把消息发送给大模型之前估算一段对话和工具 Schema 会占用多少 token,从而支撑上下文窗口预算校验、Agent 上下文压缩、token 计费等场景。读完本文,你将掌握每个计数器的定位、全部构造参数与调用方法、内部渲染与计数的实现原理,以及如何自定义属于自己的计数器。
为什么需要 Token 计数器
在生产级 LLM 应用中,很多功能需要在发送请求前知道对话的"体积":
- 判断当前对话是否超出模型的上下文窗口,超限时触发上下文压缩(Haystack 的 agent 上下文压缩机制即可借助计数器决定要丢弃多少历史消息);
- 在Agent 多轮工具调用场景中,估算不断累积的系统提示、工具结果和工具 Schema 对上下文的消耗;
- 为计费或配额管理预留估算。
ChatMessage列表和可选工具 Schema 正是模型输入的主体,token_counters 模块 围绕list[ChatMessage]与tools两个输入统一暴露计数能力。所有计数器都会把消息中的角色、文本、工具调用、工具结果以及可选的工具 Schema 纳入统计,并覆盖消息中的图片、文件等非文本内容。
TokenCounter 协议:所有计数器的统一接口
所有计数器都实现TokenCounter协议(定义见 haystack/token_counters/types/protocol.py),它要求实现三个成员:
count(messages, tools=None) -> int:返回给定消息(及可选工具 Schema)估算占用的 token 数。传入tools会将其 Schema 一并计入,置为None则只测消息本身;to_dict() -> dict[str, Any]:把计数器序列化为字典,保证其设置能够在序列化后保留下来;from_dict(cls, data) -> TokenCounter(类方法):反序列化。默认实现直接调用default_from_dict,把参数字典原样传回构造函数;只有当to_dict()输出需要先重建再传入构造函数的值(例如Secret或嵌套组件)时,才需要重写它。
协议默认的from_dict对普通标量参数(chars_per_token、encoding、tokens_per_image等)已经足够,三个内置计数器因此都没有重写from_dict。
三种内置计数器对比
| 计数器 | 计数方式 | 额外依赖 | 适合场景 |
|---|---|---|---|
ApproximateTokenCounter | 将渲染后的文本长度除以可配置的"每 token 字符数" | 无 | 快速、零依赖的估算 |
TiktokenCounter | 使用 OpenAI 的tiktoken字节对编码器在本地计数 | tiktoken | 针对 OpenAI 模型更准确的估算 |
OpenAITokenCounter | 调用 OpenAI 输入 token 计数 API(POST /v1/responses/input_tokens) | OpenAI API Key | 精确的、模型特定的计数,涵盖图片、文件与工具 |
此外,在anthropic-haystack、google-genai-haystack等官方集成包中还提供了AnthropicTokenCounter、GoogleGenAITokenCounter等 provider 专属实现(见 token counters 指南),本仓库内置的三种实现则无需安装任何集成包即可使用。
ApproximateTokenCounter:零依赖的字符比估算
ApproximateTokenCounter(源码见 haystack/token_counters/approximate_counter.py)是所有计数器中唯一无需安装任何额外依赖、也无需 warm_up 加载模型的实现,适合在初始化阶段或资源受限环境中做快速预算。
构造参数
__init__( chars_per_token: float = 4.0, tokens_per_image: int = 85, tokens_per_file: int = 1000, ) -> Nonechars_per_token(float,默认4.0):多少个字符按一个 token 计。这个值决定了估算的松紧度——对于英文内容 4 字符/token 是常用经验值,中文等字符密度较高的内容可适当调低;tokens_per_image(int,默认85):每张图片收取的 token 数。默认值对应 OpenAI 对小尺寸图片的计费,若发送大图应调高;tokens_per_file(int,默认1000):每个文件收取的 token 数,是对短文档的粗略替代值,实际成本取决于页数,发送长文档时应调高;- 异常:当
chars_per_token <= 0时抛出ValueError(源码第 44-45 行)。
count:一次简单的除法
count的实现非常直观(源码第 50-64 行):
text = _rendered_conversation(messages) + _rendered_tools(tools) text_tokens = int(len(text) / self.chars_per_token) return text_tokens + _non_text_tokens(...)当messages与tools均为空时直接返回0。文本 token 数 = 渲染后文本总长度(字符数)除以chars_per_token后取整,最后再加上图片与文件的固定估算值。
使用示例
from haystack.dataclasses import ChatMessage from haystack.token_counters import ApproximateTokenCounter counter = ApproximateTokenCounter(chars_per_token=4.0) messages = [ ChatMessage.from_user("Hello, how are you?"), ChatMessage.from_assistant("I'm good, thank you! How can I assist you today?") ] token_count = counter.count(messages) print(f"Estimated token count: {token_count}")TiktokenCounter:本地字节对编码估算
TiktokenCounter(源码见 haystack/token_counters/tiktoken_counter.py)使用 OpenAI 开源的tiktoken字节对编码器在本地完成计数,比字符比估算更贴近 OpenAI 模型的实际分词结果。
构造参数
__init__( encoding: str = "o200k_base", tokens_per_image: int = 85, tokens_per_file: int = 1000, ) -> Noneencoding(str,默认"o200k_base"):使用的tiktoken编码。o200k_base是当前 OpenAI 模型使用的编码,旧模型可改用cl100k_base等;tokens_per_image/tokens_per_file:与ApproximateTokenCounter含义一致,用于给分词器无法度量的图片、文件记固定开销;- 异常:未安装
tiktoken时在构造阶段抛出ImportError(提示运行pip install tiktoken)。测试 test_tiktoken_counter.py 明确验证了"依赖缺失在构造时报告,而非在首次 count 时才失败",这避免了错误在运行中途才暴露。
warm_up:按需加载编码器
def warm_up(self) -> None: if self._encoder is not None: return self._encoder = tiktoken.get_encoding(self.encoding)warm_up会加载编码器,首次使用时若本地无缓存会自动下载词表;重复调用是幂等的(编码器已加载则直接返回)。count()内部会自动调用warm_up(),所以日常使用无需手动预热。
两个必须知道的限制
- 仅文本:图片和文件只能按
tokens_per_image/tokens_per_file的固定值估算,无法真实分词; - 它是 OpenAI 的编码器:其他提供商的模型分词方式不同,计数会有偏差,跨 provider 使用时只能作为参考。
使用示例
from haystack.dataclasses import ChatMessage from haystack.token_counters import TiktokenCounter counter = TiktokenCounter(encoding="o200k_base") messages = [ ChatMessage.from_user("Hello, how are you?"), ChatMessage.from_assistant("I'm good, thank you! How can I assist you today?") ] token_count = counter.count(messages) print(f"Token count: {token_count}")OpenAITokenCounter:调用官方 API 获取精确计数
OpenAITokenCounter(源码见 haystack/token_counters/openai_counter.py)与前两者完全不同——它把输入发送到 OpenAI 的POST /v1/responses/input_tokens计数端点,返回的结果包含模型特定的消息与工具 Schema 格式化开销,以及图片、文件等受支持的非文本内容,是三种实现中唯一能给出"精确"计数的方案。
构造参数
__init__( model: str, *, api_key: Secret = Secret.from_env_var("OPENAI_API_KEY"), api_base_url: str | None = None, organization: str | None = None, timeout: float | None = None, max_retries: int | None = None, http_client_kwargs: dict[str, Any] | None = None, ) -> Nonemodel(str,必填):按哪个模型的分词规则计数,例如"gpt-5-mini";api_key(Secret,默认读取OPENAI_API_KEY环境变量):也可显式传入Secret.from_token("...");api_base_url(str | None):OpenAI API 的可选基础 URL,用于代理或兼容端点;organization(str | None):OpenAI 组织 ID;timeout(float | None):客户端调用超时。未设置时使用OPENAI_TIMEOUT环境变量,默认 30 秒(源码第 77 行);max_retries(int | None):最大重试次数。未设置时使用OPENAI_MAX_RETRIES环境变量,默认 5(源码第 78-80 行);http_client_kwargs(dict[str, Any] | None):用于配置底层 HTTPX 客户端的关键字参数,经由 init_http_client 构造。
生命周期:warm_up / count / close
warm_up():初始化 OpenAI 客户端(重复调用幂等)。客户端通过init_http_client构建并传入OpenAI(...);count(messages, tools=None):把每条ChatMessage通过_convert_chat_message_to_responses_api_format(来自 haystack/components/generators/chat/openai_responses.py)转换为 Responses API 输入格式;若传入了tools,则通过flatten_tools_or_toolsets展平后包装为{"type": "function", ...}并入请求,最后调用client.responses.input_tokens.count(**request)并返回response.input_tokens。空输入同样返回0;close():关闭 OpenAI 客户端及其底层 HTTP 资源,并把self.client置空。这在长生命周期服务中释放连接时很有用。
使用示例
from haystack.dataclasses import ChatMessage from haystack.token_counters import OpenAITokenCounter counter = OpenAITokenCounter("gpt-5-mini") messages = [ChatMessage.from_user("Hello, how are you?")] token_count = counter.count(messages) print(f"Token count: {token_count}")注意:该计数器需要网络请求与 API Key,计数会消耗配额与时间,适合对精确度要求高(例如超限判定、计费)的场景。
统一渲染机制:计数器内部是如何"看见"对话的
三种计数器对文本的测量都建立在同一个渲染层上(见 haystack/token_counters/utils.py),理解它就能理解"计数结果到底是什么":
_render_message:把单条ChatMessage渲染为若干行纯文本,规则包括:- 普通消息带角色前缀,如
[user] Hello、[system] rules; - 工具调用渲染为
[assistant -> tool_call] search({"q": "x"})(参数 JSON 按键排序,保证渲染结果稳定); - 工具结果渲染为
[tool:search] found it,出错时带(error)标记; - 图片与文件用占位符代替(
<image>、<file: report.pdf>),因为它们没有文本形态但同样消耗 token; - 推理内容(ReasoningContent)被刻意排除——provider 在轮次间会丢弃推理内容,它不属于被测上下文(源码第 38-39 行注释)。
- 普通消息带角色前缀,如
_rendered_conversation:把整段对话拼接为一个纯文本块,这正是计数器测量的对象;_rendered_tools:把工具 Schema 序列化为一个 JSON 块,模拟 provider 随消息一起发送的格式;_non_text_tokens:统计图片与文件数量乘以固定费率。注意它会遍历工具结果——嵌套在工具结果中的ImageContent/FileContent(例如工具返回的截图)同样会被计入,因为工具结果本身就在上下文里。
test/token_counters/test_utils.py 中完整覆盖了各类消息(系统、用户、带工具调用的助手消息、工具结果、错误工具结果、带图片和文件的用户消息、空消息)的渲染输出,可作为理解该机制的精确参考。
工具 Schema 的计数
工具 Schema 会与消息一起发送给模型并消耗上下文 token,因此所有计数器都支持传入tools:
from typing import Annotated from haystack.dataclasses import ChatMessage from haystack.token_counters import ApproximateTokenCounter from haystack.tools import tool @tool def search(query: Annotated[str, "The search query"]) -> str: """Search for documents that match the query.""" return "Search results" messages = [ChatMessage.from_user("Find information about Haystack.")] counter = ApproximateTokenCounter() token_count = counter.count(messages, tools=[search])三种计数器的行为一致:
- 传入
tools后其 Schema 计入估算(测试test_tool_schemas_add_to_the_count验证了counter.count(messages, tools=[search]) > counter.count(messages)); - 可以只数工具不数消息:
counter.count([], tools=[search])同样返回正数; messages与tools都为空时才返回0。
序列化与反序列化:让计数器配置可持久化
三个内置计数器都通过default_to_dict实现to_dict,序列化结果遵循 Haystack 统一格式——包含type(类的完整限定名)与init_parameters。例如 test_approximate_counter.py 验证的 round-trip:
data = ApproximateTokenCounter(chars_per_token=3.5, tokens_per_image=200, tokens_per_file=3000).to_dict() # data == { # "type": "haystack.token_counters.approximate_counter.ApproximateTokenCounter", # "init_parameters": {"chars_per_token": 3.5, "tokens_per_image": 200, "tokens_per_file": 3000}, # } restored = ApproximateTokenCounter.from_dict(data)TiktokenCounter的to_dict序列化encoding、tokens_per_image、tokens_per_file;OpenAITokenCounter额外序列化api_key(Secret对象)、model、api_base_url、organization、timeout、max_retries、http_client_kwargs。由于OpenAITokenCounter的to_dict输出了Secret,理论上需要重写from_dict来重建Secret——不过其默认from_dict路径依赖default_from_dict对Secret的兼容处理,实际使用时以测试与序列化工具的实际行为为准。
自定义 TokenCounter:接入自己的计数逻辑
当内置实现不满足需求时(例如对接某个提供商的专有计数端点),实现TokenCounter协议即可(参考 token counters 指南):
from typing import Any from haystack.core.serialization import default_to_dict from haystack.dataclasses import ChatMessage from haystack.token_counters import TokenCounter from haystack.tools import ToolsType class ProviderTokenCounter(TokenCounter): def count( self, messages: list[ChatMessage], tools: ToolsType | None = None, ) -> int: # Call the provider's token-counting endpoint here. ... def to_dict(self) -> dict[str, Any]: return default_to_dict(self)- 必须实现
count()与to_dict()两个方法; - 默认
from_dict()会直接还原普通构造参数;当to_dict()序列化了需要先重建的值(如Secret或嵌套组件)时,再重写from_dict()。
选型建议与适用前提
- 零依赖快速估算(CI 检查、粗略预算):选
ApproximateTokenCounter,无需安装任何包、无需网络; - OpenAI 模型的本地高精度估算:选
TiktokenCounter,需pip install tiktoken,首次warm_up会下载词表,且只对 OpenAI 编码准确; - 精确计数(含图片、文件与工具格式开销):选
OpenAITokenCounter,需要OPENAI_API_KEY与网络请求,注意其存在超时与重试配置(默认 30 秒 / 5 次重试); - 若使用 Claude、Gemini 等模型,可查阅官方集成包中的
AnthropicTokenCounter、GoogleGenAITokenCounter实现。
延伸阅读
- Token Counters 指南文档:各计数器细分指南(
docs-website/docs/token-counters/目录下的approximatetokencounter.mdx、tiktokencounter.mdx、openaitokencounter.mdx等); - Token Counters API 参考:本 API 参考原始页面;
- 源码:haystack/token_counters/(计数器实现与渲染工具)、haystack/token_counters/types/protocol.py(协议定义);
- 测试:test/token_counters/(
test_approximate_counter.py、test_tiktoken_counter.py、test_openai_counter.py、test_utils.py,覆盖计数、异常、序列化 round-trip 与工具计数行为)。
【免费下载链接】haystackOpen-source AI orchestration framework for building context-engineered, production-ready LLM applications. Design modular pipelines and agent workflows with explicit control over retrieval, routing, memory, and generation. Built for scalable agents, RAG, multimodal applications, semantic search, and conversational systems.项目地址: https://gitcode.com/GitHub_Trending/ha/haystack
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考