Tutorial D7: RAG Foundations (LlamaIndex)
Embeddings, Vector Stores, and Citation-Ready Retrieval
β CORE MISSION OF THIS TUTORIAL
By the end of this tutorial, the reader will be able to:
- β Ingest industrial documents into chunked, citation-ready "evidence units"
- β Understand embeddings + vector stores as the retrieval backbone of RAG
- β Implement top-K retrieval that is deterministic enough to audit
- β Produce answers that include citations (source + line range + chunk id)
- β Log query β retrieval β citations as an audit trail for shift handover + compliance
This tutorial builds the RAG substrate you will reuse in D12 (RAG for PLC Documentation) and beyond.
π VENDOR-AGNOSTIC ENGINEERING NOTE
This tutorial uses:
- βΈ LlamaIndex (RAG orchestration) or equivalent retrieval framework
- βΈ Vector stores: FAISS, Chroma, Pinecone, pgvector (swap per plant constraints)
- βΈ Sources: PDF manuals, SOPs, PLC code exports, CMMS/SCADA change logs
All examples are read-only and advisory. No live PLC connections.
1οΈβ£ CONCEPT OVERVIEW β WHY RAG EXISTS IN INDUSTRIAL SETTINGS
Imagine you're building a troubleshooting assistant for a packaging line. It can read: vendor manuals, your SOPs, PLC Structured Text snippets, and a change log from the last maintenance window.
The problem: a plain LLM answer can be plausible and still be un-auditable. In a plant, "sounds right" is not enough β you need to answer: Which document line supports this?
RAG is how you turn an LLM into an evidence-driven assistant: retrieve relevant source text first, then generate an answer that cites it.
- βΈ LLM-only answers are hard to verify after the fact
- βΈ RAG answers can be backed by a retrievable evidence pack
- βΈ Citations + logs are the foundation of auditability
NAIVE "PASTE THE PDF"
- β’ Token-heavy (cost grows with document size)
- β’ No stable citations
- β’ Hard to reproduce the same answer tomorrow
RAG WITH EVIDENCE PACKS
- β’ Retrieve only top-K chunks
- β’ Answer includes citations to exact sources
- β’ Log query β chunks β citations for audits
2οΈβ£ THE RAG BUILDING BLOCKS β INGEST β EMBED β STORE β RETRIEVE β CITE
RAG is not "one feature." It's a pipeline with parts you can reason about and test independently:
- βΈ Ingestion β parse manuals/SOPs/code into text with metadata
- βΈ Chunking β split into stable units you can cite (line ranges, ids)
- βΈ Embeddings β convert chunks into vectors for semantic similarity search
- βΈ Vector store β store vectors + metadata, query top-K fast
- βΈ Retrieval β return the best evidence, not the whole library
- βΈ Citations + audit log β prove what was used and when
graph LR
A[Docs: Manuals/SOPs/PLC Code]:::cyan --> B[Ingest + Chunk]:::purple
B --> C[Embed Chunks]:::purple
C --> D[Vector Store]:::cyan
D --> E[Retrieve Top-K]:::purple
E --> F[Answer + Citations]:::green
E --> G[Audit Log]:::cyan
classDef cyan fill:#04d9ff,stroke:#04d9ff,color:#000
classDef purple fill:#6366f1,stroke:#6366f1,color:#fff
classDef green fill:#00ff7f,stroke:#00ff7f,color:#000 LlamaIndex gives you a clean way to assemble these parts, but you still need to make design choices: chunk boundaries, metadata fields, citation format, and logging.
3οΈβ£ CITATIONS & AUDITABILITY β THE "WHY DID IT SAY THAT?" REQUIREMENT
In industrial automation, the most important RAG question is rarely "did it answer?" β it's "can we justify the answer during review?"
- βΈ Stable citation keys (source + line range + chunk id) survive refactors
- βΈ Audit logs let you reconstruct what the agent saw during a specific run
- βΈ Human review becomes realistic because you can click straight to evidence
Design goal: your agent should behave like an auditor β every recommendation should point to the exact source text that supports it.
Cost note (rule of thumb): embedding your doc set is usually a one-time or infrequent batch job, while "paste the manual into every prompt" charges you every run. Pricing changes by provider/model, so treat any dollar figures as placeholders and verify current rates before budgeting.
4οΈβ£ EXPERIMENTS β BUILD A CITATION-READY MINI RAG PIPELINE
You'll start with a naive approach (what most teams try first), then incrementally add: chunking, vector retrieval, citations, and an audit log contract. All experiments use simulated industrial documents (vendor manuals, SOPs, PLC code, change logs), so cost is $0.00 in API calls.
Experiment 1 β The Naive Approach (Costly + Un-auditable)
You'll simulate "paste the manuals into the prompt" and see why doc-level retrieval still fails audits.
SETUP CELL
Experiment 1A β Understand the Doc model and industrial document structure
Define a simple data structure for holding industrial documents with metadata.
from dataclasses import dataclass
@dataclass
class Doc:
source: str # Document identifier (e.g., vendor manual, SOP, PLC code)
text: str # Full document text Explanation
- - This data model tracks the source (file/document name) and the text content.
- - In real systems, you would also track version, revision date, and equipment IDs.
- - The source field will later become part of your citation format.
Takeaway
Start with a simple model that separates metadata from content.
SETUP CELL
Experiment 1A β Explain the prompt stuffing problem
Understand why pasting entire manuals into every prompt fails at scale.
# Hypothetical cost calculation for prompt stuffing
# Example: 2 vendor PDFs (200 pages each) β 400k-1M tokens
# At $0.15/1M input tokens (gpt-4o-mini as of 2025):
# 400k tokens Γ $0.15/1M = $0.06 per query
# 1M tokens Γ $0.15/1M = $0.15 per query
#
# Problem: cost grows with doc set size, not query complexity
# 1,000 queries/day Γ $0.15 = $150/day = $4,500/month
#
# Additional issues:
# - No stable citations (can't reference specific lines)
# - Answers drift as docs change (no version hash)
# - Context window limits (can't fit 10 manuals) Explanation
- - Prompt stuffing seems easy: just paste everything. But cost scales with your entire document library.
- - Citations are unstable because you cannot reliably reference "line 47 of the vendor manual" when it is pasted as a blob.
- - Reproducibility is weak: if the manual updates, the same query can produce different answers without a clear audit trail.
Takeaway
Prompt stuffing is a demo pattern, not a production pattern.
EXPERIMENT CELL
Experiment 1A β Load industrial documents and measure token cost
Execute the loading and see the scaling problem in action.
import math
import textwrap
docs = [
Doc(
source="vendor_manual_sinamics_g120.txt",
text=textwrap.dedent("""
SINAMICS G120 β Fault Codes (Excerpt)
F30005: DC link overvoltage.
Possible causes:
- Regenerative energy during deceleration (ramp too fast).
- Line voltage too high.
- Braking resistor missing or undersized.
Recommended actions:
- Increase deceleration ramp time (P1121).
- Enable/size braking resistor; verify wiring.
- Check incoming line voltage.
Notes: Clear fault after cause removed. Document ID: G120-FLT-EX-01
""").strip()
),
Doc(
source="line_sop_packaging_a.txt",
text=textwrap.dedent("""
Packaging Line A β Troubleshooting SOP (Excerpt)
Symptom: Conveyor stops after ~3 seconds, HMI shows "Photoeye Timeout".
Checklist:
1) Verify photoeye PE-17 alignment and clean lens.
2) Check PE-17 input in PLC tag table; confirm it toggles when blocked/unblocked.
3) Inspect conveyor motor overload relay OL-2; reset if tripped.
4) Review last 10 alarms in SCADA for recurring stop causes.
Safety: Lockout/Tagout required before mechanical intervention.
SOP ID: SOP-PL-A-07
""").strip()
),
Doc(
source="plc_code_conveyor_st.txt",
text=textwrap.dedent("""
// Structured Text (Excerpt) β Conveyor Control
IF StartButton AND NOT EmergencyStop THEN
MotorRunning := TRUE;
StartTimer(IN:=TRUE, PT:=T#3S); // Start grace period
END_IF
// Fault: photoeye should clear within 3 seconds of motor start
IF MotorRunning AND PhotoEye_Blocked THEN
IF StartTimer.Q THEN
Fault_PhotoEye_Timeout := TRUE;
MotorRunning := FALSE;
END_IF
END_IF
// Drive fault mapping
IF DriveFault_F30005 THEN
AlarmText := 'DC link overvoltage';
END_IF
""").strip()
),
Doc(
source="change_log_2025w41.txt",
text=textwrap.dedent("""
Change Log β Packaging Line A (Week 41)
- Updated VFD decel ramp: P1121 from 1.0s to 0.6s to improve throughput.
- Disabled braking resistor monitoring due to nuisance trips (temporary).
- Added alarm mapping for DriveFault_F30005.
CHG ID: PL-A-CHG-2025W41
""").strip()
),
]
def approx_tokens(s: str) -> int:
# Crude but useful for planning: ~1 token per 4 characters
return math.ceil(len(s) / 4)
total_tokens = sum(approx_tokens(d.text) for d in docs)
print(f"Docs loaded: {len(docs)}")
print(f"Approx prompt tokens if you paste everything: {total_tokens:,} tokens")
print("Reality check: 2 vendor PDFs (~200 pages each) can easily be 400kβ1M tokens.")
print("Naive approach risk: high cost + no audit trail (no citations to exact lines).") Expected output
Docs loaded: 4 Approx prompt tokens if you paste everything: 418 tokens Reality check: 2 vendor PDFs (~200 pages each) can easily be 400kβ1M tokens. Naive approach risk: high cost + no audit trail (no citations to exact lines).
Explanation
- - This is the scaling problem: prompt cost grows with your entire document set, not just the relevant parts.
- - Even if the answer is correct, you cannot reliably cite "the exact line" from a pasted blob.
- - Cost: $0.00 for this small demo, but real doc sets can cost $0.10+ per query with prompt stuffing.
Common mistake
Treating RAG as just attach PDFs to the prompt instead of building a retrieval + citation pipeline.
Takeaway
Prompt stuffing is easy to demo but hard to budget and impossible to audit cleanly.
SETUP CELL
Experiment 1B β Explain doc-level retrieval limitations
Understand why retrieving whole documents gives vague citations.
# Why doc-level retrieval fails for industrial review:
#
# 1. Granularity mismatch:
# - A 200-page manual is one "document"
# - Citing "vendor_manual.pdf" is too vague for review
# - Reviewer needs: "page 47, section 3.2.1, lines 5-8"
#
# 2. Keyword over-matching:
# - Doc-level matching often surfaces keyword mentions, not root causes
# - Example: PLC code mentions "F30005" (the fault code)
# - But the vendor manual explains the root cause
# - Doc-level TF-IDF might rank PLC code higher (more "F30005" density)
#
# 3. No stable citations:
# - "Top document: vendor_manual.pdf" is not actionable
# - Human reviewer cannot verify the claim without reading the entire doc Explanation
- - Doc-level matching often surfaces keyword mentions (e.g., a PLC mapping line) instead of the root-cause explanation.
- - Even if the top doc is relevant, a whole-document citation is not specific enough for review.
- - Citations like "see vendor_manual.pdf" are too vague β human reviewers need line-level or chunk-level precision.
Takeaway
Without chunk-level citations, you are still one reviewer question away from losing trust.
EXPERIMENT CELL
Experiment 1B β Run doc-level retrieval and observe weak citations
Execute doc-level retrieval and observe that it surfaces the wrong granularity.
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
query = "Why did Drive fault F30005 start appearing after the decel ramp change?"
vectorizer = TfidfVectorizer(stop_words="english")
X = vectorizer.fit_transform([d.text for d in docs])
qv = vectorizer.transform([query])
sims = cosine_similarity(qv, X)[0]
ranked = sorted(
[(docs[i].source, float(sims[i])) for i in range(len(docs))],
key=lambda x: x[1],
reverse=True
)
print("Top docs (doc-level retrieval):")
for src, score in ranked[:3]:
print(f" - {src:28s} score={score:.3f}")
print("\nNaive answer (no citations):")
print("- Likely linked to a decel ramp change or braking resistor monitoring changes.")
print("- Recommended: review P1121, braking resistor config, and line voltage.")
print("\nProblem: this answer is plausible, but you cannot prove which document line drove it.") Expected output
Top docs (doc-level retrieval): - plc_code_conveyor_st.txt score=0.210 - change_log_2025w41.txt score=0.196 - vendor_manual_sinamics_g120.txt score=0.189 Naive answer (no citations): - Likely linked to a decel ramp change or braking resistor monitoring changes. - Recommended: review P1121, braking resistor config, and line voltage. Problem: this answer is plausible, but you cannot prove which document line drove it.
Explanation
- - Doc-level retrieval can over-weight mention matches (the PLC code mentions F30005, but does not explain it).
- - The change log and vendor manual both contain relevant information, but doc-level matching cannot distinguish between them.
- - Even if the top doc is relevant, citing "plc_code_conveyor_st.txt" is not specific enough for review.
- - Cost: $0.00 in API calls, but the citation problem makes this unsuitable for shift handover audits.
Common mistake
Returning the top document as evidence instead of returning the specific chunk(s) that support the claim.
Takeaway
Without chunk-level citations, you are still one reviewer question away from losing trust.
CHECKPOINT CELL
Checkpoint β What failed (and what you need next)
Confirm you understand why naive approaches do not survive industrial review.
Explanation
- - Prompt stuffing fails budgeting: cost scales with your entire document library, not query complexity.
- - Doc-level retrieval fails granularity: it cannot point to the exact supporting lines.
- - Next: chunk documents into stable citation units, then retrieve top-K chunks with metadata.
- - The goal: every answer must cite evidence at the chunk level (source + line range + chunk id).
Takeaway
RAG is not useful in plants until it can produce evidence you can audit.
Experiment 2 β Chunking + Vector Retrieval (Citations Become Possible)
You'll build a minimal vector search over chunked "evidence units" and generate an answer with citations.
SETUP CELL
Experiment 2A β Understand the Chunk model for citation-ready evidence units
Model a chunk with source, line range, and a stable chunk_id for auditing.
from dataclasses import dataclass
import hashlib
@dataclass
class Chunk:
chunk_id: str # Hash-based stable identifier
source: str # Document name
start_line: int # First line in this chunk (1-indexed)
end_line: int # Last line in this chunk
text: str # The actual chunk text Explanation
- - Chunking is where you decide what you can cite. This model captures source + line range + content.
- - chunk_id provides a stable reference that survives text edits elsewhere in the document.
- - Line numbers are 1-indexed for human readability (matches how editors display line numbers).
Takeaway
A chunk is an evidence unit β design it so a human can review it fast.
SETUP CELL
Experiment 2A β Explain chunking strategy (line-based + overlap)
Understand why line-based chunking with overlap preserves context.
# Line-based chunking strategy:
#
# max_lines=4: Each chunk is 4 lines maximum
# overlap=1: Chunks share 1 line with the next chunk
#
# Example document (5 lines):
# Line 1: Fault F30005 definition
# Line 2: Possible cause 1
# Line 3: Possible cause 2
# Line 4: Recommended action
# Line 5: Document ID
#
# Chunks with max_lines=4, overlap=1:
# Chunk 0: Lines 1-4
# Chunk 1: Lines 4-5 (overlap at line 4)
#
# Why overlap matters:
# - Multi-line fault descriptions can span chunk boundaries
# - Overlap ensures you don't lose context at the boundary
# - Trade-off: slight storage/compute increase for better retrieval Explanation
- - Line-based chunking creates stable citations: line numbers do not change when you edit other parts of the document.
- - Overlap prevents losing context at boundaries (e.g., a fault description that spans 2 lines).
- - max_lines=4 with overlap=1 balances granularity (small enough to cite) vs context (large enough to be meaningful).
- - Alternative strategies: sentence-based (semantic but unstable), section-based (requires manual headings).
Takeaway
Overlap is cheap insurance against missing multi-line fault descriptions.
SETUP CELL
Experiment 2A β Implement chunk_by_lines with overlap
Build a chunking function that splits documents into fixed-size line-based chunks with overlap.
from typing import List
def chunk_by_lines(doc: Doc, max_lines: int = 4, overlap: int = 1) -> List[Chunk]:
"""Chunk a document by lines, with overlap to preserve boundary context."""
lines = [ln.strip() for ln in doc.text.splitlines() if ln.strip()]
out: List[Chunk] = []
i = 0
while i < len(lines):
start = i
end = min(len(lines), i + max_lines)
chunk_text = "\n".join(lines[start:end])
# Generate deterministic chunk_id from content + metadata
raw = f"{doc.source}:{start}:{end}:{chunk_text}"
cid = hashlib.sha1(raw.encode("utf-8")).hexdigest()[:10]
out.append(Chunk(
chunk_id=cid,
source=doc.source,
start_line=start + 1, # 1-indexed for human readability
end_line=end,
text=chunk_text
))
if end == len(lines):
break
i = end - overlap # Slide forward with overlap
return out Explanation
- - Line-based chunking is simple and stable: citations do not break when you edit other parts of the document.
- - Overlap prevents losing context at boundaries (common in fault-code explanations).
- - The chunk_id is deterministic: same content + metadata = same hash.
- - This function is production-ready: you can swap max_lines and overlap to tune granularity vs context.
Common mistake
Chunking too large (citations become vague) or too small (retrieval becomes noisy).
Takeaway
Overlap is cheap insurance against missing multi-line fault descriptions.
EXPERIMENT CELL
Experiment 2A β Execute chunking and inspect citation keys
Apply chunking to all documents and verify stable citation format.
# Apply chunking to all loaded documents
chunks: List[Chunk] = []
for d in docs:
chunks.extend(chunk_by_lines(d, max_lines=4, overlap=1))
print(f"Total chunks: {len(chunks)}")
print("Sample citation key:", f"{chunks[0].source}#L{chunks[0].start_line}-L{chunks[0].end_line}|{chunks[0].chunk_id}") Expected output
Total chunks: 14 Sample citation key: vendor_manual_sinamics_g120.txt#L1-L4|19a7dcc007
Explanation
- - Now you have 14 citeable evidence units instead of 4 un-citeable documents.
- - Each chunk has a stable key: source file + line range + hash-based ID.
- - This citation format is actionable: a human can navigate to the exact lines to verify the claim.
- - Cost: $0.00 in chunking operations, one-time cost to embed these chunks for vector search.
Takeaway
Chunking transforms documents into audit-friendly evidence packs.
SETUP CELL
Experiment 2B β Explain vector embedding and top-K retrieval
Understand how semantic similarity search works at the chunk level.
# Vector embeddings and top-K retrieval:
#
# 1. Embeddings convert text into vectors (arrays of numbers)
# - Example: "DC link overvoltage" β [0.23, -0.41, 0.87, ...]
# - Similar text β similar vectors (measured by cosine similarity)
#
# 2. Vector stores enable fast top-K similarity search
# - FAISS, Chroma, Pinecone, pgvector
# - Index all chunk vectors, query with question vector
# - Return K most similar chunks (K=4 is common)
#
# 3. TF-IDF vs OpenAI embeddings:
# - TF-IDF: Simple, no API calls, good for demos
# - OpenAI embeddings: Semantic, API calls, better for production
# - Cosine similarity measures how "close" vectors are (0=orthogonal, 1=identical) Explanation
- - Embeddings convert text into vectors so you can measure semantic similarity (not just keyword overlap).
- - Vector stores enable fast top-K similarity search: you query with a question and get the K most similar chunks.
- - TF-IDF is simple (no API calls, good for tutorials); production uses OpenAI/Cohere embeddings for better semantic matching.
- - Cosine similarity measures how close the query is to each chunk (higher score = more relevant).
Takeaway
Vector search is the bridge between natural language questions and citeable evidence chunks.
CORE CELL
Experiment 2B β Implement retrieve_chunks with scoring
Build the vector retrieval function that returns scored chunk evidence.
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
def make_citation(c: Chunk) -> str:
return f"[{c.source}#L{c.start_line}-L{c.end_line}|{c.chunk_id}]"
chunk_vectorizer = TfidfVectorizer(stop_words="english")
CX = chunk_vectorizer.fit_transform([c.text for c in chunks])
def retrieve_chunks(query: str, k: int = 4):
qv = chunk_vectorizer.transform([query])
sims = cosine_similarity(qv, CX)[0]
idxs = sims.argsort()[::-1][:k]
return [(chunks[i], float(sims[i])) for i in idxs]
query = "Drive fault F30005 after decel ramp change braking resistor"
hits = retrieve_chunks(query, k=4)
print("Top chunks:")
for c, score in hits:
print(f" - {make_citation(c)} score={score:.3f}") Expected output
Top chunks: - [change_log_2025w41.txt#L1-L4|eecc307bbd] score=0.323 - [vendor_manual_sinamics_g120.txt#L1-L4|19a7dcc007] score=0.227 - [vendor_manual_sinamics_g120.txt#L4-L7|8a76d73485] score=0.219 - [plc_code_conveyor_st.txt#L10-L13|d34c04c401] score=0.209
Explanation
- - Now your retrieval result is a list of citeable chunks, not a whole PDF.
- - The scores let you set thresholds (e.g., refuse to answer if top score is too low).
- - Each citation includes: source file, line range, and chunk hash for stable references.
- - Cost: $0.00 for TF-IDF; production embeddings (OpenAI) would cost ~$0.0001 per chunk embed + query.
Why this matters
Industrial review needs traceability. Chunk-level retrieval is the minimum viable unit of traceability.
Takeaway
A vector store is useful only if it returns evidence you can cite and log.
EXPERIMENT CELL
Experiment 2C β Generate an answer that must cite evidence
Build an evidence pack and an answer that includes citations.
import json
from typing import Dict, Any
def evidence_pack(query: str, hits) -> Dict[str, Any]:
pack = []
for c, score in hits:
if score <= 0:
continue
pack.append({
"citation": make_citation(c),
"score": round(score, 3),
"text": c.text
})
return {"query": query, "chunks": pack}
pack = evidence_pack(query, hits)
answer_lines = [
"Likely cause: DC link overvoltage during deceleration after the ramp was shortened.",
"Most actionable checks:",
"- Increase decel ramp time (e.g., P1121) or revert the recent change.",
"- Re-enable/verify braking resistor sizing/wiring; monitoring being disabled can hide a failure.",
"- Verify incoming line voltage is within spec.",
"",
"Citations:"
]
answer_lines += [f" - {c['citation']}" for c in pack["chunks"][:3]]
answer = "\n".join(answer_lines)
print(answer)
print("\nEvidence pack (first chunk, trimmed):")
first = dict(pack["chunks"][0])
first["text"] = first["text"].splitlines()[0] + " ..."
print(json.dumps(first, indent=2)) Expected output
Likely cause: DC link overvoltage during deceleration after the ramp was shortened.
Most actionable checks:
- Increase decel ramp time (e.g., P1121) or revert the recent change.
- Re-enable/verify braking resistor sizing/wiring; monitoring being disabled can hide a failure.
- Verify incoming line voltage is within spec.
Citations:
- [change_log_2025w41.txt#L1-L4|eecc307bbd]
- [vendor_manual_sinamics_g120.txt#L1-L4|19a7dcc007]
- [vendor_manual_sinamics_g120.txt#L4-L7|8a76d73485]
Evidence pack (first chunk, trimmed):
{
"citation": "[change_log_2025w41.txt#L1-L4|eecc307bbd]",
"score": 0.323,
"text": "Change Log β Packaging Line A (Week 41) ..."
} Explanation
- - This is the core RAG contract: the answer is derived from retrieved evidence, not model memory.
- - The evidence pack is what you store, replay, and show during troubleshooting reviews.
- - Each citation is actionable: a human can navigate to the exact lines to verify the claim.
- - Cost: $0.00 for this demo; production would add LLM generation cost (~$0.01-0.02 per answer with gpt-4o-mini).
Common mistake
Letting the model answer without forcing citations (it will eventually freewheel and hallucinate).
Takeaway
If your answer cannot cite evidence, it should downgrade to insufficient evidence.
CHECKPOINT CELL
Checkpoint β RAG fundamentals you should now have
Make sure the pipeline parts are clear before you add audit logging.
Explanation
- - Ingestion creates documents with metadata (source, version, equipment IDs).
- - Chunking creates citeable evidence units (source + line range + chunk hash).
- - Embeddings + vector store enable top-K retrieval (semantic similarity search).
- - Answer formatting must include citations to retrieved chunks (actionable for human review).
- - Next: add audit logging so you can answer "What evidence did we use for this recommendation?"
Takeaway
You now have a minimal RAG pipeline that can survive basic review.
Experiment 3 β Audit Log Contract (Reproducibility Across Shifts)
You'll write a JSONL audit record that captures query + citations + answer preview for shift handover compliance.
SETUP CELL
Experiment 3A β Explain audit log requirements for industrial RAG
Understand what needs to be logged and why for shift handover and compliance.
# Minimum viable audit record:
# - timestamp: When was this query run?
# - run_id: Unique identifier for this session/shift
# - query: What did the operator ask?
# - top_k: How many chunks were retrieved?
# - citations: Which chunks were used as evidence?
# - answer_preview: What was the answer (first line for quick scan)
#
# Production additions:
# - model_version: Which LLM/embedding model was used?
# - doc_set_hash: Hash of the document set (detect version drift)
# - retrieval_scores: Similarity scores for each chunk
# - operator_id: Who ran this query?
#
# Format: JSONL (one record per line)
# - Easy to grep/process with command-line tools
# - Append-only (no risk of corrupting existing records)
# - Each line is valid JSON (can parse one line at a time) Explanation
- - Minimum viable audit record: timestamp, query, citations, answer preview.
- - Production adds: model version, doc-set hash, retrieval scores, operator ID.
- - Format: JSONL (one record per line, easy to grep/process).
- - Purpose: "What evidence did we use for this recommendation?" during shift handover review.
Takeaway
If you cannot log it, you cannot audit it.
CORE CELL
Experiment 3A β Write a deterministic audit log record (JSONL)
Persist enough context to answer: What evidence did we use for this recommendation?
import json
from pathlib import Path
def write_audit_log(pack, answer: str, path: str = "rag_audit_log.jsonl") -> dict:
record = {
"ts": "2025-12-26T00:00:00Z",
"run_id": "demo0001",
"query": pack["query"],
"top_k": len(pack["chunks"]),
"citations": [c["citation"] for c in pack["chunks"]],
"answer_preview": answer.splitlines()[0] # Single-line preview
}
Path(path).write_text(
(Path(path).read_text() if Path(path).exists() else "") + json.dumps(record) + "\n",
encoding="utf-8"
)
return record
record = write_audit_log(pack, answer)
print(json.dumps(record, indent=2)) Expected output
{
"ts": "2025-12-26T00:00:00Z",
"run_id": "demo0001",
"query": "Drive fault F30005 after decel ramp change braking resistor",
"top_k": 4,
"citations": [
"[change_log_2025w41.txt#L1-L4|eecc307bbd]",
"[vendor_manual_sinamics_g120.txt#L1-L4|19a7dcc007]",
"[vendor_manual_sinamics_g120.txt#L4-L7|8a76d73485]",
"[plc_code_conveyor_st.txt#L10-L13|d34c04c401]"
],
"answer_preview": "Likely cause: DC link overvoltage during deceleration after the ramp was shortened."
} Explanation
- - This record is the minimum viable audit trail: query + citations + answer preview.
- - In production, also log model/version, retrieval scores, and doc-set version hashes.
- - JSONL format: one record per line, easy to grep, append-only for safety.
- - Cost: $0.00 for logging; storage cost is negligible (a few KB per query).
Why this matters
When a shift lead asks why did we do this, your system should answer with evidence, not vibes.
Takeaway
If you cannot log it, you cannot audit it.
SETUP CELL
Experiment 3B β LlamaIndex wiring (optional): same architecture, less glue
See the equivalent LlamaIndex setup. If the library is not installed, the demo falls back safely.
# LlamaIndex wiring (optional): same architecture, less glue code
# This code will fall back to the TF-IDF pipeline if llama_index is not installed.
def llamaindex_available() -> bool:
try:
import llama_index # noqa: F401
return True
except Exception:
return False
if not llamaindex_available():
print("llama_index not installed here β using the tested TF-IDF pipeline above.")
print("In your environment: pip install llama-index")
else:
# Modern LlamaIndex import style (0.10+)
from llama_index.core import VectorStoreIndex, Document
li_docs = [Document(text=d.text, metadata={"source": d.source}) for d in docs]
index = VectorStoreIndex.from_documents(li_docs)
engine = index.as_query_engine(similarity_top_k=4)
resp = engine.query("Drive fault F30005 after decel ramp change braking resistor")
print(resp.response)
print("\nCitations:")
for n in resp.source_nodes:
src = n.node.metadata.get("source")
print(f" - {src} score={n.score:.3f}") Expected output
llama_index not installed here β using the tested TF-IDF pipeline above. In your environment: pip install llama-index
Explanation
- - LlamaIndex standardizes ingestion, indexing, retrieval, and source node inspection.
- - Your core responsibility remains: metadata discipline, citation format, and audit logging.
- - LlamaIndex reduces glue code but does not automatically make outputs auditable.
- - You still need to define: chunking strategy, citation format, and audit log schema.
Common mistake
Assuming a framework automatically makes outputs auditable without defining citation + logging contracts.
Takeaway
Frameworks reduce glue code; auditability still requires deliberate design.
CHECKPOINT CELL
Checkpoint β Your RAG contract (minimum viable for industrial use)
Confirm your design has enough structure to be reviewed in an industrial setting.
Explanation
- - You retrieve top-K chunks, not entire documents (granularity for citations).
- - Every answer includes citations that map back to stable chunk ids (source + line range + hash).
- - You write an audit record capturing query + citations (shift handover compliance).
- - You can refuse to answer when evidence is weak (thresholding on similarity scores).
- - Next steps: integrate with LLM for generation, add metadata filtering, tune chunking strategy for your doc types.
Takeaway
RAG becomes industrial-grade when it can be audited, not when it can answer.
5οΈβ£ PRODUCTION NOTES β WHAT TO DECIDE BEFORE YOU SHIP THIS
- βΈ Chunking strategy: line-based (stable) vs sentence-based (semantic) vs section-based (manual headings)
- βΈ Metadata discipline: source file, page/line, equipment type, revision, effective date, change ticket id
- βΈ Vector store choice: local (FAISS/Chroma) vs managed (Pinecone/pgvector) based on latency + governance
- βΈ Evaluation: build a small "golden set" of plant questions + expected citations, then regression test retrieval
Safety reminder: RAG can surface procedures and parameter suggestions, but execution remains human-approved. Never allow auto-changes to PLC/VFD parameters from a RAG answer.
β KEY TAKEAWAYS
- β RAG is a pipeline: ingest β chunk β embed β store β retrieve β cite β log.
- β Chunking is a safety and auditability decision: it defines what can be reviewed quickly.
- β Doc-level retrieval is not enough for plants β you need chunk-level evidence.
- β Treat citations as a contract: if the model cannot cite, it should degrade to insufficient evidence.
- β Audit logs (query + citations + versions) are what make RAG answers defensible across shifts.
- β LlamaIndex reduces glue code, but auditability still requires deliberate metadata + logging design.
Further Reading
Official Documentation
-
LlamaIndex Core Concepts
Documents, nodes, indexes, and query engines explained
-
Vector Store Index Guide
Building and querying vector indexes with metadata filtering
-
OpenAI Embeddings Guide
Understanding embeddings, dimensions, and similarity search
Industrial Patterns
-
RAG for Knowledge-Intensive Tasks (Paper)
Original RAG paper demonstrating retrieval-augmented generation
-
Chunking Strategies for RAG
Fixed-size, sentence-based, and semantic chunking trade-offs
-
LLM Patterns: RAG Best Practices
Production RAG patterns including hybrid search and reranking
π NEXT TUTORIAL
D8 β Agent Memory Systems for Industrial Context
Persist line-scoped context across shifts with LangGraph checkpointing.