从零开始学 Dify - Dify Token 计算机制深度解析:如何避免 LLM 上下文长度超限错误

在构建基于大语言模型(LLM)的应用时,context\_length\_exceeded 错误是最常见且令人头疼的问题之一。当输入提示(包括对话历史、系统指令和当前查询)加上预期输出 Token 数超过模型的最大上下文窗口时,就会发生这种错误。

接下来将深入剖析 Dify 内部的 Token 计算和上下文管理逻辑。学习如何优雅地解决了这个问题。

👆👆👆欢迎关注,一起进步👆👆👆

一、核心问题:Token 限制

LLM 不直接处理单词或字符,而是处理"Token"。一个 Token 可以是一个单词、单词的一部分或标点符号。每个模型都有一个硬性限制,即单次请求能处理的 Token 数量,这被称为上下文窗口。例如,OpenAI 的 gpt-4-turbo 的上下文窗口为 128,000 个 Token,而 Anthropic 的 Claude 3.5 Sonnet 的上下文窗口为 200,000 个 Token 。

如果应用程序发送的请求过大,提供商的 API 会拒绝该请求。因此,一个健壮的应用程序必须在发送请求之前计算 Token,并智能地管理其上下文以保持在限制范围内。

二、Dify 的 Token 管理流程

Dify 充当用户和 LLM 提供商之间的智能中介。它确保发送给模型的最终负载始终有效。下图展示了这个过程:

picture.image

逻辑详解

  1. 配置收集 :当请求开始时,Dify 加载所选模型的配置。这包括两个关键数字:
  • 模型最大上下文(context\_size :模型支持的理论最大 token 数(如 128,000)。
  • 用户定义的输出 Token(max\_tokens :开发者在 Dify 界面中设置的值,为模型的 答案 保留一部分上下文窗口。
  • 预算计算 :Dify 使用简单公式计算提示的可用预算:可用提示 Token 数 = 模型最大上下文 - 用户定义的输出 Token 数

  • Token 化和计数 :这是实现的核心。Dify 使用 OpenAI 的 tiktoken 库进行精确的 Token 计算。

  • 上下文截断 :如果当前上下文的 Token 计数超过可用提示 Token 数,Dify 会智能地截断它。对于聊天应用,这通常涉及从对话历史中删除最旧的消息,直到总 Token 计数适合预算。

  • 最终 API 调用 :只有在验证和潜在截断之后,Dify 才会将负载发送给 LLM 提供商,确保请求有效并避免 context\_length\_exceeded 错误。

三、与提供商模型配置的关系

Dify 的 Token 计算逻辑与提供商模型配置中的参数有着密切的关系,在 Dify 中,每个模型都有以下关键配置参数:

  
# 模型配置示例  
model\_config = {  
    "context\_size": 128000,        # 模型的最大上下文窗口  
    "max\_tokens": 4096,            # 用户设置的最大输出 token 数  
    "model\_name": "gpt-4-turbo",   # 模型名称,用于选择正确的 tokenizer  
    "provider": "openai"           # 提供商,决定使用哪种 token 化方法  
}  

Dify 的 Token 计算流程

picture.image

四、代码分析:核心实现

1. 预先计算剩余 Token

base\_app\_runner.py 中,get\_pre\_calculate\_rest\_tokens 方法负责计算可用的 Token 预算:

  
def get\_pre\_calculate\_rest\_tokens(self, app\_record, model\_config, prompt\_template\_entity, inputs, files, query):  
    # 获取模型实例  
    model\_instance = ModelInstance(  
        provider\_model\_bundle=model\_config.provider\_model\_bundle, model=model\_config.model  
    )  
  
    # 获取模型最大上下文 token 数  
    model\_context\_tokens = model\_config.model\_schema.model\_properties.get(ModelPropertyKey.CONTEXT\_SIZE)  
  
    # 获取用户定义的输出 max\_tokens  
    max\_tokens = 0  
    for parameter\_rule in model\_config.model\_schema.parameter\_rules:  
        if parameter\_rule.
   
 name
  == "max\_tokens"or (  
            parameter\_rule.use\_template and parameter\_rule.use\_template == "max\_tokens"  
        ):  
            max\_tokens = (  
                model\_config.parameters.get(parameter\_rule.
   
 name
 )  
                or model\_config.parameters.get(parameter\_rule.use\_template or"")  
            ) or0  
  
    # 获取不包含记忆和上下文的提示消息  
    prompt\_messages, stop = self.organize\_prompt\_messages(  
        app\_record=app\_record,  
        model\_config=model\_config,  
        prompt\_template\_entity=prompt\_template\_entity,  
        inputs=inputs,  
        files=files,  
        query=query,  
    )  
  
    # 计算当前提示消息的 token 数  
    prompt\_tokens = model\_instance.get\_llm\_num\_tokens(prompt\_messages)  
  
    # 计算剩余可用 token 数  
    rest\_tokens: int = model\_context\_tokens - max\_tokens - prompt\_tokens  
    if rest\_tokens < 0:  
        raise InvokeBadRequestError(  
            "Query or prefix prompt is too long, you can reduce the prefix prompt, "  
            "or shrink the max token, or switch to a llm with a larger token limit size."  
        )  
  
    return rest\_tokens  

2. 重新计算最大 Token 数

如果提示 Token 数加上最大输出 Token 数超过模型上下文限制,Dify 会自动调整 max\_tokens 参数:

  
def recalc\_llm\_max\_tokens(self, model\_config, prompt\_messages):  
    # 获取模型实例  
    model\_instance = ModelInstance(  
        provider\_model\_bundle=model\_config.provider\_model\_bundle, model=model\_config.model  
    )  
  
    # 获取模型最大上下文 token 数  
    model\_context\_tokens = model\_config.model\_schema.model\_properties.get(ModelPropertyKey.CONTEXT\_SIZE)  
  
    # 获取用户定义的输出 max\_tokens  
    max\_tokens = 0  
    for parameter\_rule in model\_config.model\_schema.parameter\_rules:  
        if parameter\_rule.
   
 name
  == "max\_tokens"or (  
            parameter\_rule.use\_template and parameter\_rule.use\_template == "max\_tokens"  
        ):  
            max\_tokens = (  
                model\_config.parameters.get(parameter\_rule.
   
 name
 )  
                or model\_config.parameters.get(parameter\_rule.use\_template or"")  
            ) or0  
  
    # 计算当前提示消息的 token 数  
    prompt\_tokens = model\_instance.get\_llm\_num\_tokens(prompt\_messages)  
  
    # 如果提示 token 数加上最大输出 token 数超过模型上下文限制,则调整 max\_tokens  
    if prompt\_tokens + max\_tokens > model\_context\_tokens:  
        max\_tokens = max(model\_context\_tokens - prompt\_tokens, 16)  
  
        for parameter\_rule in model\_config.model\_schema.parameter\_rules:  
            if parameter\_rule.
   
 name
  == "max\_tokens"or (  
                parameter\_rule.use\_template and parameter\_rule.use\_template == "max\_tokens"  
            ):  
                model\_config.parameters[parameter\_rule.
   
 name
 ] = max\_tokens  

3. 计算剩余 Token

prompt\_transform.py 中,\_calculate\_rest\_token 方法负责计算剩余可用的 Token 数:

  
def \_calculate\_rest\_token(self, prompt\_messages, model\_config):  
    rest\_tokens = 2000  
  
    model\_context\_tokens = model\_config.model\_schema.model\_properties.get(ModelPropertyKey.CONTEXT\_SIZE)  
    if model\_context\_tokens:  
        model\_instance = ModelInstance(  
            provider\_model\_bundle=model\_config.provider\_model\_bundle, model=model\_config.model  
        )  
  
        curr\_message\_tokens = model\_instance.get\_llm\_num\_tokens(prompt\_messages)  
  
        max\_tokens = 0  
        for parameter\_rule in model\_config.model\_schema.parameter\_rules:  
            if parameter\_rule.name == "max\_tokens"or (  
                parameter\_rule.use\_template and parameter\_rule.use\_template == "max\_tokens"  
            ):  
                max\_tokens = (  
                    model\_config.parameters.get(parameter\_rule.name)  
                    or model\_config.parameters.get(parameter\_rule.use\_template or"")  
                ) or0  
  
        rest\_tokens = model\_context\_tokens - max\_tokens - curr\_message\_tokens  
        rest\_tokens = max(rest\_tokens, 0)  
  
    return rest\_tokens  

4. 管理对话历史

上下文管理的智能策略,分层截断策略。

picture.image

token\_buffer\_memory.py 中,get\_history\_prompt\_messages 方法负责获取对话历史,并在必要时进行截断:

  
def get\_history\_prompt\_messages(self, max\_token\_limit=2000, message\_limit=None):  
    # 获取对话历史消息  
    messages = query.limit(message\_limit).all()  
    thread\_messages = extract\_thread\_messages(messages)  
    messages = list(reversed(thread\_messages))  
  
    prompt\_messages = []  
    # 将消息转换为提示消息  
    for message in messages:  
        # 处理消息内容和文件  
        # ...  
  
    # 如果提示消息的 token 数超过最大限制,则进行截断  
    curr\_message\_tokens = self.model\_instance.get\_llm\_num\_tokens(prompt\_messages)  
    if curr\_message\_tokens > max\_token\_limit:  
        pruned\_memory = []  
        while curr\_message\_tokens > max\_token\_limit and len(prompt\_messages) > 1:  
            pruned\_memory.append(prompt\_messages.pop(0))  
            curr\_message\_tokens = self.model\_instance.get\_llm\_num\_tokens(prompt\_messages)  
  
    return prompt\_messages  

5. Token 计算的核心:tiktoken

Dify 使用 OpenAI 的 tiktoken 库进行精确的 Token 计算。这是一个快速的 BPE(Byte Pair Encoding)tokenizer,专为 OpenAI 模型设计。

large\_language\_model.py 中,get\_num\_tokens 方法负责获取提示消息的 Token 数:

  
def get\_num\_tokens(self, model, 
 
 credential
 s, prompt\_messages, tools=None):  
    if dify\_config.PLUGIN\_BASED\_TOKEN\_COUNTING\_ENABLED:  
        plugin\_model\_manager = PluginModelClient()  
        return plugin\_model\_manager.get\_llm\_num\_tokens(  
            tenant\_id=self.tenant\_id,  
            user\_id="unknown",  
            plugin\_id=self.plugin\_id,  
            provider=self.provider\_name,  
            model\_type=self.model\_type.value,  
            model=model,  
            
   
 credentials
 =
   
 credential
 s,  
            prompt\_messages=prompt\_messages,  
            tools=tools,  
        )  
    return0  

五、模型配置与 Token 计算的关系

Dify 中的模型配置参数直接影响 Token 计算和限制处理:

  1. ModelPropertyKey.CONTEXT_SIZE :定义模型的最大上下文窗口大小,如 GPT-4 的 128,000 或 Claude 的 200,000。
  2. max_tokens 参数 :用户在 Dify 界面中设置的值,为模型的回答保留一部分上下文窗口。
  3. 模型名称 :决定使用哪种 tokenizer,因为不同模型使用不同的 token 化规则。
  4. 提供商 :不同提供商(如 OpenAI、Anthropic)的模型可能有不同的 token 化方法和限制。

六、总结:Dify 的 Token 管理优势

Dify 的 Token 管理机制提供了几个关键优势:

  1. 自动化处理 :开发者无需手动计算 Token 或担心上下文限制。
  2. 智能截断 :当上下文过长时,Dify 会自动截断历史消息,确保请求有效。
  3. 适应性调整 :如果提示 Token 数加上最大输出 Token 数超过模型上下文限制,Dify 会自动调整 max\_tokens 参数。
  4. 多模型支持 :Dify 支持多种 LLM 提供商和模型,并为每个模型使用正确的 tokenizer。
  5. 错误预防 :通过预先计算和验证 Token 数,Dify 有效防止了 context\_length\_exceeded 错误。

通过这种方式,Dify 为开发者提供了一个无缝的体验,使他们能够专注于构建应用程序,而不必担心底层的 Token 管理复杂性。这种抽象层次的提升是 Dify 作为 LLM 应用开发平台的核心优势之一。

参考资料

https://github.com/langgenius/dify (v1.4.1)

七、推荐阅读

👆👆👆欢迎关注,一起进步👆👆👆

欢迎留言讨论哈

🧐点赞、分享、推荐 ,一键三连,养成习惯👍

0
0
0
0
评论
未登录
暂无评论