297 lecturas

Cómo construir una documentación inteligente - Basado en las incorporaciones de OpenAI (Chunking, Indexing y Searching)

por Aymeric PINEAU13m2025/05/01
Read on Terminal Reader

Demasiado Largo; Para Leer

La idea principal es indexar la documentación dividiéndola en pedazos gestionables, generando embeddings con OpenAI y realizando una búsqueda de similitud para encontrar y devolver la información más relevante a la consulta de un usuario.
featured image - Cómo construir una documentación inteligente - Basado en las incorporaciones de OpenAI (Chunking, Indexing y Searching)
Aymeric PINEAU HackerNoon profile picture
0-item

Hola a todos! quería compartir mi enfoque para crear un chatbot de "documentación inteligente" para un proyecto en el que estoy trabajando.I’m not an AI expert, so any suggestions or improvements are more than welcome!


El propósito de este post no es crear otro tutorial sobre la construcción de un chatbot basado en OpenAI. Ya hay mucho contenido sobre ese tema.index documentationDividiéndolos en manejableschunksGenerandoembeddingscon la apertura, yperforming a similarity searchpara encontrar y devolver la información más relevante a la consulta de un usuario.


En mi caso, la documentación será archivos Markdown, pero puede ser cualquier forma de texto, objeto de base de datos, etc.

¿Por qué ?

Debido a que a veces puede ser difícil encontrar la información que necesita, quería crear un chatbot que pudiera responder a preguntas sobre un tema específico y proporcionar el contexto relevante de la documentación.


Este asistente puede usarse de una variedad de maneras, como:

  • Proporcionar respuestas rápidas a las preguntas frecuentes
  • Buscar un Doc/Página como hace Algolia
  • Ayudar a los usuarios a encontrar la información que necesitan en un documento específico
  • Recuperar las preocupaciones/preguntas de los usuarios al almacenar las preguntas planteadas

Resumen

A continuación, describiré las tres partes principales de mi solución:

  1. Lectura de archivos de documentación
  2. Índice de la documentación (chunking, superposición y embedding)
  3. Buscar la documentación (y conectarla a un chatbot)

Archivo del árbol

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

Lectura de archivos de documentación

En lugar de codificar el texto de la documentación, puede escanear una carpeta para.mdLos archivos utilizan herramientas comoglob.

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

Como alternativa, puede, por supuesto, recuperar su documentación de su base de datos o CMS, etc.

Como alternativa, puede, por supuesto, recuperar su documentación de su base de datos o CMS, etc.


Indexar la documentación

Para crear nuestro motor de búsqueda, usaremos OpenAIIntegración de vectores APIPara generar nuestros embutidos.


Las incorporaciones vectoriales son una forma de representar datos en un formato numérico, que se puede utilizar para realizar búsquedas de similitud (en nuestro caso, entre la pregunta del usuario y nuestras secciones de documentación).


Este vector, constituido por una lista de números de puntos flotantes, se utilizará para calcular la similitud utilizando una fórmula matemática.

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

Basándose en este concepto, se creó Vector Database. Como resultado, en lugar de usar la API OpenAI, es posible usar una base de datos vectorial como Chroma, Qdrant o Pinecone.

Basándose en este concepto, se creó Vector Database. Como resultado, en lugar de usar la API OpenAI, es posible usar una base de datos vectorial como Chroma, Qdrant o Pinecone.

2.1 Chunk cada archivo y Overlap

Grandes bloques de texto pueden exceder los límites de contexto del modelo o causar hits menos relevantes, por lo que se recomienda dividirlos en pedazos para hacer la búsqueda más dirigida. Sin embargo, para preservar cierta continuidad entre pedazos, los superponemos por un cierto número de tokens (o caracteres). De esta manera, los límites de pedazos son menos propensos a cortar la sentencia media de contexto relevante.

Ejemplo de Chunking

En este ejemplo, tenemos un texto largo que queremos dividir en pedazos más pequeños. En este caso, queremos crear pedazos de 100 caracteres y superponerlos por 50 caracteres.


Full Text (406 characters):

En el corazón de la movilidad de la ciudad, había una antigua biblioteca que muchos habían olvidado. Sus estanterías se llenaban de libros de todos los géneros imaginables, cada uno susurrando historias de aventuras, misterios y sabiduría atemporal. Cada noche, un bibliotecario dedicado abría sus puertas, acogiendo a las mentes curiosas ansiosas de explorar el vasto conocimiento dentro.


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

Código 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;
};

Para saber más sobre el chunking, y el impacto del tamaño en la incorporación, puede consultar este artículo.

Para saber más sobre el chunking, y el impacto del tamaño en la incorporación, puede consultar este artículo.

2.2 Generación embebida

Una vez que un archivo se divide, generamos incorporaciones vectoriales para cada fragmento usando la API de OpenAI (por ejemplo,text-embedding-3-large) de

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 Generar y guardar embeddings para todo el archivo

Para evitar la regeneración de las incorporaciones cada vez, almacenaremos las incorporaciones. Puede almacenarse en una base de datos. Pero en este caso, simplemente lo almacenaremos en un archivo JSON localmente.


El siguiente código es sencillo:

  1. Iterar sobre cada documento,
  2. Coloque el documento en fragmentos,
  3. genera embeddings para cada chunk,
  4. almacenar los embeddings en un archivo JSON.
  5. Rellene el vectorStore con las incorporaciones a utilizar en la búsqueda.
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);
  }
};

Buscar la documentación

3.1 Similitud vectorial

Para responder a la pregunta de un usuario, primero generamos una incorporación para eluser's questiony luego calcular la similitud cosina entre la incorporación de la consulta y la incorporación de cada fragmento. filtramos cualquier cosa por debajo de un cierto umbral de similitud y mantenemos solo los mejores X-matches.

/**
 * 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 relevantes

Después de cocinar, alimentamos eltopEsto significa que ChatGPT ve las secciones más relevantes de sus documentos como si los hubieses escrito en la conversación.

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

Implementación de la API OpenAI para Chatbot Using Express

Para ejecutar nuestro sistema, usaremos un servidor Express.js. Aquí está un ejemplo de un pequeño punto final Express.js para manejar la consulta:

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: Crear una interfaz de chatbot

En el frontend, construí un pequeño componente React con una interfaz similar al chat. Envia mensajes a mi backend Express y muestra las respuestas. Nada demasiado fantástico, así que saltaremos los detalles.


Código Templario

Yo hice aCódigo del templopara que lo utilices como punto de partida para tu propio chatbot.

Demo en vivo

Si desea probar la implementación final de este chatbot, compruebe estodemo page.

Página demo

El Código Demo

  • Siguiente Entrada siguiente: AskDocQuestion.ts
  • Frontend: Componentes de ChatBot

Ir más allá

En Youtube, miren estoNoticias de Adrien TwarogSe trata de OpenAI Embeddings y bases de datos vectoriales.


Yo también me metí enDocumentación de búsqueda de archivos de asistentes de OpenAIPuede ser interesante si desea una solución alternativa.


Conclusión

Espero que esto le dé una idea de cómo manejar la indexación de la documentación para un chatbot:

  • Usando chunking + overlap para que se encuentre el contexto correcto,
  • Generar embeddings y almacenarlos para búsquedas rápidas de semejanza vectorial,
  • Finalmente, lo entregué a ChatGPT con el contexto pertinente.


No soy un experto en IA; esto es solo una solución que he encontrado que funciona bien para mis necesidades.please let me knowMe encantaría escuchar comentarios sobre soluciones de almacenamiento vectorial, estrategias de chunking o cualquier otro consejo de rendimiento.


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

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks