Production RAG Chatbots and Freshness
Filter, fuse, cache, guard and cite. Keep the index fresh and grade every ranking change.
- Concepts and terms
- Tasks and commands
- Annotated code
- Diagrams
- Comparisons
- Defaults and numbers
- Troubleshooting
- Questions and tips
One regulated chat turn#
Run, grade and inspect#
| Command | What it does |
|---|---|
python3 scripts/ | Makes your workspace. A fresh starter fails |
python3 scripts/ | Grades one stage of your code |
python3 scripts/ | Checks the reference solution |
python3 cli.py samples/ | Writes a citation receipt as JSON and HTML |
cli.py query orchard-index.json tokens --now 210 --max-age 60 | Queries the freshness snapshot |
cli.py <notes> "release replicas" --aliases <aliases.json> --out matches.json | Searches a markdown folder |
cli.py <baseline> <candidate> <judgments> --out retrieval-diff.json | Diffs two ranking runs per query |
python main.py | Runs a capstone scaffold offline |
From doc-qa-with-citations/README.md, rag-freshness-pipeline/README.md, semantic-notes-search/README.md, retrieval-evaluation-lab/README.md, 19.02 code/main.py, 19.02 code/ts/README.md
Cache-aware chat turn#
ok, reason = llama_guard_input(query)regex block list firsthits = retrieve(query, role, jurisdiction, corpus, k=3)context = [f"[{c.anchor()}] {c.text}" for c, _ in hits]layout = PromptLayout(system=SYSTEM_PROMPT,very stable policy=f"role={role} jurisdiction={jurisdiction}", context=context, question=query)question is not cachedcache_hit = cache.check(layout.cache_key())sha256 of prefix, 16 hexanswer = presidio_scrub(answer)redacts [email] and [ssn]cache.hit_rate()hits / (hits + misses)From 19.08 code/main.py
Codebase hybrid retrieval#
tokens = (tokenize(chunk.symbol) * 4 +symbol weight 4 tokenize(chunk.summary) * 2 +summary weight 2 tokenize(chunk.body))body weight 1dense_hits = dense.search(query, k=10)sparse_hits = bm25.search(query, k=10)fused = rrf(dense_hits, sparse_hits)k_rrf: int = 60top = rerank(query, fused, top_k=5)0.3 symbol, 0.1 summarycitations = [c.anchor() for c, _ in top]repo/path:start-endFrom 19.02 code/main.py
Project inputs, outputs and checks#
| Factor | Doc QA | Freshness | Notes search | Eval lab |
|---|---|---|---|---|
| Builds | Citation receipt | Fresh index | Lexical TF-IDF search | Regression diff |
| Input | Folder of .txt | Full corpus JSON | Folder of .md | 3 JSON files |
| Output | answer.json, answer.html | Snapshot, changes | matches.json | retrieval-diff.json |
| Key fields | start, end, sha256 | version, age, hash | score, matched | delta_ndcg |
| States | answered, abstained | insert to delete | top k matches | regressions count |
| Guard | needs_review | stale version error | k must be positive | --fail-on-regression |
From doc-qa-with-citations/README.md, solution/cli.py, rag-freshness-pipeline/README.md, solution/cli.py, semantic-notes-search/README.md, solution/cli.py, retrieval-evaluation-lab/README.md, solution/cli.py
Project functions#
| Task | Code |
|---|---|
| Load text files with SHA-256 | load_documents(root) |
| Chunk with offsets | chunk_document(doc, |
| Gate a model quote | answer(question, |
| Plan index changes | diff(previous, |
| Commit a snapshot | commit(path, |
| Drop expired evidence | retrieve(documents, |
| Build a TF-IDF index | build_index(documents, |
| Score two systems | compare_systems(systems, |
From doc-qa-with-citations/API.md, doc-qa-with-citations/solution, rag-freshness-pipeline/solution, solution/main.py, solution/main.py
Freshness ingest and query#
From rag-freshness-pipeline/solution, rag-freshness-pipeline/README.md
Defaults and numbers#
| Setting | Default | Note |
|---|---|---|
Doc QA chunk size | 200 | overlap is 30 |
--max-age | 3600 | Older evidence is skipped |
--k | 3 | Freshness, notes and eval CLIs |
| Note size limit | 100 KB | Larger notes raise an error |
| Relevance grades | 0..3 | Integers only |
| Rerank pool | top-20 | Top-5 go to synth |
| Cache hit target | 60-80% | Stable prefixes first |
| Drift alert | 5% | Weekly retrieval quality drop |
From doc-qa-with-citations/API.md, solution/cli.py, solution/cli.py, solution/main.py, Lesson 19.08, Lesson 19.02
Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
| "unsupported model answer" | Quote is not in the cited chunk | Return an exact span and its chunk id |
state is needs_review | Quote has no query term | Judge relevance by hand |
state is abstained | Retrieval found no chunks | Expected for an off-topic query |
| "stale index version" | Another writer committed first | Read the snapshot, pass its version |
| "duplicate document id" | Two incoming docs share an id | Make ids unique |
| Edited policy still cited | Index lags the source | Ingest the full corpus again |
| "symlink notes are not supported" | Note is a link | Use a regular file in the folder |
| Generated code poisons hits | Boilerplate in the corpus | Flag it "generated" and down-weight |
From doc-qa-with-citations/solution, doc-qa-with-citations/README.md, rag-freshness-pipeline/solution, rag-freshness-pipeline/README.md, solution/cli.py, Lesson 19.08, Lesson 19.02
Ranking metrics in code#
- Precision@k
- relevant hits in the top k, divided by k.
- Recall@k
- relevant hits divided by all docs with a grade above 0.
- RR
- 1 over the rank of the first relevant doc, else 0.
- nDCG
- DCG with gain
2 ** grade - 1over the ideal DCG. - Unjudged
- docs in the top k with no label. They get zero gain.
- Notes IDF
math.log((1 + len(terms)) / (1 + count)) + 1.- Doc QA score
- sum of
(1 + math.log(bag[t]))times an IDF term.
From solution/main.py, solution/main.py, solution/retrieval.py
Questions#
- Why filter by role first?
- The access policy must hold before any score.
- Why put the question last?
- The system prompt and context form the cached prefix.
- What does a re-index touch?
- Only chunks whose text changed. Edges change when imports change.
- Does a valid citation prove an answer?
- No. It can still be irrelevant or outdated.
- Is a refresh a rewrite?
- No. Same content with a later time only updates the time.
From Lesson 19.08, Lesson 19.02, doc-qa-with-citations/README.md, rag-freshness-pipeline/README.md
Tips#
- Label 100 cross-repo questions with gold file:line answers.
- Keep a 200 Q/A golden set and 50 adversarial prompts.
- Reject any claim without a
(repo/path:start-end)anchor. - Re-index a 50-file push in under 60 seconds.
From Lesson 19.08, Lesson 19.02