Skip to content

fix(generation): dignified refusal — point to closest documented topics#21

Open
yuzushi-dev wants to merge 2 commits into
mainfrom
feat/dignified-refusal
Open

fix(generation): dignified refusal — point to closest documented topics#21
yuzushi-dev wants to merge 2 commits into
mainfrom
feat/dignified-refusal

Conversation

@yuzushi-dev

Copy link
Copy Markdown
Owner

What & why

When the RAG pipeline has no direct answer in the retrieved context, Amber
replies with a bare "I don't have documentation on that topic.". This PR keeps
that verbatim refusal opening but, only when related sources are already
present in the context
, appends a short Closest documented topics: section
with 2-3 cited [[Source: X]] pointers to the nearest documented material.

Prompt-only change. No new retrieval, no new flag, no router change, no
SearchMode/thinking-level machinery. The sources listed must actually be in
the context (they carry [Document: <title>] headers via ContextBuilder
already), so nothing is fabricated and the strict grounding guardrail is intact.

Motivation (prod log triage)

Triaged 5 months of prod queries (915 turns / 379 unique). Findings:

  • 78 no-doc refusals (~9% of turns); 28 already do soft-inference, 50
    are hard refusals
    .
  • Most hard refusals are KB fact-gaps (missing version, param, OS support) —
    a "more reasoning" dial would hallucinate these, not fix them.
  • The advisory/synthesis class (where extra reasoning would help) is ~23 unique
    queries, and the flagship example ("rip & replace strategy") already answers
    well
    .

Conclusion: the real, low-risk win is a dignified refusal that points to
nearby docs — not a thinking-level feature. This is the minimal version of that.

Changes

  • src/core/generation/application/prompts/templates.py — one line: instruction
    1 of SYSTEM_PROMPT_v1.
  • tests/unit/test_refusal_prompt.py — pins the template contract (refusal
    phrase kept verbatim, marker present + cited, flows through the real
    PROMPTS["latest"]resolve_rag_prompts path, tenant-override limitation).

Test

python3 -m pytest tests/unit/test_refusal_prompt.py → 5 passed. Reverting the
prompt change fails 3/5 (tests are not vacuous). make verify gate to run
before merge.

Reviewer caveats

  1. Tenant override: a tenant with a custom rag_system_prompt in config
    bypasses the default template (effective_config.py), so the fix won't take
    effect there until the override is updated. Verify read-only on prod admin
    config after review.
  2. Prompt-only change → LLM behavior is not unit-testable. Honest behavioral
    check = replay the 50 hard refusals offline and count valid cited pointers.
    Proposed as a follow-up, out of scope here.

When the RAG context has no direct answer, keep the verbatim refusal opening
but append a short 'Closest documented topics:' section listing 2-3 related
sources already present in the context, each cited [[Source: X]]. Prompt-only
change: no new retrieval, no flags, no router. Only sources actually in the
context may be listed, so no answer is fabricated.

Motivated by prod log triage: of 78 no-doc refusals over 5 months, 50 were
hard refusals; most are KB fact-gaps where inventing content would hallucinate,
so the win is a dignified pointer to nearby docs, not a reasoning dial.
…ied refusals

The dignified-refusal prompt makes a refusal look like a real answer (it
appends a cited "Closest documented topics:" section), which breaks two
assumptions:

1. _looks_like_refusal did a bare substring match on the pinned opener and a
   no-sources check. Refusals now carry sources, and the model paraphrases the
   opener ("I don't have direct documentation on ..."), so refusals slipped
   through — getting persisted as reusable conversation memory that biases the
   retrieval-time rewriter into repeating the miss.

2. VerifiedQAService.get_similar_examples injected any admin-"verified" Q&A
   verbatim into future prompts as a positive few-shot example, with no refusal
   check. A refusal dressed as a helpful answer is easy to thumbs-up / verify,
   permanently seeding the golden set with a non-answer the model is told to
   imitate.

Add a shared, adverb-tolerant detector (src/shared/refusal.py) that also
matches the "Closest documented topics" section marker. _looks_like_refusal
now delegates to it; get_similar_examples filters out refusal-shaped answers so
they are never injected even if verified. Behavioural tests cover paraphrased
openers, the dignified-refusal shape, and real answers.
@yuzushi-dev

Copy link
Copy Markdown
Owner Author

Follow-up commit e398fc52 — closes the merge blocker found on the prod-data mirror.

The dignified refusal makes a refusal look like a real answer, which broke two assumptions:

  1. _looks_like_refusal did a bare substring match on the pinned opener + a no-sources check. Refusals now carry sources and the model paraphrases the opener ("I don't have direct documentation…", empirically observed on the mirror) → refusals slipped through and got persisted as reusable memory.
  2. VerifiedQAService.get_similar_examples injected any admin-verified Q&A verbatim as a positive few-shot example, with no refusal check → a dressed-up refusal, easy to thumbs-up/verify, permanently seeds the golden set with a non-answer.

Fix: shared adverb-tolerant detector src/shared/refusal.py (also matches the "Closest documented topics" marker); _looks_like_refusal delegates to it; get_similar_examples filters refusal-shaped answers so they're never injected even if verified. Behavioural tests added.

Deploy: pure code, no new deps → runbook restart-only (git reset + docker restart). New file src/shared/refusal.py lives under the bind-mounted src.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant