使用 FastAPI + React + ZhipuAI 实现一个的AI聊天应用

1,749 阅读5分钟

一、Why?

八月份收到了一条质谱的短信,质朴 GLM-4-Flash 模型免费面向注册用户调用 API 了。

2911ab80fb0513222903af85c48b46b.jpg

image.png

好家伙,这不的尝试自己写一个 AI 聊天软件,对的住,这AI时代的洪流吗?于是开始了探索。

二、效果

下面是自己写一个简单的前后端分离项目, 与zhipu ai 聊天截图。

chat-contents.png

三、开始之前不得不谈:Ollama

image.png

这里为什么这里要提到 Ollama 呢?其实就是之前折腾过 Ollama 了。在实现聊天的技术上有一点点积累(好比处理流式)。

Ollama 可以在本地部署大模型,支持的模型也比较多,在此基础上,也可以实现自己的 AI 应用。也正式这个契机,认识到开源项目: open-webui

四、open-webui

1_DVzotrbc2VAD5YitURZrnA.png

如果折腾过 ollama,那么 open-webui,可能会非常熟悉。目前 open-webui 已经与了 40.3k stars,受欢迎程度可见一斑。我们可以轻松的使用 docker 部署 open-webui 配合 ollama 实现本地模型聊天。

open-webui 使用 svelte + python fastapi 等技术实现,感兴趣的朋友可以自己探讨一些下。

五、技术选型

如果你看过 MongoDB 的 MERN Stack 技术栈,我们这里也简单给我们选择的技术一个定义:RFPS,当然我们也可以做一个三层架构图:

arch.png

RFPS 是的解释如下:

  • R: React 前端
  • F: FastAPI 后端
  • P: Python 后端语言
  • S: Supabase 数据库

当然 AI 要归结到 FastAPI 后端部分,zhipu AI 提供了 SDK 我们可以直接安装

poetry add zhipuai
pip install zhipuai

想一想,如果要使用这些栈,开发者必须是一个全栈开发者,并且还需要有一定的设计能力,才能将事情做的像个样子。

Python 与 FastAPI

fastapi -网页.png

download.jpg 为什么要选择 Python 进行前后端分离?

目前人工智能相关的技术还是以 Python 为主,使用 Python 更加的靠近生态。FastAPI 在些 API 上有绝佳速度。有一点要提出来就是,Python 的装饰器能够直接装饰一个单独的函数。这个点,让人觉得开发接口很舒服,非常人性化。这一点是 JavaScript 和其他语言都没有的(Rust 使用的宏注解和直接装饰函数也类似)。

zhipu AI

download.png

选择 zhipu AI 主要 zhipu 目前有一个免费调用的大模型 glm-4-flash 响应速度非常好。符合我们项目的预期。

React 和 ProChat

OIP.jpg

做一个自己的应用聊天应用,其实需求并不高,首先从开源社区中找方案,前端是 React,那么前端有好的 AI 聊天组件吗?帮我们处理好了对 markdown 文档渲染和聊天对话的抽象,我们可以只专注于业务数据对接。有的 Antd ProChat,

image.png

使用 ProChat 你能像,使用 React 组件一样,进行 AI 聊天。样式问题这里就重点谈论。

Supabase

download.png

也是没有过去多久 Supabase 开始支持正式支持 Python 客户端。使用 Supabase 的优缺点我们分析一下:

优点:

  • 使用简单,不需要 orm 等工具辅助,但又不失灵活性。
  • 能够使用 Supabase 的在线数据库,并且可以本地部署 Supabase。
  • 能够在前端技术栈中直接使用 Supabase 客栈性比较强。

缺点:

  • 如果之前没有接触过过 Supabase, 需要时间渡过学习和实践的过程。

数据结构

有了数据库,我们需要考虑数据结构了问题,数据定的好,前后端对接非常流畅:

  • sse 数据结构

image.png

data = {"id": chat_id, "content": content, "role": role}
stream_response_data = f"data: {json.dumps(data)}\n\n"
  • 数据库

image.png

为了简单起见,数据库

{"id": uuid, "chat": Chat[], "title": str, ...}

所有 chat 操作都建立在此基础上。

六、流式渲染

流式渲染时 AI 聊天应用的标配了,现在有很多的解决方式,其中 SSE 是最常见的一种

  • 后端如何的数据结构
  • 流式渲染(前后端对接)

FastAPI 流式渲染

FastAPI 通过 StreamingResponse 内置支持流式渲染(media_type 是 sse):

@router.post("/chat")
def chat(chatIn: ChatIn, background_tasks: BackgroundTasks):
    gsn = create_chat_service(chatIn, background_tasks)
    return StreamingResponse(
        gsn,
        media_type="text/event-stream",
    )

StreamingResponse 的第一个参数是一个带有 yield 的生成器。我们也简单的看一下:

def generate_stream_update(
    response,
    background_tasks,
    messages,
    chat_id: str,
):
    content_in_db = ""
    for chunk in response:
        if chunk.choices[0].delta:
            delta = chunk.choices[0].delta
            content_in_db += delta.content
            last_chunk_update(chunk, chat_id, messages, background_tasks, content_in_db)
            yield yield_string(chat_id, delta.content, delta.role)

前端流式渲染

前端处理流式渲染我们直接使用 fetch API 即可。fetch API 天然具备处理流式数据的能力。具体细节:

  • 得到流式 response 对象
  • 使用 ReadableStream 解析 response 中的 reader 对象
  • controller 控制流式数据

下面我们看看如何使用 ReadableStream + controller 控制流式数据:

export function genResponseStream(response: any, chatIdRef , cb?: (...args) => any) {
  const reader = response.body.getReader();
  const decoder = new TextDecoder("utf-8");
  const encoder = new TextEncoder();

  return new ReadableStream({
    start(controller) {
      try {
        (async () => {
          while (true) {
            const { done, value } = await reader.read();
            if (done) {
              cb?.()
              controller.close();
              break;
            }
            // 解码并累积数据块
            let result = decoder.decode(value, { stream: true });

            // 处理 SSE 数据块
            const events = result.split("\n\n"); // 假设每个事件块以 '\n\n' 结束
            result = events.pop() || ""; // 留下未处理的部分

            events.forEach((event) => {
              if (event.startsWith("data: ")) {
                const data = event.substring(6).trim();
                // 解析 JSON 数据
                try {
                  const parsedData = JSON.parse(data);
                 
                  if (!chatIdRef.current)  chatIdRef.current = parsedData.id;
                  controller.enqueue(encoder.encode(parsedData.content)); // 将内容写入流
                } catch (e) {
                  controller.close();
                  console.error("Error parsing JSON:", e);
                }
              }
            });
          }
        })();
      } catch (error) {
        controller.close();
        console.log(error);
      }
    },
  });
}

有了以上的核心技术点,实现聊天的功能就很好实现了。

七、项目与开源

其实我们实现一个就要基础功能应用,并且已经开源, 有兴趣的小伙伴可以看一下,如果对你有帮助,欢迎 star 🌟。

🌟fastapi-antd-prochat

更多

八、小结

本文主要介绍了 FastAPI + React + ProChat + zhipuai 实现聊天的应用的思考,技术选型,以及核心知识点讲解,以及开源项目。感兴趣的小伙伴可以自己实现一个 AI 聊天应用。如果文章欢迎点赞和收藏。