回调函数与异步编程|学习笔记

74 阅读6分钟

回调函数与异步编程的联系与区别

  1. 回调函数的本质

    • 回调函数是编程中的一种模式,表示一个函数作为参数被传递给另一个函数,在合适的时机被调用。它关注的是逻辑解耦,而非运行方式。
    • 它与是否异步无关。即使所有代码是同步执行的,也可以用回调函数,比如事件处理或数据处理。
  2. 异步编程的本质

    • 异步编程关注任务的调度和运行方式,允许任务在等待耗时操作时释放控制权,让系统去处理其他任务。
    • 异步编程常与回调函数结合使用。例如,在网络请求或文件I/O操作完成时触发回调函数,以此减少等待时间。

回调函数在异步编程中的重要性

在异步编程中,回调函数是一个常用工具。具体体现为:

  1. 事件驱动编程

    • 事件循环会监听不同的事件,事件触发后调用对应的回调函数。
    • 如 JavaScript 的浏览器事件(clickkeydown)或 Python 的 asyncio 事件。
  2. 数据处理链条

    • 异步任务完成后,回调函数可以作为任务的“后续动作”,完成对数据的进一步处理。例如,下载完成后处理文件。
  3. 增强代码灵活性

    • 通过回调函数,可以根据不同场景灵活定义完成后的行为,而不需要改变核心逻辑。

Python 的异步编程和事件循环机制

Python 的异步编程基于 asyncio,其核心机制是 事件循环(Event Loop)

  1. 事件循环

    • 是一个运行的循环,负责管理和调度异步任务。
    • 异步任务可以通过 await 暂停执行,把控制权还给事件循环,从而让其他任务运行。
  2. 任务队列

    • 异步任务以 Future 对象的形式被加入任务队列,事件循环根据任务的完成状态调度它们。
    • 任务可以挂起或恢复,而不会阻塞其他任务。

LangChain 中的回调机制

LangChain 的回调系统让我们可以在不同的处理阶段自定义行为。例如:

  1. 监控模型运行

    • 回调函数可以记录模型运行的输入、输出或错误信息。
    • 在链式调用中,通过回调函数可以实时分析数据流和性能。
  2. 实现资源管理

    • 通过回调机制监控资源消耗(如 API 调用的 token 数量),帮助控制成本。
  3. 日志和调试

    • LangChain 的内置回调机制允许捕获系统的每一步执行结果,尤其在链条复杂时,这有助于分析问题。
  4. 多种形式的回调处理器

    • LangChain 支持同步与异步回调处理器,通过继承 BaseCallbackHandlerAsyncCallbackHandler 实现自定义功能。
    • 例如,可以实时记录每个模型生成的 Token 或添加特殊行为来调整链条执行逻辑。

回调函数与异步机制的整合

在现代框架中,回调函数不仅是用来监控任务状态的工具,还常与异步机制结合以提升性能:

  1. 并发任务处理

    • 多个任务可同时进行,而回调函数会在任务完成时被触发。
    • 这种机制在处理高频请求或长时间运行任务(如数据处理或文件传输)时尤其高效。
  2. 错误恢复与日志跟踪

    • 异步任务失败时,可通过回调函数触发补救措施,或记录日志以便后续分析。

实践建议

  1. 回调函数的设计原则

    • 简单明确:不要让回调函数承担太复杂的逻辑。
    • 可重用性:尽量设计通用的回调函数,避免硬编码到特定任务中。
  2. 异步任务的组织

    • 对于复杂任务,可以将异步操作分割为多个小步骤,每步结束后用回调处理后续逻辑。
    • 使用高层工具(如 asyncio.gatherasyncio.create_task)并发调度任务。
  3. LangChain 的最佳实践

    • 使用 get_openai_callback 监控 token 消耗。
    • 通过日志工具结合回调函数,统一管理不同阶段的输入输出数据。
    • 编写自定义回调处理器,在链条执行的关键步骤中添加业务逻辑。

思考题

1. 将令牌计数器集成到其他记忆机制

关键点:

  1. 记忆机制的核心功能

    • LangChain 提供了多种记忆机制,例如 ConversationBufferMemory, ConversationBufferWindowMemory, EntityMemory 等。每种记忆机制的主要功能是存储并管理对话上下文。
    • 集成 get_openai_callback 后,可以实时监控令牌使用量,与模型交互的效率和成本更加清晰。
  2. 集成思路

    • 修改记忆机制的 load_memory_variablessave_context 方法,这些方法是记忆模块的核心操作点。
    • 在这些方法中引入 get_openai_callback 的上下文管理器,监控操作过程中调用 OpenAI 模型时的令牌消耗。

实现思路:

以下是一个如何将令牌计数器集成到 ConversationBufferWindowMemory 的思路


from langchain.memory import ConversationBufferWindowMemory
from langchain.callbacks import get_openai_callback

class TokenTrackingWindowMemory(ConversationBufferWindowMemory):
    def __init__(self, k=5):
        super().__init__(k=k)
        self.token_usage = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}

    def save_context(self, inputs, outputs):
        # 在保存上下文时跟踪令牌
        with get_openai_callback() as cb:
            super().save_context(inputs, outputs)
            self.token_usage["prompt_tokens"] += cb.prompt_tokens
            self.token_usage["completion_tokens"] += cb.completion_tokens
            self.token_usage["total_tokens"] += cb.total_tokens

    def get_token_usage(self):
        return self.token_usage

该实现通过重写 save_context 方法,在保存对话上下文时统计令牌使用情况,并将结果存储在 token_usage 中,供后续分析。


2. 在请求过程(run/apply方法)中引入回调机制

分析:

  1. 请求过程的结构

    • run 方法通常接收单一输入并返回结果。
    • apply 方法适用于批量处理,接收多个输入并返回多个结果。
    • 在这些方法中引入回调机制,可以实时监控模型交互中发生的操作,例如令牌使用量、时间消耗或生成结果。
  2. 实现方式

    • 修改 runapply 方法,增加对回调的支持。
    • 使用上下文管理器(如 get_openai_callback)或通过外部注入的回调函数,在模型请求前后插入逻辑。

实现思路:

以下是如何在一个自定义链的 run 方法中集成令牌计数器的示例:

from langchain.chains.base import Chain
from langchain.callbacks import get_openai_callback

class TokenTrackingChain(Chain):
    def __init__(self, llm, **kwargs):
        super().__init__(**kwargs)
        self.llm = llm
        self.token_usage = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}

    def _call(self, inputs):
        with get_openai_callback() as cb:
            # 调用 LLM 模型
            output = self.llm.predict(inputs["input"])
            self.token_usage["prompt_tokens"] += cb.prompt_tokens
            self.token_usage["completion_tokens"] += cb.completion_tokens
            self.token_usage["total_tokens"] += cb.total_tokens
        return {"output": output}

    def get_token_usage(self):
        return self.token_usage