使用 Fresh、OpenAI 和 Supabase 构建您自己的 ChatGPT 风格文档搜索
在上个月我们的 SaasKit 获得积极反响之后,我们与 Supabase 合作,为您带来又一个 Deno Fresh 入门套件。这次,我们使用 OpenAI 文本补全(Text Completion) API 创建了一个自定义的 ChatGPT 风格文档搜索。
这是基于 Supabase 文档的、托管在 Deno Deploy 上的演示截图。
Supabase 提供的免费托管式 PostgresDB 与 OpenAI 的 GPT-3 搭配使用堪称完美,因为该数据库带有 pgvector 扩展,可让您存储嵌入(embeddings)并执行向量相似度搜索。这两者都是构建 GPT-3 应用所需的。
如果您想深入了解更多细节,请查看这篇博文。或者您可以直接动手,开始构建您自己的自定义 ChatGPT 文档搜索。
让我们开始编程吧!
技术细节
构建您自己的自定义 ChatGPT 涉及四个步骤
- ⚡️ GitHub Action 预处理知识库(`docs` 文件夹中的
.mdx
文件)。 - ⚡️ GitHub Action 使用 pgvector 将嵌入(embeddings)存储在 Postgres 中。
- 🏃 运行时 执行向量相似度搜索以找到与问题相关的内容。
- 🏃 运行时 将内容注入 OpenAI GPT-3 文本补全提示,并将响应流式传输到客户端。
⚡️ GitHub Action
步骤 1 和 2 通过 GitHub Action 执行,每当我们对 `main` 分支进行更改时都会触发。
当 main
分支合并时,此 生成嵌入(generate-embeddings) 脚本将执行,它会执行以下任务:
- 使用
.mdx
文件预处理知识库 - 使用 OpenAI 生成嵌入(embeddings)
- 将嵌入(embedding)存储在 Supabase 中
以下是工作流程图:
我们可以使用 setup-deno GitHub Action 在 GitHub Actions 中通过 Deno 执行 TypeScript 脚本。此 action 还允许我们使用 npm specifiers。
以下是 GitHub Action 的 yml
文件
name: Generate Embeddings
on:
push:
branches:
- main
workflow_dispatch:
jobs:
generate-embeddings:
runs-on: ubuntu-latest
env:
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
steps:
- uses: actions/checkout@v3
- uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- run: deno task embeddings
除了存储嵌入(embeddings)之外,此脚本还会为每个 .mdx
文件生成一个校验和,并将其存储在另一个数据库表中,以确保仅当文件更改时才重新生成嵌入。
🏃 运行时
步骤 3 和 4 在运行时执行,每当用户提交问题时。此时会执行以下系列任务:
- 边缘函数接收查询并使用 OpenAI 为其生成嵌入(embedding)
- 嵌入向量用于在 Supabase 上使用
pgvector
执行向量相似度搜索,从而返回相关文档。 - 文档和查询被发送到 OpenAI,响应则流式传输到客户端。
以下是详细描述步骤的工作流程图:
在代码中,用户提示始于 SearchDialog
island。
然后,vector-search
API 端点 生成嵌入(embedding),然后在 Supabase 上执行向量搜索。当它获得相关文档的响应时,它会为 OpenAI 组装提示。
const prompt = codeBlock`
${oneLine`
You are a very enthusiastic Supabase representative who loves
to help people! Given the following sections from the Supabase
documentation, answer the question using only that information,
outputted in markdown format. If you are unsure and the answer
is not explicitly written in the documentation, say
"Sorry, I don't know how to help with that."
`}
Context sections:
${contextText}
Question: """
${sanitizedQuery}
"""
Answer as markdown (including related code snippets if available):
`;
const completionOptions: CreateCompletionRequest = {
model: "text-davinci-003",
prompt,
max_tokens: 512,
temperature: 0,
stream: true,
};
// The Fetch API allows for easier response streaming over the OpenAI client.
const response = await fetch("https://api.openai.com/v1/completions", {
headers: {
Authorization: `Bearer ${OPENAI_KEY}`,
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify(completionOptions),
});
// Proxy the streamed SSE response from OpenAI
return new Response(response.body, {
headers: {
...corsHeaders,
"Content-Type": "text/event-stream",
},
});
最后,SearchDialog
island 使用 EventSource Web API 处理从 OpenAI API 返回的服务器发送事件(server-sent events)。这正是我们能够将 OpenAI 生成的响应流式传输到客户端的原因。
const onSubmit = (e: Event) => {
e.preventDefault();
answer.value = "";
isLoading.value = true;
const query = new URLSearchParams({ query: inputRef.current!.value });
const eventSource = new EventSource(`api/vector-search?${query}`);
eventSource.addEventListener("error", (err) => {
isLoading.value = false;
console.error(err);
});
eventSource.addEventListener("message", (e: MessageEvent) => {
isLoading.value = false;
if (e.data === "[DONE]") {
eventSource.close();
return;
}
const completionResponse: CreateCompletionResponse = JSON.parse(e.data);
const text = completionResponse.choices[0].text;
answer.value += text;
});
isLoading.value = true;
};
接下来是什么?
这就是开源和现代网络的全部力量,触手可及。立即尝试,我们迫不及待地想看到您将构建什么!
以下是有关使用 OpenAI 和 ChatGPT 进行构建的其他资源。
- 阅读关于我们如何为 Supabase 文档构建 ChatGPT 的博文。
- [文档] pgvector:嵌入(Embeddings)和向量相似度
- 观看 Greg 在 Rabbit Hole Syndrome YouTube 频道上的“我是如何构建这个”的视频。
最后,我们衷心感谢 Asher Gomez 在此项目中的贡献。
不要错过任何更新 - 在 Twitter 上关注我们!