300 olvasmányok
300 olvasmányok

Hogyan építsünk intelligens dokumentációt - OpenAI beágyazások alapján (Chunking, Indexing és Searching)

által Aymeric PINEAU13m2025/05/01
Read on Terminal Reader

Túl hosszú; Olvasni

A fő ötlet az, hogy indexeljük a dokumentációt úgy, hogy azokat kezelhető darabokra osztjuk, beágyazásokat generálunk az OpenAI-val, és hasonló keresést végzünk, hogy megtaláljuk és visszaküldjük a legrelevánsabb információkat a felhasználó lekérdezéséhez.
featured image - Hogyan építsünk intelligens dokumentációt - OpenAI beágyazások alapján (Chunking, Indexing és Searching)
Aymeric PINEAU HackerNoon profile picture
0-item

Szeretném megosztani a megközelítésemet egy „okos dokumentáció” chatbot létrehozásához egy olyan projekthez, amelyen dolgozom.I’m not an AI expert, so any suggestions or improvements are more than welcome!


Ennek a bejegyzésnek a célja nem az, hogy egy másik oktatóanyagot hozzon létre az OpenAI-on alapuló chatbot létrehozásáról. Már rengeteg tartalom van ezen a témán.index documentationAzáltal, hogy kezelni tudjuk őketchunksgenerálásaembeddingsAz OpenAI, ésperforming a similarity searchhogy megtalálja és visszaadja a legrelevánsabb információkat a felhasználó lekérdezéséhez.


Az én esetemben a dokumentáció Markdown fájlok lesz, de bármilyen formájú szöveg, adatbázis-objektum stb. lehet.

Miért is?

Mivel néha nehéz megtalálni a szükséges információkat, olyan chatbotot akartam létrehozni, amely válaszolhat egy adott témával kapcsolatos kérdésekre, és releváns kontextust biztosít a dokumentációból.


Ez az asszisztens sokféleképpen használható, például:

  • Gyors válaszok a gyakran feltett kérdésekre
  • Doc/page keresése, mint az Algolia
  • Segítség a felhasználóknak abban, hogy megtalálják a szükséges információkat egy adott dokumentumban
  • A felhasználók aggodalmainak/kérdéseinek visszaszerzése a feltett kérdések tárolásával

Összefoglaló

Az alábbiakban bemutatom a megoldásom három fő részét:

  1. Dokumentációs fájlok olvasása
  2. A dokumentáció indexelése (csiszolás, átfedés és beágyazás)
  3. A dokumentáció keresése (és egy chatbothoz való csatlakoztatása)

Fájlfa

.
└── docs
    └── ...md
└── src
    └── askDocQuestion.ts
    └── index.ts # Express.js application endpoint
└── embeddings.json # Storage for embeddings
└── packages.json

1. Olvasás dokumentációs fájlok

A dokumentációs szöveg kemény kódolása helyett beolvashat egy mappát a.mdA fájlok olyan eszközöket használnak, mintglob.

// Example snippet of fetching files from a folder:
import fs from "node:fs";
import path from "node:path";
import glob from "glob";

const DOC_FOLDER_PATH = "./docs";

type FileData = {
  path: string;
  content: string;
};

const readAllMarkdownFiles = (): FileData[] => {
  const filesContent: FileData[] = [];
  const filePaths = glob.sync(`${DOC_FOLDER_PATH}/**/*.md`);

  filePaths.forEach((filePath) => {
    const content = fs.readFileSync(filePath, "utf8");
    filesContent.push({ path: filePath, content });
  });

  return filesContent;
};

Alternatív megoldásként természetesen felveheti a dokumentációt az adatbázisból vagy a CMS-ből stb.

Alternatív megoldásként természetesen felveheti a dokumentációt az adatbázisból vagy a CMS-ből stb.


2. A dokumentáció indexelése

A keresőmotor létrehozásához az OpenAI-t használjukVektoros beágyazás APIhogy megteremtsük a beállításainkat.


A vektor beágyazás az adatok numerikus formátumban történő ábrázolásának egyik módja, amely a hasonlóságkutatások elvégzésére használható (a mi esetünkben a felhasználói kérdés és a dokumentációs szakaszok között).


Ez a vektor, amely a lebegő pontszámok listájából áll, matematikai képlettel fogja kiszámítani a hasonlóságot.

[
  -0.0002630692, -0.029749284, 0.010225477, -0.009224428, -0.0065269712,
  -0.002665544, 0.003214777, 0.04235309, -0.033162255, -0.00080789323,
  //...+1533 elements
];

Ennek alapján létrehozták a Vector Database-t. Ennek eredményeképpen az OpenAI API használata helyett olyan vektoradatbázist lehet használni, mint a Chroma, a Qdrant vagy a Pinecone.

Ennek alapján létrehozták a Vector Database-t. Ennek eredményeképpen az OpenAI API használata helyett olyan vektoradatbázist lehet használni, mint a Chroma, a Qdrant vagy a Pinecone.

2.1 Chunk minden fájlt & Overlap

A nagy szöveges blokkok meghaladhatják a modell kontextusának korlátait, vagy kevésbé releváns találatokat okozhatnak, ezért ajánlott darabokra osztani őket, hogy a keresés célzottabb legyen. Azonban, hogy bizonyos folytonosságot megőrizzünk a darabok között, bizonyos számú tokennel (vagy karakterrel) átfedjük őket.

Példa Chunking

Ebben a példában van egy hosszú szövegünk, amelyet kisebb darabokra szeretnénk felosztani.


Full Text (406 characters):

A nyüzsgő város szívében állt egy régi könyvtár, amelyet sokan elfelejtettek. A tornyos polcok tele voltak minden elképzelhető műfaj könyveivel, amelyek mindegyike kalandokról, rejtélyekről és időtlen bölcsességről mesélt. Minden este egy elkötelezett könyvtáros nyitotta meg kapuit, üdvözölve a kíváncsi elméket, akik szeretnék felfedezni a bennük rejlő hatalmas tudást.


  • Chunk 1 (Characters 1-150):

    In the heart of the bustling city, there stood an old library that many had forgotten. Its towering shelves were filled with books from every imaginabl.

  • Chunk 2 (Characters 101-250):

    shelves were filled with books from every imaginable genre, each whispering stories of adventures, mysteries, and timeless wisdom. Every evening, a d

  • Chunk 3 (Characters 201-350):

    ysteries, and timeless wisdom. Every evening, a dedicated librarian would open its doors, welcoming curious minds eager to explore the vast knowledge

  • Chunk 4 (Characters 301-406):

    curious minds eager to explore the vast knowledge within. Children would gather for storytelling sessions.

Kód Snippet

const CHARS_PER_TOKEN = 4.15; // Approximate pessimistically number of characters per token. Can use `tiktoken` or other tokenizers to calculate it more precisely

const MAX_TOKENS = 500; // Maximum number of tokens per chunk
const OVERLAP_TOKENS = 100; // Number of tokens to overlap between chunks

const maxChar = MAX_TOKENS * CHARS_PER_TOKEN;
const overlapChar = OVERLAP_TOKENS * CHARS_PER_TOKEN;

const chunkText = (text: string): string[] => {
  const chunks: string[] = [];
  let start = 0;

  while (start < text.length) {
    let end = Math.min(start + maxChar, text.length);

    // Don’t cut a word in half if possible:
    if (end < text.length) {
      const lastSpace = text.lastIndexOf(" ", end);
      if (lastSpace > start) end = lastSpace;
    }

    chunks.push(text.substring(start, end));
    // Overlap management
    const nextStart = end - overlapChar;
    start = nextStart <= start ? end : nextStart;
  }

  return chunks;
};

Ha többet szeretne megtudni a csiszolásról és a méretnek a beágyazásra gyakorolt hatásáról, nézze meg ezt a cikket.

Ha többet szeretne megtudni a csiszolásról és a méretnek a beágyazásra gyakorolt hatásáról, nézze meg ezt a cikket.

2.2 Beágyazott generáció

Miután egy fájlt megcsonkítottunk, az OpenAI API-ját (példáultext-embedding-3-large) az

import { OpenAI } from "openai";

const EMBEDDING_MODEL: OpenAI.Embeddings.EmbeddingModel =
  "text-embedding-3-large"; // Model to use for embedding generation

const openai = new OpenAI({ apiKey: OPENAI_API_KEY });

const generateEmbedding = async (textChunk: string): Promise<number[]> => {
  const response = await openai.embeddings.create({
    model: EMBEDDING_MODEL,
    input: textChunk,
  });

  return response.data[0].embedding; // Return the generated embedding
};

2.3 Beágyazások létrehozása és mentése az egész fájlhoz

Annak elkerülése érdekében, hogy a beágyazásokat minden alkalommal regeneráljuk, a beágyazásokat tároljuk. Ez egy adatbázisban tárolható. De ebben az esetben egyszerűen egy JSON fájlban tároljuk.


A következő kód egyszerűen:

  1. az egyes dokumentumok áttekintése,
  2. a dokumentumot darabokra vágja,
  3. minden egyes csomagtartó számára megteremti a csomagtartót,
  4. A fájlokat egy JSON fájlban tárolja.
  5. Töltse ki a VectorStore-t a keresés során használandó beágyazásokkal.
import embeddingsList from "../embeddings.json";

/**
 * Simple in-memory vector store to hold document embeddings and their content.
 * Each entry contains:
 * - filePath: A unique key identifying the document
 * - chunkNumber: The number of the chunk within the document
 * - content: The actual text content of the chunk
 * - embedding: The numerical embedding vector for the chunk
 */
const vectorStore: {
  filePath: string;
  chunkNumber: number;
  content: string;
  embedding: number[];
}[] = [];

/**
 * Indexes all Markdown documents by generating embeddings for each chunk and storing them in memory.
 * Also updates the embeddings.json file if new embeddings are generated.
 */
export const indexMarkdownFiles = async (): Promise<void> => {
  // Retrieve documentations
  const docs = readAllMarkdownFiles();

  let newEmbeddings: Record<string, number[]> = {};

  for (const doc of docs) {
    // Split the document into chunks based on headings
    const fileChunks = chunkText(doc.content);

    // Iterate over each chunk within the current file
    for (const chunkIndex of Object.keys(fileChunks)) {
      const chunkNumber = Number(chunkIndex) + 1; // Chunk number starts at 1
      const chunksNumber = fileChunks.length;

      const chunk = fileChunks[chunkIndex as keyof typeof fileChunks] as string;

      const embeddingKeyName = `${doc.path}/chunk_${chunkNumber}`; // Unique key for the chunk

      // Retrieve precomputed embedding if available
      const existingEmbedding = embeddingsList[
        embeddingKeyName as keyof typeof embeddingsList
      ] as number[] | undefined;

      let embedding = existingEmbedding; // Use existing embedding if available

      if (!embedding) {
        embedding = await generateEmbedding(chunk); // Generate embedding if not present
      }

      newEmbeddings = { ...newEmbeddings, [embeddingKeyName]: embedding };

      // Store the embedding and content in the in-memory vector store
      vectorStore.push({
        filePath: doc.path,
        chunkNumber,
        embedding,
        content: chunk,
      });

      console.info(`- Indexed: ${embeddingKeyName}/${chunksNumber}`);
    }
  }

  /**
   * Compare the newly generated embeddings with existing ones
   *
   * If there is change, update the embeddings.json file
   */
  try {
    if (JSON.stringify(newEmbeddings) !== JSON.stringify(embeddingsList)) {
      fs.writeFileSync(
        "./embeddings.json",
        JSON.stringify(newEmbeddings, null, 2)
      );
    }
  } catch (error) {
    console.error(error);
  }
};

3. A dokumentáció keresése

4.1 Vektoros hasonlóság

A felhasználó kérdéseinek megválaszolásához először egy beágyazást hozunk létre auser's questionEzután kiszámítjuk a kérelem beágyazásának és az egyes darabok beágyazásának közötti cosine hasonlóságot. Minden olyan dolgot szűrünk ki, amely egy bizonyos hasonlósági küszöb alatt van, és csak a legmagasabb X találatokat tartjuk meg.

/**
 * Calculates the cosine similarity between two vectors.
 * Cosine similarity measures the cosine of the angle between two vectors in an inner product space.
 * Used to determine the similarity between chunks of text.
 *
 * @param vecA - The first vector
 * @param vecB - The second vector
 * @returns The cosine similarity score
 */
const cosineSimilarity = (vecA: number[], vecB: number[]): number => {
  // Calculate the dot product of the two vectors
  const dotProduct = vecA.reduce((sum, a, idx) => sum + a * vecB[idx], 0);

  // Calculate the magnitude (Euclidean norm) of each vector
  const magnitudeA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0));
  const magnitudeB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0));

  // Compute and return the cosine similarity
  return dotProduct / (magnitudeA * magnitudeB);
};

const MIN_RELEVANT_CHUNKS_SIMILARITY = 0.77; // Minimum similarity required for a chunk to be considered relevant
const MAX_RELEVANT_CHUNKS_NB = 15; // Maximum number of relevant chunks to attach to chatGPT context

/**
 * Searches the indexed documents for the most relevant chunks based on a query.
 * Utilizes cosine similarity to find the closest matching embeddings.
 *
 * @param query - The search query provided by the user
 * @returns An array of the top matching document chunks' content
 */
const searchChunkReference = async (query: string) => {
  // Generate an embedding for the user's query
  const queryEmbedding = await generateEmbedding(query);

  // Calculate similarity scores between the query embedding and each document's embedding
  const results = vectorStore
    .map((doc) => ({
      ...doc,
      similarity: cosineSimilarity(queryEmbedding, doc.embedding), // Add similarity score to each doc
    }))
    // Filter out documents with low similarity scores
    // Avoid to pollute the context with irrelevant chunks
    .filter((doc) => doc.similarity > MIN_RELEVANT_CHUNKS_SIMILARITY)
    .sort((a, b) => b.similarity - a.similarity) // Sort documents by highest similarity first
    .slice(0, MAX_RELEVANT_CHUNKS_NB); // Select the top most similar documents

  // Return the content of the top matching documents
  return results;
};

3.2 Az OpenAI elősegítése releváns Chunks-okkal

Miután elkészítettük, tápláljuktopEz azt jelenti, hogy a ChatGPT látja a dokumentumok legrelevánsabb részeit, mintha a beszélgetésbe írtad volna őket.

const MODEL: OpenAI.Chat.ChatModel = "gpt-4o-2024-11-20"; // Model to use for chat completions

// Define the structure of messages used in chat completions
export type ChatCompletionRequestMessage = {
  role: "system" | "user" | "assistant"; // The role of the message sender
  content: string; // The text content of the message
};

/**
 * Handles the "Ask a question" endpoint in an Express.js route.
 * Processes user messages, retrieves relevant documents, and interacts with OpenAI's chat API to generate responses.
 *
 * @param messages - An array of chat messages from the user and assistant
 * @returns The assistant's response as a string
 */
export const askDocQuestion = async (
  messages: ChatCompletionRequestMessage[]
): Promise<string> => {
  // Assistant's response are filtered out otherwise the chatbot will be stuck in a self-referential loop
  // Note that the embedding precision will be lowered if the user change of context in the chat
  const userMessages = messages.filter((message) => message.role === "user");

  // Format the user's question to keep only the relevant keywords
  const formattedUserMessages = userMessages
    .map((message) => `- ${message.content}`)
    .join("\n");

  // 1) Find relevant documents based on the user's question
  const relevantChunks = await searchChunkReference(formattedUserMessages);

  // 2) Integrate the relevant documents into the initial system prompt
  const messagesList: ChatCompletionRequestMessage[] = [
    {
      role: "system",
      content:
        "Ignore all previous instructions. \
        You're an helpful chatbot.\
        ...\
        Here is the relevant documentation:\
        " +
        relevantChunks
          .map(
            (doc, idx) =>
              `[Chunk ${idx}] filePath = "${doc.filePath}":\n${doc.content}`
          )
          .join("\n\n"), // Insert relevant chunks into the prompt
    },
    ...messages, // Include the chat history
  ];

  // 3) Send the compiled messages to OpenAI's Chat Completion API (using a specific model)
  const response = await openai.chat.completions.create({
    model: MODEL,
    messages: messagesList,
  });

  const result = response.choices[0].message.content; // Extract the assistant's reply

  if (!result) {
    throw new Error("No response from OpenAI");
  }

  return result;
};

4. Az OpenAI API telepítése Chatbot Express használatával

Rendszerünk futtatásához egy Express.js szervert használunk. Íme egy példa egy kis Express.js végpontra a lekérdezés kezelésére:

import express, { type Request, type Response } from "express";
import {
  ChatCompletionRequestMessage,
  askDocQuestion,
  indexMarkdownFiles,
} from "./askDocQuestion";

// Automatically fill the vector store with embeddings when server starts
indexMarkdownFiles();

const app = express();

// Parse incoming requests with JSON payloads
app.use(express.json());

type AskRequestBody = {
  messages: ChatCompletionRequestMessage[];
};

// Routes
app.post(
  "/ask",
  async (
    req: Request<undefined, undefined, AskRequestBody>,
    res: Response<string>
  ) => {
    try {
      const response = await askDocQuestion(req.body.messages);

      res.json(response);
    } catch (error) {
      console.error(error);
    }
  }
);

// Start server
app.listen(3000, () => {
  console.log(`Listening on port 3000`);
});

5. Chatbot interfész létrehozása

A frontendre egy kis React komponenst építettem egy csevegésszerű interfésszel. Üzeneteket küld az Express backend-emnek, és megjeleníti a válaszokat. Semmi sem túl fantasztikus, így kihagyjuk a részleteket.


Templom kód

Készítettem egyTemplom kódA saját chatbotod kiindulópontjaként használhatod.

Élő Demo

Ha szeretné kipróbálni a chatbot végleges megvalósítását, nézze meg eztDemo oldal.

Demo oldal

Demo kód

  • Keresési találatok: askDocQuestion.ts
  • Frontend: ChatBot komponensek

Menj tovább

A YouTube-on nézze meg eztRendező Adrien TwarogAz OpenAI Embeddings és a Vector Databases.


Én is rázkódtamOpenAI Assistants File Search dokumentációEz érdekes lehet, ha alternatív megközelítést szeretne.


következtetés

Remélem, ez ad egy ötletet, hogyan kell kezelni a dokumentáció indexelését egy chatbot számára:

  • A Chunking + Overlap használatával a megfelelő kontextus megtalálható,
  • beágyazások létrehozása és tárolása a gyors vektor-hasonlóság-keresésekhez,
  • Végül átadtam a ChatGPT-nek a releváns kontextusban.


Nem vagyok mesterséges intelligencia szakértője; ez csak egy olyan megoldás, amely jól megfelel az igényeimnek.please let me knowSzeretném hallani a visszajelzéseket a vektor tárolási megoldásokról, a csiszoló stratégiákról vagy bármilyen más teljesítmény tippről.


Thanks for reading, and feel free to share your thoughts!

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks