回调函数与异步编程基础
回调函数
回调函数,你可能并不陌生。它是函数A作为参数传给另一个函数B,然后在函数B内部执行函数A。当函数B完成某些操作后,会调用(即“回调”)函数A。这种编程模式常见于处理异步操作,如事件监听、定时任务或网络请求。
在编程中,异步通常是指代码不必等待某个操作完成(如I/O操作、网络请求、数据库查询等)就可以继续执行的能力。异步机制的实现涉及事件循环、任务队列和其他复杂的底层机制。这与同步编程形成对比,在同步编程中,操作必须按照它们出现的顺序完成。例如:
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}")
# 使用不同的回调函数
compute(3, 4, print_result) # 输出: The result is: 7
compute(3, 4, square_result) # 输出: The squared result is: 49
上述示例中,虽然 compute 调用了回调函数,但并未体现异步操作。回调函数仅是一种设计模式,与同步或异步操作无直接关系。
异步编程与回调函数
异步编程允许代码在等待某个操作(如I/O操作或网络请求)完成时继续执行其他任务,从而提升效率。例如:
python
复制代码
import asyncio
async def compute(x, y):
print("Starting compute...")
await asyncio.sleep(0.5) # 模拟异步操作
result = x + y
print(f"Finished compute: {result}")
async def another_task():
print("Starting another task...")
await asyncio.sleep(1)
print("Finished another task...")
async def main():
print("Main starts...")
await asyncio.gather(compute(3, 4), another_task())
print("Main ends...")
asyncio.run(main())
以上代码中,compute 和 another_task 是两个独立的异步任务,使用 asyncio.gather 并发执行。当某任务遇到 await 暂停时,另一个任务可以继续执行,充分体现了异步编程的优势。
LangChain中的回调机制
LangChain 提供了强大的回调机制,用于在应用的不同阶段插入自定义逻辑,例如日志记录、事件跟踪和数据流操作。通过实现 CallbackHandler 接口,可以创建自己的回调处理器来监听和处理特定事件。
回调处理器结构
LangChain 提供了基础的 BaseCallbackHandler 和异步的 AsyncCallbackHandler,其方法包括:
on_llm_start:LLM任务开始时调用。on_llm_new_token:新Token生成时调用。on_llm_end:LLM任务结束时调用。on_llm_error:LLM任务发生错误时调用。
以下是LangChain内置的回调处理器示例:
StdOutCallbackHandler:将事件信息输出到控制台。FileCallbackHandler:将事件日志写入文件。
回调机制的使用
LangChain支持两种方式添加回调:构造函数回调 和 请求回调。
构造函数回调
在组件初始化时传入回调处理器:
python
复制代码
from langchain.callbacks import FileCallbackHandler
from langchain.chains import LLMChain
from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate
handler = FileCallbackHandler("output.log")
llm = OpenAI()
prompt = PromptTemplate.from_template("1 + {number} = ")
chain = LLMChain(llm=llm, prompt=prompt, callbacks=[handler], verbose=True)
answer = chain.run(number=2)
以上代码将事件同时输出到控制台和日志文件 output.log,适用于全局日志记录。
请求回调
如果只需为某次特定请求设置回调,可在请求时传入:
python
复制代码
chain.run(number=2, callbacks=[handler])
自定义回调处理器
通过继承 BaseCallbackHandler 和 AsyncCallbackHandler,可以实现自定义回调。例如:
python
复制代码
import asyncio
from langchain.callbacks.base import BaseCallbackHandler, AsyncCallbackHandler
from langchain.schema import LLMResult
class MySyncHandler(BaseCallbackHandler):
def on_llm_new_token(self, token: str, **kwargs):
print(f"新Token生成: {token}")
class MyAsyncHandler(AsyncCallbackHandler):
async def on_llm_start(self, **kwargs):
print("任务开始...")
await asyncio.sleep(0.5)
async def on_llm_end(self, response: LLMResult, **kwargs):
print("任务结束...")
await asyncio.sleep(0.5)
通过同步和异步回调,可以实现更灵活的事件处理逻辑。
令牌计数器的实现
LangChain的 get_openai_callback 提供了一种上下文管理器,用于统计与OpenAI交互中消耗的Token数量。以下是使用回调函数实现的令牌计数器示例:
python
复制代码
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("我姐姐明天要过生日,我需要一束生日花束。")
conversation("她喜欢粉色玫瑰,颜色是粉色的。")
conversation("我又来了,还记得我昨天为什么要来买花吗?")
print("总计使用的Tokens:", cb.total_tokens)
运行结果中,cb.total_tokens 将输出对话总共消耗的Tokens数。
异步回调示例
以下是一个异步回调实现的例子,用于演示如何在多个并发任务中统计Tokens:
python
复制代码
import asyncio
async def additional_interactions():
with get_openai_callback() as cb:
await asyncio.gather(
*[llm.agenerate(["描述三种适合生日的花束"]) for _ in range(3)]
)
print("额外交互中使用的Tokens:", cb.total_tokens)
asyncio.run(additional_interactions())
此示例中,使用 asyncio.gather 并发运行多个任务,每个任务都在回调中统计消耗的Tokens。
自定义回调函数
我们也可以通过BaseCallbackHandler和AsyncCallbackHandler来自定义回调函数。下面是一个示例。
import asyncio
from typing import Any, Dict, List
from langchain.chat_models import ChatOpenAI
from langchain.schema import LLMResult, HumanMessage
from langchain.callbacks.base import AsyncCallbackHandler, BaseCallbackHandler
# 创建同步回调处理器
class MyFlowerShopSyncHandler(BaseCallbackHandler):
def on_llm_new_token(self, token: str, **kwargs) -> None:
print(f"获取花卉数据: token: {token}")
# 创建异步回调处理器
class MyFlowerShopAsyncHandler(AsyncCallbackHandler):
async def on_llm_start(
self, serialized: Dict[str, Any], prompts: List[str], **kwargs: Any
) -> None:
print("正在获取花卉数据...")
await asyncio.sleep(0.5) # 模拟异步操作
print("花卉数据获取完毕。提供建议...")
async def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None:
print("整理花卉建议...")
await asyncio.sleep(0.5) # 模拟异步操作
print("祝你今天愉快!")
# 主要的异步函数
async def main():
flower_shop_chat = ChatOpenAI(
max_tokens=100,
streaming=True,
callbacks=[MyFlowerShopSyncHandler(), MyFlowerShopAsyncHandler()],
)
# 异步生成聊天回复
await flower_shop_chat.agenerate([[HumanMessage(content="哪种花卉最适合生日?只简单说3种,不超过50字")]])
# 运行主异步函数
asyncio.run(main())
在这个鲜花店客服的程序中,当客户问及关于鲜花的建议时,我们使用了一个同步和一个异步回调。
MyFlowerShopSyncHandler 是一个同步回调,每当新的Token生成时,它就简单地打印出正在获取的鲜花数据。
而 MyFlowerShopAsyncHandler 则是异步的,当客服开始提供鲜花建议时,它会模拟数据的异步获取。在建议完成后,它还会模拟一个结束的操作,如向客户发出感谢。
这种结合了同步和异步操作的方法,使得程序能够更有效率地处理客户请求,同时提供实时反馈。
这里的异步体现在这样几个方面。
- 模拟延时操作:在MyFlowerShopAsyncHandler中,我们使用了await asyncio.sleep(0.5)来模拟其他请求异步获取花卉信息的过程。当执行到这个await语句时,当前的on_llm_start函数会“暂停”,释放控制权回到事件循环。这意味着,在这个sleep期间,其他异步任务(如其他客户的请求)可以被处理。
- 回调机制:当ChatOpenAI在处理每个新Token时,它会调用on_llm_new_token方法。因为这是一个同步回调,所以它会立即输出。但是,开始和结束的异步回调on_llm_start和on_llm_end在开始和结束时都有一个小的延时操作,这是通过await asyncio.sleep(0.5)模拟的。
- 事件循环:Python的syncio库提供了一个事件循环,允许多个异步任务并发运行。在我们的例子中,虽然看起来所有的操作都是按顺序发生的,但由于我们使用了异步操作和回调,如果有其他并发任务,它们可以在await暂停期间运行。
为了更清晰地展示异步的优势,通常我们会在程序中同时运行多个异步任务,并观察它们如何“并发”执行。但在这个简单的例子中,我们主要是通过模拟延时来展示异步操作的基本机制。
因此说,回调函数为异步操作提供了一个机制,使你可以定义“当操作完成时要做什么”,而异步机制的真正实现涉及更深层次的底层工作,如事件循环和任务调度。
用 get_openai_callback 构造令牌计数器
下面,我带着你使用LangChain中的回调函数来构造一个令牌计数器。这个计数功能对于监控大模型的会话消耗以及成本控制十分重要。
在构造令牌计数器之前,我们来回忆一下第11课中的记忆机制。我们用下面的代码生成了ConversationBufferMemory。
from langchain import OpenAI
from langchain.chains import ConversationChain
from langchain.chains.conversation.memory import ConversationBufferMemory
# 初始化大语言模型
llm = OpenAI(
temperature=0.5,
model_name="gpt-3.5-turbo-instruct")
# 初始化对话链
conversation = ConversationChain(
llm=llm,
memory=ConversationBufferMemory()
)
# 第一天的对话
# 回合1
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)
同时,我们也给出了各种记忆机制对Token的消耗数量的估算示意图。
不过,这张图毕竟是估算,要真正地衡量出每种记忆机制到底耗费了多少个Token,那就需要回调函数上场了。
下面,我们通过回调函数机制,重构这段程序。为了做到这一点,我们首先需要确保在与大语言模型进行交互时,使用了get_openai_callback上下文管理器。
在Python中,一个上下文管理器通常用于管理资源,如文件或网络连接,这些资源在使用前需要设置,在使用后需要清理。上下文管理器经常与with语句一起使用,以确保资源正确地设置和清理。
get_openai_callback被设计用来监控与OpenAI交互的Token数量。当你进入该上下文时,它会通过监听器跟踪Token的使用。当你退出上下文时,它会清理监听器并提供一个Token的总数。通过这种方式,它充当了一个回调机制,允许你在特定事件发生时执行特定的操作或收集特定的信息。
具体代码如下:
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()
)
# 使用context manager进行token counting
with get_openai_callback() as cb:
# 第一天的对话
# 回合1
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)
# 输出使用的tokens
print("\n总计使用的tokens:", cb.total_tokens)
这里,我使用了get_openai_callback上下文管理器来监控与ConversationChain的交互。这允许我们计算在这些交互中使用的总Tokens数。
输出:
总计使用的tokens: 966
下面,我再添加了一个additional_interactions异步函数,用于演示如何在多个并发交互中计算Tokens。
当我们讨论异步交互时,指的是我们可以启动多个任务,它们可以并发(而不是并行)地运行,并且不会阻塞主线程。在Python中,这是通过asyncio库实现的,它使用事件循环来管理并发的异步任务。
import asyncio
进行更多的异步交互和token计数
async def additional_interactions(): with get_openai_callback() as cb: await asyncio.gather( *[llm.agenerate(["我姐姐喜欢什么颜色的花?"]) for _ in range(3)] ) print("\n另外的交互中使用的tokens:", cb.total_tokens)
运行异步函数
asyncio.run(additional_interactions())
简单解释一下。
1. `async def`:这表示additional_interactions是一个异步函数。它可以使用await关键字在其中挂起执行,允许其他异步任务继续。
1. `await asyncio.gather(...)`:这是asyncio库提供的一个非常有用的方法,用于并发地运行多个异步任务。它会等待所有任务完成,然后继续执行。
1. `*[llm.agenerate(["我姐姐喜欢什么颜色的花?"]) for _ in range(3)]`:这实际上是一个Python列表解析,它生成了3个 llm.agenerate(...)的异步调用。asyncio.gather将并发地运行这3个调用。
由于这3个llm.agenerate调用是并发的,所以它们不会按顺序执行,而是几乎同时启动,并在各自完成时返回。这意味着,即使其中一个调用由于某种原因需要更长时间,其他调用也不会被阻塞,它们会继续并完成。