回调函数与异步编程|第七届字节青训营

105 阅读8分钟

回调函数与异步编程:提升代码效率与灵活性的关键

在当今软件开发的世界中,回调函数(Callback Functions)和异步编程(Asynchronous Programming)是两个至关重要的概念。它们不仅为开发者提供了处理复杂任务的有效工具,还显著提升了代码的效率和响应能力。本文将深入探讨回调函数和异步编程的基本概念,分析它们之间的关系,并重点介绍它们在LangChain框架中的实际应用。

一、回调函数概述

什么是回调函数?

回调函数是一种编程模式,其中一个函数(称为回调函数)作为参数传递给另一个函数。当特定事件发生或操作完成时,回调函数被调用。这种模式广泛应用于事件监听、定时任务和网络请求等场景。

回调函数的简单示例

让我们通过一个简单的Python示例来理解回调函数的工作原理:

def compute(x, y, callback):
    result = x + y
    callback(result)

def print_result(value):
    print(f"The result is: {value}")

def square_result(value):
    print(f"The squared result is: {value**2}")

# 使用 print_result 作为回调
compute(3, 4, print_result)  # 输出: The result is: 7

# 使用 square_result 作为回调
compute(3, 4, square_result)  # 输出: The squared result is: 49

在上述示例中,compute 函数接受两个数值和一个回调函数。当计算完成后,它调用传入的回调函数并传递结果。通过这种方式,compute 函数可以在不同的上下文中复用,而不需要了解具体的回调逻辑。

回调函数的优势与局限

优势:

  1. 灵活性:允许函数在执行过程中动态指定行为,提高代码的可复用性。
  2. 解耦:调用方和被调用方通过回调函数进行通信,降低了模块间的耦合度。

局限:

  1. 回调地狱:多个嵌套回调会导致代码难以阅读和维护。
  2. 错误处理复杂:在回调函数中处理错误可能变得复杂,特别是在多层回调嵌套的情况下。

二、异步编程基础

什么是异步编程?

异步编程是一种编程范式,允许程序在等待某些耗时操作(如I/O操作、网络请求或数据库查询)完成时,继续执行其他任务。这种方式有效提升了程序的并发能力和响应速度,特别适用于需要高并发处理的应用场景。

异步编程的简单示例

Python的asyncio库为异步编程提供了强大的支持。以下是一个简单的异步示例:

import asyncio

async def compute(x, y, callback):
    print("Starting compute...")
    await asyncio.sleep(0.5)  # 模拟异步操作
    result = x + y
    callback(result)
    print("Finished compute...")

def print_result(value):
    print(f"The result is: {value}")

async def another_task():
    print("Starting another task...")
    await asyncio.sleep(1)
    print("Finished another task...")

async def main():
    print("Main starts...")
    task1 = asyncio.create_task(compute(3, 4, print_result))
    task2 = asyncio.create_task(another_task())
    
    await task1
    await task2
    print("Main ends...")

asyncio.run(main())

在这个示例中,compute 函数模拟了一个异步操作。当执行到 await asyncio.sleep(0.5) 时,函数会暂停执行,将控制权交还给事件循环。这允许 another_task 函数同时执行,展示了异步编程的并发能力。

异步编程的优势

  1. 高效利用资源:在等待I/O操作时,CPU可以执行其他任务,提高资源利用率。
  2. 响应性:特别适用于需要快速响应用户操作的应用,如Web服务器和GUI应用。
  3. 可扩展性:支持处理大量并发连接,适合高并发场景。

三、回调函数与异步编程的关系

回调函数和异步编程常常结合使用。回调函数提供了一种在异步操作完成时执行特定代码的机制,而异步编程通过事件循环和任务调度实现了并发执行。两者结合使用,可以更高效地处理复杂的异步任务,提高程序的响应能力和性能。

然而,单纯使用回调函数进行异步编程可能导致“回调地狱”,使代码难以维护。为了解决这个问题,现代编程语言引入了async/await语法,使异步代码更加清晰和易读。

四、LangChain中的回调机制

LangChain简介

LangChain是一个用于构建与大语言模型(LLM)交互的框架。它为开发者提供了便捷的工具和接口,帮助构建复杂的对话系统、自动化任务和数据处理流程。

回调处理器在LangChain中的应用

LangChain的回调机制允许开发者在应用程序的不同阶段进行自定义操作,如日志记录、监控和数据流处理。这一机制通过CallbackHandler接口实现,极大地增强了框架的灵活性和可扩展性。

基本回调处理器

LangChain提供了多种内置的回调处理器,例如:

  • StdOutCallbackHandler:将所有事件记录到标准输出。
  • FileCallbackHandler:将日志记录到指定的文件中。

开发者还可以通过继承BaseCallbackHandlerAsyncCallbackHandler来自定义回调处理器,以满足特定需求。

LangChain回调机制的示例

以下示例展示了如何在LangChain中结合使用回调机制和loguru日志库,将相关事件输出到标准输出和文件中:

from loguru import logger
from langchain.callbacks import FileCallbackHandler
from langchain.chains import LLMChain
from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate

logfile = "output.log"

# 设置loguru记录日志到文件
logger.add(logfile, colorize=True, enqueue=True)
handler = FileCallbackHandler(logfile)

llm = OpenAI()
prompt = PromptTemplate.from_template("1 + {number} = ")

# 初始化LLMChain,添加回调处理器并启用详细输出
chain = LLMChain(llm=llm, prompt=prompt, callbacks=[handler], verbose=True)
answer = chain.run(number=2)
logger.info(answer)

在这个例子中,verbose=True参数相当于将一个输出到控制台的回调处理器添加到LLMChain对象中。这在调试时非常有用,因为它会将所有事件信息输出到控制台,同时记录到output.log文件中。

五、自定义回调处理器

同步与异步回调处理器

LangChain允许开发者创建同步和异步的回调处理器,以适应不同的应用场景。

创建同步回调处理器

以下示例展示了如何创建一个同步回调处理器,用于在生成新token时打印信息:

from langchain.callbacks.base import BaseCallbackHandler

class MySyncHandler(BaseCallbackHandler):
    def on_llm_new_token(self, token: str, **kwargs) -> None:
        print(f"New token generated: {token}")
创建异步回调处理器

以下示例展示了如何创建一个异步回调处理器,在LLM开始和结束时执行异步操作:

import asyncio
from typing import Any, Dict, List
from langchain.callbacks.base import AsyncCallbackHandler
from langchain.schema import LLMResult

class MyAsyncHandler(AsyncCallbackHandler):
    async def on_llm_start(
        self, serialized: Dict[str, Any], prompts: List[str], **kwargs: Any
    ) -> None:
        print("LLM is starting...")
        await asyncio.sleep(0.5)  # 模拟异步操作
        print("LLM started.")

    async def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None:
        print("LLM has finished processing.")
        await asyncio.sleep(0.5)  # 模拟异步操作
        print("LLM processing completed.")

应用自定义回调处理器

将自定义的回调处理器应用到LangChain中:

from langchain.chat_models import ChatOpenAI
from langchain.schema import HumanMessage

async def main():
    chat = ChatOpenAI(
        max_tokens=100,
        streaming=True,
        callbacks=[MySyncHandler(), MyAsyncHandler()],
    )

    await chat.agenerate([[HumanMessage(content="推荐三种适合生日的花卉,不超过50字。")]])

# 运行主异步函数
asyncio.run(main())

在这个示例中,MySyncHandler会在每生成一个新的token时打印信息,而MyAsyncHandler则在LLM开始和结束时执行异步操作,模拟数据获取和处理过程。这种结合使用同步和异步回调处理器的方法,使程序能够高效地处理请求并提供实时反馈。

六、使用get_openai_callback进行令牌计数

在与大语言模型交互时,监控令牌(tokens)的使用量对于成本控制和性能优化至关重要。LangChain提供了get_openai_callback上下文管理器,用于监控与OpenAI交互时的令牌消耗。

令牌计数示例

以下示例展示了如何使用get_openai_callback来监控与ConversationChain的交互中使用的令牌数量:

from langchain import OpenAI
from langchain.chains import ConversationChain
from langchain.chains.conversation.memory import ConversationBufferMemory
from langchain.callbacks import get_openai_callback

# 初始化大语言模型
llm = OpenAI(temperature=0.5, model_name="gpt-3.5-turbo-instruct")

# 初始化对话链
conversation = ConversationChain(
    llm=llm,
    memory=ConversationBufferMemory()
)

# 使用上下文管理器进行令牌计数
with get_openai_callback() as cb:
    # 第一天的对话
    conversation("我姐姐明天要过生日,我需要一束生日花束。")
    print("第一次对话后的记忆:", conversation.memory.buffer)

    # 回合2
    conversation("她喜欢粉色玫瑰,颜色是粉色的。")
    print("第二次对话后的记忆:", conversation.memory.buffer)

    # 回合3 (第二天的对话)
    conversation("我又来了,还记得我昨天为什么要来买花吗?")
    print("\n第三次对话后的提示:\n", conversation.prompt.template)
    print("\n第三次对话后的记忆:\n", conversation.memory.buffer)

# 输出使用的令牌
print("\n总计使用的令牌:", cb.total_tokens)

在这个例子中,get_openai_callback被用作上下文管理器,通过监听器跟踪令牌的使用情况。当上下文结束时,它会提供总的令牌数量,帮助开发者了解每次交互的成本。

异步环境中的令牌计数

为了展示在异步环境中如何使用回调函数进行令牌计数,可以添加一个异步函数来处理多个并发的交互:

import asyncio
from langchain.callbacks import get_openai_callback

async def additional_interactions(llm):
    with get_openai_callback() as cb:
        await asyncio.gather(
            *[llm.agenerate(["我姐姐喜欢什么颜色的花?"]) for _ in range(3)]
        )
    print("\n另外的交互中使用的令牌:", cb.total_tokens)

# 运行异步函数
asyncio.run(additional_interactions(llm))

在这个异步示例中,asyncio.gather同时启动多个llm.agenerate调用,它们并发执行而不相互阻塞。get_openai_callback继续监控这些并发任务的令牌使用情况,并在所有任务完成后输出总的令牌数量。