跳到主要内容
A deno next to a mechanical deno.

使用 Fresh、OpenAI 和 Supabase 构建你自己的 ChatGPT 风格文档搜索

继上个月我们的 SaasKit 获得积极反响之后,我们与 Supabase 合作,为你带来又一个 Deno Fresh starter。这一次,我们使用 OpenAI 文本补全 API 创建了一个自定义的 ChatGPT 风格文档搜索。

Screenshot of the chatgpt doc search.

基于 Supabase 文档的 在 Deno Deploy 上托管的演示 截图。

Supabase 的免费托管 PostgresDB 非常适合与 OpenAI 的 GPT-3 一起使用,因为该数据库自带扩展 pgvector,允许你存储嵌入向量并执行向量相似度搜索。这两者都是构建 GPT-3 应用程序所必需的。

如果你想深入了解更多细节,请查看这篇博文。或者你可以直接开始构建你自己的自定义 ChatGPT 文档搜索。

让我们开始编码吧!

技术细节

构建你自己的自定义 ChatGPT 涉及四个步骤

  1. ⚡️ GitHub Action 预处理知识库(你的docs文件夹中的.mdx文件)。
  2. ⚡️ GitHub Action 使用 pgvector 将嵌入向量存储在 Postgres 中。
  3. 🏃 运行时 执行向量相似度搜索,找到与问题相关的内容。
  4. 🏃 运行时 将内容注入 OpenAI GPT-3 文本补全提示,并将响应流式传输到客户端。

⚡️ GitHub Action

步骤 1 和 2 通过 GitHub Action 在每次我们对main分支进行更改时发生。

main 分支合并时,将执行此 generate-embeddings 脚本,该脚本执行以下任务

  1. 使用 .mdx 文件预处理知识库
  2. 使用 OpenAI 生成嵌入向量
  3. 将嵌入向量存储在 Supabase 中

以下是发生的工作流程图

Workflow of pre processing knowledge base and generating embeddings.

我们可以使用 setup-deno GitHub Action 在 GitHub Actions 中通过 Deno 执行 TypScript 脚本。此 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

除了存储嵌入向量外,此脚本还会为你的每个 .mdx 文件生成校验和,并将其存储在另一个数据库表中,以确保仅在文件发生更改时才重新生成嵌入向量。

🏃 运行时

步骤 3 和 4 在运行时发生,每当用户提交问题时都会发生。当这种情况发生时,将执行以下任务序列

  1. 边缘函数接收查询,并使用 OpenAI 为查询生成嵌入向量
  2. 嵌入向量用于在 Supabase 上使用 pgvector 执行向量相似度搜索,这将返回相关的文档
  3. 文档和查询被发送到 OpenAI,响应被流式传输到客户端

以下是描述更详细步骤的工作流程图

Workflow at runtime

在代码中,用户提示以 SearchDialog island 开始。

然后,vector-search API 端点 生成嵌入向量,然后在 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 返回的 服务器发送事件。这使我们能够将响应从 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;
};

下一步是什么?

这就是开源和现代 Web 技术的全部力量在你指尖汇聚。 立即试用,我们迫不及待地想看看你将构建什么!

以下是学习使用 OpenAI 和 ChatGPT 构建的更多资源。

最后,我们要感谢 Asher Gomez 对这个项目的贡献。

不要错过任何更新 - 在 Twitter 上关注我们!