一篇涵盖agent和工具的简单流到复杂流的指南。
对于LLM来说,流式传输已成为一个越来越受欢迎的特性。他的理念是在生成过程中迅速返回令牌,而不是等到完整响应生成后再返回内容。
对于简单的用例来说,流实际上非常容易实现,但当我们开始引入像agent这样具有自身运行逻辑的东西时,它可能变得复杂,因为这些逻辑可能会阻碍我们进行流的尝试。幸运的是,我们可以让他起作用----只是需要一些额外的努力。
我们将从简单的实现开始,为大语言模型在终端中实现流式传输,但是在笔记的最后,我们将通过FastApi为Agent实现更加复杂的流任务。
首先,让我们安装所有我们即将使用的library。
!pip install -qU \
openai==0.28.0 \
langchain==0.0.301 \
fastapi==0.103.1 \
"uvicorn[standard]"==0.23.2
LLM流式传输到stdout
流的最简单的形式就是是简单的在生成token时将他们打印出来。为了设置这个,我们需要使用两个特定的参数初始化一个支持流式传输的LLM(并不是所有的模型都支持)。
streaming=True
,打开流式传输callbacks=[SomeCallBackHere()]
,在这里传递一个LangChain的callback类,(或者多个callback的列表)。
streaming
参数不言而喻。回调参数和回调类则不太好理解---他们实际上充当一些小代码块,在我们的LLM生成每个令牌是执行某些操作。如前所述,流式传输最简单的形式是,在生成token时打印他们,例如使用StreamingStdOutCallbackHandler
。
import os
from langchain.chat_models import ChatOpenAI
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY") or "YOUR_API_KEY"
llm = ChatOpenAI(
openai_api_key=os.getenv("OPENAI_API_KEY"),
temperature=0.0,
model_name="gpt-3.5-turbo",
streaming=True, # ! important
callbacks=[StreamingStdOutCallbackHandler()] # ! important
)
如果我们运行LLM,我们将看到响应正在被流式输出的。
from langchain.schema import HumanMessage
# create messages to be passed to chat LLM
messages = [HumanMessage(content="tell me a long story")]
llm(messages)
这出奇的容易,但一旦我们开始使用aagent,事情就会变得复杂的多。让我们先初始化一个agent。
from langchain.memory import ConversationBufferWindowMemory
from langchain.agents import load_tools, AgentType, initialize_agent
# initialize conversational memory
memory = ConversationBufferWindowMemory(
memory_key="chat_history",
k=5,
return_messages=True,
output_key="output"
)
# create a single tool to see how it impacts streaming
tools = load_tools(["llm-math"], llm=llm)
# initialize the agent
agent = initialize_agent(
agent=AgentType.CHAT_CONVERSATIONAL_REACT_DESCRIPTION,
tools=tools,
llm=llm,
memory=memory,
verbose=True,
max_iterations=3,
early_stopping_method="generate",
return_intermediate_steps=False
)
我们已经将StreamingStdOutCallbackHandler
添加到代理中,因为我们使用了与该回调相同的LLM。那么让我们看看在运行代理时会得到什么。
prompt = "Hello, how are you?"
agent(prompt)
不错,但我们现在面临的问题是如何流式传输来自LLM的整个输出。因为我们正在使用agent,所以LLM被指示输出我们这里看到的JSON格式,以便agent逻辑可以处理工具使用,多'思考'步骤,等等。例如,如果我们问一个数学问题,我们将看到以下内容:
agent("what is the square root of 71?")
在开发过程中看到这个很有趣,但在实际用例中,我们都希望对这个流进行一些清理。为此,我们可以采用两种方法---要门构建自定义回调处理器,要么使用LangChain中专门构建的回调处理器(像往常一样,LangChain对任何情况都有解决方案)。让我们来尝试LangChain专门构建的FinalStreamingStdOutCallbackHandler
。
我们将覆盖在这里找到的已有的callbacks
属性:
agent.agent.llm_chain.llm
使用新的回调器:
from langchain.callbacks.streaming_stdout_final_only import (
FinalStreamingStdOutCallbackHandler,
)
agent.agent.llm_chain.llm.callbacks = [
FinalStreamingStdOutCallbackHandler(
answer_prefix_tokens=["Final", "Answer"]
)
]
让我们来试用一下:
agent("what is the square root of 71?")
不太对,我们确实应该清理一下answer_prefix_tokens
参数,但这很难做大。通常使用自定义回调处理器会更容易,如下所示:
import sys
class CallbackHandler(StreamingStdOutCallbackHandler):
def __init__(self):
self.content: str = ""
self.final_answer: bool = False
def on_llm_new_token(self, token: str, **kwargs: any) -> None:
self.content += token
if "Final Answer" in self.content:
# now we're in the final answer section, but don't print yet
self.final_answer = True
self.content = ""
if self.final_answer:
if '"action_input": "' in self.content:
if token not in ["}"]:
sys.stdout.write(token) # equal to `print(token, end="")`
sys.stdout.flush()
agent.agent.llm_chain.llm.callbacks = [CallbackHandler()]
让我们再试一次:
agent("what is the square root of 71?")
agent.agent.llm_chain.llm
这个不算完美,但情况正在好转。现在,在绝大多数场景,我们不太可能只是简单的打印输出到终端或者一个notebook。当我们想要做一些更复杂的事情,比如将数据流式输入到另外一个API,我们需要做一些不一样的事情。
在Agent中使用FastAPI
在大多数场景中,我们需要将我们的LLM、Agent等等设置到API之后。让我们把这个也加入进来,看看如何使用FastApi为Agent实现流式传输。
首先,我们将创建一个简答的main.py
脚本来包含我们的FastAPI逻辑。源码看这里
为了执行这个API,请导航到该目录并运行uvicorn main:app --reload
。一旦完成,你可以通过查看下一个单元的输出中的🤙
状态来确认他正在运行:
import requests
res = requests.get("http://localhost:8000/health")
res.json()
{'status': '🤙'}
res = requests.get("http://localhost:8000/chat",
json={"text": "hello there!"}
)
res
<Response [200]>
res.json()
{'input': 'hello there!',
'chat_history': [],
'output': 'Hello! How can I assist you today?'}
与我们的Stdout流式输出不同,我们现在需要将我们的token传递给一个生成器函数,该函数通过StreamingResponse对象将这些令牌传递给FastAPI。为此,我们需要使用异步代码,否则我们的生成器在生成完成之前并不会开始发出任何事情。
我们的回调处理器访问队列,每当token生成时,它将token放入到队列中。我们的生成函数异步的检测队列中是否有新的token被添加进来。一旦生成器看到有令牌被添加,它就会获取该令牌并将其传递给我们的StreamingResponse
为了看到他的实际效果,我们将定义一个名为get_stream
的流请求函数:
def get_stream(query: str):
s = requests.Session()
with s.get(
"http://localhost:8000/chat",
stream=True,
json={"text": query}
) as r:
for line in r.iter_content():
print(line.decode("utf-8"), end="")
get_stream("hi there!")
"Hello! How can I assist you today?"