Skip to content

The model is the lever: qwen3:4b at 82.5% against llama3.2:3b at 40% - #18

Merged
punnerud merged 4 commits into
mainfrom
feat/answer-by-selection
Aug 11, 2026
Merged

punnerud merged 4 commits into
mainfrom
feat/answer-by-selection

Conversation

@punnerud

Copy link
Copy Markdown
Owner

Started as an attempt to fix the last known failure — the model computing a value exactly and then answering with a different number. It ends somewhere else.

The headline

Same 40 generated arithmetic questions, same code, same day:

seed 20260811 seed 555 pooled
qwen3:4b-instruct-2507 18/20 (90%) 15/20 (75%) 33/40 (82.5%)
llama3.2:3b 10/20 (50%) 6/20 (30%) 16/40 (40%)

+42.5 points, 95% CI [+23.3, +61.7], replicated on an independent battery, no group backwards, and in fewer steps (5.5 against 7.2).

Unit questions — the group this repository has spent the most effort on, through labels, gates, compression and exact relations — went 0/2 → 2/2 by changing the model.

The design that lost, and is kept off

Answer-by-selection: the model picks a letter, the value comes from our record, so it cannot state a number no tool computed. The guarantee is real and holds. It still lost:

overall computed-but-unused
synthesis 9/20 2
selection 7/20 7

The second column is the indictment — it exists to remove that failure and tripled it, trading writing the wrong number for choosing the wrong fact. It wins on the narrow case it was built for (unit questions, 6/18 against 0/18 pooled over three seeds, CI [+11.6, +55.1]) and that is not enough to pay for the rest.

So select_answer defaults to False, with the machinery, 23 tests and a --select switch kept. Tried on qwen3 as well: still adds nothing.

The real bug it uncovered

The regression check earned itself immediately:

Q: what is 17,433,193,048 × 3,565?
A: "Calculator_verification: 42.5."

42.5 is (17/100)*250 — the example in my own schema description. Another run answered with "Second in 23 week: 13910400", the example from the convert field. The model was copying the only concrete numbers it had been shown. Every prompt example is now a placeholder, and a test fails on any bare three-digit number in the system prompt, the answer prompt or any schema description.

This was invisible before: a leaked example used to be one wrong number among many, and selection made it a labelled candidate competing on equal terms.

Also

  • Thousand separators on every fact the model reads back. Llama-3 chunks digit runs into threes left to right, so 13910400 tokenises as [139][104][00] and returns as "139,104,000" — faithful copying of a mis-segmented value.
  • A Result-Ignore detector: long numbers in an answer that no tool computed. A measurement, not a rewrite.
  • --model on the bench, so the next comparison is one flag.
  • Three test doubles were wrong before the code was: hash() is randomised per process, a 512-bucket sha1 still collided, and a four-digit guard flagged the year 1989.

311 tests pass; all pinned numbers check.

The failure has a name in the literature -- Result-Ignore: the final answer states
something other than what the tool returned. ToolFailBench measures it at 30% on
an 8B Llama, and llama3.2:3b sits below that model on every grounding benchmark
that covers it (50.79% against Qwen3-4B's 88.19%). Five runs in this repository's
own battery computed the right value exactly and then answered something else.

Persuasion was already exhausted: a dedicated heading, an explicit instruction not
to recompute, deduplication, and restricting which facts are shown. So the model
no longer writes the number. It picks a LETTER, and the value comes back from our
record -- what it chose and what is reported cannot drift apart.

Four things had to be true, and each was measured rather than assumed:

* A LABEL. "(86400*378) = 32659200" does not say it is seconds in 54 weeks.
  Offering unlabelled expressions as candidates scored 0 of 5 -- the model
  rejected all of them -- while the same values labelled scored 9 of 12. The step
  schema now asks what a result IS.

* A label that DESCRIBES. Asked what its result was, the model wrote "604800/7"
  and "seconds". The first is the sum again, the second fits any of four values.
  Labels that are expressions, or that fit two different values, are dropped: a
  label that does not discriminate is worse than none, because it looks like one.

* NO NUMBERS IN THE CHOICE. Letters alone. Showing the values alongside was tried
  and measured worse, which fits the design rather than contradicting it.

* A DETERMINISTIC GATE. An enum forces a choice, and asked "what is the capital of
  France?" with a settled number in front of it the model chose the number 3 times
  out of 3 -- and 3 of 3 again when the options carried labels. So the code decides
  whether a numeric answer is on the table; NONE is the second line, not the first.

Comparisons are settled here too, as Fraction comparisons, and handed over as
relations. Choosing between 3801755 and 3169068 is not a judgement.

Also adds a Result-Ignore detector: long numbers in a synthesised answer that no
tool computed. A measurement rather than a rewrite -- it turns a silent wrong
answer into a count we can watch. The selection path is immune by construction.

And groups thousands in every fact the model reads back. Llama-3 chunks a digit
run into threes LEFT to right, so 13910400 tokenises as [139][104][00] and comes
back as "139,104,000" -- the model faithfully copying a value it mis-segmented.

Three test doubles in here were wrong before the code was: hash() is randomised
per process, a 512-bucket sha1 still collided, and a four-digit guard flagged the
year 1989. The vocabulary is now built exactly and collisions are impossible.

22 new tests, 310 pass. The battery number is NOT claimed: three runs of near
identical configurations gave 4/6, 1/6 and 2/6, which is the noise floor at n=6
rather than an effect. A pooled measurement is running.
The regression check earned itself immediately. Three groups went backwards --
long_addition 2/2 to 0/2, long_multiplication and powers 1/2 to 0/2 -- and the
answers name the cause:

  Q: what is 17,433,193,048 * 3,565?
  A: "Calculator_verification: 42.5."

42.5 is (17/100)*250, the example in my own schema description. Another run
answered the same question with "Second in 23 week: 13910400" -- the example from
the convert field. The model was not reasoning badly. It was copying the only
concrete numbers it had been shown, which is exactly what a small model does with
a worked example sitting in its context.

Every example in every prompt is now a placeholder: "using the numbers from THIS
question and no others", "<quantity> <unit> to <unit>". A test walks the system
prompt, the answer prompt and every schema description and fails on any bare
number of three digits or more, allowing only numbers bound to a noun about the
output ("under 700 characters"), which cannot be mistaken for a question's data.

This was invisible before selection existed: a leaked example used to be one
wrong number among the model's own, and now it is a labelled candidate answer
competing on equal terms. Making the failure legible is what surfaced it.
It lost. Full battery, same questions, same model:

                          overall   computed-but-unused
  synthesis (0.6.0)        9/20              2
  selection                7/20              7

The second column is the indictment. Selection exists to remove exactly that
failure -- a value computed exactly and then not used -- and it more than tripled
it. The guarantee it does provide is real and holds: the answer can no longer be
a number no tool computed, because the model returns a letter and the value comes
from our record. But it trades writing the wrong number for choosing the wrong
fact, and on this model that trade is bad.

On the narrow case it was built for it does win -- unit questions, pooled over 18
questions from three seeds, 6/18 against 0/18 with the field off, 95% CI
[+11.6, +55.1]. That is a real effect on a group where the model is otherwise
helpless. It is not enough to pay for the rest.

So `select_answer` defaults to False and the machinery stays, tested, behind a
flag and a `--select` switch on the bench. The research says the ceiling here is
the model's grounding -- 50.79% for llama3.2:3b against 88.19% for qwen3:4b-
instruct-2507 -- so this is worth turning on when the model changes, and worth
leaving off until it does.

Recording a design that did not work, with the number that says so, is worth more
than turning it on and hoping. The measurements are in docs/claims/battery_*.json
and the harness is one flag away from re-running them.
Same 40 generated questions, same code, same day:

                  seed 20260811   seed 555   pooled
  qwen3:4b-2507      18/20 90%    15/20 75%   33/40  82.5%
  llama3.2:3b        10/20 50%     6/20 30%   16/40  40.0%

+42.5 points, 95% CI [+23.3, +61.7], replicated on an independent battery, and
no group went backwards. Unit questions -- the group this repository has spent
the most effort on, through labels, gates, compression and exact relations --
went 0/2 to 2/2 by changing the model. It also uses FEWER steps: 5.5 against 7.2.

That is larger than every design change here put together, and it retires the
open question from the selection work: the research said the ceiling was the
model's grounding rather than the prompt, and this is that claim measured on our
own harness rather than on a leaderboard. Selection was tried on qwen3 too and
still adds nothing, which is the same answer from the other direction.

Both directions are pinned, on two batteries, because the comparison is only
worth anything if it cannot quietly become a comparison of two different things.

One pinned claim moved rather than broke: unaided accuracy on the default battery
was 2 of 20 and is now 5. The battery was re-run after concrete examples were
removed from the prompts, so dropping copyable numbers helped the arm WITHOUT
arithmetic as well. The old figure measured a different prompt, so the new one
is recorded with the reason instead of the tolerance being widened to hide it.

Adds --model to the bench, so any future comparison is one flag rather than a
rebuild.
@punnerud
punnerud merged commit 79b2fd6 into main Aug 11, 2026
6 checks passed
@punnerud
punnerud deleted the feat/answer-by-selection branch August 11, 2026 16:41
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