ट्यूटोरियल

Ollama और pgvector से अपने लैपटॉप पर RAG चैटबॉट बनाएं

अपने डॉक्यूमेंट इंडेक्स करें, embeddings को Postgres में रखें और लोकल मॉडल से जवाब पाएं। न API key, न क्लाउड बिल, करीब एक घंटे का काम।

Retrieval-augmented generation: मॉडल याद से नहीं, आपके डॉक्यूमेंट से जवाब देता है।
इस पेज पर

"अपने डॉक्यूमेंट से चैट करें" वाले ज़्यादातर ट्यूटोरियल एक API key से शुरू होते हैं और हर महीने के बिल पर खत्म होते हैं। यह ट्यूटोरियल एक RAG चैटबॉट बनाता है जो पूरी तरह आपकी मशीन पर चलता है। आखिर में आपके पास दो छोटी Python स्क्रिप्ट होंगी जो Markdown फ़ाइलों के किसी भी फ़ोल्डर के बारे में सवालों के जवाब देंगी, सिर्फ़ ओपन-सोर्स टूल्स से: मॉडल के लिए Ollama और सर्च के लिए pgvector वाला Postgres।

RAG को लोकल क्यों चलाएं?

लैंग्वेज मॉडल सिर्फ़ वही जानता है जिस पर उसे ट्रेन किया गया था। Retrieval-augmented generation (RAG) इसका हल है: यह आपके डॉक्यूमेंट के सबसे काम के हिस्से ढूँढता है और उन्हें सवाल के साथ मॉडल को देता है। लोकल चलाने का मतलब है कि आपके डॉक्यूमेंट कभी आपके लैपटॉप से बाहर नहीं जाते, और आप बिना खर्च की चिंता किए जितना चाहें प्रयोग कर सकते हैं।

अगर जवाब आपके अपने डॉक्यूमेंट के तीन पैराग्राफ़ से समझाया जा सकता है, तो आम तौर पर एक छोटा लोकल मॉडल काफ़ी है।

ध्यान दें

यहाँ इस्तेमाल होने वाले चैट मॉडल के लिए करीब 8 GB खाली RAM चाहिए। छोटी मशीन पर हल्का मॉडल लें; कोड नहीं बदलता।

आप क्या बनाएंगे

docs/*.mdchunkspgvectortop 4 matchesllama3.2answer
चित्र 1. डॉक्यूमेंट एक बार टुकड़ों में बँटकर embed और स्टोर होते हैं; हर सवाल पर मॉडल के जवाब से पहले सबसे करीबी टुकड़े निकाले जाते हैं।
  • एक ingest स्क्रिप्ट, जो Markdown फ़ाइलों को टुकड़ों (chunks) में बाँटती है और हर टुकड़े का embedding स्टोर करती है।
  • vector टाइप वाली एक Postgres टेबल, ताकि similarity search बस एक SQL क्वेरी हो।
  • एक ask स्क्रिप्ट, जो संदर्भ (context) निकालती है और लोकल मॉडल को कॉल करती है।

Embedding संख्याओं की एक सूची है जो किसी टेक्स्ट के अर्थ को दर्शाती है। मिलते-जुलते अर्थ वाले टेक्स्ट की संख्याएँ भी मिलती-जुलती होती हैं, इसी से डेटाबेस बिना कीवर्ड मिलाए "इस सवाल के सबसे करीबी पैराग्राफ़" ढूँढ पाता है।

Ollama और Postgres सेट अप करें

  1. Ollama इंस्टॉल करें और देखें कि ollama --version वर्ज़न दिखाता है।
  2. एक चैट मॉडल और एक embedding मॉडल डाउनलोड करें।
  3. 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 से) कनेक्ट करें और यह चलाएं:

schema.sqlSQL
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 करें और टेबल में डालें। ओवरलैप की वजह से जो वाक्य दो टुकड़ों की सीमा पर कटता है, वह पड़ोसी टुकड़ों में से किसी एक में पूरा मिल जाता है।

ingest.pyPython
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-text768लंबे टेक्निकल डॉक्यूमेंट के लिए अच्छा डिफ़ॉल्ट
mxbai-embed-large1024बेहतर retrieval, पर ingest धीमा
all-minilm384छोटा और तेज़, जल्दी प्रोटोटाइप के लिए

अपने डॉक्यूमेंट से सवाल पूछें

सवाल को भी उसी तरह embed करें, <=> cosine distance ऑपरेटर से चार सबसे करीबी टुकड़े निकालें और उन्हें संदर्भ के रूप में मॉडल को दें।

ask.pyPython
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" किसी भी बड़े मॉडल से ज़्यादा सटीकता देता है।

वह एक लाइन का prompt जो जवाबों को सही रखता है

जब जवाब गलत हों

ज़्यादातर गलत जवाब मॉडल की वजह से नहीं, retrieval की वजह से आते हैं। बड़ा मॉडल आज़माने से पहले rows प्रिंट करें और पढ़ें कि असल में क्या निकाला गया:

  • सही टुकड़ा मिला ही नहीं। छोटे टुकड़े, बड़ा LIMIT या ऊपर की टेबल से बेहतर embedding मॉडल आज़माएं।
  • सही टुकड़ा मिला, पर आधा कटा हुआ। ओवरलैप बढ़ाएं।
  • मॉडल संदर्भ को नज़रअंदाज़ करता है। system prompt छोटा और साफ़ रखें, और संदर्भ ऊपर की तरह system message में ही दें।
कोर्स में शामिल

इस स्क्रिप्ट को वेब UI वाले चैटबॉट में बदलना Learning Development with AI के Hour 11 का विषय है, और इसे टीम के लिए चलाना Hour 12 में आता है।

आगे क्या करें

अब आपके पास हर RAG सिस्टम का मूल ढाँचा है: ingest, retrieve, generate। यहाँ से सबसे बड़ा फ़ायदा बेहतर chunking (कैरेक्टर गिनने के बजाय headings पर बाँटना), हर जवाब के साथ स्रोत दिखाने, और सभी मॉडल के आगे एक गेटवे लगाने से मिलता है, ताकि आप मॉडल आसानी से बदल सकें।

Rahul Agarwal

इंस्ट्रक्टर, GeeksWithGeeks

15 साल से वेब डेवलपर। डेवलपर्स और फ़ाउंडर्स को ओपन-सोर्स AI टूल्स से असली प्रोडक्ट बनाना सिखाते हैं।

अगला ट्यूटोरियल ईमेल पर पाएं

नई पोस्ट आने पर एक ईमेल, चलने को तैयार कोड के साथ। कोई स्पैम नहीं।

सब्सक्राइब करके आप GeeksWithGeeks से ईमेल पाने के लिए सहमत होते हैं। कभी भी अनसब्सक्राइब करें।