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. Azáltal, hogy kezelni tudjuk őket generálása Az OpenAI, és hogy megtalálja és visszaadja a legrelevánsabb információkat a felhasználó lekérdezéséhez. index documentation chunks embeddings performing a similarity search 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: Dokumentációs fájlok olvasása A dokumentáció indexelése (csiszolás, átfedés és beágyazás) 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 A fájlok olyan eszközöket használnak, mint . .md glob // 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áljuk hogy megteremtsük a beállításainkat. Vektoros beágyazás API 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ául ) az text-embedding-3-large 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: az egyes dokumentumok áttekintése, a dokumentumot darabokra vágja, minden egyes csomagtartó számára megteremti a csomagtartót, A fájlokat egy JSON fájlban tárolja. 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 a Ezutá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. user's question /** * 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áljuk Ez azt jelenti, hogy a ChatGPT látja a dokumentumok legrelevánsabb részeit, mintha a beszélgetésbe írtad volna őket. top 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 egy A saját chatbotod kiindulópontjaként használhatod. Templom kód Élő Demo Ha szeretné kipróbálni a chatbot végleges megvalósítását, nézze meg ezt . Demo oldal Demo oldal Demo kód Keresési találatok: askDocQuestion.ts Frontend: ChatBot komponensek Menj tovább A YouTube-on nézze meg ezt Az OpenAI Embeddings és a Vector Databases. Rendező Adrien Twarog Én is rázkódtam Ez érdekes lehet, ha alternatív megközelítést szeretne. OpenAI Assistants File Search dokumentáció 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. Szeretné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. please let me know Thanks for reading, and feel free to share your thoughts!