Your team ships an internal AI assistant grounded on company documents. The demo is excellent: ask about the refund policy, and the model answers with confident prose. Then production happens.
A customer asks about a refund exception that lives in a footnote, an old policy PDF outranks the current one, the retrieved chunk contradicts the billing system, and the assistant answers anyway. Support escalates it. The obvious fix feels mechanical: add more documents, tune the chunk size, add a reranker, tell the model to “be careful.”
But the deeper problem is not that the model lacked text to read. The problem is that the system had no reliable way to decide:
- What information is allowed to be used?
- What information is current enough to trust?
- What answer can be verified?
- What action is safe to take?
- What should happen when confidence is low?
Retrieval-Augmented Generation helped solve a knowledge-access problem. It did not solve the reliability problem.
TL;DR: RAG gives a model relevant context. It does not make answers correct, authorized, current, auditable, or safe to act on. Reliable AI applications are built from task routing, grounded evidence, deterministic validation, permission-aware retrieval, verification, evaluation, observability, and graceful failure. RAG is one component, not the architecture.
📋 Table of Contents
- The Wrong Problem
- 1. Stop Treating RAG as the Architecture
- 2. Make the Model Show Its Evidence
- 3. Put Deterministic Code Around the Nondeterministic Step
- 4. Design Retrieval for the Question, Not the Document
- 5. Enforce Permissions Before Retrieval, Not After Generation
- 6. Add a Verifier That Is Boring but Brutal
- 7. Build an Evaluation Harness Before You Tune Prompts
- 8. Make Failure Visible and Recoverable
- 9. Choose the Smallest Context Mechanism That Works
- A Decision Guide for the Next AI Feature
- Production Checklist
The Wrong Problem
RAG became popular because it addressed a visible limitation: large language models did not know your private documents, your internal policies, your product catalog, or yesterday’s incident report. Retrieval gave the model a better prompt.
That was useful. But many production failures are not caused by missing context alone.
They come from:
- A document that is technically relevant but outdated.
- Two documents that contradict each other.
- A user who should not see the retrieved document.
- A question that requires an action, not a paragraph.
- A model that fills gaps when retrieval is incomplete.
- A system that cannot explain why it produced an answer.
- No evaluation set to detect regressions after a prompt change.
- No fallback path when retrieval, tools, or model calls fail.
RAG can make these problems worse by making bad answers look more trustworthy. A hallucination with a citation can be more dangerous than a hallucination without one, because users assume the citation means verification happened.
The real problem is not “give the model more text.” The real problem is building a system that behaves predictably under uncertainty.
Reliable AI applications are not reliable because the model is smart. They are reliable because the surrounding system constrains the model.
1. Stop Treating RAG as the Architecture
Scenario:
A user asks, “Cancel my subscription and email me the final invoice.” The system treats this as a chat question, retrieves help-center articles about cancellation, and generates a friendly summary. Nothing is cancelled.
Why it matters:
Many AI features are built as “chat over documents” even when the user’s goal is transactional. RAG can supply information, but it cannot safely perform business actions by itself. When every request becomes a retrieval problem, the system loses structure: no clear intent, no typed parameters, no idempotency, no audit trail.
Solution:
Separate the task before deciding whether retrieval is needed. Most production requests fall into a few categories:
- Lookup: “What is our policy on expired credits?”
- Analysis: “Summarize these three support tickets.”
- Action: “Create a draft invoice for this customer.”
- Mixed: “Find the customer’s contract and start the renewal workflow.”
A simple routing layer prevents RAG from becoming a catch-all.
from typing import Literal
from pydantic import BaseModel
class Intent(BaseModel):
task: Literal["lookup", "action", "analysis", "mixed"]
entity: str | None = None
requires_user_confirmation: bool = False
def parse_intent(query: str) -> Intent:
# The LLM call is hidden behind this boundary.
# The important part is that the rest of the system
# receives a typed object, not free text.
...
def handle(query: str, user) -> object:
intent = parse_intent(query)
if intent.task == "action":
return run_action_workflow(query, user)
if intent.task == "lookup":
return grounded_lookup(query, user)
if intent.task == "analysis":
return analytical_summary(query, user)
return mixed_workflow(query, user)
Why this works:
The model is no longer responsible for improvising the entire workflow. It can help classify intent or extract parameters, but the execution path is explicit. Action paths can use permissions, validation, retries, and audit logs. Lookup paths can use citations and freshness checks. Analysis paths can use constrained summarization.
💡 Practical note:
If the user is trying to press a button, do not force them to ask a question. Reliable AI features often look less like magic chat and more like intent-aware UI.
2. Make the Model Show Its Evidence
Scenario:
The assistant says, “Yes, you can refund after 35 days in exceptional cases.” Support asks, “Where does it say that?” Nobody knows. The model may have retrieved a fragment, inferred too much, or blended two policies.
Why it matters:
If an AI application cannot show the evidence behind an answer, it cannot be audited. “Because the model said so” is not a production behavior.
Solution:
Require structured evidence for any answer that matters. At minimum, capture:
- The document or record ID.
- The section, page, or URL.
- A short quoted passage.
- The retrieval timestamp.
- Whether the system considers information insufficient.
from datetime import datetime
from pydantic import BaseModel, Field, model_validator
class Citation(BaseModel):
document_id: str
section: str
quote: str
retrieved_at: datetime
class GroundedAnswer(BaseModel):
answer: str
citations: list[Citation] = Field(default_factory=list)
insufficient_information: bool = False
@model_validator(mode="after")
def validate_citations(self):
if self.insufficient_information:
if self.citations:
raise ValueError("Insufficient answers should not cite evidence")
else:
if not self.citations:
raise ValueError("A grounded answer requires at least one citation")
return self
def citations_match_retrieval(answer: GroundedAnswer, retrieved_docs: set[str]) -> bool:
return all(citation.document_id in retrieved_docs for citation in answer.citations)
Why this works:
This turns “grounding” from a prompt instruction into a system constraint. The model cannot merely claim it used the documents. The application can reject answers whose citations do not match retrieved evidence.
But do not stop at document IDs. If possible, verify that the quoted passage actually appears in the retrieved chunk, or at least matches it closely enough for your audit needs.
⚠️ Gotcha:
Citations can become decorative. A model can learn to attach plausible-looking references without truly using them. If you do not validate citations against retrieval results, you may be building a better-looking hallucination.
3. Put Deterministic Code Around the Nondeterministic Step
Scenario:
The model reads a support policy and decides the customer deserves a 17% refund. It outputs prose, maybe JSON, maybe a number with a percent sign. Your billing system expects cents. Support has to manually reinterpret the answer.
Why it matters:
Language models are excellent at interpretation and terrible at being trusted as the final source of business logic. If the model computes values, chooses policy branches, or decides side effects directly, you have moved critical logic into a probabilistic layer.
Solution:
Use the model to extract, classify, or summarize. Then hand the result to deterministic code that enforces rules.
from pydantic import BaseModel, Field
class RefundRequest(BaseModel):
order_id: str
requested_amount_cents: int = Field(gt=0)
reason: str
def evaluate_refund(request: RefundRequest, policy) -> dict:
if request.requested_amount_cents > policy.auto_approve_limit_cents:
return {
"status": "approval_required",
"review_queue": "payments",
}
if policy.is_blocked(request.order_id):
return {
"status": "denied",
"reason_code": "POLICY_BLOCKED",
}
return {
"status": "approved",
"refund_amount_cents": request.requested_amount_cents,
}
The model can read the customer’s message and produce a RefundRequest. It should not be the final authority on whether the refund is allowed.
Why this works:
The nondeterministic part is narrowed. The model becomes a translator between unstructured user input and typed domain objects. Business rules stay in code where they can be tested, versioned, reviewed, and debugged.
This is especially important for:
- Pricing
- Discounts
- Refunds
- Access changes
- Sending messages
- Creating tickets
- Modifying records
- Calling external APIs
- Anything with compliance implications
🧠 The important part:
If a function can compute it, do not ask the model to compute it. If a rule can be expressed deterministically, do not bury it in a prompt.
4. Design Retrieval for the Question, Not the Document
Scenario:
Your refund policy is a table. The left column says “Trial plan,” the right column says “Not eligible.” Your chunker splits the table, retrieves only the right column, and the model concludes that all refunds are unavailable.
Why it matters:
A lot of RAG pain is not model pain. It is document-modeling pain. Teams often chunk text mechanically, then wonder why retrieval returns fragments that are semantically incomplete.
Embeddings are good at similarity. They are not automatically good at:
- Scope
- Exceptions
- Tables
- Hierarchical headings
- Versioned policies
- Tenant-specific rules
- “Applies only to enterprise customers”
- “Valid until January 1”
- “See section 4.2 for details”
Solution:
Index information in units that match how users ask questions.
Instead of only chunking by token count, consider the natural unit of meaning:
- A policy section
- An API endpoint
- A product SKU
- A support ticket
- A contract clause
- A configuration item
- A runbook step
- A customer account record
Attach useful metadata:
{
"document_id": "policy-refund-2026",
"title": "Refund Policy",
"section": "Exceptions",
"audience": "customer",
"product": ["starter", "pro"],
"effective_date": "2026-01-01",
"status": "published",
"owner": "billing-team"
}
Then combine retrieval strategies deliberately:
- Use metadata filters before or during search.
- Use hybrid search when keyword exactness matters.
- Use reranking after broad recall, not as a substitute for recall.
- Preserve parent context when a child chunk depends on a heading or preamble.
- Keep tables structured when tables are the source of truth.
- Rewrite or decompose queries when users ask compound questions.
Why this works:
Retrieval quality sets the ceiling for grounded generation. If the right evidence is not retrieved, the model has three bad options: refuse, guess, or blend incomplete evidence into something plausible.
🔍 Why this matters:
Evaluate retrieval separately from generation. If retrieval recall is poor, no amount of prompt engineering will make the final system reliable.
5. Enforce Permissions Before Retrieval, Not After Generation
Scenario:
An employee asks, “What is the compensation band for this role?” The document exists. Retrieval finds it. The model sees it. Now you ask the model to “only share what the user is allowed to see.”
This is already too late.
Why it matters:
Authorization is not a generation problem. Once sensitive content enters the model’s context, the system has already crossed a boundary. The model may summarize it, hint at it, leak it through tone, or be manipulated into revealing it through prompt injection.
Solution:
Apply access control at the retrieval layer. The search index should respect the same permissions as the source system.
def search_for_user(user, query: str):
filters = {
"tenant_id": user.tenant_id,
"acl_groups": {"$in": user.acl_groups},
"status": "published",
}
return search_index.query(
text=query,
filters=filters,
)
In practice, permission-aware retrieval requires more than one filter. You need to think about:
- Tenant isolation
- User roles
- Group membership
- Document classification
- Expired access
- Deleted documents
- Draft versus published content
- Whether existence itself is sensitive
- Whether a denied answer should say “not found” or “not permitted”
Why this works:
The model only sees what the user is allowed to see. You are not relying on the model to behave ethically or follow redaction instructions after being exposed to forbidden data.
This also improves security against indirect prompt injection. If retrieved documents contain malicious instructions, the blast radius is smaller when retrieval is scoped tightly and actions require separate authorization.
🚨 Production warning:
Do not use post-generation moderation as your primary access-control mechanism. It can be useful as defense in depth, but it is not a substitute for retrieval-level authorization.
6. Add a Verifier That Is Boring but Brutal
Scenario:
The retrieved policy says, “Refunds are not available after 30 days except where required by law.” The model answers, “You are eligible for a refund because we value your loyalty.” That is not contradiction-free just because the model used polite language.
Why it matters:
Generation can drift from evidence even when retrieval is good. The model may overgeneralize, soften a restriction, infer an exception, or prioritize user satisfaction over policy accuracy.
Solution:
Add a verification layer. The verifier should be simpler, narrower, and more controlled than the generator.
Sometimes verification can be deterministic:
def verify_refund_answer(answer: GroundedAnswer, policy) -> bool:
text = answer.answer.lower()
if "guaranteed" in text and not policy.allows_guarantee_language:
return False
if "always" in text and not policy.allows_absolute_language:
return False
if answer.insufficient_information:
return True
if not citations_match_retrieval(answer, policy.allowed_doc_ids):
return False
return True
Other times, verification needs another model or classification step:
- Does the answer contradict the cited evidence?
- Does the answer introduce a claim not present in the evidence?
- Does the answer fail to answer the user’s actual question?
- Does the answer contain medical, legal, financial, or safety-sensitive claims?
- Does the answer require human review?
The verifier does not need to be clever. It needs to be strict.
A good verification layer can:
- Block unsupported claims.
- Force a refusal when evidence is missing.
- Escalate to a human when risk is high.
- Require clarification when the question is ambiguous.
- Replace generated prose with a templated response for sensitive cases.
Why this works:
Reliability often comes from separation of concerns. The generator proposes. The verifier disposes. The system does not assume that fluency equals correctness.
💡 Practical note:
If your verifier is another large model with no constraints, you have not removed uncertainty; you have only moved it. Use deterministic checks wherever possible.
7. Build an Evaluation Harness Before You Tune Prompts
Scenario:
You change the system prompt to make answers more concise. The demo looks better. Two weeks later, the assistant stops mentioning legal exceptions, and nobody notices until a customer complains.
Why it matters:
Without evaluations, improving an AI application becomes guesswork. You optimize for the examples you remember, not the distribution you actually serve.
Solution:
Build an evaluation harness before spending weeks tuning prompts. Start with real failure cases, not synthetic happy paths.
A useful eval case includes:
- The user query
- The user role or permissions
- The expected retrieval sources
- Required answer constraints
- Forbidden claims
- Expected refusal behavior, if applicable
- Whether an action should occur
- id: refund-exception-001
query: "Can I get a refund after 35 days if my trial was extended?"
user_role: customer
expected_retrieval:
- "policies/refunds.md#exceptions"
answer_must_contain:
- "not eligible"
answer_must_not_contain:
- "always"
- "guaranteed"
expected_action: none
Track different metrics for different layers:
Retrieval metrics
- Recall@k
- Precision@k
- Presence of required document section
- Metadata filter correctness
- Freshness of retrieved documents
Generation metrics
- Faithfulness to evidence
- Absence of unsupported claims
- Refusal quality
- Tone and format compliance
- Citation validity
Task metrics
- Did the user achieve the goal?
- Was the correct workflow selected?
- Was the action authorized?
- Was human handoff triggered when needed?
- Did the system avoid irreversible side effects?
Use automated checks where possible, but do not pretend everything can be automated. LLM-as-judge can help triage, but high-stakes categories need human review.
Why this works:
Evals turn reliability into an engineering property. You can compare retrieval configurations, prompt versions, models, and verification rules against the same corpus. You can detect regressions before users do.
⚠️ Gotcha:
Include cases where the correct answer is “I don’t know” or “I can’t do that.” If your eval set only rewards confident answers, your system will learn to be confidently wrong.
8. Make Failure Visible and Recoverable
Scenario:
The vector index is stale because a document sync failed. The assistant keeps answering from old policy text. The answers look fine. The behavior is wrong.
Why it matters:
AI applications have many quiet failure modes: retrieval timeouts, empty results, permission misconfiguration, tool failures, model rate limits, malformed structured output, stale embeddings, and prompt regressions. If failures are invisible, users will discover them at the worst time.
Solution:
Treat AI features like distributed systems, because they are.
Log and trace the important boundaries:
logger.info(
"ai_response",
request_id=request_id,
user_id=user.id,
prompt_version="v23",
model="model-name",
retrieved_doc_ids=[citation.document_id for citation in answer.citations],
latency_ms=latency_ms,
outcome="grounded" if verified else "refused",
)
At minimum, capture:
- Prompt version
- Model identifier
- Retrieval query
- Retrieved document IDs
- Filters applied
- Tool calls made
- Structured output schema version
- Latency
- Token usage
- Verification result
- Final user-facing outcome
- Whether the response was escalated
Then design recovery paths:
- If retrieval fails, say retrieval failed.
- If no evidence is found, refuse or ask for clarification.
- If structured output fails validation, retry with a stricter schema or fall back.
- If a tool call fails, do not pretend it succeeded.
- If an action is irreversible, require confirmation.
- If confidence is low, route to a human.
- If the data source is stale, show the freshness boundary.
For actions, use ordinary production safeguards:
- Idempotency keys
- Timeouts
- Retries with backoff
- Dead-letter queues
- Approval gates
- Audit logs
- Dry-run mode
Why this works:
Users can tolerate failure much better than silent misbehavior. A system that says, “I found no current policy for this, so I’m escalating,” is more reliable than one that guesses politely.
🔍 Why this matters:
Reliability is not just producing the right answer. It is failing in a way that does not damage trust.
9. Choose the Smallest Context Mechanism That Works
Scenario:
A team builds a complex RAG pipeline for five stable policy pages. Another team stuffs thousands of documents into a long-context window and wonders why cost and latency explode. A third team fine-tunes a model to answer questions about facts that change weekly.
Why it matters:
Not every problem needs the same mechanism. Reliability decreases when you add unnecessary moving parts.
Modern AI applications have several ways to give a model context:
| Mechanism | Best when | Main risk | Reliability requirement |
|---|---|---|---|
| Deterministic API lookup | Structured data, account state, pricing, inventory | Overlooking edge cases | Strong schema and business rules |
| Tool use / function calling | Live data, actions, workflows | Unsafe side effects | Validation, authorization, idempotency |
| Long context | Small, stable document sets; cross-document reasoning | Cost, latency, attention drift | Freshness, permissions, evals |
| RAG | Large, changing private corpus | Bad retrieval, stale chunks | Retrieval quality, citations, ACLs |
| Fine-tuning | Style, format, domain language, structured extraction | Stale knowledge, hard updates | Separate facts from behavior, evals |
The practical question is not “Should we use RAG?” It is: “What is the smallest, most controllable way to get the right information into the system?”
Sometimes the best answer is not retrieval at all.
If the question is “What is this customer’s current plan?” call the billing API.
If the question is “Summarize this one contract clause?” pass the clause directly.
If the question is “Which runbook applies to this alert?” use metadata search and a workflow.
If the question is “Answer across 20,000 support tickets,” then retrieval, summarization, and evaluation become essential.
Why this works:
Every extra component adds a failure mode. Chunking can break meaning. Retrieval can miss evidence. Long context can increase cost. Fine-tuning can bake in stale behavior. Tools can cause side effects. The more restrained the architecture, the easier it is to make reliable.
💡 Practical note:
Do not use fine-tuning to teach a model facts that change often. Use it to teach format, judgment style, extraction behavior, or domain language. Put volatile knowledge in retrieval, tools, or databases.
A Decision Guide for the Next AI Feature
When building or debugging an AI feature, start with the type of failure you are trying to prevent.
If answers are factually wrong
Check retrieval first.
Ask:
- Was the right document retrieved?
- Was the right section retrieved?
- Was the document current?
- Was the chunk complete?
- Was the question ambiguous?
- Did the model invent a bridge between weak evidence?
If retrieval recall is bad, improve indexing, metadata, query decomposition, or source data before changing the system prompt.
If answers are unauthorized
Do not fix this with a prompt.
Fix:
- Retrieval filters
- User permissions
- Tenant isolation
- Document lifecycle state
- Source-system access tokens
- Action authorization
The model should not be asked to forget what it has already seen.
If the model takes the wrong action
Move action logic out of generation.
Use:
- Typed intent
- Schema validation
- Explicit confirmation
- Tool allowlists
- Idempotent execution
- Human approval for risky operations
A model should not be one sentence away from mutating production state.
If answers are inconsistent after prompt changes
You need evals.
Start with:
- Real support escalations
- Known edge cases
- Regulatory-sensitive questions
- Ambiguous queries
- Missing-document cases
- Permission-denied cases
- High-cost action cases
Then make changes only when the eval suite improves.
If the system feels “almost trustworthy”
Add verification and uncertainty handling.
A good default behavior is:
If evidence is strong and permissions are clear:
answer with citations.
If evidence is partial:
state what is known and what is missing.
If evidence is absent:
say so and offer escalation.
If action is risky:
require confirmation.
That is less magical than unconditional confidence. It is also closer to what users need.
Production Checklist
Before shipping an AI feature that reads documents, answers users, or takes actions, I would want these in place:
Task design
- The system distinguishes lookup, analysis, and action.
- Actions use explicit workflows, not free-form generation.
- High-risk operations require confirmation or approval.
Retrieval
- Documents are indexed by meaningful units, not arbitrary chunks.
- Metadata includes owner, status, version, audience, and effective date.
- Retrieval is evaluated separately from generation.
- Stale or draft content is excluded unless explicitly allowed.
Permissions
- Retrieval respects user, group, tenant, and document-level access.
- The model is not exposed to forbidden content and then told to redact.
- Prompt-injection risk is considered for retrieved documents.
Grounding
- Answers require citations when they assert facts.
- Citations are validated against retrieved evidence.
- The system can return “insufficient information.”
Validation
- Structured outputs are validated with schemas.
- Business rules are enforced in deterministic code.
- The model does not calculate policy-critical values when code can do it.
Verification
- Unsupported claims are blocked or escalated.
- Contradictions between answer and evidence are detected.
- Sensitive categories trigger stricter handling.
Evaluation
- There is a golden set from real failures.
- Retrieval, grounding, refusal, and task success are measured.
- Prompt and model changes go through CI.
Observability
- Logs include prompt version, model, retrieved documents, and verification outcome.
- Latency, token usage, and failure modes are visible.
- User feedback and escalations feed back into evals.
Failure handling
- Retrieval failures are visible.
- Tool failures do not become fake successes.
- Low-confidence answers are refused, clarified, or escalated.
- Human handoff is a designed path, not an accident.
RAG gives your model something to read. Reliability comes from deciding what it may read, what it may say, what it may do, and how it fails.
Top comments (0)