构建个性化的ChatGpt助手:通过SEO技术优化您的文档
#javascript #react #chatgpt #documentation

使用向量嵌入上下文

chatgpt是一种精美的成长和学习资源,何时接受有关您使用的技术的教育。

最初,我希望Chatgpt帮助我创建Hilla应用程序。但是,由于它在2021年接受了数据培训,因此它给出了与现实不符的制造答复。

我可以改善的另一个领域是,希拉支持前端的反应并点亮。我需要确保答复将相关框架考虑到上下文。

这是我建立助手的方法,该助手使用最新文档为Chatgpt提供相关答复。

关键原则:嵌入

chatgpt与其他大语言模型一样,具有有限的上下文大小,必须适合您的问题,相关的背景事实和回答。例如,get-3.5-turbo的令牌最大为4096,大约等于3000个单词。将最有用的文档件结合在提示中对于引起有意义的答复至关重要。

嵌入是确定这些关键文档部分的实用方法。嵌入是一种将文本含义编码到代表多维空间中位置的向量中的技术。具有相似含义的文本靠近在一起,而具有不同含义的文本则更分开。

Image description

这个想法与彩色拾取器的想法相似。包含红色,绿色和蓝色值的三元素向量可用于表示每种颜色。具有可比值的颜色具有相似的值,而具有不同值的颜色具有不同的值。

Image description

足以知道本文的OpenAI API converts text into embeddings。如果您想了解嵌入方式的功能,this article是一个美丽的起点。

一旦生成了内容的嵌入,您就可以通过找到与查询最密切相关的零件来快速发现最相关的位。

概述:提供文档作为chatgpt的上下文

以下是让chatgpt使用您的文档作为上下文所需的高级程序:

为您的文档创建嵌入

  1. 将您的文档分为较小的块,例如通过标题,然后为每个文档生成一个嵌入(向量)。
  2. 在矢量数据库中保存嵌入,源文本和其他元数据。

Image description

以文档为上下文提供响应

  • 对用户查询进行嵌入。

  • 使用嵌入式,搜索矢量数据库中的文档的n部分与查询最相关。

  • 创建一个提示,告诉chatgpt仅使用可用文档回答给定查询。

  • 要为提示产生一个完成,请使用OpenAI API。

Image description

在随后的部分中,我将进一步详细介绍我如何执行这些过程。

用过的工具

源代码

我将简单地提及以下代码中最重要的部分。您可以在GitHube

上找到源代码

文档处理

Hilla documentation写在Asciidoc。以下是将它们转换为嵌入所需的过程:

  • 应使用asciidoctor处理ASCIIDOC文件以包括代码段和其他包含物。

  • 基于HTML文档结构,将生成的文档分为各节。

  • 要保存令牌,请将材料转换为纯文本。

  • 如有必要,将零件分成较小的碎片。

  • 为文本的每个块制作嵌入向量。

  • Pinecone应用于保存嵌入向量和源文本。

Asciidoc的处理

async function processAdoc(file, path) {
  console.log(`Processing ${path}`);

  const frontMatterRegex = /^---[\s\S]+?---\n*/;


  const namespace = path.includes('articles/react') ? 'react' : path.includes('articles/lit') ? 'lit' : '';
  if (!namespace) return;

  // Remove front matter. The JS version of asciidoctor doesn't support removing it.
  const noFrontMatter = file.replace(frontMatterRegex, '');

  // Run through asciidoctor to get includes
  const html = asciidoctor.convert(noFrontMatter, {
    attributes: {
      root: process.env.DOCS_ROOT,
      articles: process.env.DOCS_ARTICLES,
      react: namespace === 'react',
      lit: namespace === 'lit'
    },
    safe: 'unsafe',
    base_dir: process.env.DOCS_ARTICLES
  });

  // Extract sections
  const dom = new JSDOM(html);
  const sections = dom.window.document.querySelectorAll('.sect1');

  // Convert section html to plain text to save on tokens
  const plainText = Array.from(sections).map(section => convert(section.innerHTML));

  // Split section content further if needed, filter out short blocks
  const docs = await splitter.createDocuments(plainText);
  const blocks = docs.map(doc => doc.pageContent)
    .filter(block => block.length > 200);

  await createAndSaveEmbeddings(blocks, path, namespace);
}

创建嵌入并保存它们

async function createAndSaveEmbeddings(blocks, path, namespace) {

  // OpenAI suggests removing newlines for better performance when creating embeddings. 
  // Don't remove them from the source.
  const withoutNewlines = blocks.map(block => block.replace(/\n/g, ' '));
  const embeddings = await getEmbeddings(withoutNewlines);
  const vectors = embeddings.map((embedding, i) => ({
    id: nanoid(),
    values: embedding,
    metadata: {
      path: path,
      text: blocks[i]
    }
  }));

  await pinecone.upsert({
    upsertRequest: {
      vectors,
      namespace
    }
  });
}

从Openai获取嵌入

export async function getEmbeddings(texts) {
  const response = await openai.createEmbedding({
    model: 'text-embedding-ada-002',
    input: texts
  });
  return response.data.data.map((item) => item.embedding);
}

使用上下文搜索

到目前为止,我们已经将文档分为可管理的块,并将其放在向量数据库中。当用户提出问题时,我们必须执行以下操作:

  • 根据问的查询而创建一个嵌入。

  • 搜索矢量数据库中的十个最相关的文档部分。

  • 创建一个问题,其中有许多文档部分包装到1536个令牌中,而2560的响应则留下了。

async function getMessagesWithContext(messages: ChatCompletionRequestMessage[], frontend: string) {

  // Ensure that there are only messages from the user and assistant, trim input
  const historyMessages = sanitizeMessages(messages);

  // Send all messages to OpenAI for moderation.
  // Throws exception if flagged -> should be handled properly in a real app.
  await moderate(historyMessages);

  // Extract the last user message to get the question
  const [userMessage] = historyMessages.filter(({role}) => role === ChatCompletionRequestMessageRoleEnum.User).slice(-1)

  // Create an embedding for the user's question
  const embedding = await createEmbedding(userMessage.content);

  // Find the most similar documents to the user's question
  const docSections = await findSimilarDocuments(embedding, 10, frontend);

  // Get at most 1536 tokens of documentation as context
  const contextString = await getContextString(docSections, 1536);

  // The messages that set up the context for the question
  const initMessages: ChatCompletionRequestMessage[] = [
    {
      role: ChatCompletionRequestMessageRoleEnum.System,
      content: codeBlock`
          ${oneLine`
            You are Hilla AI. You love to help developers! 
            Answer the user's question given the following
            information from the Hilla documentation.
          `}
        `
    },
    {
      role: ChatCompletionRequestMessageRoleEnum.User,
      content: codeBlock`
          Here is the Hilla documentation:
          """
          ${contextString}
          """
        `
    },
    {
      role: ChatCompletionRequestMessageRoleEnum.User,
      content: codeBlock`
          ${oneLine`
            Answer all future questions using only the above        
            documentation and your knowledge of the 
            ${frontend === 'react' ? 'React' : 'Lit'} library
          `}
          ${oneLine`
            You must also follow the below rules when answering:
          `}
          ${oneLine`
            - Do not make up answers that are not provided 
              in the documentation 
          `}
          ${oneLine`
            - If you are unsure and the answer is not explicitly 
              written in the documentation context, say 
              "Sorry, I don't know how to help with that"
          `}
          ${oneLine`
            - Prefer splitting your response into 
              multiple paragraphs
          `}
          ${oneLine`
            - Output as markdown
          `}
          ${oneLine`
            - Always include code snippets if available
          `}
        `
    }
  ];

  // Cap the messages to fit the max token count, removing earlier messages if necessary
  return capMessages(
    initMessages,
    historyMessages
  );
}

当用户提出问题时,我们使用getMessagesWithContext()检索必须发送到chatgpt的消息。然后,使用OpenAI API获取完整并将响应提供给客户。

export default async function handler(req: NextRequest) {
  // All the non-system messages up until now along with 
  // the framework we should use for the context.
  const {messages, frontend} = (await req.json()) as {
    messages: ChatCompletionRequestMessage[],
    frontend: string
  };
  const completionMessages = await getMessagesWithContext(messages, frontend);
  const stream = await streamChatCompletion(completionMessages, MAX_RESPONSE_TOKENS);
  return new Response(stream);
}

源代码

感谢您与我保持联系,直到最后。您是一个很棒的读者!

ahsan mangal
希望您能发现它有益和吸引人。如果您喜欢这些内容,请考虑以后关注我以获取更多这样的文章。保持好奇并继续学习!