Ollama और pgvector से अपने लैपटॉप पर RAG चैटबॉट बनाएं
अपने डॉक्यूमेंट इंडेक्स करें, embeddings को Postgres में रखें और लोकल मॉडल से जवाब पाएं। न API key, न क्लाउड बिल, करीब एक घंटे का काम।
इस पेज पर
"अपने डॉक्यूमेंट से चैट करें" वाले ज़्यादातर ट्यूटोरियल एक API key से शुरू होते हैं और हर महीने के बिल पर खत्म होते हैं। यह ट्यूटोरियल एक RAG चैटबॉट बनाता है जो पूरी तरह आपकी मशीन पर चलता है। आखिर में आपके पास दो छोटी Python स्क्रिप्ट होंगी जो Markdown फ़ाइलों के किसी भी फ़ोल्डर के बारे में सवालों के जवाब देंगी, सिर्फ़ ओपन-सोर्स टूल्स से: मॉडल के लिए Ollama और सर्च के लिए pgvector वाला Postgres।
RAG को लोकल क्यों चलाएं?
लैंग्वेज मॉडल सिर्फ़ वही जानता है जिस पर उसे ट्रेन किया गया था। Retrieval-augmented generation (RAG) इसका हल है: यह आपके डॉक्यूमेंट के सबसे काम के हिस्से ढूँढता है और उन्हें सवाल के साथ मॉडल को देता है। लोकल चलाने का मतलब है कि आपके डॉक्यूमेंट कभी आपके लैपटॉप से बाहर नहीं जाते, और आप बिना खर्च की चिंता किए जितना चाहें प्रयोग कर सकते हैं।
अगर जवाब आपके अपने डॉक्यूमेंट के तीन पैराग्राफ़ से समझाया जा सकता है, तो आम तौर पर एक छोटा लोकल मॉडल काफ़ी है।
यहाँ इस्तेमाल होने वाले चैट मॉडल के लिए करीब 8 GB खाली RAM चाहिए। छोटी मशीन पर हल्का मॉडल लें; कोड नहीं बदलता।
आप क्या बनाएंगे
- एक ingest स्क्रिप्ट, जो Markdown फ़ाइलों को टुकड़ों (chunks) में बाँटती है और हर टुकड़े का embedding स्टोर करती है।
vectorटाइप वाली एक Postgres टेबल, ताकि similarity search बस एक SQL क्वेरी हो।- एक ask स्क्रिप्ट, जो संदर्भ (context) निकालती है और लोकल मॉडल को कॉल करती है।
Embedding संख्याओं की एक सूची है जो किसी टेक्स्ट के अर्थ को दर्शाती है। मिलते-जुलते अर्थ वाले टेक्स्ट की संख्याएँ भी मिलती-जुलती होती हैं, इसी से डेटाबेस बिना कीवर्ड मिलाए "इस सवाल के सबसे करीबी पैराग्राफ़" ढूँढ पाता है।
Ollama और Postgres सेट अप करें
- Ollama इंस्टॉल करें और देखें कि
ollama --versionवर्ज़न दिखाता है। - एक चैट मॉडल और एक embedding मॉडल डाउनलोड करें।
- Docker में pgvector एक्सटेंशन वाला Postgres चलाएं, फिर Python पैकेज इंस्टॉल करें।
ollama pull llama3.2
pulling manifest
success
ollama pull nomic-embed-text
success
docker run -d --name pgvector -e POSTGRES_PASSWORD=dev -p 5432:5432 pgvector/pgvector:pg16
pip install ollama "psycopg[binary]" pgvector numpyटर्मिनल ब्लॉक का कॉपी बटन सिर्फ़ कमांड कॉपी करता है, आउटपुट की लाइनें नहीं, ताकि आप सीधे अपने टर्मिनल में पेस्ट कर सकें।
अगर आपकी मशीन पर पोर्ट 5432 पहले से किसी और Postgres ने ले रखा है, तो कोई दूसरा होस्ट पोर्ट दें, जैसे -p 5433:5432, और नीचे की connection string भी उसी हिसाब से बदलें।
pgvector में embeddings स्टोर करें
टेबल बनाएं
nomic-embed-text मॉडल हर टुकड़े के लिए 768 संख्याएँ लौटाता है, इसलिए कॉलम vector(768) है। किसी भी SQL क्लाइंट से (या docker exec -it pgvector psql -U postgres से) कनेक्ट करें और यह चलाएं:
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE docs (
id bigserial PRIMARY KEY,
source text NOT NULL,
body text NOT NULL,
embedding vector(768)
);source कॉलम हर टुकड़े के साथ फ़ाइल का नाम रखता है, ताकि बाद में आप दिखा सकें कि जवाब कहाँ से आया।
डॉक्यूमेंट के टुकड़े करें और embed करें
हर फ़ाइल को करीब 800 कैरेक्टर के ओवरलैप वाले टुकड़ों में बाँटें, हर टुकड़े को embed करें और टेबल में डालें। ओवरलैप की वजह से जो वाक्य दो टुकड़ों की सीमा पर कटता है, वह पड़ोसी टुकड़ों में से किसी एक में पूरा मिल जाता है।
from pathlib import Path
import numpy as np
import ollama
import psycopg
from pgvector.psycopg import register_vector
conn = psycopg.connect("postgresql://postgres:dev@localhost:5432/postgres", autocommit=True)
register_vector(conn)
def embed(text: str) -> list[float]:
return ollama.embed(model="nomic-embed-text", input=text)["embeddings"][0]
def chunk(text: str, size: int = 800, overlap: int = 120):
for start in range(0, len(text), size - overlap):
yield text[start:start + size]
# one row per chunk
for path in Path("docs").glob("*.md"):
for piece in chunk(path.read_text()):
conn.execute(
"INSERT INTO docs (source, body, embedding) VALUES (%s, %s, %s)",
(path.name, piece, np.array(embed(piece))),
)स्क्रिप्ट के पास docs फ़ोल्डर में कुछ Markdown फ़ाइलें रखें और python ingest.py चलाएं। दोबारा चलाने पर टुकड़े फिर से जुड़ जाते हैं, इसलिए फिर से ingest करने से पहले TRUNCATE docs; से टेबल खाली कर लें।
अगर बाद में embedding मॉडल बदलते हैं, तो vector का साइज़ भी बदल जाता है। टेबल दोबारा बनाएं और सब कुछ फिर से ingest करें; एक कॉलम में अलग-अलग साइज़ नहीं रह सकते, और अलग मॉडल के vectors की तुलना वैसे भी नहीं हो सकती।
कौन-सा embedding मॉडल चुनें? ज़्यादातर डॉक्यूमेंट के लिए डिफ़ॉल्ट से शुरू करें, और तभी बदलें जब जवाब साफ़ मिलने वाली बातें भी छोड़ने लगें।
| मॉडल | Dimensions | किसके लिए अच्छा |
|---|---|---|
nomic-embed-text | 768 | लंबे टेक्निकल डॉक्यूमेंट के लिए अच्छा डिफ़ॉल्ट |
mxbai-embed-large | 1024 | बेहतर retrieval, पर ingest धीमा |
all-minilm | 384 | छोटा और तेज़, जल्दी प्रोटोटाइप के लिए |
अपने डॉक्यूमेंट से सवाल पूछें
सवाल को भी उसी तरह embed करें, <=> cosine distance ऑपरेटर से चार सबसे करीबी टुकड़े निकालें और उन्हें संदर्भ के रूप में मॉडल को दें।
import sys
import numpy as np
import ollama
import psycopg
from pgvector.psycopg import register_vector
conn = psycopg.connect("postgresql://postgres:dev@localhost:5432/postgres")
register_vector(conn)
question = sys.argv[1] if len(sys.argv) > 1 else "How do I rotate the API keys?"
query = np.array(ollama.embed(model="nomic-embed-text", input=question)["embeddings"][0])
rows = conn.execute(
"SELECT source, body FROM docs ORDER BY embedding <=> %s LIMIT 4",
(query,),
).fetchall()
context = "\n\n".join(f"[{source}]\n{body}" for source, body in rows)
reply = ollama.chat(model="llama3.2", messages=[
{"role": "system", "content": f"Answer only from this context. If the answer is not there, say so.\n\n{context}"},
{"role": "user", "content": question},
])
print(reply["message"]["content"])
print("\nSources:", ", ".join(sorted({source for source, _ in rows})))इसे कोट में सवाल देकर चलाएं, जैसे python ask.py "How do I reset my password?"। पहले जवाब छपता है, फिर वे फ़ाइलें जिनसे संदर्भ आया।
"Answer only from this context" किसी भी बड़े मॉडल से ज़्यादा सटीकता देता है।
जब जवाब गलत हों
ज़्यादातर गलत जवाब मॉडल की वजह से नहीं, retrieval की वजह से आते हैं। बड़ा मॉडल आज़माने से पहले rows प्रिंट करें और पढ़ें कि असल में क्या निकाला गया:
- सही टुकड़ा मिला ही नहीं। छोटे टुकड़े, बड़ा
LIMITया ऊपर की टेबल से बेहतर embedding मॉडल आज़माएं। - सही टुकड़ा मिला, पर आधा कटा हुआ। ओवरलैप बढ़ाएं।
- मॉडल संदर्भ को नज़रअंदाज़ करता है। system prompt छोटा और साफ़ रखें, और संदर्भ ऊपर की तरह system message में ही दें।
इस स्क्रिप्ट को वेब UI वाले चैटबॉट में बदलना Learning Development with AI के Hour 11 का विषय है, और इसे टीम के लिए चलाना Hour 12 में आता है।
आगे क्या करें
अब आपके पास हर RAG सिस्टम का मूल ढाँचा है: ingest, retrieve, generate। यहाँ से सबसे बड़ा फ़ायदा बेहतर chunking (कैरेक्टर गिनने के बजाय headings पर बाँटना), हर जवाब के साथ स्रोत दिखाने, और सभी मॉडल के आगे एक गेटवे लगाने से मिलता है, ताकि आप मॉडल आसानी से बदल सकें।