尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

【Bug已解决】core: `count_tokens_approximately` over-counts audio, video, and file blocks by their base6…

【Bug已解决】core: `count_tokens_approximately` over-counts audio, video, and file blocks by their base6… 【Bug已解决】core:count_tokens_approximatelyover-counts audio, video, and file blocks by their base64 length 解决方案一、现象长什么样langchain-core提供的count_tokens_approximately一个轻量、不需要真模型的近似 token 计数器常用于判断这条消息会不会超窗口在处理多模态消息时会严重高估 token 数一条携带音频/视频/文件块的HumanMessage其中content里放的是 base64 编码字符串count_tokens_approximately把这些 base64 文本当成普通文本按字符数 / 4估算 token但 base64 编码会把二进制膨胀约 1.33 倍每 3 字节变 4 字符而且音频/视频本身在模型侧是走专门的 tokenizer/按秒计费的根本不是逐字符 token结果一个 1 MB 的音频base64 后约 1.37 MB 文本被估算成 ~35 万 token而模型实际可能只按 ~10 秒音频计几十个 token触发后果上下文窗口裁剪trim_messages错误地认为消息巨长把本该保留的对话历史提前砍掉或触发不必要的分片导致回答质量断崖式下降更隐蔽纯文本对话一切正常一旦用户上传个语音/视频逻辑就乱套极难定位。一句话近似计数器把 base64 媒体当纯文本数导致对多模态块 token 数的灾难性高估。二、背景count_tokens_approximately的设计目的是快速、无模型依赖地估个大概典型实现是遍历消息内容对文本块按len(text) // 4估算英语约 4 字符/token对结构化块给个经验常数。它被广泛用于trim_messages的token_counter参数决定裁剪边界一些中间件的是否超长预判测试里替代真实 tokenizer避免联网。问题出在多模态内容块上。现代 chat 模型如 Claude、GPT-4o支持image_url、input_audio、video等块其content经常是 base64 字符串。对这些块继续用字符/4显然错误——base64 的字符数既不代表语义 token也不代表模型真实计费 token。三、根因根因是近似计数器没有区分文本块与媒体块的计量规则def count_tokens_approximately(messages) - int: total 0 for msg in messages: content msg.content if isinstance(content, str): total len(content) // 4 # 文本合理 elif isinstance(content, list): for block in content: if isinstance(block, str): total len(block) // 4 elif isinstance(block, dict): # 问题把 dict 里的 base64 字段也当文本数了 text _extract_text(block) # 会抓到 base64 total len(text) // 4 # 高估 return total当block是{type: input_audio, input_audio: {data: base64, ...}}时_extract_text把 base64 当文本取出并//4于是高估。正确做法对媒体类型块audio/video/file/image 的 base64使用与文本完全不同的估算——要么用经验常数如按时长/按文件大小档位要么直接不计入字符估算、改用专门的媒体计量。四、最小可运行复现from langchain_core.messages import HumanMessage def count_tokens_approximately_buggy(messages) - int: total 0 for msg in messages: content msg.content if isinstance(content, str): total len(content) // 4 elif isinstance(content, list): for block in content: if isinstance(block, dict): # 把所有字符串字段拼起来当文本数 for v in block.values(): if isinstance(v, str): total len(v) // 4 elif isinstance(v, dict): for vv in v.values(): if isinstance(vv, str): total len(vv) // 4 return total # 模拟一段 1MB 音频的 base64约 1.37M 字符 fake_base64 A * (1024 * 1024 * 4 // 3) msg HumanMessage(content[ {type: text, text: 请总结这段录音}, {type: input_audio, input_audio: {data: fake_base64, format: wav}}, ]) estimated count_tokens_approximately_buggy([msg]) print(buggy estimate:, estimated) # 约 35 万 token严重高估运行后estimated是个几十万级的数字而真实模型对这段音频可能只计几十 token差距上千倍。五、解决方案第一层最小直接修复最小修复在遍历内容块时识别媒体类型对 base64 媒体块跳过逐字符估算改用经验常数或按媒体类型给固定估值MEDIA_TYPES {input_audio, audio, video, file, image_url} def count_tokens_approximately_fixed(messages, media_token_hint: int 256) - int: total 0 for msg in messages: content msg.content if isinstance(content, str): total len(content) // 4 elif isinstance(content, list): for block in content: if isinstance(block, dict): btype block.get(type) if btype in MEDIA_TYPES: # 媒体块不再按 base64 长度数用经验估值 total media_token_hint else: for v in block.values(): if isinstance(v, str): total len(v) // 4 return total这样一段音频只占media_token_hint默认 256个 token而不是几十万估算回到合理量级。六、解决方案第二层结构化改进把不同内容块类型的计量规则抽成可配置的策略作为唯一事实来源后续新增模态只需扩展映射from dataclasses import dataclass, field from enum import Enum from typing import Dict class BlockKind(str, Enum): TEXT text AUDIO audio VIDEO video IMAGE image FILE file UNKNOWN unknown dataclass(frozenTrue) class LangChainCountTokensApproxPolicy: 近似 token 计数策略按内容块类型分别计量。 规则 - 文本字符数 / 4 - 音频/视频/图片/文件用每块的固定经验估值不再数 base64 text_chars_per_token: int 4 media_token_hint: Dict[BlockKind, int] field(default_factorylambda: { BlockKind.AUDIO: 256, BlockKind.VIDEO: 512, BlockKind.IMAGE: 256, BlockKind.FILE: 256, BlockKind.UNKNOWN: 64, }) def _kind_of(self, block: dict) - BlockKind: t block.get(type, ) if audio in t: return BlockKind.AUDIO if video in t: return BlockKind.VIDEO if image in t: return BlockKind.IMAGE if file in t: return BlockKind.FILE if t text: return BlockKind.TEXT return BlockKind.UNKNOWN def count(self, messages) - int: total 0 for msg in messages: content msg.content if isinstance(content, str): total len(content) // self.text_chars_per_token elif isinstance(content, list): for block in content: if isinstance(block, dict): kind self._kind_of(block) if kind is BlockKind.TEXT: for v in block.values(): if isinstance(v, str): total len(v) // self.text_chars_per_token else: total self.media_token_hint[kind] return total不同模型对同一媒体的真实 token 计价不同可把media_token_hint按模型名配置做到近似但贴近真实。七、解决方案第三层断言 / CI 守护用测试锁死媒体块不得按 base64 长度估算from langchain_core.messages import HumanMessage import pytest from your_module import LangChainCountTokensApproxPolicy, BlockKind def test_audio_block_not_counted_by_base64(): policy LangChainCountTokensApproxPolicy() fake_b64 A * (1024 * 1024 * 4 // 3) # 约 1.37M 字符 msg HumanMessage(content[ {type: text, text: 请总结}, {type: input_audio, input_audio: {data: fake_b64, format: wav}}, ]) estimated policy.count([msg]) # 必须远小于 base64 长度/4约 35 万应当是个小数字 assert estimated 1024 assert estimated 256 # 至少计入一个媒体经验值 def test_text_block_still_counted_normally(): policy LangChainCountTokensApproxPolicy() msg HumanMessage(contenthello world) assert policy.count([msg]) 2 # len11 // 4 2 def test_video_block_uses_video_hint(): policy LangChainCountTokensApproxPolicy() msg HumanMessage(content[{type: video, video: {data: x}}]) assert policy.count([msg]) policy.media_token_hint[BlockKind.VIDEO]把这些测试纳入langchain-core的count_tokens相关测试套件防止回归。八、排查清单你的token_counter是否用了count_tokens_approximately若是检查多模态消息是否被高估。打印每条消息估算值看音频/视频块是否出现几十万级的异常数字。确认trim_messages是否因高估而误删历史。媒体块是否走了字符/4分支应改为经验常数。不同模型对同一媒体的真实计费不同hint 是否应按模型配置CI 是否覆盖音频/视频块不得按 base64 估算的断言九、小结count_tokens_approximately把 base64 编码的音频/视频/文件块当成纯文本按字符/4估算导致对多模态内容的 token 数灾难性高估进而让trim_messages等逻辑误删历史、触发错误分片。最小修复是对媒体块改用经验常数而非数 base64结构化做法是抽成LangChainCountTokensApproxPolicy按内容块类型分别计量最后用 pytest 把媒体块不得按 base64 长度估算锁进 CI确保多模态场景下近似计数保持合理量级。
返回列表