DEV Community

RAG Hallucination Diagnosis: Evidence Gating Beats Embeddings for Ask-Your-Docs Chatbot Answers

Short answer: A docs chatbot should abstain whenever it cannot assemble enough directly relevant evidence for a moderation report. For classifying gaming reports before human review, choose evidence gating over a larger context window: retrieval may propose evidence, but a separate policy must decide whether the system may answer. This favors quality over shaving a little latency from the happy path. The distinction matters because a fluent category label can still be unsupported. Embeddings answer a proximity question. They don't prove that the retrieved passage governs this game mode, policy version, region, or report type. Chunking can preserve more local meaning, and a larger context window can carry more text, yet neither mechanism turns weak evidence into a warranted decision. Decision, invariants, and failure boundaries This architecture decision record chooses a two-stage path: retrieve candidate policy passages, then gate generation on evidence quality and scope. A report that passes the gate receives a suggested moderation category plus citations. A report that fails it goes to human review with a machine-readable reason such as no_policy_match , scope_conflict , or ambiguous_evidence . Abstention is a successful outcome, not an exception. Three invariants define the boundary. The answer must cite text that supports the selected category. Every cited passage must carry the policy version and scope used during retrieval. Conflicting passages must not be silently averaged into a confident label. Those rules are more useful than a blanket instruction to "use the context," because they can be tested before and after generation. Keep generation outside the authority boundary. The model can summarize evidence and suggest a label; the moderation service owns the final state transition, validates the response schema, and routes uncertain cases to reviewers. This resembles the discipline needed in OTP delivery: a provider accepting a request doesn't establish that the user received the message. Each boundary needs its own observable result. No citation, no classification. The failure modes are broader than bad chunk size. A current policy can retrieve an obsolete appendix with similar wording. A player report can mention harassment while actually describing impersonation. An audio transcript can lose the proper noun that distinguishes a player from a game item. A long context can contain the correct paragraph and a contradictory paragraph at once. In each case, adding tokens may make the prompt look richer while leaving the decision boundary undefined. OWASP's LLM application guidance is a useful threat-modeling starting point because retrieved content and model output both cross trust boundaries. How should an ask-your-docs RAG chatbot fix wrong answers despite embeddings? Start by turning "wrong" into outcomes that an evaluation can distinguish. Retrieval failure means the supporting policy never entered the candidate set. Scope failure means the candidate belongs to the wrong policy version, locale, game mode, or enforcement tier. Evidence failure means the candidate is topically similar but does not entail the proposed category. Generation failure means adequate evidence was present but the output contradicted it, omitted a required citation, or broke the schema. These failures need different repairs. Treating all of them as hallucination leads teams to tune the retriever when the missing component is an authorization rule. Build a small evaluation set from realistic moderation-report shapes, but don't let it become a pile of easy keyword matches. Include paraphrases, short angry reports, mixed allegations, references that depend on earlier conversation, and near-neighbor policies that use the same nouns but prescribe different outcomes. Audio reports deserve their own slice: an open-source speech recognizer can produce the transcript, but the transcript should retain provenance and remain an upstream input, not be mistaken for original evidence. Human reviewers should label the expected category, the supporting policy passages, and whether abstention is acceptable. Then measure the stages separately. Retrieval evaluation asks whether the labeled support appears in the candidates. Gate evaluation asks whether supported cases pass and unsupported or conflicting cases stop. Answer evaluation checks the category and verifies that each citation actually backs the claim. End-to-end accuracy alone hides compensation: a generator may guess correctly after retrieval fails, which looks good in a dashboard and teaches the team nothing useful. I'm not sure one universal similarity threshold exists; corpus vocabulary, embedding model, and policy density change the score distribution. A held-out set and an explicit review of false accepts are what resolve that uncertainty. The practical fix is usually metadata filtering plus evidence checks, not more prompt decoration. Index atomic policy units with their heading path, version, effective period, jurisdiction, and report taxonomy. Retrieve with hard scope filters where the request supplies those fields. Rerank candidates against the actual allegation. Finally, require sufficient support and reject conflicts before calling the generator. Chunk boundaries still matter: keep exceptions and the rule they qualify together, and don't merge unrelated sanctions merely to reach a target token count. Comparing a larger context window with evidence gating | Decision factor | Larger context window | Evidence-gated retrieval | |---|---|---| | Main benefit | Carries more candidate text into one generation call | Makes the permission to answer explicit | | Main risk | Irrelevant or conflicting passages remain available to the model | Conservative thresholds can send more work to reviewers | | Latency shape | More input must travel through the generation path | Retrieval and validation add stages, but abstentions can skip generation | | Best fit | Synthesis where broad recall matters and errors are reversible | Moderation triage where an unsupported label can misroute human review | | Debugging signal | Often reveals only that the final answer was wrong | Separates retrieval, scope, evidence, and generation failures | The quality-versus-latency choice isn't free. Evidence gating adds a reranking or validation step and more telemetry. It can also lower automation when thresholds are cautious. For pre-review classification, that is the right bias: a queue item marked uncertain is visible and recoverable, while a confident but unsupported category can send a report to the wrong workflow. Teams with a low-risk internal search tool, loose synthesis requirements, and users who always inspect sources may reasonably prefer the simpler large-context path. Don't use generation retries as the default response to uncertainty. Retrying the same evidence changes wording more readily than it changes warrant. Retry retrieval only when the next attempt changes a declared variable, such as query decomposition or a scope filter; record that change so the evaluation can tell which path helped. The same principle applies to rate limits in messaging systems - an unexamined retry loop creates load without proving delivery. Critical path in Python The critical path below is deliberately generic. The retriever and generator are interfaces, while the moderation policy remains ordinary application code. Thresholds are configuration derived from evaluation, not constants copied from an article. from dataclasses import dataclass from typing import Protocol @dataclass(frozen=True) class Passage: text: str source_url: str policy_version: str scope: str relevance: float class Retriever(Protocol): def search(self, query: str, filters: dict[str, str]) -> list[Passage]: ... class Generator(Protocol): def classify(self, report: str, evidence: list[Passage]) -> dict: ... def classify_report( report: str, scope: str, policy_version: str, minimum_relevance: float, retriever: Retriever, generator: Generator, ) -> dict: candidates = retriever.search( report, filters={"scope": scope, "policy_version": policy_version}, ) eligible = [ passage for passage in candidates if passage.scope == scope and passage.policy_version == policy_version and passage.relevance >= minimum_relevance ] if not eligible: return {"status": "review", "reason": "no_policy_match"} versions = {passage.policy_version for passage in eligible} scopes = {passage.scope for passage in eligible} if len(versions) != 1 or len(scopes) != 1: return {"status": "review", "reason": "scope_conflict"} result = generator.classify(report, eligible) cited_urls = set(result.get("citations", [])) allowed_urls = {passage.source_url for passage in eligible} if not cited_urls or not cited_urls.issubset(allowed_urls): return {"status": "review", "reason": "unsupported_citation"} return { "status": "suggested", "category": result["category"], "citations": sorted(cited_urls), "policy_version": policy_version, } URL membership alone does not prove entailment. The code enforces cheaper structural checks in the request path; semantic support still needs a validator or a constrained category-to-policy mapping, tested against the labeled set. Production code should also log candidate identifiers, filter values, configured threshold version, gate reason, and the final reviewer correction without storing more player content than policy allows. Compliance starts at the event schema, not at the audit dashboard. Watch p50 and tail latency per stage, but pair them with quality signals: retrieval support rate, abstention rate by report type, citation validation failures, and reviewer overrides. A falling abstention rate is not automatically good. If reviewer overrides rise at the same time, the gate has become permissive. Slice results by language, input channel, policy version, and report category so a healthy aggregate doesn't hide an audio-transcript or locale-specific gap. Rejected option

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.