fix(generation): flag high-confidence answers that cite nothing - #15
Merged
Conversation
The model can draw on retrieved vault content in say_this without populating used_source_chunk_ids -- the system prompt forbids this but doesn't reliably prevent it (confirmed non-deterministic: identical query/context, repeated calls, citations present on some runs and absent on others). Previously this passed through as unflagged "high confidence, zero sources" -- the badge state a user has no way to distinguish from a genuinely well-cited answer. Rather than trying to verify what the model actually used (lexical/ relevance matching against say_this -- more surface area, fuzzy thresholds, new failure modes), this adds one cheap deterministic check: self-reported HIGH confidence with an empty used_source_chunk_ids is downgraded one level and flagged in limitations. A self-reported MEDIUM/LOW with no citations is left alone, since the model already hedged honestly in that case (e.g. "not aware of X in your notes") and re-flagging it would misrepresent an honest answer as a suspected violation. Root behavior (why citations are sometimes dropped at all) is a separate, not-yet-addressed follow-up -- see docs/superpowers/plans/ 2026-07-19-phase-3-grounded-answers.md's citation-recall note.
query_runs previously captured retrieval metadata (chunk ids, scores, selected sources) but not the final confidence/limitations the response actually carried -- made it impossible to audit after the fact whether a given historical query was downgraded (e.g. by the citation-recall check) without re-running it. Adds both as columns, populated from the same AnswerResult the response body is built from.
…confidence is unexplained The model can self-report MEDIUM/LOW confidence with an empty limitations list -- confirmed non-deterministic against a live query (identical query/context, repeated calls, sometimes explained, sometimes not). The prompt only requires an explanation for one specific case (an unsupported personal claim), not generally whenever confidence drops below HIGH. Same philosophy as the citation-recall check: don't try to fix model prompt adherence, just refuse to let a degraded confidence stand with no stated reason. Fires only when limitations is genuinely empty, so it never overrides an existing downgrade's own explanation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Found while poking at the Phase 4 web UI: real
/api/querycalls kept coming back with a self-reportedconfidencethe response gave no way to trust or audit —highwith zero sources, or a downgraded confidence with no explanation. Three commits, each closing one gap in that trust chain:1.
fix(generation): flag high-confidence answers that cite nothingThe model can draw on retrieved vault content in
say_thiswithout populatingused_source_chunk_ids— the system prompt forbids this but doesn't reliably prevent it (confirmed non-deterministic: identical query/context, repeated calls, citations present on some runs and absent on others). Rather than trying to verify what the model used (lexical/relevance matching againstsay_this— more surface area, fuzzy thresholds, a second system built to catch the first one lying), this adds one cheap deterministic check: self-reportedHIGHconfidence with an emptyused_source_chunk_idsgets downgraded one level and flagged inlimitations. A self-reportedMEDIUM/LOWwith no citations is left alone — that's the model already hedging honestly (e.g. "I'm not aware of this in your notes"), and re-flagging it would misrepresent an honest answer as a suspected violation.2.
feat(api): log confidence and limitations on query_runsquery_runscaptured retrieval metadata (chunk ids, scores, selected sources) but not the finalconfidence/limitationsthe response actually carried — made it impossible to audit after the fact whether a given historical query was downgraded, without re-running it. Adds both columns, populated from the sameAnswerResultthe response body is built from. Migration0003.3.
fix(generation): fall back to a generic limitation when a below-HIGH confidence is unexplainedInvestigating the above surfaced a second, distinct gap: the model can self-report
MEDIUM/LOWconfidence with an emptylimitationslist — confirmed non-deterministic against the same live query repeated many times (sometimes explained, sometimes not). The prompt only requires an explanation for one specific case (an unsupported personal claim), not generally whenever confidence drops belowHIGH. Same philosophy as commit 1: don't try to fix model prompt adherence, just refuse to let a degraded confidence stand with no stated reason. Fires only whenlimitationsis genuinely empty, so it never overrides an existing downgrade's own explanation — confirmed live that both checks can fire together (model's own explanation + the citation-recall flag, stacked).Root behavior not addressed here, flagged as a follow-up: why the model sometimes omits citations or explanations for content/confidence it demonstrably has opinions about isn't fixed, just made visible and auditable. Two directions worth considering later: a retry-on-incomplete-structured-output loop, or making these fields schema-required so an incomplete response fails parsing and forces a retry.
Test plan
generation/test_service.py: 6 new tests across the three commits (flagged-and-downgraded case, honest-hedge guard rail, fallback-limitation case x2, well-cited-HIGH regression guard, no-double-flagging regression guard)test_query_api.py: updated to assertconfidence/limitationsare persisted toquery_runsruff format --check/ruff check: cleangpt-oss:20b), repeated runs across multiple real queries (terraform drift, Helm, Kubernetes vs ECS, Ansible, Meridian, CI/CD) - confirmed viaquery_runs, post-fix, zero rows show a below-highconfidence with empty limitations, and the previously-silenthigh/0-sources pattern no longer appears unflagged🤖 Generated with Claude Code