Pełnie lokalny PDF RAG z użyciem LangChain, Ollamy i Chromy
Pełnie lokalna aplikacja PDF RAG: LangChain przetwarza pliki, Ollama je włącza, Chroma przechowuje wektory, a odpowiedzi działają offline bez żadnych kluczy API chmurowych.
Źródła: Implement RAG with Langchain and local Ollama models
Zbuduj w pełni lokalny pipeline RAG dla plików PDF za pomocą LangChain, Ollamy i ChromaDB — rozmawiaj z dokumentami bez użycia chmurowych API i bez wychodzenia danych poza maszynę.
Dlaczego lokalne rozwiązania są ważne
Chmurowe modele LLM są wygodne, ale przy każdym zapytaniu tekst jest przesyłany przez sieć kogoś innego. Umowy prawnicze, dokumenty medyczne oraz wewnętrzne raporty często nie mogą zaakceptować takiego kompromisu. Lokalna architektura eliminuje to ryzyko, umożliwiając jednoczesne zadawanie pytań w języku naturalnym do własnych plików.
Co otrzymujesz
- Przyjmowanie plików PDF do trwałego magazynu wektorowego
- Odpowiadanie na pytania w języku naturalnym oparte na tych dokumentach
- Robota offline podczas przetwarzania — nie są potrzebne klucze API po pobraniu modeli
Stack: LangChain do orkiestracji, Ollama do lokalnej rozmowy i tworzenia embeddingów, ChromaDB jako magazyn wektorowy.
Architektura
Ścieżka pobierania: bajty pliku PDF trafiają do ładowacza opartego na PyPDF, są dzielone na fragmenty, a następnie Ollama wstawia każdy z tych fragmentów do Chromy.
Ścieżka zadawania pytań: pytanie trafia do mechanizmu wyszukiwania, pasujące fragmenty stają się kontekstem, a lokalny model Ollama do rozmów pisze odpowiedź.
Dwie fazy zapewniają uczciwe koszty:
- Pobieranie — raz (lub gdy zmieniają się dokumenty): analiza, dzielenie na fragmenty, tworzenie embeddingów, przechowywanie
- Wyszukiwanie — przy każdym zapytaniu: najważniejsze fragmenty stają się kontekstem dla LLM
Płacisz za tworzenie embeddingów, gdy zmienia się treść; interaktywne zapytania pozostają szybkie.
Wymagania wstępne
Zainstaluj Ollama i pobierz tutaj używane modele:
ollama pull embeddinggemma:latest # embedding model
ollama pull gemma4:latest # chat model
gemma4 został dostosowany pod konkretne instrukcje. Aby zmniejszyć zużycie zasobów, spróbuj gemma2:2b; dla silniejszych procesów rozumowania użyj gemma4:27b — odpowiednio zaktualizuj ciągi konfiguracyjne.
Ustawienie projektu
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
Główne pakiety z wymagań:
langchain
langchain-chroma
langchain-community
langchain-ollama
langchain-text-splitters
pypdf
chromadb
python-dotenv
Zmień domyślne ustawienia za pomocą środowiska bez edycji kodu:
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=gemma4:latest
OLLAMA_EMBED_MODEL=embeddinggemma:latest
Proces pobierania danych (src/ingest.py)
Ładowanie dokumentów
def load_documents(pdf_path: str | Path) -> list:
path = Path(pdf_path)
loader = (
PyPDFLoader(str(path))
if path.is_file()
else PyPDFDirectoryLoader(str(path))
)
return loader.load()py
Jeden pomocnik obsługuje pojedynczy plik lub katalog. Ładowarki LangChain zawsze zwracają jednolitą listę obiektów Document w obu przypadkach.
Dzielenie na fragmenty
def split_documents(documents: list) -> list:
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
)
return splitter.split_documents(documents)
RecursiveCharacterTextSplitter preferuje dzielenie na akapity, następnie zdania, a potem słowa, aby zachować granice semantyczne. Przekrycie 200 znaków pozwala na zachowanie zdania przecinającego granicę w obu sąsiednich fragmentach, co zmniejsza liczby nieudanych wyświetleń na krawędziach.
Zaimportowanie i przechowywanie
def build_vector_store(chunks: list) -> Chroma:
embeddings = OllamaEmbeddings(model=EMBEDDING_MODEL, base_url=OLLAMA_BASE_URL)
VECTOR_STORE_DIR.mkdir(parents=True, exist_ok=True)
return Chroma.from_documents(
documents=chunks,
embedding=embeddings,
collection_name=COLLECTION_NAME,
persist_directory=str(VECTOR_STORE_DIR),
)
Ollama serwuje embeddingi lokalnie na porcie 11434. Chroma przechowuje je w katalogu vector_store/ (SQLite wraz z plikami indeksów HNSW), dzięki czemu ponowne uruchomienie procesu nie wymusza całkowitego ponownego importowania.
Łańcuch wyświetlania (src/retrieval.py)
LangChain LCEL łączy kroki za pomocą operatora |.
Śablon promptu
RAG_PROMPT = ChatPromptTemplate.from_messages([
(
"system",
"You are an assistant that answers questions based on the provided context "
"from PDF documents. Use only the information in the context to answer. "
"If the answer is not in the context, say so clearly.\n\nContext:\n{context}"
),
("human", "{question}"),
])
Wiadomość systemowa umacnia odpowiedzi w odniesieniu do pobranego kontekstu — to główna zasada RAG chroniąca przed halucynacjami wynikającymi wyłącznie z treningu wstępnego.
Budowanie łańcucha
def build_rag_chain(k: int = 4):
store = load_vector_store()
retriever = store.as_retriever(search_kwargs={"k": k})
llm = ChatOllama(model=MODEL_NAME, base_url=OLLAMA_BASE_URL)
return (
{"context": retriever | _format_docs, "question": RunnablePassthrough()}
| RAG_PROMPT
| llm
| StrOutputParser()
)
Z lewej na prawą: retriever wybiera k najbardziej podobnych fragmentów na podstawie podobieństwa embeddingów, narzędzia pomocnicze formatują te fragmenty w jeden ciąg tekstowy kontekstowego, szablon promptu łączy ten kontekst z pytaniem użytkownika, a ChatOllama generuje odpowiedź.
Interaktywny cykl zadawania pytań
# Index a directory of PDFs (default: data/pdfs/)
python main.py ingest
Lekka interfejs CLI (lub otoczka HTTP) ładuje łańcuch tylko raz i obsługuje pytania aż do wyjścia. Utrzymuj k jako parametr dostępny do regulacji, aby móc pogodzić rozmiar kontekstu z opóźnieniem.
Rozruch od początku do końca
# Index a specific file
python main.py ingest --path /path/to/contract.pdf
# Ask a one-off question
python main.py query "What are the payment terms?"
# Interactive session
python main.py chat
Wskazz folder z plikami PDF, upewnij się, że katalog przechowujący wektory jest wypełniony, a następnie zadaj zapytanie. Wszystko — embeddingi, wektory, proces generowania — pozostaje na lokalnym serwerze.
Wniosek
Local RAG nie jest już czymś egzotycznym. Ollama dostarcza modele, Chroma przechowuje wektory, a LangChain łączy elementy grafu. Dla prywatnych korpusów ta trójka wystarcza do stworzenia przydatnego asystenta dokumentowego bez konieczności korzystania z chmury czy przesyłania danych na zewnątrz.