跳至主要内容
Deno 2 终于发布啦 🎉️
了解更多
A deno next to a mechanical deno.

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

在我们上个月发布了 SaasKit 之后,我们与 Supabase 合作,为您带来另一个 Deno Fresh 启动器。这次,我们使用 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 通过 Deno 在 GitHub Actions 中执行 TypScript 脚本。此操作还允许我们使用 npm 说明符

以下是在 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 岛屿 开始。

然后,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 岛屿使用 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 上关注我们!