BLOG

Auto-optimizing Jev: half the errors, 1/7 the cost

We pointed our autoresearch loop at Jev, a classifier that can't be fine-tuned. It ended up making half as many mistakes as a reasoning model, for a seventh of the cost and a thirtieth of the latency.

What's autoresearch

Autoresearch is a research loop that runs itself: a coding agent pointed at a benchmark and a budget. It proposes changes, tests them cheaply, measures the survivors, keeps what holds up, and carries what it learned into the next round - the experiment cycle a researcher would run, executed end to end by software.

Two things about it matter for what follows. It trains nothing: the loop optimizes the world around a model - the code that decides what the model sees before it is asked. And it is built to measure: a change counts only when it survives scoring, not when it sounds convincing. This post is one run of that loop on one real task.

What's Jev

TypeSafe recently released Jev, the first of what they call System One models. We handed it to our autoresearch loop with a benchmark and a budget and let it work.

Jev is a classifier, and it will classify almost anything. You give it three things: state, the material to judge; instructions, the question you want answered; and criteria, the options it is allowed to answer with.

It returns one of those options, a probability for each of them, and a confidence.

One Jev callEverything on the left is yours to change. Nothing else is.STATE我【还】没有吃饭the material to judgeINSTRUCTIONSWhich reading does the markedcharacter have here?CRITERIAháistill, yet, not until nowhuánto return, to give backJevone call, no text outOUTPUThái0.97huán0.03A probability for every option, so code can act on the close ones.
Fig. 1 - The whole interface. You choose the state, the question and the options; Jev picks one of the options and says how sure it is. No free text goes in and none comes out.

Jev is a general-purpose classifier: it can answer a closed-set task without task-specific examples or training.

It generates no text. So there is no chain of thought to read, nothing to fine-tune, and no way to ask it to think step by step. What you can change is the request, and what you put in front of it.

The task

Converting Chinese characters to pinyin is a "grapheme-to-phoneme" task: turning written symbols into their pronunciation. Most characters have exactly one pronunciation and can be handled with a simple table lookup. A few hundred - the polyphonic characters - have several, and the correct reading depends on the word containing the character or, when it stands alone, on its grammatical role. For example: 还 is hái when it means still, huán when it means to give back.

One character, two readingsThe character 还 is marked in two sentences. In 我还没有吃饭, it is hái, still. In 他还了钱就走了, it is huán, to return. The sentence supplies context; the model predicts the marked character.One character, two readingsThe sentence supplies context. The highlighted character is the target.SentencePredicted readingI haven't eaten yet.hái还没 · not yetHe returned the money and left.huán还钱 · to return money
Fig. 2 - The task. The same character in two sentences takes two different readings; the disambiguator reads the whole sentence and answers for the highlighted character only.

Some popular published models for this, g2pM and g2pW, have to be hosted: g2pW is a 606 MB model that takes 6.7 seconds a sentence on CPU.

Why it's harder than it looks Metrics on exceptions

A few hundred polyphonic characters out of several thousand sounds like a rounding error. It is not: they are the common ones, about a third of the text, and within them one reading usually dominates - so two thirds of the work is a free table lookup, and a system that always answers the most common reading scores 98.0% without reading a word of context.

Where the difficulty sits184,372 scored characters33.5% need a decision66.5% have one reading and are freeWithin a polyphonic character thereadings are lopsided too. The redslice is every occurrence that doesnot take that character's usualreading - the only part a model canget wrong.100 of the 179 characters with 50 ormore occurrences take one reading99% of the time.occurrences (square-root scale) · share taking the usual reading5,284 · 99.6%2,705 · 95.8%2,083 · 99.4%1,004 · 59.9%988 · 91.0%906 · 53.4%442 · 50.5%71 · 95.8%
Fig. 3 - The task is a rare-event problem twice over. A third of characters need a decision at all, and within those a single reading usually accounts for almost everything.

What is left is exceptions, and they are not one kind of thing: 朝 is cháo in a dynasty and zhāo in the morning; 地 is the particle de between a manner phrase and its verb and everywhere else; and named entities - 华为 is Huáwéi the company, 美的 is Měidí the appliance brand, 朝阳 is a district of Beijing - are where the ordinary reading of each character is the wrong one.

Chinese is also written without spaces, so the word a character belongs to is itself a guess: 项目的金牌 contains 目的, purpose, where 的 is , but segmented as 项目 / 的 / 金牌 it is the particle de - and segmenters get such cases wrong often enough that they cannot simply be believed. Wrong pinyin is a wrong pronunciation, so the scorer gives no partial credit.

LLM baseline

A frontier model can instead work sequence-to-sequence over a whole sentence, or classify each ambiguous character separately. Neither approach needs task-specific training or a model you host yourself.

On our benchmark - the 61,750 positions that need a decision, drawn from 1,085 human-edited texts (details below) - DeepSeek V4.1 Flash with reasoning on, used over the whole sentence, makes 285 mistakes, at $11 a pass over the corpus and eight seconds a sentence. It only aligned its output consistently in reasoning mode; without reasoning, the predicted readings were frequently assigned to the wrong characters. That is the baseline for the comparisons below.

Jev baseline

Our first Jev request was the obvious one: the sentence with every polyphonic character tagged, each reading described by its dictionary senses, one line of instruction. It made 316 mistakes against 1,434 for a plain phrase-table lookup (pypinyin), for $1.22 a pass - a ninth of the reasoning model's price, a thirtieth of its latency. But 316 against 285 is decent, not a win: a replacement has to beat the thing it replaces. The gap - 31 mistakes - was the loop's job.

Every number in this post is a count of mistakes on the 61,750 positions that need a decision, not an accuracy - accuracy is a poor scoreboard here.

Dataset, metric, method The corpus, the split, and how we score

1,085 Chinese texts for language students and 61,750 positions where a reading has to be chosen, every one approved by a human editor. The loop worked on twelve characters; their sentences are split in half, so we develop on one half and score the other only to confirm. Each configuration was then run across all 61,750 decision positions, covering all 341 polyphonic characters, not only the twelve worked characters. That whole-corpus comparison includes both halves of the twelve.

What the corpus actually asks of a modeleach bar is the row above it, magnifiedevery scored character184,372polyphonic - a decision is needed61,75034% of the textof those, the ones not taking the character's usual reading3,6956.0% of positions needing a decision, 2.0% of the textOnly 3,695 positions take a reading other than their character's usual one.
Fig. 4 - The work is concentrated twice. A third of the text needs a decision, and within that a seventeenth of the decisions go against the character's habit. Everything else is a lookup a table could do.

Accuracy is a poor scoreboard here and we mostly avoid it. Everything below is a count of mistakes, and where a rate is useful we give it on the 61,750 positions that need a decision.

Handcrafting experiments is too slow

Unlike a traditional classifier, Jev needs no training. We could therefore use a separate configuration for each of the 341 character types, rather than train a separate model for each one.

To start, we wrote better requests by hand - ordered procedures, part-of-speech rules, worked examples. Occasionally something helped the sentences it was written for; usually it did nothing for the character's other samples on the split, or made them worse by distracting the model. Unlike a reasoning model, Jev did not reliably follow a sequence of instruction steps or reason through indirect rules. Saying what the distinction is moves the number; saying what to think about and what order to think in, like a recipe, does not.

Manual work was too slow for exploring many configuration combinations and testing them well. Jev was newly released, and each idea required a written variant, a paid pass over the corpus, and a read-through.

Auto-optimizing Jev

We pointed Kiln's auto-optimizer at the problem. Importantly, we targeted it at the harness; not the model, and not only the wording of the request, but all the code that decides what reaches the model. It had a small set of pre-approved libraries to build its harness from. They included jieba for word segmentation and access to CC-CEDICT (an English-Chinese dictionary) lookups for words covering the marked character and the readings those words provide.

We asked it to work one character at a time, twelve in all. Nothing couples the requests to each other, so each character is a small problem on its own terms: a closed set of answers, its own sentences, and a score that moves independently of every other character. Several can be poked at in parallel, and whatever survives on one gets tried on the next.

One wave of the loop, per characternothing is trained; every arm is a change to the code around the callTHE WAVE - DEVELOPMENT HALF ONLYPlanchanges to tryProbeyes or no, no scoringBuild and measuresurvivors become arms, scored together18312most die heremistakes left, development halfPromotethe winner becomesthe next baselineheld-out half,read once, by codememory - what worked and what was rejected, carried into the next wave
Fig. 5 - The split is enforced by where the steps sit, not by discipline. Planning, probing, building and measuring all happen inside the wave, on the development half. Promotion is outside it, and is the only thing that reads the other half.

The probe step is the cheap one, and it does most of the filtering. A probe is one yes-or-no question about a mechanism - does the segmenter actually keep this word together, does the dictionary have anything to offer here at all - answered from a handful of sentences, with no scoring run behind it. This ruled out many ideas before we built and measured them across the full split.

What survives gets built. The loop then clusters the remaining failures and infers their shared cause before choosing the next change. Take 得, a very common character that can be read either de or . Its leftover mistakes looked like unrelated sentences, but the loop found a single cause: the correct reading depends on whether the character stands alone between words or sits buried inside a longer word, and the harness never checked which.

The harness needed to identify whether the character was part of a word or stood alone before asking Jev to choose a reading.

Every proposed change is evaluated on that character's entire development half - we split each worked character's sentences in two, and keep the other half back for scoring. We reject it if it fixes its target cases but creates mistakes elsewhere, or if the improvement is smaller than about fifteen positions, the variation we saw across repeated calls. A surviving change is then scored once on the held-out half, which the loop does not access while working on that character.

Experiments

The loop's first instinct was the same as ours: write the semantics down. The rule for each reading, the constructions each one turns up in, when to prefer which. With a reasoning model this approach works, but Jev doesn't have reasoning. Then additional prompt engineering ideas: grammar labels for adjacent characters, English translations, and more context.

The autoresearch loop ran these experiments competently. None of it worked. The translations changed zero answers at roughly triple the tokens. The extra context landed inside the noise floor, and the whole article added to context cost 39% more input tokens for the same result. The grammar labels each helped on their own and cost sixteen to twenty hard cases the moment they were stacked with anything else.

A change that fixes its target character often breaks a different one, so no result can be trusted on its target cases alone. That is why every change was re-scored on the character's whole development half before it could be kept.

For Jev: show the evidence, not the steps

Multi-step instructions weren't helping, since Jev doesn't reason. Autoresearch eventually found a pattern that worked. It pivoted to case-specific hints, computed per sentence: segment the sentence, look up every dictionary word covering the marked character, note the reading each word gives and whether the segmenter keeps the word in one piece, then ask exactly the same question as before. The loop called that block word evidence.

The loop developed it on twelve characters, then we tested it unchanged on twenty other, unseen characters: it took 97 mistakes down to 11 and made none of them worse. Common combinations become lookups instead of grammar judgment calls.

This is not a dictionary lookup. pypinyin already uses a phrase table and still makes 1,434 mistakes on these positions - the harness supplies the candidate readings, and Jev makes the call the dictionary cannot.

Chaining Jev

Autoresearch then found a second optimization that worked: a Jev call is almost free and returns in under 300 ms, so the harness can afford more than one per decision. Instead of asking for the reading directly, it asks an easier question first - which word is this character part of? - and uses the answer to settle the reading. For one character (要), it went from 28 mistakes to 0 on its held-out sentences. It didn't help for most characters, so the loop updated the harness to make the second call only for characters where it had a measurable impact.

Everything else that went nowhere, or backwards Reasonable ideas that didn't move the number

Each of these is a reasonable idea that a reasonable person might ship, but doesn't actually help.

  • Listing example words under each reading. Word lists get matched against the text, firing even when the marked character is not part of the listed word. Senses can't be matched that way, so the requests describe senses only.
  • An LLM segmenter instead of jieba. One more character fixed.
  • A named-entity layer. Tag proper nouns with a POS tagger, take the dictionary's reading for them. Fixed 4 positions and broke 7.
  • Listing every candidate word, once the request already names one. The configuration that wins hands the model a single dictionary word covering the character and the reading that word gives it. We tried adding the rest of the candidates too, each with its own reading and its own boundary check. That scored 151 against 147: noise.
End to end example See the loop make progress across checkpoints

Small changes make a big difference - and sometimes distract

为 is the clearest character to watch. It is wèi as a preposition, wéi as a verb, and the corpus is almost evenly split between them, so no default can help. Here is where the loop ended up, before any detail: 41 wrong, then 7, then 2.

The next three blocks are examples of configurations the loop tried for 为. They are alternatives, not requests sent one after another. Jev gets one request per sentence; each block is a version of that request, scored on the same 462 held-out sentences.

checkpoint 1 · name the options 41 wrong
state        sentence: 业内人士认为,…不仅【为】行业提供了新思路…
instructions `sentence` is a Chinese sentence with one character
             wrapped in 【 】: 为. Which pinyin reading does that
             marked occurrence of 为 have?
criteria     wèi: wèi
             wéi: wéi

The two options are just the two answers spelled out. Nothing in the request says what separates them, so the model has to supply the entire distinction itself. That is enough for the obvious sentences and not for the rest. This bare request is what the loop measured every character against - barer than the unoptimized corpus pass, which described each reading by its dictionary senses.

checkpoint 2 · describe the distinction 7 wrong
state        sentence: 业内人士认为,…不仅【为】行业提供了新思路…
instructions `sentence` is a Chinese sentence with one character
             wrapped in 【 】: 为. Which pinyin reading does that
             marked occurrence of 为 have?
criteria     wèi: preposition. It introduces who or what something
                  is done for, or the reason it happens. What
                  follows is a noun phrase, and the main verb comes
                  after that.
             wéi: verb. To be, to act as, to become, to count as,
                  linking one thing to what it is or turns into.

Six times better, and the loop wrote it after reading the failures rather than from a grammar book. What survived was part of speech. Everything else it tried - worked examples, an ordered procedure, a list of constructions - was a wash or worse.

The seven that remain are all one shape: 华为, 行为, 人为, 认为, 因为. In every one the character is the second syllable of a two-syllable word rather than a preposition or a verb in its own right, and the word decides the reading - 认为 is rèn wéi, 因为 is yīn wèi, and nothing about the surrounding sentence decides either. The criteria ask which part of speech the character is, and the honest answer is neither, so the model answers a question about a role the character is not playing.

charactersentencesname the optionsdescribe the distinctioncompute the word
4624172
2203231
484130

地 is the honest counter-example. Describing the distinction made it worse than naming the options, because every failure followed something that could be read as a modifier - which is exactly what the description told the model to look for. The computed word fixed it.

The winning configuration

What won, in the end, was neither naming the options nor describing the distinction. It was computing the answer's precondition before asking: which word the character is sitting in, what reading the dictionary gives that word, and whether the segmenter agrees the word is really there. On the same 462 sentences, that took 为 from 7 wrong to 2:

checkpoint 3 · compute the word 2 wrong
state        sentence: …交警知道了这件事,认【为】他闯红灯是为了救人…
             word: 认为 (为 is syllable 2 of 2)
             dictionary: 认为 = rèn wéi (为 is syllable 2 of 2;
                         the segmenter keeps this word together):
                         to believe; to think; to consider.
instructions … If the character sits inside a word the segmenter
             and the dictionary agree on, that word's reading is
             the answer, whatever the surrounding grammar
             suggests. Only when it stands on its own does the
             question become which part of speech it is.

Nothing about 为 is written into that. The word, the reading and the note are computed per sentence.

The segmenter cuts the sentence. Every dictionary word covering the target is looked up. Each one is reported with the reading it gives the character, and with whether the segmenter kept that word in one piece.

That last part is the one that took longest. In 项目的金牌, "the project's gold medal", the middle two characters happen to spell the word 目的 (mùdì, "purpose"), in which 的 is said . A dictionary lookup takes the bait. But the phrase actually divides 项目 / 的 / 金牌 - "project", possessive particle, "gold medal" - so 的 is the particle de. The word the lookup matched straddles a boundary and is not there at all, and only the segmenter's cut can say so.

polyphone_prompts/lexicon.py python
def word_evidence(char, sentence, only_other_than="",
                   candidates=None):
    lo, hi, token = token_span(sentence)     # the segmenter's word
    index = sentence.index("【")
    lines = []
    for match in words_around(sentence):     # every dictionary word
        if only_other_than and match.reading == only_other_than:
            continue                  # 的 shows only exceptions
        begins = index - match.offset
        together = lo <= begins and begins + len(match.word) <= hi
        note = (
            "the segmenter keeps this word together"
            if together
            else "the segmenter splits this across two words, so it "
            "is probably a coincidence of characters rather than "
            "the word here"
        )
        if candidates and _unstressed_variant(match.reading, candidates):
            note += ("; the dictionary writes this syllable "
                     "unstressed, and the same word is often said "
                     "with the full tone instead")
        lines.append(
            f"{match.word} = {' '.join(match.syllables)} "
            f"({char} is syllable {match.offset + 1} of {len(match.word)}; "
            f"{note}): {match.gloss()}"
        )
    return {
        "word": (f"{token} ({char} is syllable "
                 f"{index - lo + 1} of {hi - lo})"),
        "dictionary": ("\n".join(lines[:4])
                       or "no dictionary word covers it"),
    }

The whole thing, forty lines.

Two arguments matter. only_other_than drops candidates offering a reading the character takes almost always, so 的 is shown only the rare words where it is not the particle. candidates enables the tone caveat: CC-CEDICT writes 关系 with an unstressed 系 and the corpus keeps the tone, and relaying that without comment cost 系 nine sentences.

Results

Cost and correctnesscheapermore expensivefewer errorsmore errorsJev, unoptimized$1.22 · 316 wrong · < 300 msDeepSeek 4.1 Flash$11.00 · 285 wrong · 8,500 msJev, optimized$1.60 · 147 wrong · < 300 ms
configurationmistakescost / passmedian latency
pypinyin, phrase table1,434--
Jev, unoptimized316$1.22< 300 ms
DeepSeek V4.1 Flash, reasoning on285$11.008,500 ms
Jev, optimized with autoresearch147$1.60< 300 ms

The optimized request makes half the mistakes of the reasoning model it replaces - 147 against 285 - for a seventh of its cost and a thirtieth of its latency. The unoptimized request was already a ninth of the price; the loop closed then surpassed the quality gap.

The whole campaign - every experiment, probe, ablation and corpus pass - cost about $18, less than two passes of the reasoning model it was measured against.

Detailed findings Per-character results

The autoresearch loop focused on twelve characters, then confirmed the techniques it discovered there generalized to held-out sentences and unseen characters.

The twelve characters the loop worked on

Per character, held-out halfreadings named, nothing elseafter the loop04 → 141 → 228 → 032 → 120 → 27 → 21 → 01 → 01 → 10 → 00 → 00 → 0
Fig. 6 - All twelve characters the loop worked on. Dots sit at the mistake count on a square-root scale; the labels give exact numbers. The four characters at the top carry the result. The four at the bottom were already close to perfect, making the point that most characters had little to win.
characterpositions, both halvespypinyinDeepSeekJev, unoptimizedJev, optimized
5,2848341
906650412
74606280
442812321
18050202
717272
98849010
4668110
28517111
2930000
37147000
2177000
all twelve10,249294151359

This is the complete set of twelve characters the loop worked on, not a ranking of every polyphonic character in the corpus. The position count covers both halves. All four mistake columns are scored on the 5,116 held-out positions - the half of each character's sentences the loop never worked on. Jev, unoptimized is the starting request, whose criteria name the two readings and say nothing else; Jev, optimized is what the loop arrived at. DeepSeek (reasoning on) and pypinyin are scored on the same held-out positions for reference.

What solved this case

The final configuration is hard to find, hard to measure, and not intuitive. Hard to find: most reasonable ideas change nothing, and a few quietly make things worse somewhere else. Hard to measure: effects are local to one character, regressions turn up on others, and the gap between a real gain and noise is about fifteen positions. Not intuitive: the strategy that fixed almost everything was not a better sentence about the readings - it was a segmenter and a dictionary.

The prompt is only one part of the system, and not the part that mattered. What solved the task was the work around the whole harness: wiring up jieba for segmentation and CC-CEDICT for candidate words, checking whether the segmenter agrees a word is really there, and shaping all of it into the one form Jev judges best - a single closed choice among named options. The model does the one thing it is good at; the harness does everything else. That division of labor is what the loop found, and finding it is what the loop is for.

Harness optimization beats model optimization

Increasingly, the biggest wins come from optimizing the harness, not the model. Kiln offers many ways to optimize agents - fine-tuning, GEPA prompt optimization, model selection, hyperparameter tuning - but none of that matters if your harness is unoptimized.

Instead of tuning a prompt or skill to handle 50 parallel cases, branch into smaller, more focused decisions. Instead of optimizing prompts to help a model navigate 200 tools, build a smaller/better set of tools or add expert sub-agents. The two wins for this project - word evidence and chained model calls - both lived in the harness, not the model.

Autoresearch loops make harness optimization practical. They can drive hundreds of experiments autonomously. Jev's case was particularly interesting: the more typical methods used for LLMs failed, so the loop had to try a range of new ideas. The research model had no knowledge of Jev's quirks - Jev had only just been released - but it still found a solution.

Oh, and if you want to optimize your agent - please reach out (discord/founders)! We can apply these techniques and more to your use case.

References

Jump to section
Newsletter

New posts in your inbox.

Build AI that actually works.

Ship custom AI products with evals, fine-tuning, and prompt optimization built in.

macOS, Windows, and Linux