288 letture

Come costruire una documentazione intelligente - basata su OpenAI Embeddings (Chunking, Indexing e Searching)

di Aymeric PINEAU13m2025/05/01
Read on Terminal Reader

Troppo lungo; Leggere

L'idea principale è quella di indicizzare la documentazione dividendola in pezzi gestibili, generando embeddings con OpenAI e eseguendo una ricerca di somiglianza per trovare e restituire le informazioni più rilevanti alla query di un utente.
featured image - Come costruire una documentazione intelligente - basata su OpenAI Embeddings (Chunking, Indexing e Searching)
Aymeric PINEAU HackerNoon profile picture
0-item

Ciao a tutti! volevo condividere il mio approccio alla creazione di un chatbot “smart documentation” per un progetto su cui sto lavorando.I’m not an AI expert, so any suggestions or improvements are more than welcome!


Lo scopo di questo post non è quello di creare un altro tutorial sulla costruzione di un chatbot basato su OpenAI. Ci sono già un sacco di contenuti su questo argomento.index documentationDividendoli in gestibilichunksdi generareembeddingscon l’apertura eperforming a similarity searchper trovare e restituire le informazioni più rilevanti alla query di un utente.


Nel mio caso, la documentazione sarà file Markdown, ma può essere qualsiasi forma di testo, oggetto di database, ecc.

Ma perché ?

Poiché a volte può essere difficile trovare le informazioni di cui hai bisogno, volevo creare un chatbot che potesse rispondere a domande su un argomento specifico e fornire il contesto pertinente dalla documentazione.


Questo assistente può essere utilizzato in vari modi, come ad esempio:

  • Fornire risposte rapide alle domande più frequenti
  • Cercare un doc/pagina come Algolia fa
  • Aiutare gli utenti a trovare le informazioni di cui hanno bisogno in un documento specifico
  • Riscoprire le preoccupazioni / domande degli utenti memorizzando le domande poste

riassunto

Di seguito, elencherò le tre parti principali della mia soluzione:

  1. Leggere i file di documentazione
  2. Indice della documentazione (chunking, sovrapposizione e incorporazione)
  3. Cercare la documentazione (e collegarla a un chatbot)

File albero

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

1 Leggere i file di documentazione

Invece di codificare il testo della documentazione, è possibile eseguire la scansione di una cartella per.mdFile che utilizzano strumenti comeglob.

// 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;
};

In alternativa, puoi naturalmente recuperare la tua documentazione dal tuo database o CMS, ecc.

In alternativa, puoi naturalmente recuperare la tua documentazione dal tuo database o CMS, ecc.


Indice della documentazione

Per creare il nostro motore di ricerca, useremo OpenAIInserimento di vettori APIper generare i nostri embeddings.


Gli embeddings vettoriali sono un modo per rappresentare i dati in formato numerico, che può essere utilizzato per eseguire ricerche di somiglianza (nel nostro caso, tra la domanda dell'utente e le sezioni della nostra documentazione).


Questo vettore, costituito da un elenco di numeri di punti galleggianti, verrà utilizzato per calcolare la somiglianza utilizzando una formula matematica.

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

Sulla base di questo concetto, è stato creato il database Vector. Come risultato, invece di utilizzare l'API OpenAI, è possibile utilizzare un database vettoriale come Chroma, Qdrant o Pinecone.

Sulla base di questo concetto, è stato creatoDatabase vettorialeDi conseguenza, invece di utilizzare l'API OpenAI, è possibile utilizzare un database vettoriale comeChromadiCattedraleoPinocchio.

2.1 Chunk ogni file e sovrapposizione

Grandi blocchi di testo possono superare i limiti del contesto del modello o causare hit meno rilevanti, quindi si consiglia di dividerli in pezzi per rendere la ricerca più mirata. Tuttavia, per preservare una certa continuità tra i pezzi, li sovrapponiamo con un certo numero di token (o caratteri).

Esempio di Chunking

In questo esempio, abbiamo un testo lungo che vogliamo suddividere in pezzi più piccoli. In questo caso, vogliamo creare pezzi di 100 caratteri e sovrapporliarli con 50 caratteri.


Full Text (406 characters):

Nel cuore della vivace città, c’era una vecchia biblioteca che molti avevano dimenticato.Le sue scaffali tortuose erano piene di libri di ogni genere immaginabile, ognuno sussurrando storie di avventure, misteri e saggezza senza tempo.Ogni sera, un bibliotecario dedicato apriva le sue porte, accogliendo le menti curiose desiderose di esplorare la vasta conoscenza all’interno.I bambini si riunivano per le sessioni di racconto.


  • 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.

Codice 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;
};

Per saperne di più sul crunking, e l'impatto della dimensione sull'incollaggio, puoi controllare questo articolo.

Per saperne di più sul chunking, e l'impatto della dimensione sull'incollaggio, puoi controllareQuesto articolo.

2.2 Generazione incarnata

Una volta che un file viene tagliato, si generano incorporazioni vettoriali per ciascun pezzo utilizzando l'API di OpenAI (ad esempio,text-embedding-3-largee) il

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 Generare e salvare incorporamenti per l'intero file

Per evitare la rigenerazione degli embeddings ogni volta, memorizzeremo gli embeddings. Può essere memorizzato in un database. Ma in questo caso, lo memorizzeremo semplicemente in un file JSON localmente.


Il seguente codice è semplice:

  1. Iterato su ogni documento,
  2. tagliare il documento in pezzi,
  3. generare embeddings per ogni pezzo,
  4. Inserisci il file in un file JSON.
  5. Completa il vectorStore con gli embeddings da utilizzare nella ricerca.
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 Ricerca della documentazione

3.1 Similarietà vettoriale

Per rispondere alla domanda di un utente, generamo prima un incorporamento per iluser's questione quindi calcolare la somiglianza cosina tra l'incorporazione della query e l'incorporazione di ciascun pezzo. filtriamo qualsiasi cosa al di sotto di una certa soglia di somiglianza e manteniamo solo le corrispondenze X superiori.

/**
 * 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 Prompting OpenAI con Chunks pertinenti

Dopo aver mangiato, si nutretopQuesto significa che ChatGPT vede le sezioni più rilevanti dei tuoi documenti come se li avessi digitati nella conversazione.

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;
};

Implementazione di OpenAI API per Chatbot Using Express

Per eseguire il nostro sistema, utilizzeremo un server Express.js. Ecco un esempio di un piccolo endpoint Express.js per gestire la query:

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`);
});

UI: Creare un’interfaccia Chatbot

Sul frontend, ho costruito un piccolo componente React con un'interfaccia simile a chat. Invia messaggi al mio backend Express e visualizza le risposte.


Template Codice

Ho fatto unTemplate Codiceper voi di utilizzare come punto di partenza per il vostro chatbot.

Demo dal vivo

Se vuoi testare l'implementazione finale di questo chatbot, controlla questodemo page.

Sito demo

Il mio codice demo

  • Titolo originale: AskDocQuestion.ts
  • Frontend: componenti ChatBot

Andare oltre

Su YouTube, guardate questoIl video di Adrien TwarogSi tratta di OpenAI Embeddings e database vettoriali.


Anche io mi sono schiantatoDocumentazione di ricerca dei file di OpenAI AssistantsQuesto potrebbe essere interessante se si desidera un approccio alternativo.


Conclusione

Spero che questo ti dia un'idea di come gestire l'indicizzazione della documentazione per un chatbot:

  • Utilizzando chunking + sovrapposizione in modo che sia trovato il contesto giusto,
  • Generare embeddings e memorizzarli per ricerche veloci di somiglianza vettoriale,
  • Infine, l'ho consegnato a ChatGPT con il contesto pertinente.


Non sono un esperto di intelligenza artificiale; questa è solo una soluzione che ho trovato funziona bene per le mie esigenze.please let me knowMi piacerebbe ascoltare feedback sulle soluzioni di archiviazione vettoriale, sulle strategie di chunking o su altri suggerimenti di prestazioni.


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

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks
OSZAR »