写点什么

【代码实践】使用 FastAPI+Supabase+LangChain 开发 AI 应用

作者:张文平
  • 2023-08-22
    湖北
  • 本文字数:9081 字

    阅读完需:约 30 分钟

【代码实践】使用FastAPI+Supabase+LangChain开发AI应用

为什么选择这三个组合

  • OpenAI 官方 SDK 是 Python,此开发语言首选 Python

  • FastAPI 是 Python 语言编写的高性能的现代化 Web 框架

  • LangChain 是 AI 应用开发的主流框架,能方便的组合各种 AI 技术进行应用开发

  • MemFire Cloud 提供 Supabase 托管,LangChain 原生支持 Supabase API

  • MemFire Cloud 提供向量数据库支持,向量数据库是开发知识库应用的必选项

FastAPI 介绍

FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框架,使用 Python 3.6+ 开发。

关键特性:

  • 「快速」:可与 NodeJS 和 Go 并肩的极高性能。最快的 Python web 框架之一。

  • 「高效编码」:提高功能开发速度约 200% 至 300%。

  • 「更少 bug」:减少约 40% 的人为(开发者)导致错误。

  • 「智能」:极佳的编辑器支持。处处皆可自动补全,减少调试时间。

  • 「简单」:设计的易于使用和学习,阅读文档的时间更短。

  • 「简短」:使代码重复最小化。通过不同的参数声明实现丰富功能。bug 更少。

  • 「健壮」:生产可用级别的代码。还有自动生成的交互式文档。

  • 「标准化」:基于(并完全兼容)API 的相关开放标准:OpenAPI(以前被称为 Swagger) 和 JSON Schema。

官方文档:https://fastapi.tiangolo.com/zh/

ChatGPT 介绍

准确来说 ChatGPT 只是 openai 基于 GPT 模型开发的一个应用,只不过这个词更流行,更广为人知。对于开发者来说,更准确的说法是 GPT 模型。目前 openai 提供的模型包括:

每个模型下面又分别有很多细分的模型,在文本和代码生成场景,官方推荐使用两个模型:gpt-3.5-turbo or gpt-4,本文使用目前成本更优的gpt-3.5-turbo 。相对应的, gpt-4 能理解更复杂的指令,也会尽可能不胡言乱语,但是 gpt-4 成本要高一些,推理速度要慢一些。

GPT 模型的应用场景:

  • 编写各种类型的文档,包括学术论文

  • 生成代码

  • 基于知识库进行问题回答

  • 分析文本,包括阅读论文、解读代码等都能胜任

  • 对话工具,比如客服

  • 为软件提供自然语言接口

  • 教学导师

  • 翻译

  • 游戏角色 AI

openai 的更多资源请参考:

https://platform.openai.com/docs

https://github.com/openai/openai-cookbook

MemFire Cloud 介绍

本文主要使用了 MemFire Cloud 的 BaaS 服务提供的数据库自动生成 API 以及向量数据库能力,用以存储和检索 embedding 之后的向量数据。MemFire Cloud 的 BaaS 服务还提供了其他一些方便开发者进行应用开发的功能:

  • 完整的用户注册、登录、权限管理 API,web 前端和 App 开发人员可以直接调用,几行代码完成用户注册登录功能。

  • 微信小程序、手机短信验证码、github、apple 等第三方用户接入支持。

  • 最流行的 postgres 关系数据库支持,自动生成增删查改 API 接口,支持向量化插件 pgvector。

  • 提供对象存储接口,方便应用开发者上传下载图片、文档等。

  • 静态托管和自定义域名服务,可以让开发者不需要为部署前端代码专门购买云服务器。

  • 云函数可以方便处理复杂和敏感的业务逻辑,也可以用来托管完整的 api 服务。

MemFire Cloud 更多信息,请参考:https://memfiredb.com

GPT 初体验

下面是 openai 官方的一个例子:

import osimport openaiopenai.organization = "org-kjUiGhsu6S3CI2MUch25dMOF"openai.api_key = os.getenv("OPENAI_API_KEY")
openai.ChatCompletion.create(  model="gpt-3.5-turbo",  messages=[        {"role": "system", "content": "You are a helpful assistant."},        {"role": "user", "content": "Who won the world series in 2020?"},        {"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},        {"role": "user", "content": "Where was it played?"}    ])
复制代码

参数必填项:

  • model:模型选择,这里用的是 gpt-3.5-turbo

  • messages:信息,可以只包含一个问题,也可以包含更多上下文,以帮助 AI 更准确的回答问题。

AI 开发的最主要的工作就是组装合适的 messages,以达到更精确的回答用户问题的目的。一条 message 包含 role 和 content 两个元素。其中 role 包含:

  • system:相当于系统设定,一般用来告诉 ai 他要扮演什么角色,如何回答问题等。

  • assistant:辅助信息,帮助 AI 更好的理解用户的当前问题。通常用于包含多轮对话的上下文信息,也可以用来给出一些问答样例。

  • user:用户的提问内容。

FastAPI 编写后端服务

如果你使用过 FastAPI,可以跳过本节内容。

如果你有 flask 或 django 的基础,强烈建议阅读官方的这两个教程,可以帮助你快速了解 fastapi 的使用方法。

https://fastapi.tiangolo.com/zh/tutorial/sql-databases/

https://fastapi.tiangolo.com/zh/tutorial/bigger-applications/

如果你是 web 服务的新手,建议从头阅读 fastapi 的教程文档,写的非常好。

https://fastapi.tiangolo.com/zh/tutorial/first-steps/

我们先以上面 openai 的官方示例,来看一下如何使用 fastapi 来编写服务端代码,完成与 openai 的交互,并暴露接口给你的 web 或 app 客户端使用。为了方便,我们将所有代码放在一个 main.py 中演示,如果你要实现一个完整的应用,建议参考大型应用开发这篇教程,模块化组织你的代码。

接口定义

一个简单的可运行的接口定义:

from fastapi import FastAPIapp = FastAPI()
@app.get("/hello")async def hello():    return {"message": "你好"}
复制代码

安装下列依赖就可以运行了:

# 安装依赖pip install "uvicorn[standard]==0.23.1" "fastapi[all]==0.99.1"
# 运行服务uvicorn main:app --reload
复制代码

访问接口:

curl http://127.0.0.1:8000/hello
复制代码

封装 openai 调用接口

了解了 fastapi 的接口定义方法,我们就能很快将 openai 的官方示例封装成接口:

from fastapi import FastAPIimport openai
app = FastAPI()
openai.organization = "org-kjUiGhsu6S3CI2MUch25dMOF"openai.api_key = os.getenv("OPENAI_API_KEY")
@app.get("/openai")async def openai():    return openai.ChatCompletion.create(      model="gpt-3.5-turbo",      messages=[            {"role": "system", "content": "You are a helpful assistant."},            {"role": "user", "content": "Who won the world series in 2020?"},            {"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},            {"role": "user", "content": "Where was it played?"}        ]    )
复制代码

错误处理

在调用 openai 的接口时,如果发生错误,openai 会抛出异常。在 FastAPI 的服务代码中,如果我们不处理 openai 的异常,FastAPI 会将异常抛出,并返回客户端 500 Internal Server Error。通常我们需要以更结构化的方式将具体的错误信息返回给接口调用者。

FastAPI 提供了异常处理的回调接口,以 openai 的异常为例,可以通过如下方式注册异常处理函数,以更友好、统一的结构返回错误信息:

from fastapi import FastAPI, Requestfrom fastapi.responses import JSONResponsefrom openai import OpenAIError
app = FastAPI()
# 捕获异常,并返回JSON格式的错误信息@app.exception_handler(OpenAIError)async def openai_exception_handler(request: Request, exc: OpenAIError):    return JSONResponse(        status_code=exc.http_status,        content={'code': exc.code, 'message': exc.error},    )
复制代码

了解了 FastAPI 的 api 定义方法和错误处理方法,我们基本上可以完成一个简单的 web 服务程序了。

基于 openai 开发知识库应用的基本原理

LangChain 的文档中对 QA 场景的 AI 应用开发有比较具体的讲解,感兴趣的可以深入阅读:

https://python.langchain.com/docs/use_cases/question_answering/

下图是知识库类 AI 应用的基本开发流程:


从图中可以看到,大致可以分为如下步骤:

  1. 文档处理:将 pdf、docx、数据库中的数据转换成文本内容。目前 openai 的 chat 模型接收的都是文本数据。

  2. 数据切分:将处理过后的文本内容进行切分。切分的目的是方便语义化检索,让检索内容更精确,另外也是为了适配 AI 接口的限制。gpt-3.5-turbo 的最大 tokens 数量是 4k,gpt-3.5-turbo-16k 也只有 16k,因此文档切分是必需的步骤。

  3. 向量化:openai 提供了 embedding 模型,专门用来将文本转换成向量化数据,向量化的目的是方便后续根据用户的输入来检索相似度高的知识。这里就需要用到向量数据库来存储 embedding 模型的输出。 以上是知识库处理步骤,这一部分通常是运行在应用的后台,需要持续不断的获取最新的知识(比如最新的产品文档、技术手册),并更新向量数据库中的数据。

接下来是用户问答的处理流程:

  1. 知识检索:将用户的问题向量化,然后到向量化数据库中检索相似度最高的知识(可以根据需要选取相似度高的前 n 项)。

  2. AI 辅助解答:将检索到的知识以及设定好的 prompt 信息一起发送给 ai 模型,ai 模型会结合自己已有的知识以及知识库中检索到的知识进行最终的答案生成。

LangChain 基本用法

根据上面的流程,我们完全可以自主的实现一个 ai 应用了,为什么要引入 LangChain 呢?

如果你通读了 LangChain 的文档(https://python.langchain.com/docs/use_cases/question_answering/),对于如何借助 LangChain 完成知识库应用应该有了基本的认识。结合上面的基本原理,我们来看下 LangChain 能为我们提供哪些能力。

  1. 数据加载能力

在上面的步骤中,我们首先要完成的是将已有的知识文档处理成文本数据。LangChain 目前已经内置类非常多的文档类型处理能力,包括常见的 pdf、docx、markdown、html、json、csv 等,同时兼容了一百多种数据源,几乎囊括了市面上所有最常用的服务,包括 S3、Bilibili、EverNote、Github、Hacker News、Slack、Telegram 等等。

下面是加载 Web 数据的 WebBaseLoader 的使用方法:

from langchain.document_loaders import WebBaseLoaderloader = WebBaseLoader("https://lilianweng.github.io/posts/2023-06-23-agent/")data = loader.load()
复制代码
  1. 数据切分能力

LangChain 提供了文本切分工具,可以方便的将加载后的文本进行切分处理。上面将网页内容加载到 data 对象之后,可以使用 RecursiveCharacterTextSplitter 进行文本切分:

from langchain.text_splitter import RecursiveCharacterTextSplittertext_splitter = RecursiveCharacterTextSplitter(chunk_size = 500, chunk_overlap = 0)all_splits = text_splitter.split_documents(data)
复制代码
  1. 向量化能力

LangChain 支持常见的向量化数据库以及 embedding 模型接入能力,以 MemFire Cloud 托管的 SupaBase 和 openai embedding 模型为例(参考https://python.langchain.com/docs/integrations/vectorstores/supabase):

import osfrom supabase.client import Client, create_clientfrom langchain.vectorstores import SupabaseVectorStorefrom langchain.embeddings import OpenAIEmbeddings
supabase_url = os.environ.get("SUPABASE_URL")supabase_key = os.environ.get("SUPABASE_SERVICE_KEY")client: Client = create_client(supabase_url, supabase_key)
vector_store = SupabaseVectorStore.from_documents(    all_splits, OpenAIEmbeddings(), client=client)
复制代码

要使用 LangChain + pgvector 的向量化数据库能力,需要在 MemFire Cloud 上创建应用,并开启 vector 扩展,然后创建 documents 表和函数。可以使用 SQL 语句完成这些操作:

-- Enable the pgvector extension to work with embedding vectorscreate extension vector;
-- Create a table to store your documentscreate table documents (    id bigserial primary key,    content text, -- corresponds to Document.pageContent    metadata jsonb, -- corresponds to Document.metadata    embedding vector(1536) -- 1536 works for OpenAI embeddings, change if needed);
CREATE FUNCTION match_documents(query_embedding vector(1536), match_count int)   RETURNS TABLE(       id uuid,       content text,       metadata jsonb,       -- we return matched vectors to enable maximal marginal relevance searches       embedding vector(1536),       similarity float)   LANGUAGE plpgsql   AS $   # variable_conflict use_columnBEGIN   RETURN query   SELECT       id,       content,       metadata,       embedding,       1 -(documents.embedding <=> query_embedding) AS similarity   FROM       documents   ORDER BY       documents.embedding <=> query_embedding   LIMIT match_count;END;$;
复制代码
  1. 知识检索

上面介绍 LangChain 组合 openai embedding 和 pgvector 进行向量化处理和存储,LangChain 的 vectorstore 可以直接实现向量化检索功能,将用户的问题转换为最切近的知识库数据:

query = "How to build llm auto agent"matched_docs = vector_store.similarity_search(query)
复制代码

matched_docs 就是与用户提问相关性最高的的知识库内容。

接下来就可以将 matched_docs + 用户的提问打包提交给 AI,让 AI 帮我们生成最终的答案了。

如何将知识库和用户问题打包提交给 openai

在 GPT 初体验章节,我们已经介绍了 GPT 接口的使用方法和参数含义,这里我们可以使用 assistant 角色将我们的知识库打包成 messages,然后将用户的问题以 user 角色打包到 messages 中,最后调用 openai 的接口:

messages=[   {"role": "assistant", "content": doc.page_content} for doc in matched_docs]messages.append({"role": "user", "content": query})response = openai.ChatCompletion.create("gpt-3.5-turbo", messages=messages)
复制代码

你也可以将文档和问题全部打包成 user 角色的 message,大概的格式:

content = '\n'.join[doc.page_content for doc in matched_docs]content += f'\n问题:{query}'messages=[   {"role": "user", "content": content }]response = openai.ChatCompletion.create("gpt-3.5-turbo", messages=messages)
复制代码

如何让 AI 生成更符合预期的结果

我们都知道,ChatGPT 有的时候会胡言乱语,一般会发生在 GPT 模型中没有相关的知识的时候。为了让 AI 回答的更严谨,我们可以给 AI 一些明确的指令,比如:

docs = '\n'.join[doc.page_content for doc in matched_docs]content = f"'''{docs}'''"content += f'\n问题:{query}'
messages = [    {"role": "system", "content": "我会将文档内容以三引号(''')引起来发送给你,如果你无法从我提供的文档内容中找到答案,请回答:\"我无法找到该问题的答案\"。请使用中文回答问题。"},    {"role": "user", "content": content }]response = openai.ChatCompletion.create("gpt-3.5-turbo", messages=messages)
复制代码

这里有一份 GPT 的最佳实践(https://platform.openai.com/docs/guides/gpt-best-practices)可以参考。

使用 MemFire Cloud 作为 AI 的记忆体

openai 的 GPT 模型本身是没有记忆力的,如果我们希望知识库应用能像 ChatGPT 一样跟使用者进行连续的对话,需要让我们的应用有记忆能力,并将记忆的信息在下一次对话时发送给 openai 的模型,以便模型了解前面跟用户聊了些什么。

另外 openai 的接口是有 token 限制的,当连续对话的内容超出了一次 api 调用的 token 限制时,需要压缩历史对话信息。有两种压缩方式:

「方式 1」:让 openai 提炼历史对话的概要信息,然后使用概要信息加最新问题进行问答;

「方式 2」:从历史对话中检索与最新问题相关性比较高的内容,发送给 openai;不论哪种方式,你都需要对当前会话的历史数据进行记录:

  1. 方式 1:需要记录不断迭代的摘要信息。因为 token 数量限制,你不能一次性获得所有历史对话的摘要,因此需要不停的叠加历史摘要和最新对话数据,生成新的摘要并以会话 id 为标识,记录到数据库中。

  2. 方式 2:需要将历史对话的信息通过 embedding 模型向量化,要并为每个会话构建上下文知识库索引,检索的时候,只检索当前会话的相似内容。

Supabase SDK 提供了非常方便的操作数据库的接口,以下为记录会话历史消息的表以及基本的操作方法:

-- 历史消息表定义create table history_messages (    id bigserial primary key,    session_id text, -- 会话id    role text, -- 会话角色(user or ai)    message text, -- 会话信息    create_at timestamp default(now()) -- 创建时间)
复制代码

操作历史消息表的方法:

import osfrom supabase.client import Client, create_clientfrom langchain.vectorstores import SupabaseVectorStore
# 初始化客户端url = os.environ.get("SUPABASE_URL")key = os.environ.get("SUPABASE_SERVICE_KEY")client: Client = create_client(url, key)
# 往会话xxxxx插入一条历史消息client.table("history_messages").insert({"session_id": "xxxxx", "role": "user", "message": "你好"}).execute()
# 查询会话id是xxxxx的所有历史消息client.table("history_messages").select("*").eq("session_id", "xxxxx").execute()
复制代码

「完整代码」

前面我们讲解了使用 FastAPI+SupaBase+LangChain 进行 GPT 知识库开发的基本原理和关键路径的代码实现。完整的实现代码已经上传到了 github,感兴趣的可以自己玩一下:

GitHub - iswarezwp/supabase-qa: 使用 FastAPI+Supabase+LangChain 开发 GPT 应用(地址:https://github.com/iswarezwp/supabase-qa)

代码主要完成了如下一些基本功能:

  • 使用 FastAPI 作为 Web 服务端框架完成了基本的 Web 服务端开发

  • 使用 MemFire Cloud 作为向量数据和个人文档数据存储

  • 使用 LangChain 进行 AI 应用开发,加载本地磁盘目录上的文档,计算 embedding、存储到向量数据库

  • 使用 OpenAI 的 GPT 模型,完成问答功能实现

  • 使用 Next.js 开发了一个简单的 UI 界面用于问答演示

下面主要介绍一下如何部署使用该代码。

准备工作

  1. 首先我们需要一个 SupaBase 应用

MemFire Cloud 提供了 SupaBase 应用的托管,因此可以先在 MemFire Cloud 上创建应用,后面需要用到应用的 API URL 和 Service Role Key。可以在应用的应用设置->API 页面找到相应的配置。

  1. 创建应用后,在应用的 SQL 执行器页面执行如下脚本

-- Enable the pgvector extension to work with embedding vectorscreate extension vector;
-- Create a table to store your documentscreate table documents (    id uuid primary key,    content text, -- corresponds to Document.pageContent    metadata jsonb, -- corresponds to Document.metadata    embedding vector(1536) -- 1536 works for OpenAI embeddings, change if needed);
CREATE FUNCTION match_documents(query_embedding vector(1536), match_count int)   RETURNS TABLE(       id uuid,       content text,       metadata jsonb,       -- we return matched vectors to enable maximal marginal relevance searches       embedding vector(1536),       similarity float)   LANGUAGE plpgsql   AS $   # variable_conflict use_columnBEGIN   RETURN query   SELECT       id,       content,       metadata,       embedding,       1 -(documents.embedding <=> query_embedding) AS similarity   FROM       documents   ORDER BY       documents.embedding <=> query_embedding   LIMIT match_count;END;$;
复制代码
  1. 准备好用来测试的文档目录

默认需要将文档放到app/docs下,可以通过环境变量指定其他目录

  1. 准备好 openai 的账号请参考网上教程申请一个 openai 账号,后面代码运行需要用到 openai 的 API KEY

如何运行

linux 下运行

  • 安装依赖

pip install -r app/requirements.txt
复制代码
  • 设置参数 SUPABASE_URL/SUPABASE_KEY 分别对应应用URLservice_role密钥。注意 service_role 秘钥具有比较高的数据库操作权限,只能用于服务端配置,不要泄漏。

export DOCS_PATH=./docsexport SUPABASE_URL="your-api-url"export SUPABASE_KEY="your-service-role-key"export OPENAI_API_KEY="your-openai-api-key"
复制代码
  • 运行

uvicorn main:app --reload --host 0.0.0.0
复制代码
  • docker 运行

docker build -t memfirecloud-qa:v1 .docker run -p 8000:80 \    -e SUPABASE_URL="your-api-url" \    -e SUPABASE_KEY="your-service-role-key" \    -e OPENAI_API_KEY="your-openai-api-key" \    -v ./docs:/docs \    memfirecloud-qa:v1
复制代码

windows 下运行

与 linux 类似,设置相关环境变量,然后运行:

uvicorn main:app --reload --host 0.0.0.0
复制代码

如何访问

用浏览器访问: http://your-ip:8000/ 可以显示一个简陋的问答页面

支持的参数配置

# 本地文档路径export DOCS_PATH=./docs
# memfire cloud 应用的API URL和Service role keyexport SUPABASE_URL="your-api-url"export SUPABASE_KEY="your-service-role-key"
# 使用openai / baidu 的大模型export QA_BACKEND="openai" # 默认值
# openai 相关配置(QA_BACKEND=openai是需要)export OPENAI_ORGANIZATION="your-openai-organization"export OPENAI_API_KEY="your-openai-api-key"export OPENAI_MODEL="gpt-3.5-turbo"  # 默认值
# 百度相关配置(QA_BACKEND=baidu时需要)export BAIDU_API_KEY="your-baidu-api-key"export BAIDU_API_SECRET="your-baidu-api-secret"export BAIDU_MODEL="ERNIE-Bot-turbo" # 默认值
复制代码

接下来可以做的事情

  • 过滤掉重复文档,避免应用重启或者添加重复文档是重新计算 embedding

  • 程序运行中支持增量添加新文档,实时更新知识库

  • 支持对话(chat),目前只是问答(QA),不能连续对话

  • 支持百度文心一言接口(已完成 api 的封装)

发布于: 2023-08-22阅读数: 22
用户头像

张文平

关注

Supabase先行者 2020-08-24 加入

还未添加个人简介

评论

发布
暂无评论
【代码实践】使用FastAPI+Supabase+LangChain开发AI应用_FastApi_张文平_InfoQ写作社区