一、Why?
八月份收到了一条质谱的短信,质朴 GLM-4-Flash 模型免费面向注册用户调用 API 了。
好家伙,这不的尝试自己写一个 AI 聊天软件,对的住,这AI时代的洪流吗?于是开始了探索。
二、效果
下面是自己写一个简单的前后端分离项目, 与zhipu ai 聊天截图。
三、开始之前不得不谈:Ollama
这里为什么这里要提到 Ollama 呢?其实就是之前折腾过 Ollama 了。在实现聊天的技术上有一点点积累(好比处理流式)。
Ollama 可以在本地部署大模型,支持的模型也比较多,在此基础上,也可以实现自己的 AI 应用。也正式这个契机,认识到开源项目: open-webui
四、open-webui
如果折腾过 ollama,那么 open-webui,可能会非常熟悉。目前 open-webui 已经与了 40.3k stars,受欢迎程度可见一斑。我们可以轻松的使用 docker 部署 open-webui 配合 ollama 实现本地模型聊天。
open-webui 使用 svelte + python fastapi 等技术实现,感兴趣的朋友可以自己探讨一些下。
五、技术选型
如果你看过 MongoDB 的 MERN Stack 技术栈,我们这里也简单给我们选择的技术一个定义:RFPS,当然我们也可以做一个三层架构图:
RFPS 是的解释如下:
- R: React 前端
- F: FastAPI 后端
- P: Python 后端语言
- S: Supabase 数据库
当然 AI 要归结到 FastAPI 后端部分,zhipu AI 提供了 SDK 我们可以直接安装
poetry add zhipuai
pip install zhipuai
想一想,如果要使用这些栈,开发者必须是一个全栈开发者,并且还需要有一定的设计能力,才能将事情做的像个样子。
Python 与 FastAPI
为什么要选择 Python 进行前后端分离?
目前人工智能相关的技术还是以 Python 为主,使用 Python 更加的靠近生态。FastAPI 在些 API 上有绝佳速度。有一点要提出来就是,Python 的装饰器能够直接装饰一个单独的函数。这个点,让人觉得开发接口很舒服,非常人性化。这一点是 JavaScript 和其他语言都没有的(Rust 使用的宏注解和直接装饰函数也类似)。
zhipu AI
选择 zhipu AI 主要 zhipu 目前有一个免费调用的大模型 glm-4-flash 响应速度非常好。符合我们项目的预期。
React 和 ProChat
做一个自己的应用聊天应用,其实需求并不高,首先从开源社区中找方案,前端是 React,那么前端有好的 AI 聊天组件吗?帮我们处理好了对 markdown 文档渲染和聊天对话的抽象,我们可以只专注于业务数据对接。有的 Antd ProChat,
使用 ProChat 你能像,使用 React 组件一样,进行 AI 聊天。样式问题这里就重点谈论。
Supabase
也是没有过去多久 Supabase 开始支持正式支持 Python 客户端。使用 Supabase 的优缺点我们分析一下:
优点:
- 使用简单,不需要 orm 等工具辅助,但又不失灵活性。
- 能够使用 Supabase 的在线数据库,并且可以本地部署 Supabase。
- 能够在前端技术栈中直接使用 Supabase 客栈性比较强。
缺点:
- 如果之前没有接触过过 Supabase, 需要时间渡过学习和实践的过程。
数据结构
有了数据库,我们需要考虑数据结构了问题,数据定的好,前后端对接非常流畅:
- sse 数据结构
data = {"id": chat_id, "content": content, "role": role}
stream_response_data = f"data: {json.dumps(data)}\n\n"
- 数据库
为了简单起见,数据库
{"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 + React + ProChat + zhipuai 实现聊天的应用的思考,技术选型,以及核心知识点讲解,以及开源项目。感兴趣的小伙伴可以自己实现一个 AI 聊天应用。如果文章欢迎点赞和收藏。