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:
- 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.md
A 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:
- 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 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 oldalDemo 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!