概述如何使用开源工具利用流式传输来构建简单的代理聊天机器人
欢迎来到雲闪世界。我们的代理应用程序模型。我们将向您展示如何使用流式传输来构建该模型,以便您可以创建出色的用户体验。图片由作者提供。 在这篇文章中,我们将介绍如何构建一个代理聊天机器人,利用Burr的(我是作者)流式传输功能、FastAPI 的 StreamingResponse和React查询的服务器发送事件 (SSE)向用户发送响应。所有这些都是开源工具。这针对的是那些想要更多地了解 Python 中的流式传输以及如何为其代理/应用程序添加交互性的人。虽然我们使用的工具相当具体,但这些课程应该适用于广泛的流式响应实现。
首先,我们将讨论为什么流式传输很重要。然后,我们将介绍我们使用的开源工具。我们将通过一个示例向您介绍可用于入门的代码,然后分享更多资源和替代实现。
您可以在此处跟踪 Burr + FastAPI 代码,并在此处跟踪前端代码。 您也可以通过运行来运行此示例(您需要一个 OPENAI_API_KEY 环境变量)pip install “burr[start]” && burr
,然后导航到 localhost:7241/demos/streaming-chatbot(浏览器将自动打开,只需单击左侧的 demos/streaming-chatbot 即可。 注意,此示例需要burr>=0.23.0
。
为何要进行流式传输?
虽然通过网络进行流媒体传输是一项90 年代的技术,如今已无处不在(视频游戏、流媒体电视、音乐等),但最近生成式 AI 应用的激增引起了人们对逐字提供和渲染流式文本的兴趣。
LLM 是一项有趣的技术(甚至可能很有用),但运行速度相对较慢,用户不喜欢等待。幸运的是,可以将结果流式传输,以便用户在生成 LLM 响应时看到它。此外,鉴于 LLM 通常机械而沉闷的性质,流式传输可以使它们看起来更具交互性,几乎就像它们在思考一样。
适当的实施将允许跨多个服务边界的流通信,从而使中间代理能够在向用户呈现流数据时增强/存储它。
聊天机器人架构的简单展示
编辑
虽然这些都不是火箭科学,但使 Web 开发变得简单且基本标准化的相同工具(OpenAPI / FastAPI / React + 朋友等)都具有不同程度的支持,这意味着您经常有多种与您习惯不同的选择。流式传输通常是框架设计中的事后考虑,这会导致各种限制,您可能直到构建到一半才知道。
让我们介绍一下用来实现上述堆栈的一些工具,然后看一个示例。
开源工具
我们将利用这些工具来构建这个,它们彼此之间是很好地解耦的 — — 如果你愿意,你可以交换类似的工具,但仍然应用相同的课程/代码。
伯尔
Burr是一个轻量级 Python 库,可用于将应用程序构建为状态机。您可以使用一系列操作(这些操作可以是修饰函数或对象)构建应用程序,这些操作声明来自状态的输入以及来自用户的输入。这些操作指定自定义逻辑(委托给任何框架)以及如何更新状态的说明。状态是不可变的,这允许您在任何给定点检查它。Burr 处理编排、监控、持久性等)。
@action(reads=["count"], writes=["count"]) def counter(state: State) -> State: return state.update(counter=state.get("count", 0) +1)
您将 Burr 操作作为应用程序的一部分运行 — 这使您可以将它们与一系列(可选)从一个操作到另一个操作的条件转换串联在一起。
from burr.core import ApplicationBuilder, default, expr app = ( ApplicationBuilder() .with_actions( count=count, done=done # implementation left out above ).with_transitions( ("counter", "counter", expr("count < 10")), # Keep counting if the counter is < 10 ("counter", "done", default) # Otherwise, we're done ).with_state(count=0) .with_entrypoint("counter") # we have to start somewhere .build() )
Burr 带有一个用户界面,可以实现监控/遥测,以及在执行期间保持状态/执行任意代码的挂钩。
您可以将其可视化为流程图,即图形/状态机:
Burr 免费为您提供这张图片。图片由作者提供。
并使用本地遥测调试器对其进行监控:
操作系统遥测 UI 可告诉您应用程序在任何给定时间点的状态
虽然上述示例只是一个简单的说明,但 Burr 通常用于代理(如本例)、RAG 应用程序和人机交互 AI 界面。请参阅存储库示例,了解一组(更详尽的)用例。稍后我们将介绍流媒体和一些更强大的功能。
快速API
FastAPI是一个框架,可让您在 REST API 中公开 Python 函数。它有一个简单的界面 — — 您编写函数,然后修饰它们,然后运行脚本 — — 通过OpenAPI将其转变为具有自文档化端点的服务器。
@app.get("/") def read_root(): return {"Hello": "World"}
@app.get("/items/{item_id}") def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q}
FastAPI 提供了无数好处。它是异步原生的,通过 OpenAPI 提供文档,并且易于在任何云提供商上部署。它与基础设施无关,通常可以水平扩展(只要考虑到状态管理)。有关更多信息,请参阅此页面。
反应
React 无需介绍 — — 它是一款非常流行的工具,为互联网提供了强大的支持。即使是最近流行的工具(如 next.js/remix)也是基于它构建的。有关更多阅读内容,请参阅react.dev。我们将使用 React 以及typescript和tailwind,但您通常可以用自己喜欢的前端工具替换它,并能够重复使用本文的大部分内容。
构建一个简单的 Agentic 聊天机器人
让我们构建一个简单的代理聊天机器人 — 它具有代理性,因为它实际上进行了两个 LLM 调用:
- 确定要查询的模型的调用。我们的模型将有几个“模式” — — 生成一首诗、回答一个问题等等……
- 对实际模型的调用(在本例中为提示 + 模型组合)
对于 OpenAI API 来说,这更像是一个玩具示例 — 他们的模型是令人印象深刻的万事通。话虽如此,这种工具委托模式在各种各样的 AI 系统中都有体现,这个例子可以清晰地推断出来。
在 Burr 中对代理进行建模
建模为状态机
为了利用 Burr,我们将代理应用程序建模为状态机。基本逻辑流程如下:
我们从用户提示输入(顶部)开始。然后我们检查安全性,如果不安全,我们将使用“不安全”的具体响应。否则,我们决定模式,并根据状态字段模式的值进行切换。每个都返回流式响应。一旦它们完成流式传输,它就会回到提示并等待另一个用户输入……图片来自作者。
为了使用 Burr 对此进行建模,我们将首先使用流式 API创建相应的操作。然后我们将它们绑定在一起作为应用程序。
流媒体动作
在 Burr 中,操作可以利用同步和异步 API。在本例中,我们将使用async。Burr 中的流式函数也可以与非流式操作混合搭配,但为了简化,我们将所有内容实现为流式。因此,无论是从 OpenAPI 流式传输(它有自己的异步流式传输接口),还是返回固定的抱歉,我无法回答这个问题的响应,它仍将实现为生成器。
对于那些不熟悉的人来说,生成器是一种 Python 构造,可以对一系列值进行高效、惰性求值。它们由yield
关键字创建,该关键字将控制权从函数交还给调用者,直到需要下一个项。异步生成器的功能类似,但它们还会在yield上交出对事件循环的控制。阅读有关同步生成器和异步生成器的更多信息。
Burr 中的流式操作被实现为一个生成器,它产生元组,包括:
- 中间结果(在本例中为消息中的增量令牌)
- 如果已完成,则最终状态更新;如果仍在生成,则为 None
因此,最终的产量将表明流已完成,并输出最终结果以供稍后存储/调试。使用一些自定义提示操作代理 OpenAI 的基本响应如下所示:
@streaming_action(reads=["prompt", "chat_history", "mode"], writes=["response"]) async def chat_response( state: State, prepend_prompt: str, model: str = "gpt-3.5-turbo" ) -> AsyncGenerator[Tuple[dict, Optional[State]], None]: """A simple proxy. This massages the chat history to pass the context to OpenAI, streams the result back, and finally yields the completed result with the state update. """ client = _get_openai_client() # code skipped that prepends a custom prompt and formats chat history chat_history_for_openai = _format_chat_history( state["chat_history"], prepend_final_promprt=prepend_prompt) result = await client.chat.completions.create( model=model, messages=chat_history_api_format, stream=True ) buffer = [] async for chunk in result: chunk_str = chunk.choices[0].delta.content if chunk_str is None: continue buffer.append(chunk_str) yield {"delta": chunk_str}, None result = { "response": {"content": "".join(buffer), "type": "text", "role": "assistant"}, } yield result, state.update(**result).append(chat_history=result["response"])
在示例中,我们还有一些其他流式操作 — 这些操作将代表“终端”操作 — 当状态机完成它们时,这些操作将触发工作流暂停。
构建应用程序
要构建应用程序,我们首先要构建一个图形。我们将使用Burr 的Graph API,这样我们就可以把图形的形状与其他应用程序问题分离开来。在 Web 服务中,Graph API 是一种表达状态机逻辑的非常干净的方式。您可以全局构建一次,然后在每个单独的应用程序实例中重复使用它。图形构建器如下所示 — 请注意,它引用了上面的chat_response函数:
# Constructing a graph from actions (labeled by kwargs) and # transitions (conditional or default). graph = ( GraphBuilder() .with_actions( prompt=process_prompt, check_safety=check_safety, decide_mode=choose_mode, generate_code=chat_response.bind( prepend_prompt="Please respond with *only* code and no other text" "(at all) to the following", ), # more left out for brevity ) .with_transitions( ("prompt", "check_safety", default), ("check_safety", "decide_mode", when(safe=True)), ("check_safety", "unsafe_response", default), ("decide_mode", "generate_code", when(mode="generate_code")), # more left out for brevity ) .build() )
最后,我们可以将它们添加到一个应用程序中 — 它公开了与服务器交互的正确的执行方法:
# Here we couple more application concerns (telemetry, tracking, etc…). app = ApplicationBuilder() .with_entrypoint("prompt") .with_state(chat_history=[]) .with_graph(graph) .with_tracker(project="demo_chatbot_streaming") .with_identifiers(app_id=app_id) .build() )
当我们想要运行它时,我们可以调用astream_results。它接收一组停止条件,并返回一个AsyncStreamingResultContainer
(一个缓存结果并确保调用 Burr 跟踪的生成器),以及触发停止的操作。
# Running the application as you would to test, # (in a jupyter notebook, for instance). action, streaming_container = await app.astream_result( halt_after=["generate_code", "unsafe_response", ...], # terminal actions inputs={ "prompt": "Please generate a limerick about Alexander Hamilton and Aaron Burr" } )
async for item in streaming_container: print(item['delta'], end="")
在 Web 服务器中公开
现在我们有了 Burr 应用程序,我们将使用服务器发送事件 (SSE) 与 FastAPI 的流响应 API集成。虽然我们不会深入研究 SSE,但 TL;DR 是它们充当 Web 套接字的单向(服务器 → 客户端)版本。您可以在最后的链接中阅读更多内容。
要在 FastAPI 中使用这些,我们将端点声明为返回 StreamingResponse 的函数 — — 一个包装生成器的类。标准是提供特殊形状的流式响应,“data: <contents> \n\n”。在此处详细了解原因。虽然这主要是为了EventSource API(我们将绕过它而使用 fetch 和getReader()),但我们将保留这种标准格式(以便任何使用 EventSource API 的人都可以重用此代码)。
我们已经单独实现了_get_application
一个实用函数来通过 ID 获取/加载应用程序。
该函数将是一个 POST 端点,因为我们正在向服务器添加数据,尽管也可以很容易地成为一个 PUT。
@app.post("/response/{project_id}/{app_id}", response_class=StreamingResponse) async def chat_response(project_id: str, app_id: str, prompt: PromptInput) -> StreamingResponse: """A simple API that wraps our Burr application.""" burr_app = _get_application(project_id, app_id) chat_history = burr_app.state.get("chat_history", []) action, streaming_container = await burr_app.astream_result( halt_after=chat_application.TERMINAL_ACTIONS, inputs=dict(prompt=prompt.prompt) )
async def sse_generator(): yield f"data: {json.dumps({'type': 'chat_history', 'value': chat_history})}\n\n"
async for item in streaming_container: yield f"data: {json.dumps({'type': 'delta', 'value': item['delta']})} \n\n"
return StreamingResponse(sse_generator())
请注意,我们在函数内部定义了一个生成器,它包装了 Burr 结果并将其转换为 SSE 友好的输出。这使我们能够对结果施加一些结构,我们将在前端使用这些结构。不幸的是,我们必须自己解析它,因为 fastAPI 不支持对 StreamingResponse 进行严格类型化。
此外,我们在执行之前实际上会在开始时就生成整个状态。虽然这并非绝对必要(我们也可以有一个单独的聊天历史记录 API),但它将使渲染更加容易。
为了测试这一点,您可以使用请求库Response.iter_lines API。
构建 UI
现在我们已经准备好了服务器、状态机和 LLM,让我们让它看起来更漂亮!这就是所有东西联系在一起的地方。虽然您可以下载并使用示例中的全部代码,但我们将重点关注单击“发送”时查询 API 的函数。
编辑
UI 如下所示。您可以通过 Burr 附带的打包 Telemetry UI 运行它。图片由作者提供。
首先,让我们使用 fetch 查询我们的 API(显然要根据您的端点进行调整,在这种情况下,我们将所有 /api 调用代理到另一台服务器…):
// A simple fetch call with getReader() const response = await fetch( `/api/v0/streaming_chatbot/response/${props.projectId}/${props.appId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: currentPrompt }) } ); const reader = response.body?.getReader();
这看起来像一个普通的旧 API 调用,利用了 typescript异步 API。这将提取一个读取器对象,这将帮助我们在结果传入时进行流式传输。
让我们定义一些数据类型来利用我们上面创建的结构。除了数据类型(使用openapi-typescript-codegenChatItem
生成)之外,我们还将定义两个类,它们对应于服务器返回的数据类型。
// Datatypes on the frontend. // The contract is loose, as nothing in the framework encodes it type Event = { type: 'delta' | 'chat_history'; };
type ChatMessageEvent = Event & { value: string; };
type ChatHistoryEvent = Event & { value: ChatItem[]; };
接下来,我们将遍历读取器并进行解析。这假设 React 中存在以下状态变量:
setCurrentResponse
/currentResponse
setDisplayedChatHistory
我们通读并根据“数据:”进行拆分,然后循环拆分并根据事件类型进行解析/反应。
// Loop through, continually getting the stream. // For each item, parse it as our desired datatype and react appropriately. while (true) { const result = await reader.read(); if (result.done) { break; } const message = decoder.decode(result.value, { stream: true }); message .split('data: ') .slice(1) .forEach((item) => { const event: Event = JSON.parse(item); if (event.type === 'chat_history') { const chatMessageEvent = event as ChatHistoryEvent; setDisplayedChatHistory(chatMessageEvent.value); } if (event.type === 'delta') { const chatMessageEvent = event as ChatMessageEvent; chatResponse += chatMessageEvent.value; setCurrentResponse(chatResponse); } }); }
我们省略了一些清理/错误处理代码(清除、在请求之前/之后初始化状态变量、处理失败等) — — 您可以在示例中看到更多。
最后,我们可以渲染它(注意,这是指在上面的代码之外设置/取消设置的附加状态变量,以及仅显示带有适当图标的聊天消息的 ChatMessage 反应组件)。
<!-- More to illustrates the example --> <div className="flex-1 overflow-y-auto p-4 hide-scrollbar" id={VIEW_END_ID}> {displayedChatHistory.map((message, i) => ( <ChatMessage message={message} key={i} /> ))} {isChatWaiting && ( <ChatMessage message={{ role: ChatItem.role.USER, content: currentPrompt, type: ChatItem.type.TEXT }} /> )} {isChatWaiting && ( <ChatMessage message={{ content: currentResponse, type: ChatItem.type.TEXT, role: ChatItem.role.ASSISTANT }} /> )} </div> <!-- Note: We've left out the isChatWaiting and currentPrompt state fields above, see StreamingChatbot.tsx for the full implementation. -->
我们终于有了完整的应用程序!点击此处查看全部代码。
替代的 SSE 工具
请注意,我们上面介绍的只是使用 FastAPI/react/Burr 进行流式传输的一种方法。您还可以使用许多其他工具,包括:
- EventSource API — 标准但仅限于获取/请求
- FetchEventSource API (似乎无人维护,但构建良好)
以及许多其他博客文章(非常棒!我读了这些文章作为入门)。这些文章也会让您更好地了解建筑。
包起来
在这篇文章中,我们涵盖了很多内容 — — 我们讨论了 Burr、FastAPI 和 React,讨论了如何使用 OpenAI API 构建流式代理聊天机器人,构建了整个堆栈,并一路传输数据!虽然您可能不会使用每一种技术,但各个部分应该能够独立工作。
要下载并使用此示例,您可以运行:
pip install "burr[start]" burr # will open up in a new window
请注意,您需要一个来自 OpenAI 的 API 密钥才能完成此特定演示。您可以在此处找到 Burr + FastAPI 代码,并在此处找到前端代码。
感谢关注雲闪世界。(亚马逊aws和谷歌GCP服务协助解决云计算及产业相关解决方案)
订阅频道(https://t.me/awsgoogvps_Host)
TG交流群(t.me/awsgoogvpsHost)