Unë doja të ndaja qasjen time në krijimin e një "dokumentacioni inteligjent" chatbot për një projekt që unë jam duke punuar në.I’m not an AI expert, so any suggestions or improvements are more than welcome!
Qëllimi i këtij postimi nuk është të krijojë një tutorial tjetër për ndërtimin e një chatbot bazuar në OpenAI. Ka tashmë shumë përmbajtje në këtë temë.index documentationduke i ndarë në të menaxhueshmechunkstë gjenerojëembeddingsme të hapur, dheperforming a similarity searchpër të gjetur dhe kthyer informacionin më të rëndësishëm në pyetjen e një përdoruesi.
Në rastin tim, dokumentacioni do të jetë skedarët Markdown, por mund të jetë çdo formë e tekstit, objekti i bazës së të dhënave, etj.
Përse ?
Për shkak se ndonjëherë mund të jetë e vështirë për të gjetur informacionin që ju nevojitet, unë doja të krijoja një chatbot që mund të përgjigjet në pyetje në lidhje me një temë të veçantë dhe të sigurojë kontekstin e duhur nga dokumentacioni.
Ky asistent mund të përdoret në mënyra të ndryshme, të tilla si:
- Përgjigje të shpejta për pyetjet më të shpeshta
- Kërkimi i një doc/faqe si Algolia bën
- Ndihmon përdoruesit të gjejnë informacionin që kanë nevojë në një dokument të veçantë
- Kërkimi i shqetësimeve / pyetjeve të përdoruesve duke ruajtur pyetjet e bëra
përmbledhje
Më poshtë, unë do të përshkruaj tre pjesët kryesore të zgjidhjes sime:
- Leximi i dosjeve të dokumentacionit
- Indeksimi i dokumentacionit (chunking, overlap, dhe embedding)
- Kërkimi i dokumentacionit (dhe lidhja e tij deri në një chatbot)
Arkivi i pemës
.
└── docs
└── ...md
└── src
└── askDocQuestion.ts
└── index.ts # Express.js application endpoint
└── embeddings.json # Storage for embeddings
└── packages.json
1 Leximi i dosjeve të dokumentacionit
Në vend që të kodoni tekstin e dokumentit, mund të skanoni një dosje për.md
Përdorimi i mjeteve të tilla siglob
.
// 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;
};
Si alternativë, natyrisht që mund të merrni dokumentacionin tuaj nga baza juaj e të dhënave ose CMS etj.
Si alternativë, natyrisht që mund të merrni dokumentacionin tuaj nga baza juaj e të dhënave ose CMS etj.
Indeksimi i dokumentacionit
Për të krijuar motorin tonë të kërkimit, ne do të përdorim OpenAIVektorë Embedings APIpër të ndërtuar ndërtesat tona.
Embeddings vektor janë një mënyrë për të përfaqësuar të dhënat në një format numerik, të cilat mund të përdoren për të kryer kërkime ngjashmërie (në rastin tonë, midis pyetjes së përdoruesit dhe seksioneve të dokumentacionit tonë).
Ky vektor, i përbërë nga një listë e numrave të pikave fluturuese, do të përdoret për të llogaritur ngjashmërinë duke përdorur një formulë matematikore.
[
-0.0002630692, -0.029749284, 0.010225477, -0.009224428, -0.0065269712,
-0.002665544, 0.003214777, 0.04235309, -0.033162255, -0.00080789323,
//...+1533 elements
];
Bazuar në këtë koncept, u krijua Vector Database. Si rezultat, në vend të përdorimit të OpenAI API, është e mundur të përdorni një bazë të dhënash vektorike si Chroma, Qdrant ose Pinecone.
Bazuar në këtë koncept, u krijua Vector Database. Si rezultat, në vend të përdorimit të OpenAI API, është e mundur të përdorni një bazë të dhënash vektorike si Chroma, Qdrant ose Pinecone.
2.1 Chunk Çdo File & Overlap
Blloqe të mëdha të tekstit mund të tejkalojnë kufijtë e kontekstit të modelit ose të shkaktojnë goditje më pak relevante, kështu që rekomandohet t'i ndajmë ato në copa për ta bërë kërkimin më të synuar. megjithatë, për të ruajtur disa vazhdimësi midis copave, ne i mbivendosim ato me një numër të caktuar tokenësh (ose karaktereve). Në këtë mënyrë, kufijtë e copave janë më pak të prirur për të prerë mesfushën relevante të kontekstit.
Shembuj të Chunking
Në këtë shembull, kemi një tekst të gjatë që duam ta ndajmë në copa më të vogla.Në këtë rast, duam të krijojmë copa prej 100 karaktereve dhe t’i mbulojmë ato me 50 karaktere.
Full Text (406 characters):
Në zemër të qytetit të gjallë, qëndronte një bibliotekë e vjetër që shumë njerëz e kishin harruar. Sheshet e saj kulmuese ishin të mbushura me libra nga çdo zhanër i imagjinueshëm, secila duke pëshpëritur histori aventurash, mistere dhe urtësi të përjetshme. Çdo mbrëmje, një bibliotekar i përkushtuar do të hapte dyert e tij, duke mirëpritur mendjet kurioze të etura për të eksploruar njohuritë e gjera brenda.
-
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.
Kodi i shkurtër
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;
};
Për të mësuar më shumë në lidhje me grumbullimin, dhe ndikimin e madhësisë në embedding, ju mund të shikoni këtë artikull.
Për të mësuar më shumë në lidhje me grumbullimin, dhe ndikimin e madhësisë në embedding, ju mund të shikoni këtë artikull.
2.2 Gjenerata e ndërtuar
Pasi një skedar është copëtuar, ne gjenerojmë embeddings vektor për çdo copë duke përdorur API OpenAI (p.sh.,text-embedding-3-large
) të
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 Krijimi dhe ruajtja e embeddings për të gjithë dosjen
Për të shmangur rigjenerimin e embeddings çdo herë, ne do të ruajmë embeddings. ajo mund të ruhet në një bazë të dhënash. por në këtë rast, ne thjesht do ta ruajmë atë në një skedar JSON në vend.
Kodi i mëposhtëm thjesht:
- përcjell çdo dokument,
- Shpërndaje dokumentin në copa,
- për çdo shtresë, për çdo shtresë,
- Shkarkoni të dhënat në një skedar JSON.
- Plotësoni vektorStore me futjet që do të përdoren në kërkim.
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) Kërkimi i dokumentacionit
3.1 Vektorë të ngjashëm
Për t'iu përgjigjur pyetjes së një përdoruesi, ne së pari gjenerojmë një embedding përuser's questiondhe pastaj llogarisim ngjashmërinë cosine midis futjes së pyetjes dhe futjes së secilit copë. Ne filtrojmë çdo gjë nën një kufi të caktuar të ngjashmërisë dhe mbajmë vetëm ndeshjet më të larta X.
/**
* 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 me Chunks përkatëse
Pas tërmetit, ne ushqejmëtopKjo do të thotë se ChatGPT i sheh seksionet më të rëndësishme të dokumenteve tuaja sikur t’i kishit shkruar ato në bisedë.
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;
};
Implementimi i OpenAI API për Chatbot duke përdorur Express
Për të ekzekutuar sistemin tonë, ne do të përdorim një server Express.js. Këtu është një shembull i një pika të vogël Express.js për të trajtuar pyetjen:
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: Krijimi i një ndërfaqe chatbot
Në frontend, kam ndërtuar një komponent të vogël React me një ndërfaqe të ngjashme me chat. Ajo dërgon mesazhe në backend-in tim Express dhe tregon përgjigjet.
Kodi i tempullit
Unë bëra njëKodi i tempullitpër ju që të përdorni si një pikë fillestare për chatbot tuaj.
Demo të gjalla
Nëse doni të testoni zbatimin përfundimtar të këtij chatbot, kontrolloni këtëFaqe demo.
Faqe demoKodi i Demo
- Fjalë kyçe askDocQuestion.ts
- Frontend: Komponentët ChatBot
Shkoni më tej
Në YouTube, shikoni këtëFjalë kyçe Adrien TwarogPërdorimi i OpenAI Embeddings dhe Vector Databases
Edhe unë rashë mbiAsistentët e kërkimit të skedarëve të OpenAI, e cila mund të jetë interesante nëse doni një qasje alternative.
Konkludimi
Unë shpresoj se kjo ju jep një ide se si të merren me indeksimin e dokumentacionit për një chatbot:
- Duke përdorur chunking + overlap në mënyrë që të gjendet konteksti i duhur,
- Gjenerimi i embeddings dhe ruajtja e tyre për kërkime të shpejta të ngjashmërisë së vektorëve,
- Së fundi, ia dorëzova atë ChatGPT me kontekstin përkatës.
Unë nuk jam një ekspert i inteligjencës artificiale; kjo është vetëm një zgjidhje që kam gjetur që punon mirë për nevojat e mia.please let me knowUnë do të doja të dëgjoja reagime në lidhje me zgjidhjet e ruajtjes së vektorëve, strategjitë e grumbullimit ose këshilla të tjera të performancës.
Thanks for reading, and feel free to share your thoughts!