AgentScope 令牌计数:覆写 1 个方法,看懂账单
【免费下载链接】agentscopeBuild and run agents you can see, understand and trust.项目地址: https://gitcode.com/GitHub_Trending/ag/agentscope
AgentScope 的模型计费不靠猜。它的令牌计数器只有一个入口:ChatModelBase.count_tokens。本文讲清这个方法的契约、默认估算算法,以及自定义 token 计数的写法,让你控制上下文压缩的触发时机,管住账单。
复现场景:压缩晚触发一步,整次请求作废
你跑一个长任务 agent,上下文越滚越大,压缩本应提前介入。但估算值偏低,压缩没触发,下一次请求撞上服务商的上下文硬限,整轮推理和工具结果全部作废。反过来,估算偏高会过早压缩,有用历史被换成摘要,你还为摘要多付一次调用费。
⚠️ 两个问题都归到一个返回值:await self.model.count_tokens(...)。
打开 _base.py:count_tokens 的契约与默认算法
打开 src/agentscope/model/_base.py,第 369 行附近的签名长这样:
async def count_tokens( self, messages: list[Msg], tools: list[dict] | None, ) -> int: ...默认实现做的事很直白:
- 遍历每条消息的内容块,把
TextBlock、思考块、工具调用的 input、工具结果文本全部拼进一个字符串; - 文本 token 数 = UTF-8 字节数除以 4,四舍五入;
- 每个
DataBlock(图片、文件)按固定 2000 计,常量叫_MULTIMODAL_DATA_BLOCK_TOKEN_ESTIMATE; tools参数先json.dumps,并入文本一起算。
中文一个字符占 3 字节,除以 4 后约等于 1 字符 1 token,默认值日常够用。但它和各家真实分词器有偏差,要精确的模型计费,就覆写它。
盘点 src/agentscope/model/:9 个子类都能挂计数器
打开src/agentscope/model/目录,__init__.py里导出 9 个具体类:AnthropicChatModel、DashScopeChatModel、DeepSeekChatModel、GeminiChatModel、MoonshotChatModel、OllamaChatModel、OpenAIChatModel、OpenAIResponseModel、XAIChatModel。每个类都在同名子包的_model.py里,继承ChatModelBase。
扩展点就在基类 docstring 里写明的一句话:子类可覆写count_tokens,换上针对自家分词器的实现。你的模型落在 9 家之一,继承对应类即可,格式化、重试逻辑原样保留;全新服务商,则直接继承ChatModelBase。
在你的项目里覆写 count_tokens
在你自己的项目里新建一个模型类,仓库本身是只读的。以 OpenAI 系为例,核心就这几行:
import json import tiktoken from agentscope.model import OpenAIChatModel enc = tiktoken.encoding_for_model("gpt-4o") class MyOpenAIModel(OpenAIChatModel): async def count_tokens(self, messages, tools=None) -> int: texts = [] for msg in messages: for block in msg.get_content_blocks(): texts.append(getattr(block, "text", "") or str(block)) if tools: texts.append(json.dumps(tools, ensure_ascii=False)) return len(enc.encode("".join(texts)))注意两点:get_content_blocks()是基类遍历消息用的同一个入口,块结构别自己另起炉灶;tools的计法保持和基类一致,这样压缩前后的数字可比。多模态消息里,DataBlock你可以沿用每块 2000 的扁平估算,也可以按服务商的图片计价公式细化。
这个数字的去向:context_size 与工具结果切分
谁在消费这个数字?打开src/agentscope/agent/_agent.py,调用点有十几处,关键的两处:
- 压缩判定:
estimated_tokens < trigger_ratio * context_size就跳过。ContextConfig里trigger_ratio默认 0.8、上限 0.9,context_size默认 32768,所以默认触发线约 26214; - 工具结果切分:
_split_tool_result_for_compression用一个单块反复调用count_tokens,找到不超过tool_result_limit的切分点。单条巨型工具结果会触发多次计数,精确但慢的分词器在这里成本会放大,超大型工具输出可以考虑缓存。
估算准不准,直接决定压缩在 26214 这条线附近是「刚刚好」还是「差一截」。
写断言:照抄 model_count_tokens_test.py 的写法
tests/model_count_tokens_test.py 给了现成范式:40 万字符的 base64 图片加上 "hi",断言总数等于 2001;再断言 base64 与 URL 两种来源的DataBlock估算一致,都是 2000。测试用的MockModel在tests/utils.py。
照这个风格给你的计数器写一条:
class MyCounterTest(IsolatedAsyncioTestCase): async def test_count_matches_tokenizer(self): model = MyOpenAIModel() # 构造参数按你的凭据填 msg = UserMsg(name="u", content=TextBlock(text="hello")) n = await model.count_tokens([msg], None) self.assertEqual(n, 1)再补一条 DataBlock 断言,确认图片不会被当成 base64 文本按字节计费,验证就齐了。
计数只是估算,分词器才是账单——把count_tokens覆写成官方分词器,压缩时机、工具切分、模型计费这三件事就对齐了。
【免费下载链接】agentscopeBuild and run agents you can see, understand and trust.项目地址: https://gitcode.com/GitHub_Trending/ag/agentscope
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考