Skip to content

fix(tests): stop the LLM judge mistaking context for the response - #3546

Merged
nicoloboschi merged 1 commit into
mainfrom
fix/llm-judge-context-delimiting
Aug 17, 2026
Merged

fix(tests): stop the LLM judge mistaking context for the response#3546
nicoloboschi merged 1 commit into
mainfrom
fix/llm-judge-context-delimiting

Conversation

@nicoloboschi

Copy link
Copy Markdown
Collaborator

The bug

test_reflect_split_synthesis::test_facts_from_distinct_chunks_reach_the_answer failed four times across four CI runs, on three different PRs — including #3543, which changed nothing but PL/pgSQL maintenance routines. Every failure carried byte-identical judge reasoning, so it was never a flake.

Criteria: The answer mentions BOTH hobbies: Zara's beekeeping AND Marco's vintage synthesizers.
Response: * Zara keeps bees on her rooftop.
          * Marco collects vintage synthesizers.
Judge:    "The response does not mention either of the hobbies. It only states that
           the memory data contained two hobby facts."

The response plainly meets the criteria. The judge's sentence is a near-verbatim paraphrase of the test's context, not its response — and that is the whole bug.

tests/llm_judge.py built the prompt as:

f"## Response to evaluate\n{response}\n"
f"{context_block}\n"          # -> "\n\nContext provided to the system:\n{context}"
f"## Criteria\n{criteria}\n\n"

The response and criteria get ## headers; the context gets a bare line. So a multi-line response runs straight into the context with nothing marking where one ends and the other begins. This test's response is a two-bullet markdown list immediately followed by a context that opens "The memory data contained two hobby facts…" — and the judge read that opening sentence as the answer.

Any judge call passing a prose context is exposed. This one hit it reliably because its context restates the facts.

The fix

Tag each section rather than merely heading it. Headers alone cannot delimit, because a response may itself be markdown — a response containing ## Criteria would forge a section. Tags survive that. The system prompt now states outright that only <response> is judged and that <context> is background, never the subject.

Prompt assembly moves into build_judge_messages() so it is covered by fast unit tests instead of only by the LLM tests whose outcome it silently decides.

Verification

A/B against the live judge, using the exact response/criteria/context from the failing test, bypassing the reflect pipeline to isolate the prompt:

verdict
old prompt 0/4 criteria met — every run reproducing the CI reasoning verbatim
new prompt 4/4 criteria met

test_facts_from_distinct_chunks_reach_the_answer passes again. Five new unit tests cover the assembly: sections stay separated, an absent context leaves no empty block, the system prompt scopes judgement, and a response containing its own ## headers cannot forge a section. The regression test uses the exact bullet-list-plus-prose shape that broke.

Risk

Scoping the judge strictly to <response> could in principle flip a verdict in some other test that was passing because context leaked into the judgement. Core LLM tests and the six LLM-acceptance jobs exercise the whole judge surface, so CI is the check on that rather than my assertion.

🤖 Generated with Claude Code

The judge prompt gave the response and criteria `##` headers but appended the
context as a bare "Context provided to the system:" line. A multi-line response
therefore ran straight into the context with nothing marking the boundary, and
the judge read across it.

It did so deterministically. test_facts_from_distinct_chunks_reach_the_answer
failed four times across four CI runs — on three different PRs, including one
that touched nothing but PL/pgSQL — always with the judge quoting the *context*
back as though it were the answer ("It only states that the memory data contained
two hobby facts"), while the real response was a two-bullet list naming both
facts and plainly met the criteria.

Tag each section instead of merely heading it: tags survive a response that is
itself markdown, which headers do not — a response containing "## Criteria" could
otherwise forge a section. The system prompt now says outright that only
<response> is judged and that <context> is background, never the subject.

Verified by A/B against the live judge with the exact inputs from the failing
test: the old prompt was judged "criteria not met" 4/4 times with the CI
reasoning verbatim; the new one 4/4 "met". The test itself passes again.

Prompt assembly is pulled into build_judge_messages() so it is covered by fast
unit tests rather than only by the LLM tests whose outcome it decides.
@nicoloboschi
nicoloboschi merged commit 58d9744 into main Aug 17, 2026
108 checks passed
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