Building a Local RAG with LangChain + Ollama + pgvector: A Complete Hands-On Walkthrough from 0 to 1 (with uv Dependency Management and an Interview Guide)#
This post is a directly actionable document. Follow it top to bottom and you will build a local RAG (retrieval-augmented generation) system from scratch and understand the key concepts and code. All core scripts come with comments to make learning and interview review easier.
Code repository: https://github.com/ByronFinn/rag-lab.git — complete runnable example code
Goals and Outcomes#
What you will get:
- A locally runnable RAG system: embed your documents into a vector store, retrieve, and generate answers with an LLM.
- A reusable engineering scaffold: LangChain + Ollama + pgvector + uv.
- Interview-ready principles and code details: the full pipeline of retrieval, splitting, embedding, recall, reranking, and answer generation.
System Architecture and Data Flow#
| |
- Ollama: runs the LLM and embedding model locally (the examples use
qwen3:8bandqwen3-embedding:4b). - LangChain: calls the Ollama API directly and orchestrates the flow of load → split → embed → store → retrieve → generate.
- pgvector: PostgreSQL’s vector extension for storing and retrieving document vectors.
- uv: a blazing-fast, reproducible Python dependency and virtual environment manager.
Prerequisites#
- OS: macOS / Linux / WSL2 / Windows (WSL2 recommended)
- Installed: Docker (with Compose), curl
- Network access to the Ollama model repository (models are pulled automatically on first run)
Without Docker you can also install PostgreSQL + pgvector and Ollama manually — the steps are the same. This post assumes Docker launches the backend services in one shot.
One-Command Backend (Docker)#
Create a project rag-lab/ in your working directory with the following files.
1) docker-compose.yml#
| |
2) litellm.yaml#
| |
3) Start the containers#
| |
Verify:
| |
The pgvector image ships with the extension — you generally don’t need to run
CREATE EXTENSION vector;(required only if you self-manage PG).
Managing the Python Environment with uv#
uv is a Python package/environment manager written in Rust — extremely fast, zero mental overhead.
1) Install uv#
| |
2) Initialize the project and virtual environment#
| |
3) Edit pyproject.toml#
Fill in the dependencies under [project]:
| |
Install dependencies and generate the lock file:
| |
4) .env (for the local scripts)#
| |
Project Structure and Config Files#
The recommended final layout:
| |
Ingesting Data: ingest.py (Annotated)#
| |
Best practices for Markdown processing
Why UnstructuredMarkdownLoader: compared with a plain TextLoader, UnstructuredMarkdownLoader parses markdown structure much better (headings, code blocks, lists, etc.), improving semantic understanding quality.
The strategy=“fast” setting:
strategy="fast"balances speed and quality and suits most RAG scenarios. If you need finer structural parsing, try other strategies.Supported file types: the current configuration handles .txt and .md files. For PDFs, convert them to text first or use a dedicated PDF processing tool.
Key points
- Larger
chunk_sizepacks more information into each chunk but reduces recall diversity; smaller does the opposite. Fine-tune within 600–1,200. - The first run creates the
langchain_pg_*tables; no manual table creation needed.
Retrieval Q&A: query.py (Annotated)#
| |
Key points
- The retriever’s
kandsearch_type(e.g.mmr) noticeably affect answer completeness and deduplication. - For Q&A, keep
temperaturewithin 0.0–0.3; raise it a bit for creative writing.
Run and Verify#
- Start the backend containers (Ollama / LiteLLM / pgvector):
| |
Prepare data: drop some
.txtor.mdfiles intodata/.Vectorize and ingest:
| |
- Retrieve and query:
| |
- Quick health check:
| |
Performance Tuning and Best Practices#
- Splitting strategy: for long technical documents raise
chunk_sizeto 1000–1200; for legal/spec-style text go larger still to preserve context. - Retrieval parameters:
k=4~8; MMR improves diversity (fewer near-duplicate chunks). - Index optimization: recent pgvector supports HNSW; create an HNSW index on the
embeddingcolumn to speed up retrieval on larger datasets. - Caching: cache final answers or retrieval results at the application layer for repeated questions.
- Logging and observability: enable LiteLLM logging and Prometheus export; watch latency, failure rate, and retries.
Common Problems and Troubleshooting Checklist#
- LiteLLM 401:
OPENAI_API_KEYcan be any non-empty string;OPENAI_API_BASEmust point tohttp://localhost:4000. - Ollama 404: confirm the
docker composelogs show the models beingpull-ed; or rundocker exec -it ollama ollama pull <model>manually. - psycopg connection failure: wait for the pg health check to pass; check
PG_URLand port mapping. - Garbled Chinese text: use
autodetect_encoding=TrueinTextLoader; keep source files UTF-8. - Empty PDF extraction: try
pypdf/unstructured; or convert totxtfirst. - Irrelevant retrieval: increase
chunk_size/k; or switch to a stronger embedding model (e.g.mxbai-embed-large).
Bonus: Makefile and One-Command Shortcuts#
Create a Makefile in the project root:
| |
From then on:
make up→make ingest→make query→make down
Security and Production Advice#
- Isolation: in production, keep PG, LiteLLM, and Ollama on an internal network; expose only the application-layer API.
- Authentication: add gateway auth/signing in front of the LiteLLM gateway; prevent abuse.
- Privacy: be explicit about telemetry and logging policies; avoid writing sensitive data to disk.
- Observability: instrument the pipeline (retrieval latency, recall rate, answer length) with alert thresholds.
Interview Guide (High-Frequency Questions and Answer Approaches)#
The Q&A below is based on this article’s implementation and covers high-frequency topics from principles to engineering.
1) What is RAG and why do we need it?#
Key points: RAG reduces hallucination and improves freshness by “retrieving relevant knowledge + generating answers”. Compared with pure generation, RAG can pull in the latest documents and private knowledge; compared with pure retrieval, RAG can compose natural-language answers.
2) How does document splitting affect retrieval quality?#
Key points: large chunk_size → high information density per chunk but low diversity; small → high diversity but fragmented context. 800–1200 is the engineering sweet spot in practice; fine-tune by text style.
3) Why pgvector?#
Key points: coexists with relational data, mature transactions and permissions, easy deployment; recent versions support HNSW indexes with excellent query speed; rich ecosystem (backup, monitoring, managed cloud).
4) How to choose an embedding model?#
Key points: consider language, domain, and budget; for Chinese/English multilingual, qwen3-embedding:4b/mxbai-embed-large are recommended; for specialized domains like legal or code, prefer domain models. Compare with retrieval Hit@k and nDCG.
5) Why use Ollama directly instead of LiteLLM?#
Key points: using langchain-ollama directly simplifies the architecture, reduces dependencies, and gets Ollama’s performance benefits; no extra proxy layer, lowering latency and complexity. For purely local deployments, direct integration is more efficient.
6) How to diagnose irrelevant retrieval?#
Key points: check whether splitting is reasonable, whether the same embedding model is used, whether text preprocessing was done; increase k or enable mmr; add a rerank stage if needed (e.g. BGE/Cohere Rerank).
7) How to reduce hallucination?#
Key points: the prompt must state “answer only from the passages”; provide citations/numbering; lower the temperature; if necessary add an answerable check or a validation chain.
8) How to evaluate?#
Key points:
- Retrieval layer: Hit@k, Recall, nDCG;
- Generation layer: LLM-as-a-Judge against reference answers, factuality metrics (Faithfulness / Groundedness). In engineering, build offline Q/A pairs and run periodic regressions.
9) Key risks in production deployment?#
Key points: permissions and data masking, cost and latency, observability and alerting, model drift and data freshness, version rollback and A/B testing.
10) Hand-write a minimal RAG data flow?#
Approach: describe the steps “load → split → embed → store → retrieve → build prompt → generate” and give the key parameters (chunk, k, temperature). Refer to ingest.py and query.py in this post.
11) Why uv?#
Key points: extremely fast installs, automatic virtual-environment management, lock files for reproducibility; use uv sync --frozen in CI/CD to guarantee dependency consistency.
12) How to switch between local and cloud models?#
Key points: in real projects, just change the model and base_url parameters of langchain_ollama — no LiteLLM needed.
13) What does chunk overlap do?#
Key points: prevents important information from being cut off at chunk boundaries and keeps context continuous. Rule of thumb: 10-20% of chunk_size.
14) Why MMR retrieval over plain top-k?#
Key points: MMR (Maximal Marginal Relevance) adds diversity while preserving relevance, avoiding recall of overly similar document fragments.
15) How to handle splitting for long documents?#
Key points:
- Structured documents: split along semantic boundaries like headings and paragraphs
- Unstructured documents: use dynamic chunk_size combined with overlap
- Specialized documents: incorporate domain knowledge and design a dedicated splitting strategy
16) What are RAG evaluation metrics?#
Key points:
- Retrieval metrics: Hit@k, MRR, nDCG, Recall@K
- Generation metrics: BLEU, ROUGE, BERTScore
- Task metrics: EM (Exact Match), F1-score
- Human evaluation: factuality, consistency, relevance
17) How to design RAG prompts?#
Key points:
- Define the role: “You are a professional Q&A assistant”
- Constrain the answer scope: “only based on the provided document content”
- Require source citations: “mark which document number you are citing”
- Handle unanswerable cases: “if the documents contain no relevant information, say so explicitly”
18) Where are a RAG system’s performance bottlenecks?#
Key points:
- Vector search speed: index type (HNSW vs IVF), vector dimensionality
- LLM inference latency: model size, batching, concurrency control
- Database connections: connection pooling, query optimization, caching strategy
- Network latency: where model services are deployed, data-transfer optimization
19) Why UnstructuredMarkdownLoader instead of a plain TextLoader?#
Key points:
- Structure-aware: UnstructuredMarkdownLoader recognizes and preserves markdown’s semantic structure (headings, code blocks, lists, etc.)
- Better splitting: splits along semantic structure rather than naive character counts
- Better retrieval quality: a structured content representation enables more precise vectorization and retrieval
- Strategy configuration: supports parameters like
strategy="fast"to balance speed and quality
20) How to optimize loading strategies for different document types?#
Key points:
- Technical docs: prefer UnstructuredMarkdownLoader to preserve structure
- Code docs: combine syntax highlighting with special handling for code blocks
- Tabular data: use loaders that support table parsing
- Mixed documents: choose the best strategy based on the dominant content and query patterns
