在使用LangChain进行历史会话管理时,开发者经常会遇到token计数相关的错误。最近我在一个项目中尝试使用阿里云的Qwen-plus模型时,遇到了一个典型的报错:
python复制NotImplementedError: get_num_tokens_from_messages() is not presently implemented for model qwen-plus
这个错误的本质是LangChain的ChatOpenAI类在设计时主要针对OpenAI官方模型,当尝试兼容第三方模型(如阿里云的Qwen系列)时,其内置的token计数方法无法正常工作。
问题的核心在于LangChain的token计数机制。OpenAI官方模型有专门的tokenizer来计算消息的token数量,而Qwen等第三方模型虽然提供了API兼容模式,但底层的tokenizer实现并不相同。
具体来说,ChatOpenAI类的get_num_tokens_from_messages()方法依赖于OpenAI的tokenizer实现。当使用Qwen-plus这样的非OpenAI模型时,这个方法会直接抛出NotImplementedError,因为它无法确定如何准确计算这些消息的token数量。
注意:token计数在大语言模型应用中非常重要,它直接关系到API调用成本、上下文窗口管理和消息裁剪策略的有效性。
LangChain社区在较新版本(>=0.2)中提供了对阿里云通义千问的官方支持。理论上,我们可以改用ChatTongyi类来替代ChatOpenAI:
python复制from langchain_community.chat_models import ChatTongyi
tongyi_model = ChatTongyi(
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
api_key="your_api_key",
model="qwen-plus",
)
然而,在实际使用中,这种方法仍然存在问题。系统会发出警告:
code复制UserWarning: Using fallback GPT-2 tokenizer for token counting. Token counts may be inaccurate for non-GPT-2 models.
这个警告表明ChatTongyi类虽然可用作token_counter,但其底层没有实现Qwen模型的准确token计数方法。作为回退方案,LangChain使用了GPT-2的tokenizer,这会导致几个问题:
为了解决这个问题,我们需要实现一个自定义的token计数函数,专门针对Qwen模型。以下是完整的实现方案:
python复制import tiktoken
def count_tokens_in_messages(messages):
"""准确计算Qwen/GPT-3.5/GPT-4模型的messages token数"""
encoding = tiktoken.get_encoding("cl100k_base") # Qwen使用的编码
tokens_per_message = 4 # 每条消息的固定开销
tokens_per_name = -1 # Qwen不支持name字段
num_tokens = 0
for message in messages:
num_tokens += tokens_per_message
num_tokens += len(encoding.encode(message.content))
num_tokens += len(encoding.encode(message.type)) # message类型
num_tokens += 3 # 每轮对话的额外开销
return num_tokens
这个实现参考了OpenAI官方的token计数方式,但针对Qwen模型做了适当调整:
cl100k_base编码:这是Qwen、GPT-3.5和GPT-4共同使用的编码方式tokens_per_message=4:每条消息的固定开销(role + 分隔符等)tokens_per_name=-1:Qwen不支持name字段,所以忽略+3:每轮对话的系统开销(如<|im_start|>等特殊token)现在我们可以将这个自定义函数集成到LangChain的消息裁剪器中:
python复制from langchain_core.messages import trim_messages
trimmer = trim_messages(
max_tokens=65,
strategy="last",
token_counter=count_tokens_in_messages,
include_system=True,
allow_partial=False,
start_on="human",
)
在使用这个解决方案时,需要特别注意Qwen模型的一些独特行为:
对于生产环境的应用,可以考虑以下优化措施:
为了确保我们的token计数准确,应该进行充分的测试:
python复制# 测试用例
test_messages = [
SystemMessage(content="你是助手"),
HumanMessage(content="你好"),
AIMessage(content="你好!有什么可以帮您?")
]
print(count_tokens_in_messages(test_messages)) # 预期输出约25-30个token
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 使用ChatTongyi | 官方支持,简单易用 | token计数不准确 | 快速原型开发 |
| 自定义计数函数 | 计数准确,可控性强 | 需要额外实现 | 生产环境 |
| 使用GPT-2回退 | 无需额外代码 | 计数严重不准确 | 不推荐 |
根据项目需求选择合适的方案:
理解token计数需要先了解tokenizer的工作方式。现代LLM通常使用Byte Pair Encoding (BPE)算法:
Qwen使用的cl100k_base编码具有以下特性:
在Chat模型中,一条完整的消息通常包含:
所有这些元素都会被计入最终的token数量,这也是为什么我们需要在自定义函数中添加固定开销。
在实际应用中,我们可以实现更智能的token管理策略:
python复制def adaptive_trimmer(messages, max_tokens):
"""根据内容重要性动态分配token预算"""
base_tokens = count_tokens_in_messages([messages[0]]) # system消息
remaining = max_tokens - base_tokens
# 根据消息重要性分配权重
weights = {
'system': 0.2,
'human': 0.5,
'ai': 0.3
}
# 实现动态裁剪逻辑
# ...
更高级的方案可以考虑对话上下文的重要性:
在生产环境中,应该监控:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| Token计数明显偏少 | 使用了错误的编码方式 | 确保使用cl100k_base |
| 计数结果不稳定 | 消息格式不一致 | 统一消息类型处理 |
| 性能低下 | 频繁调用编码器 | 实现缓存机制 |
| 裁剪后对话不连贯 | 策略过于激进 | 调整max_tokens或策略 |