fix: prevent LLM hallucination when retrieval returns low-signal results
- Strengthen synthesizer system prompt: hard 'respond with exactly' constraint instead of soft 'say so'; removes any wiggle room for the model to supplement from training data - Add early return in synthesize() when chunks is empty (belt-and-suspenders alongside the existing guard in chat.py) - Add MIN_SIGNAL threshold (0.01) in retriever: if the top combined score is below the threshold, return empty so the caller's no-results path fires instead of sending noise chunks to the LLM
This commit is contained in:
parent
895d0b6129
commit
347b391c6e
2 changed files with 25 additions and 4 deletions
|
|
@ -171,6 +171,13 @@ class Retriever:
|
||||||
return bm25 * 0.5 + vec * 0.5
|
return bm25 * 0.5 + vec * 0.5
|
||||||
|
|
||||||
ranked = sorted(merged.values(), key=_combined, reverse=True)[:top_k]
|
ranked = sorted(merged.values(), key=_combined, reverse=True)[:top_k]
|
||||||
|
# Discard results where the best match is pure noise (neither BM25 term
|
||||||
|
# overlap nor vector similarity exceeded the minimum signal threshold).
|
||||||
|
# This lets the caller's empty-result guard fire instead of sending
|
||||||
|
# low-confidence chunks to the LLM where it fills gaps with training data.
|
||||||
|
MIN_SIGNAL = 0.01
|
||||||
|
if ranked and _combined(ranked[0]) < MIN_SIGNAL:
|
||||||
|
return []
|
||||||
adjacent = _fetch_adjacent(ranked, db_path)
|
adjacent = _fetch_adjacent(ranked, db_path)
|
||||||
return ranked + adjacent
|
return ranked + adjacent
|
||||||
|
|
||||||
|
|
@ -189,5 +196,8 @@ class Retriever:
|
||||||
)
|
)
|
||||||
for r in self._bm25.query(query, top_k=top_k, doc_ids=doc_ids)
|
for r in self._bm25.query(query, top_k=top_k, doc_ids=doc_ids)
|
||||||
]
|
]
|
||||||
|
MIN_SIGNAL = 0.01
|
||||||
|
if hits and hits[0].bm25_score < MIN_SIGNAL:
|
||||||
|
return []
|
||||||
adjacent = _fetch_adjacent(hits, db_path)
|
adjacent = _fetch_adjacent(hits, db_path)
|
||||||
return hits + adjacent
|
return hits + adjacent
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,18 @@ from dataclasses import dataclass
|
||||||
from app.services.retriever import RetrievedChunk
|
from app.services.retriever import RetrievedChunk
|
||||||
|
|
||||||
_SYSTEM_PROMPT = (
|
_SYSTEM_PROMPT = (
|
||||||
"You are a helpful document assistant. "
|
"You are a document assistant. "
|
||||||
"Answer the user's question using ONLY the provided document excerpts. "
|
"Answer questions using ONLY the document excerpts provided. "
|
||||||
"For each claim, cite the source page as [p.N]. "
|
"Cite every claim with the source page as [p.N]. "
|
||||||
"If the excerpts are insufficient, say so. Do not invent information."
|
"If the excerpts do not contain the answer, respond with exactly: "
|
||||||
|
"'I could not find an answer to that question in the indexed documents.' "
|
||||||
|
"Do NOT use knowledge from outside the provided excerpts. "
|
||||||
|
"Do NOT speculate, infer, or guess beyond what is explicitly stated."
|
||||||
|
)
|
||||||
|
|
||||||
|
_NO_RESULTS_ANSWER = (
|
||||||
|
"I could not find any relevant passages in the indexed documents for that question. "
|
||||||
|
"Try rephrasing, or check that the relevant document has been ingested."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -42,6 +50,9 @@ class Synthesizer:
|
||||||
history: list[dict],
|
history: list[dict],
|
||||||
chunks: list[RetrievedChunk],
|
chunks: list[RetrievedChunk],
|
||||||
) -> SynthesisResult:
|
) -> SynthesisResult:
|
||||||
|
if not chunks:
|
||||||
|
return SynthesisResult(answer=_NO_RESULTS_ANSWER, citations=())
|
||||||
|
|
||||||
# 1500 chars (~300 words) per chunk: enough to capture definitions that
|
# 1500 chars (~300 words) per chunk: enough to capture definitions that
|
||||||
# appear mid-paragraph without blowing past a 32k-context model's limit.
|
# appear mid-paragraph without blowing past a 32k-context model's limit.
|
||||||
context_parts = [f"[p.{c.page_number}]\n{c.text[:1500]}" for c in chunks]
|
context_parts = [f"[p.{c.page_number}]\n{c.text[:1500]}" for c in chunks]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue