Scribe 調校實測:作者以一份問卷調校、兩份問卷檢查,避免高估成效
One Packet to Tune, Two to Check: Tuning Scribe Without Fooling Myself
作者以一份14頁問卷調校 Scribe,另用兩份問卷檢查;在三份問卷共531個答案中,最終配置的準確率為96%。未送覆核答案的準確率為99.8%,約22%答案仍需覆核,單份問卷處理時間約47分鐘;啟用 prefix caching 後約37分鐘,並多一個錯誤答案逃過覆核。作者指出,兩份檢查問卷曾影響調整決策,因此這些結果不屬於 part 1 設定的私下留出評估。
Part 2 ended with reading accuracy dropping sharply on handwriting the pipeline had never seen, while the review gate held. It also said part 3 would be the private held-out evaluation. That evaluation needs fresh packets, and it's still coming. This post answers the question that comes first: on a long form I could tune for, how far could tuning go, and how would I know the gains were real?
I moved Scribe to a 14-page questionnaire packet with 177 answers per packet, on pages full of tick grids, yes/no underlines, timed tasks and tester-written scores. I tuned on one filled packet and kept two back as check packets.
On 531 checked answers across the three packets, the final configuration ended up with:
- 96% read exactly right, up from 93% (94.4% to 96.0% on the two check packets alone).
- 99.8% of the answers accepted without review were correct, up from 97.5%.
- One wrong answer slipped past review, down from 11 (9 of those 11 were on the tuning packet).
- About 22% of answers go to review, up from 18%: the second model adds review work.
- A packet took about 47 minutes, down from about 70 when I first added the second model. Prefix caching later brought it to about 37, with one more answer escaping review.
None of this counts toward part 1's targets: I tuned on one of these packets, and the check packets shaped some decisions along the way. The rest of this post is how I got there, what I threw away, and where the method leaked.
The setup
The pages always come in the same order: questionnaires, a page that a tester scores, and several pages of timed tasks and number fields. I built one schema for the packet: 177 answer fields, 89 checks between related answers, and a rule that a page's position in a complete packet decides which page it is.
I had three filled copies. They have never been published, so the models tested here cannot have seen these filled pages in training (blank forms of this kind may well be public).
The answer key was drafted automatically (two independent AI reads of each page, then an adjudicating pass), and two people, one of them me, then reviewed every value. The first review changed 2 of the 603 values in that first key, the two reviewers agreed on 99.2% (598 of 603), and I settled the five disagreements. People checking a pre-filled draft can miss what the draft missed, so the key is very good rather than perfect. It shrank to 531 answers when I dropped some fields, below.
The rules, which I settled after a first experiment had already been scored on all three packets:
- one packet for tuning (the hardest at baseline: 86% exact, against 94% on another)
- two check packets, kept out of the tuning loop and run to confirm a change
- every comparison is paired: each answer is right or wrong in both runs, and only the answers that changed count
A fourth rule came later, from a result described below: a change stays only if it improves the tuning packet and holds on the check packets.
Fix the scorer before trusting a gain
My first experiment compared prompts with and without short "reading skills": form-agnostic notes on how pen marks work (underlines, crossings-out, handwritten digits, empty boxes). On the first packet scored, the skills version looked 1.5 points better.
It wasn't. The model writes [] for "nothing ticked", and my answer key writes a blank. Both mean the same thing, but the scorer counted them as different, and that one convention produced the whole gain. Scored fairly, the skills version was half a point behind on that packet, and across all three packets the two versions were within two answers of each other.
I check both scorings now and decide on the fair one; every number in this post uses it. Part 2's lesson again, from the other direction: before you trust a gain, check that your scorer isn't the thing that changed.
Look at what the model actually saw
Accuracy numbers told me which answers were wrong, not why. So I built a debugging view that shows, for every wrong answer, the image the model read next to where the answer actually is on the page.
It found two bugs that no metric would have explained:
- One page was never cropped at all. The step that finds each answer's box asks for every box on a page in one reply. On the tester-scored page that reply hit its length limit, the cut-off JSON failed to parse, and all 22 fields meant to be cropped silently fell back to the whole page. Fix: ask in batches of ten, and keep every complete box from a cut-off reply.
- A grid was read one column off. Each row of a tick grid is ticked under one of three column headings. Cropping a single row removed the headings, so the model miscounted columns and gave wrong answers with full confidence. Fix: read those rows from the whole page.
A third pattern, invented "yes" answers on blank rows of a timed-task page, turned out to be the crop catching a circle from the row above. Same fix.
On the tuning packet alone, three rounds of changes took exact accuracy from 85.6% to 89.1% and accepted-answer accuracy from 90.6% to 98.6%. Not all of that was better reading: the middle round also sent every tick on the tester-scored page straight to review, which takes the worst answers out of the accepted pile, and added an underline rule I'd noticed on these forms (the word where the underline starts is the one chosen) to the skills and to 26 yes/no hints.
The change that looked fine and wasn't
Once the box-finding worked, the tester-scored page was finally cropped. On the tuning packet that changed nothing (64% before, 64% after). I almost kept it.
On the two check packets it dropped that page from 94% to 86%. A crop shows only part of a row of ticks, and the old bug had been hiding the problem by never cropping at all.
I reverted it, and that's when "holds on the check packets" became a hard rule rather than a final formality. It also means the check packets have now shaped a decision; more on that below.
Read what you actually use
The tester-scored page has a tick per item and a handwritten score per section. The ticks were the problem: 15 of 66 wrong at the start, and once I sent every tick to review, 22 extra review items per packet (about 11% of all its answers). The written scores had one error across all three packets, and they are the values that get used downstream.
A synthetic page with invented items, drawn for this post. Ticks can land over the border, be crossed out or be faint; the written section score is the tester's own count.
So the schema now reads only the written scores and drops the tick fields. That changes the task rather than improving the reading: the page's worst fields leave the scoring, review load drops by 22 items per packet, and each packet needs 24 fewer model calls. To keep comparisons honest, the summary at the top and every comparison from here on score both runs on the same reduced set of 531 answers; the figures before this point used the original 603.
With all the fixes so far, and no second model yet, exact accuracy across the three packets went from 93.2% to 95.3% (12 answers fixed, 1 broken), and wrong answers escaping review fell from 11 to 4. On the check packets alone the gain was smaller and not significant: 94.4% to 95.5% (5 fixed, 1 broken).
The second model, revisited
Part 1's best idea was a second, different model re-reading each answer. Adding it back on this packet let one fewer wrong answer through (4 to 3), but the review load jumped from 17% to 27%.
Looking at its disagreements, 92% were false alarms (55 of 60), and 42 of those were answers the main model had read from a crop that the second model re-read from the whole page. On a dense page, the small model often couldn't find the field.
I tried two fixes, one at a time:
- Also re-check blank answers. Dropped. Review load on the tuning packet rose from 27% to 36%, and every new flag was a false alarm: the small model "saw" answers on empty lines.
- Re-read the same image the main model read. Kept. False alarms dropped from 16 to 10 on the tuning packet, without changing any answer.
So a second read is useful when it's a fair second look at the same thing, and much less useful as a way to confirm that something is empty. Whether it earns its keep on this form is still open: compared with the tuned pipeline without it, the final setup let 1 wrong answer through instead of 4, and raised review load from 17% to 22%. A difference of three answers is more than nothing and less than proof.
Faster, without making the answers worse
A 70-minute packet was too slow. My condition for speed work: no change could make the answers worse, checked answer by answer with a paired run.
More than half the time went into reading the prompt rather than writing the reply: each whole-page read sends about 5,100 tokens, about 3,650 of them the page image.
| Change | Time saved | Answers |
|---|---|---|
| read 4 answers on a page at once, and skip the page-type check when page order already settles it | 18% (tuning packet) | 176 of 177 identical |
| also cap the short note the model writes after each answer at 60 characters | 24% in total (tuning packet) | 2 changed, both for the better |
| reuse the processed page image across that page's whole-page questions (prefix caching) | 22% more per packet | 20 wrong instead of 21, but 2 escaped review instead of 1 (run-to-run noise in the second model) |
Packet time went from about 70 minutes to about 47, then to about 37.
Things I tried and dropped:
- 8 answers at once instead of 4: 1% faster. The server was already saturated.
- No reading skills: 6-11% faster, but one more wrong answer escaped review on the check packets. One answer is within the noise, but the cause was specific: a yes/no question whose own hint didn't mention the underline rule had been getting it only from the skill. I kept the skills as a safety call, not a measured win, and fixing that hint now would be a fix found on a check packet.
- Speculative decoding (a small "drafter" proposes tokens for the main model to check in one step): on synthetic test images a single request ran at 20-23 tokens/s instead of 15, but in the pipeline it was 6.6% slower. In the serving library, a running speculative batch takes no new requests until its longest reply ends, and with a drafter loaded the library refuses the structured JSON output Scribe relies on. Along the way I found a batching bug in the library and reported it upstream.
The two that got through
Part 2 ended by auditing every answer that skipped review, so here are the two in the current setup.
- A free-text answer with an extra letter. The model added a plural "s" to one word of a two-word answer. The meaning is the same, but exact scoring counts it wrong, and the second model made the same misread, so nothing flagged it.
- A yes/no underline that ran under both words. The line starts under "Yes" and carries on under "No". By the rule in the reading skills, where the line starts decides, so the answer is Yes. The model went by where it ended and read No, with high confidence. Without caching, the second model disagreed and the answer went to review; with caching it agreed, and it slipped through.
A synthetic recreation of the second case: an invented question and a generated pen mark, drawn for this post.
So one cosmetic error and one real flip, on a mark a person would hesitate over too. The fix I'd test next, on new packets rather than these: send any underline that spans both options straight to review.
What this doesn't show
- It's three packets. 531 answers is enough to see large effects, not small ones. The second model doesn't give identical answers from run to run under batching, so treat any one- or two-answer difference in this post as noise.
- The check packets aren't clean. The schema and answer key were built from all three packets, the first experiment was scored on all three, and three later decisions used check-packet results: reverting the crops, reading only the written scores (the tick error counts behind it covered all three packets), and keeping the reading skills.
- Few writers. Three packets can't show how this behaves across many writers and testers.
- It's not the held-out evaluation. Part 1 set the targets (at least 98% accuracy on accepted answers, at most 2% invented answers escaping review) to be measured only on a private held-out set. These packets don't qualify, and the untuned baseline was already above 98% on the check packets, so this says more about the form than about the tuning.
What I'm taking away
- A tuning/checking split pays for itself, even a leaky one. The one harmful change looked neutral on the packet I tuned on.
- Check the scorer, and look at what the model saw, before trusting a number. The first "gain" was a scoring convention; the two worst bugs were silent until I saw the crops.
- Read what is used, and score before and after on the same set.
- A second model should get a fair second look, not a harder question.
- Speed work must not make answers worse, and a speed-up only counts under the batching you actually run.
What's next
The private held-out evaluation, as planned in part 2: fresh packets the schema has never seen, filled by several people. And the checkbox detection stage from part 1 is still on the list.
The numbers, for engineers
Same machine as parts 1 and 2. Main reader Qwen3.8-27B (8-bit), second model Qwen3-VL-8B (8-bit), served locally with mlx-vlm 0.6.15. 531 answers across 3 packets (one for tuning, two for checking), scored fairly ([] = blank). Accepted acc = accuracy of answers accepted without review. Escaped = wrong answers that were not sent to review.
| Configuration | Exact | Accepted acc | Wrong | Escaped | Flags | Min / packet |
|---|---|---|---|---|---|---|
| Baseline (main model, crops by field type, no second model) | 93.2% | 97.5% | 36 | 11 | 18.1% | ~52 (older, larger schema) |
| Tuned (fixes above, written scores only) | 95.3% | 99.1% | 25 | 4 | 17.3% | ~36 (estimate) |
| + second model | 94.9% | 99.2% | 27 | 3 | 26.7% | ~70 |
| + same-image re-read, 4 at once, no page-type check, note cap | 96.0% | 99.8% | 21 | 1 | 21.8% | ~47 |
| + prefix caching (current) | 96.2% | 99.5% | 20 | 2 | ~22% | ~37 |
Check packets only (354 answers), exact / accepted acc / escaped: baseline 94.4% / 99.3% / 2; bold row 96.0% / 100.0% / 0 (of 275 accepted answers, which still allows a true error rate of about 1%); current 96.3% / 99.6% / 1.
Paired comparisons (exact McNemar on the answers that changed): bold row vs baseline 15 fixed / 0 broken, p = 0.00006 (check packets only: 6 / 0, p = 0.03); tuned vs baseline 12 / 1, p = 0.003 (check packets only: 5 / 1, p = 0.22); bold row vs the first second-model run 8 / 2, p = 0.11; skills vs no skills p = 0.73. Read these as rough: the all-packet figures include the packet the changes were chosen on, one fix often flips several answers on the same page, and I ran many comparisons.
Prefix caching: the main model mixes two attention types, so mlx-vlm caches exact prompt snapshots, by default 16 tokens before the end of the prompt, a point no two questions share. A 4,000-token guard moves the snapshot to the end of the page image (cached prefix about 3,984 tokens; prompt processing for later whole-page questions about 6.6 s to 1.8 s). Cropped reads and the second model get no reuse.
Other rounds: no JSON constraint on its own, no gain; speculative drafter 89% of drafted tokens accepted on the synthetic probe, +6.6% time vs the same setup without the JSON constraint and about +9% vs the configuration I run.
This is part 3 of a series. Part 1: Confidence Is Theater: What I Learned Building Scribe. Part 2: One Imperfect Page per Hundred: Testing Scribe on Data I Didn't Curate.
Pipeline code and the public benchmarks from parts 1 and 2: github.com/kod201/scribe
Every Scribe run in this post ran locally on one machine.
來源:DEV Community · dev.to


