Skip to content

yash176A/Selfcorrecting-rag

Repository files navigation

Self-Correcting RAG

A retrieval-augmented generation pipeline that grades its own answers and refuses to guess. An evaluator agent scores every answer for grounding and relevance; low-scoring answers trigger a bounded retrieval-refinement loop, and if the loop cannot find support, the system abstains instead of hallucinating.

ci tests python license offline

What this solves

  • Naive RAG answers even when it should not. On out-of-domain questions, a single-pass RAG makes something up. This pipeline abstained on 100% of out-of-domain questions in the eval, where the baseline hallucinated on 100% of them.
  • Grounding is enforced, not assumed. Every answer is scored by a separate evaluator agent before it is returned. Answers that drift from the retrieved context are refined or rejected.
  • Deterministic and reviewable in one command. The default backend needs no API keys and no model downloads, so you can clone it and reproduce every number in this README in under a minute.

Architecture

flowchart LR
    Q[Question] --> R[retrieve]
    R --> G[generate]
    G --> E[evaluate<br/>grounding + relevance]
    E -->|grounded| DONE[return answer]
    E -->|weak, budget left| RF[refine<br/>widen recall + expand query]
    RF --> R
    E -->|weak, budget spent| AB[abstain<br/>fail closed]
Loading

The pipeline is a LangGraph state machine. The only branching point is the evaluator, which decides between accepting the answer, refining (looping back to retrieval with wider recall), or abstaining once the iteration budget is spent. See ADR 001 for why this is a graph and not a linear chain.

Tech stack

Layer Choice Why it was chosen here
Orchestration LangGraph Expresses a bounded loop with a conditional back-edge and a serializable trace.
Vector search FAISS (IndexFlatIP) Inner-product over normalized vectors equals cosine similarity, with zero infra to run.
Embeddings Pluggable (offline / OpenAI) Deterministic offline default for reproducibility; swap to OpenAI for production.
Generation Pluggable (offline / OpenAI) Offline extractive generator keeps CI deterministic; OpenAI for real answers.
Evaluator Embedding cosine + lexical support Grades faithfulness and question-relevance; the trigger for self-correction.

The problem, in plain terms

Retrieval-augmented systems are trusted precisely because they are supposed to answer from a known corpus. The failure that erodes that trust is not a wrong fact, it is a confident wrong fact: the model is asked something the corpus does not cover, and it answers anyway. A support bot that invents a refund policy, or a document assistant that fabricates a clause, is worse than one that says "I cannot find that."

This project treats abstention as a first-class outcome. An evaluator agent grades every answer on two axes, faithfulness to the retrieved context and relevance to the question, and the pipeline only returns an answer that clears both. When it cannot, it says so.

How the self-correction loop works

  1. Retrieve the top-k passages for the query (tight k on the first pass).
  2. Generate an answer constrained to those passages.
  3. Evaluate the answer:
    • Grounding = how well the answer is supported by the retrieved context (semantic cosine plus the fraction of answer tokens actually present in the context).
    • Relevance = how well the answer addresses the question.
  4. Route: if both grades clear their thresholds, return. If not and there is iteration budget left, refine (widen recall, then augment the query with salient context terms) and loop. If the budget is spent and the best attempt is still weak, abstain.

The abstention path is deliberate fail-closed behavior. Returning nothing is safer than returning an unsupported answer.

Evaluation

The harness in eval/run_eval.py compares a naive single-pass baseline against the full self-correcting pipeline on three question tiers, using the deterministic offline backend so results are reproducible on any machine.

benchmark

Tier Metric Naive baseline Self-correcting
Answerable (n=10) answer accuracy 80% 70%
Out-of-domain (n=8) hallucination rate 100% 0%
Near-domain unanswerable (n=4) hallucination rate 100% 100%

Reproduce with python eval/run_eval.py.

Reading these honestly:

  • The headline result is the out-of-domain tier: the loop eliminates hallucination on questions unrelated to the corpus, which the baseline answers every time.
  • Answer accuracy drops by one question (80% to 70%) because the pipeline abstains on one borderline answerable question rather than risk an unsupported answer. That is the recall cost of a conservative abstention policy, and it is the correct trade in safety-critical settings.
  • The near-domain tier is an honest limitation. These questions reuse corpus vocabulary ("what is the monthly cost of the platform") but have no supporting fact. A lexical evaluator cannot tell that the specific fact is missing; catching these reliably needs an LLM grader, which is the production configuration described below. It is documented rather than hidden. See ADR 002.

Pipeline latency (offline backend, orchestration overhead only, real-world latency is dominated by LLM inference): p50 2.8 ms, p95 12.2 ms, p99 12.7 ms over 66 runs on CPU.

Quickstart

git clone <your-fork-url> && cd selfcorrecting-rag
pip install -e ".[dev]"

# ask a question (offline, no keys needed)
scrag -q "What bounds the refinement loop?"

# watch it abstain on something the corpus cannot answer
scrag -q "Who wrote Pride and Prejudice?"

# reproduce the evaluation numbers above
python eval/run_eval.py

# run the tests
pytest -q

Production path (real LLM and embeddings)

The offline default is a test harness. To run against real models, set two environment variables and provide a key:

export OPENAI_API_KEY=sk-...
export SCRAG_LLM_BACKEND=openai
export SCRAG_EMBED_BACKEND=openai
scrag -q "your question"

In this configuration the generator can produce fluent answers (and can therefore also fabricate), so the grounding signal becomes an active guard rather than a trivially-satisfied one, and the near-domain gap above closes. Thresholds, k values, and the iteration budget are all environment-overridable (see src/selfcorrecting_rag/config.py).

Failure modes

If this happens The system does this
Vector index is empty or unavailable Returns an explicit low-confidence abstention, never a fabricated answer.
Question is out of domain Loops, fails to find support, abstains.
Refinement budget is exhausted Returns the highest-scoring attempt, or abstains if even the best is weak.
OpenAI backend is selected but no key set Fails fast at construction with a clear error, rather than silently degrading.

Testing

12 tests cover chunking and retrieval, the evaluator's grounding and relevance grades, and the graph's answer/abstain/budget behavior. CI runs lint, the test suite, and the eval on Python 3.10 and 3.12.

Project structure

src/selfcorrecting_rag/   embeddings, vector store, llm, evaluator, graph, cli, config
data/corpus/              sample knowledge base (markdown)
eval/                     dataset + reproducible evaluation harness
tests/                    unit and integration tests
docs/                     architecture decision records
assets/                   generated benchmark chart

Security notes

API keys are read from environment variables and never written to logs. The same code runs in local development and production; only the backend configuration changes.

License

MIT

About

Self-correcting RAG pipeline: a LangGraph evaluator loop that grades every answer for grounding and relevance, refines weak retrievals, and abstains instead of hallucinating. Runs offline with zero API keys.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors