Handling Missing Evidence and Retrieval Failures

Ma Mahalakshmi V Updated 16 Sep 2026
7 min read ·Lesson 83 of 224

Why This Deserves Its Own Lesson

A RAG-based assistant fails silently in a specific and dangerous way: when the vector store doesn't contain a good answer to a question, the model can still produce a fluent, confident-sounding response drawn from its general training knowledge rather than the retrieved documents — and to a user, that response looks exactly like a properly grounded one. This is often worse than the model plainly saying "I don't know," because an ungrounded but confident answer erodes the entire premise of building a document-grounded assistant in the first place: trustworthiness. This lesson is about detecting and handling that failure mode deliberately, rather than hoping instructions alone prevent it.

The Three Distinct Failure Modes

It helps to separate what looks like one problem ("the assistant gave a bad answer") into three distinct causes, because each has a different fix:

  1. No relevant documents exist at all. The knowledge base genuinely doesn't cover the topic the user is asking about.
  2. Relevant documents exist but weren't retrieved. The right chunk is in the vector store, but similarity search didn't surface it — often due to a phrasing mismatch, poor chunking, or an overly narrow metadata filter (Lesson 5's "filter matching nothing" mistake is one cause of this).
  3. Relevant chunks were retrieved, but the model ignored or misused them, generating an answer that isn't actually supported by the retrieved text even though the evidence was present in context.

Case 1 is a content gap — the fix is adding documents. Case 2 is a retrieval quality problem — the fixes are in Lesson 7 (document preparation) and Lesson 5 (filtering). Case 3 is a generation discipline problem — the fix is instructions and verification, covered below. Conflating these three during debugging leads to fixing the wrong layer of the system.

Instructing the Model to Acknowledge Gaps

The first and cheapest layer of defense is explicit instruction, already introduced in Lesson 4 and Lesson 6:

from openai import OpenAI

client = OpenAI()

GROUNDED_INSTRUCTIONS = (
    "You are a knowledge base assistant. Answer only using information "
    "retrieved via file search. If the retrieved documents do not contain "
    "enough information to answer confidently, respond with: "
    "\"I don't have enough information in the knowledge base to answer that.\" "
    "Do not use general knowledge to fill gaps, and do not guess."
)

response = client.responses.create(
    model="gpt-5.6-terra",
    instructions=GROUNDED_INSTRUCTIONS,
    input="What is our policy on parental leave in Germany specifically?",
    tools=[{"type": "file_search", "vector_store_ids": ["vs_hr_policies_68f2"]}],
)

print(response.output_text)

Prescribing an exact fallback phrase ("I don't have enough information...") rather than a vague instruction like "say you're not sure" is deliberate: a consistent, recognizable fallback string is something your application code can detect programmatically (shown next), which turns "the model was appropriately uncertain" into a signal your system can act on, rather than something buried in free-form prose that's hard to parse reliably.

Instructions alone are not a guarantee — models can still occasionally answer from general knowledge despite being told not to, particularly for well-known topics adjacent to what's in the documents. Instructions reduce the frequency of this failure; they don't eliminate it, which is why the additional layers below matter.

Detecting the No-Search-Happened Case

Sometimes the model answers without invoking file_search at all — a general knowledge question that doesn't clearly relate to the knowledge base might not trigger the tool. Detecting this from the structured output lets you flag it explicitly:

def was_file_search_invoked(response):
    return any(item.type == "file_search_call" for item in response.output)


def get_retrieved_file_count(response):
    count = 0
    for item in response.output:
        if item.type == "file_search_call":
            results = getattr(item, "results", None) or []
            count += len(results)
    return count

Note: The presence and structure of a results field on a file_search_call item, and whether an empty search is represented as zero results versus the item being absent entirely, are version-specific details — confirm current behavior against official documentation.

was_file_search_invoked checks whether any item in the response's output was a file_search_call at all, which tells you whether retrieval was attempted. get_retrieved_file_count goes a step further, counting how many actual results came back from any search calls that did happen. A response where was_file_search_invoked is False for a question that should clearly be answerable from the knowledge base is worth flagging for review — either the question fell outside the model's judgment of when to search, or something about the request configuration (wrong vector_store_ids, for instance) prevented it.

Detecting Low-Confidence or Empty Retrieval

Combining the fallback-phrase detection with the structured retrieval signal gives a more complete picture than either alone:

FALLBACK_PHRASE = "I don't have enough information in the knowledge base to answer that."


def classify_response(response):
    searched = was_file_search_invoked(response)
    result_count = get_retrieved_file_count(response)
    used_fallback = FALLBACK_PHRASE in response.output_text

    if used_fallback:
        return "acknowledged_gap"
    if not searched:
        return "no_search_performed"
    if result_count == 0:
        return "search_found_nothing"
    return "answered_with_evidence"

This function assigns each response to one of four categories: the model explicitly acknowledged a gap using the expected fallback phrase, no search was attempted at all, a search ran but found nothing, or the response was answered with retrieved evidence present. Logging this classification alongside every response in a production system turns an invisible failure mode into a measurable metric — you can track what fraction of real user questions fall into search_found_nothing over time, which is a direct, actionable signal that your knowledge base has content gaps worth filling.

You can test this classification logic entirely with fake response objects, without any real API call:

class FakeSearchCall:
    def __init__(self, results):
        self.type = "file_search_call"
        self.results = results


class FakeResponse:
    def __init__(self, output_text, output):
        self.output_text = output_text
        self.output = output


def test_classify_response_acknowledged_gap():
    response = FakeResponse(
        output_text="I don't have enough information in the knowledge base to answer that.",
        output=[FakeSearchCall(results=[])],
    )
    assert classify_response(response) == "acknowledged_gap"
    print("PASS: fallback phrase is classified as acknowledged_gap")


def test_classify_response_no_search_performed():
    response = FakeResponse(output_text="Paris is the capital of France.", output=[])
    assert classify_response(response) == "no_search_performed"
    print("PASS: absence of any search call is classified as no_search_performed")


def test_classify_response_search_found_nothing():
    response = FakeResponse(
        output_text="Based on general practice, notice periods are typically 30 days.",
        output=[FakeSearchCall(results=[])],
    )
    assert classify_response(response) == "search_found_nothing"
    print("PASS: an empty result set is classified as search_found_nothing")


def test_classify_response_answered_with_evidence():
    response = FakeResponse(
        output_text="Per the policy document, the notice period is 45 days.",
        output=[FakeSearchCall(results=["chunk_1", "chunk_2"])],
    )
    assert classify_response(response) == "answered_with_evidence"
    print("PASS: non-empty results are classified as answered_with_evidence")


test_classify_response_acknowledged_gap()
test_classify_response_no_search_performed()
test_classify_response_search_found_nothing()
test_classify_response_answered_with_evidence()

Each test constructs a minimal fake response representing one of the four scenarios and asserts classify_response returns the expected category. Notice the third test in particular: it represents the dangerous case where the model answered fluently ("typically 30 days") despite an empty result set — exactly the failure this whole lesson is about — and confirms the classifier correctly flags it as search_found_nothing rather than letting it pass as a normal answer. Having this test suite means you can safely refine the classification logic later without accidentally breaking detection of this specific case.

Escalation Paths for Detected Gaps

Detecting a gap is only useful if something happens as a result. Reasonable responses, depending on your application, include:

  • Surfacing a visible "I couldn't find this in the knowledge base" message to the user, rather than a hidden log entry only you see.
  • Offering to hand off to a human (a support ticket, a "contact us" prompt) specifically when classify_response returns search_found_nothing or no_search_performed.
  • Falling back to web search for questions that are legitimately answerable from current public information but not from your private documents — the subject of Lesson 9.
  • Aggregating search_found_nothing classifications over time into a report of candidate topics to add to the knowledge base, closing the loop back into the document preparation work from Lesson 7.

Common Mistakes

Relying solely on prompt instructions to prevent ungrounded answers, without any programmatic verification, which leaves you unable to detect or measure how often the model still answers from general knowledge despite being told not to.

Treating "the model produced text" as equivalent to "the model found evidence," conflating fluency with grounding — always check whether file_search actually ran and returned results, not just whether a coherent answer came back.

Not distinguishing between the three failure modes (no documents exist, documents exist but weren't retrieved, documents were retrieved but ignored), which leads to fixing the wrong part of the system — adding documents when the real problem is chunking, for instance.

Best Practices

Prescribe an exact, detectable fallback phrase in your grounding instructions, so your application code can programmatically recognize when the model is acknowledging a gap rather than parsing free-form uncertainty language.

Classify and log every response's evidence status, not just its final text, so that gaps in your knowledge base become a measurable, trackable metric rather than something only noticed when a user complains.

Build an explicit escalation path for detected gaps — a visible message, a human handoff, or a fallback to web search — rather than letting a detected failure mode dead-end silently in a log file no one reviews.

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.output_text Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Memorizing Names Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources Reducing Unsupported Claims with Grounded Generation Testing Freshness-Sensitive AI Answers Production Considerations for Web-Grounded Applications Understanding File Search and Retrieval-Augmented Generation Creating and Organizing Vector Stores Uploading Documents for Retrieval Connecting Vector Stores to Responses API Requests Designing Document Metadata and Filtering Strategies Building a PDF Question-Answering Application Improving Retrieval Quality With Better Document Preparation Handling Missing Evidence and Retrieval Failures Combining File Search With Web Search Building a Production Knowledge-Base Assistant What the Code Interpreter Tool Is Designed For Running Python-Based Analysis Through the OpenAI SDK Uploading Datasets for Analysis Analyzing CSV and Spreadsheet Data Generating Charts and Data Summaries Handling Generated Files and Downloadable Artifacts Building a Data-Analysis Assistant Combining Code Execution with Structured Outputs Validating Generated Calculations and Results Security and Sandbox Considerations for Code Execution Understanding Multimodal Input with the OpenAI SDK Sending Images to a Model Image Analysis from URLs and Uploaded Files Extracting Text and Information from Screenshots Building an Image-Question-Answering Application Combining Image Input with Structured Output Analyzing Multiple Images in One Request Handling Image Quality and Input Limitations Designing Multimodal Prompts for Reliable Results Building a Practical Vision-Powered Python Application Understanding Speech-to-Text and Text-to-Speech Workflows Transcribing Audio with the OpenAI SDK Working with Uploaded Audio Files Handling Timestamps and Transcription Metadata Building a Meeting Transcription Workflow Generating Spoken Responses from Text Handling Long Audio and Processing Failures Combining Audio with Text and Tool Calling Building an End-to-End Python Voice Application What Embeddings Are and When to Use Them Generating Embeddings With the OpenAI API Preparing Text for Embedding Comparing Vectors With Cosine Similarity Building a Simple Semantic Search Engine in Python Storing Embeddings in a Database Metadata Filtering for Semantic Search Chunking Strategies for Better Retrieval Evaluating Semantic Search Quality Building a Document Similarity Application When Batch Processing Makes Sense Designing Large-Volume AI Processing Pipelines Using Asynchronous Python with the OpenAI SDK Running Concurrent Requests Safely Controlling Concurrency and Avoiding Rate Limits Tracking Batch Job Progress Handling Partial Failures in Bulk Workloads Retrying Failed Items Without Duplicating Successful Work Designing Resumable AI Processing Jobs Building a Production Batch-Processing Pipeline Batch Processing Makes Sense Large-Scale AI Processing Pipelines Async Python with OpenAI SDK Safe Concurrent Requests Concurrency & Rate Limits Batch Progress Tracking Partial Failure Handling Safe Retry Handling Resumable AI Jobs Production Batch Pipeline System–User Data Separation Reusable App Instructions Prompt Templates & Variables Extraction & Classification Prompts Summarization & Transformation Prompts Explicit Output Requirements Prompt Version Management Prompt Testing & Evaluation Reusable Python Prompt Library API Key Security Secure API Key Storage Secure Secret Management Prompt Injection Prevention Trusted vs. Untrusted Content Tool Argument Validation Sensitive Data Handling Secure Logging AI Action Authorization Production AI Security Checklist Why AI Applications Need Evaluation Beyond Unit Tests Unit Testing OpenAI SDK Integration Code Mocking API Responses in Python Tests Testing Structured Outputs Against Schemas Testing Tool-Calling Workflows Building a Small Evaluation Dataset Measuring Accuracy, Consistency, and Failure Rates Regression Testing Prompts and Model Changes Human Evaluation Versus Automated Evaluation Creating a Repeatable Evaluation Pipeline AI Request Monitoring Token Cost Management Usage Metrics Design Reducing Model Calls Prompt & Context Optimization Model Selection & Optimization AI Caching Strategies Interactive Latency Optimization Usage Dashboards & Budget Alerts Performance & Cost Checklist Every API Call Starts Fresh Fixing API Statelessness Server-Side Conversation Memory Limits of Response Chaining What We're Building Conversation Memory Challenges Preparing an OpenAI SDK Application for Deployment Environment-Specific Configuration for Development and Production Deploying a Python AI Service with Docker Container Health Checks and Startup Configuration Managing Secrets in Cloud Deployments Background Workers for Long-Running AI Tasks Queues and Asynchronous Job Architectures Scaling AI Workloads Horizontally Monitoring Production Incidents and Failures Production Deployment Checklist for OpenAI SDK Applications Reusable OpenAI Service Classes AI Client Dependency Injection Typed AI Responses Python Configuration Management AI Request Decorators Centralized AI Error Handling Clean SDK Abstractions Reusable OpenAI Utilities Internal AI Python Libraries SDK Integration Maintenance Production AI Chatbot Document Q&A System Web Research Assistant Customer Support Agent AI Data Analysis Assistant Image Analysis App Meeting Transcription & Summary Semantic Document Search Multi-Tool AI Agent Production OpenAI SDK App Why "It Looked Fine When I Tested It" Isn't Enough Timing Note Status Note Pre-Decision Status Note Current Availability Note
Ask about this post
AI Ask about this post

Ask questions about Handling Missing Evidence and Retrieval Failures and get answers drawn from it.

Signed-in readers only.