ASDRP · Agentic RAG Track · Module 04
Slice 8 real metals-market documents into overlapping chunks, embed them locally, and run your first meaningful retrieval query.
What we'll cover
Track 1 · Why chunk?
Track 2 · Chunking strategy
LangChain's splitter tries to break on natural boundaries — paragraphs → sentences → words → characters — only moving to a finer boundary when the chunk is still too big.
from langchain_text_splitters import (
RecursiveCharacterTextSplitter,
)
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=60,
)
# Split ONE document string → list[str]
pieces = splitter.split_text(long_text)
print(f"{len(pieces)} chunks from 1 doc")
Separator priority: \n\n → \n → → ""
Track 2 · The corpus
| # | File | Topic |
|---|---|---|
| 01 | spot_price_and_quotes | Bid/ask, LBMA fix |
| 02 | gold_fundamentals | Gold supply & demand |
| 03 | silver_and_industrial | Silver photovoltaics |
| 04 | platinum_palladium | PGMs & autocatalysts |
| 05 | investment_vehicles | ETFs, futures, coins |
| 06 | drivers_and_macro | USD, rates, geopolitics |
| 07 | risk_and_portfolio | VaR, diversification |
| 08 | futures_and_derivatives | COMEX contracts |
from pathlib import Path
raw = [
{"source": p.name,
"page_content": p.read_text()}
for p in sorted(
Path("corpus").glob("*.md"))
]
Track 3 · Build & query
from pathlib import Path
from sentence_transformers import SentenceTransformer
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
# 1. Load corpus
raw_docs = [{"source": p.name, "page_content": p.read_text()}
for p in sorted(Path("corpus").glob("*.md"))]
# 2. Chunk
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=60)
chunks = [(d["source"], piece)
for d in raw_docs
for piece in splitter.split_text(d["page_content"])]
# 3. Embed (local model, no API key)
model = SentenceTransformer("all-MiniLM-L6-v2") # 384-dim
vectors = model.encode([c[1] for c in chunks])
# 4. Index in Qdrant :memory:
client = QdrantClient(":memory:")
client.create_collection("metals_kb",
vectors_config=VectorParams(size=384, distance=Distance.COSINE))
client.upsert("metals_kb", points=[
PointStruct(id=i, vector=v.tolist(), payload={"text": t, "source": s})
for i, ((s, t), v) in enumerate(zip(chunks, vectors))])
Track 3 · Retrieval
def retrieve(query: str, k: int = 5) -> list[str]:
"""Return top-k chunk texts for a query."""
q_vec = model.encode(query).tolist()
hits = client.search(
"metals_kb", query_vector=q_vec, limit=k
)
return [h.payload["text"] for h in hits]
# Example
results = retrieve(
"How is the LBMA gold price set?", k=5
)
for i, r in enumerate(results, 1):
print(f"[{i}] {r[:120]}…")
Track 2 · Parameters
| chunk_size | chunk_overlap | Effect | Risk |
|---|---|---|---|
| 50–100 | 10–20 | Very fine-grained — great precision | Loses sentence context; embeddings become noisy |
| 500 | 60 | Capstone default — balanced | Still may split a table or formula mid-way |
| 1 000–2 000 | 100–200 | Fewer chunks, richer context per hit | One vector encodes many ideas; precision suffers |
| 0 (= whole doc) | 0 | Simplest — no splitting | Embeds 2 000-word doc as one point; retrieval is poor |
Summary
Next: Module 05 migrates the local embedder to cloud Ollama via LiteLLM and builds the first prompt-stuffing RAG answer. · Run the notebook: 04_chunking_corpus.ipynb