Fine-tuning danbot
2026-05-26 — 2026-05-30
In Which Before-and-After Prose Pairs Are Harvested From Manual Edits, a LoRA Adapter Is Trained on Qwen3 8B via Together AI, and a Four-Signal Evaluation Harness Is Assembled.
Claude is a bad ghostwriter for me. Not unintelligible, not slow, not even particularly inaccurate — just wrong in the way LinkedIn posts are wrong: smooth, hedging, structurally signposted, breathlessly enthusiastic in flat places. I have tried the standard ladder of prompt-side fixes: a hand-tuned /dan-voice skill with a banned-words list and a structural slop catalogue, a Vale ruleset as a mechanical safety net, in-context examples of my own prose stuffed into the system prompt as few-shot exemplars, multiple rounds of “rewrite this in Dan’s voice” against a frontier model. The output comes out marginally more Dan-shaped each iteration and still smells like a press release. There is a floor on how much taste we can install via prompt engineering, and currently I am still trapped, according to this metaphor, in the taste basement. No matter what I do, I cannot dissuade Claude from poorly chosen mixed metaphors like “standard ladder of prompt-side fixes” or the “floor on how much taste we can install via prompt engineering.” The text pudding is laced with choke-hazard “dan-shaped”, and “load-bearing” tuppences.
Look, I’m not claiming my style is perfect. But I am claiming that
- It is mine, and
- it is low on grating LLM quasi-metaphors like “this is the growing seam we can hand off”, and
- that when I write something on Monday in my own style, reading it back on Friday, I discern my own intent without excessive strain.
All these are untrue of Claude’s output.
If I could train a small specialist model that bakes the style into weights rather than pleading for it via prompt, every AI draft I touch could pass through it and come out sounding like me, or at least, hopefully not substantially worse despite having been interfered with by an LLM.
So! Fine-tuning danbot!
My policy on this blog is that slop should be disclosed, not concealed — see the three robots marker at the top of this post? That means it is slop af.
My policy is not to have no AI. Sometimes it is useful to sketch stuff out with AI, or do background research, or make tedious and complicated edits, or whatever.
It is not useful to pretend that AI is a human, nor specifically that it is me. That does not mean that it should be horrible to read.
1 Goals
- Less onerous clean-up of AI edits.
- More comprehensible prose in general
2 Non-goals
- Full Dan impersonation
- Evading slop detectors
3 Methods
The plan is to train a small LoRA adapter on top of a mid-sized open-weights model and treat it as a specialist prose styler: feed it AI-flavoured draft text, get back something closer to my own voice. Four steps:
- Collect real edit pairs. Every time I rewrite an AI draft, capture the (before, after) diff at paragraph..
- Synthesise more pairs. Ask the LLM (ideally several LLMs) to paraphrase paragraphs of my own prose, and then use the slop paraphrase as the source, and the original as the target. This might be lower-signal than the actual edit pairs, so I imagine down-weighting them in the training set. But: easy to generate these so YOLO.
- Train a LoRA. Run LoRA SFT on some small, non-tool-using model (e.g. Qwen3 8B) in a managed cloud pipeline (~$10 per training run for the 8B, cheap enough to iterate freely).
- Wrap it in a CLI.
uv run ai-style <file>runs the LoRA model as a text transformer.
Will this work? idk. Easy-ish to find out though and a fun learning project. In the worst case we still have a labelled corpus of my own prose that is useful for fun experiments with LLMs.
3.1 Whose slop?
I have a lot of examples of my style. There are about 1.6M words of prose I authored with my human hands on this site, for example, the the slop free ones marked clearly.
If I want to turn these into training data, I need to turn them in to slop. So I take a sample of my paragraphs, and for each one, ask an LLM to turn it into slop (“rewrite this passage to be clearer and more polished”). The output of that process is the example slop I wish to eliminate. Looking at the LoRAs out there, I imagine that a few thousand such examples might be enough to nudge the model towards my style. A smaller secondary set is generated by explicitly asking for the worst slop patterns from my own slop catalogue; that covers the long-tail mannerisms the paraphrase pipeline tends to underrepresent.
I suspect, from the literature that both sheer input length (Levy, Jacoby, and Goldberg 2024) and accumulated conversation turns (Laban et al. 2025) degrade AI output. The worst of the flows probably an LLM already gravid with context. Proposal: I generate the slop across growing sessions rather than fresh conversations, feeding a fresh, unrelated paragraph each turn and keeping each rewrite, so the examples span the range from empty- to exhausted-context. If I tag each with how full the context was, I can see where the slop gets worse and weight those examples up, or at least just see how important this factor is.
Now, which model? I use Claude a lot, so maybe I could just undo Claude? But I use other models too.
Slop seems to come in two layers. There’s a deep humus under-layer — throat-clearing, “it’s worth noting”, the rule-of-three, “It’s not X ; it’s Y”, signposting, marking key phrases in bold — that every RLHF’d model produces. Atop that is the litter inhabited by model-specific tics: GPT’s “delve” and “tapestry”, Claude’s particular hedging cadence, and so on. Attar et al. (2026) study 284 interpretable linguistic features across 27 LLMs and 10 text domains. They argue that most signals are model- or domain-specific. This suggests multi-source training might be the go.
Let us call the target here a Dan-ifier rather than a de-Claude-ifier: rewrite the input as I’d write it, whatever the source. Soto, Chen, and Andrews (2025) argues that mimicking a specific human author’s style is a strong method for removing obvious AI tics, so we have hope that this is feasible.
Moreover, I would be training a small open-source model to do this text transformation, i.e. not Claude. It will have its own tics to squish. Antoun, Sagot, and Seddah (2023) argue that small open models are easier to detect— by machine admittedly, but I assume humans also could find it easy. As such, it seems especially important to generate some of the slop using specifically my own utility base-model.
In any case I don’t want to fix on Claude, because even Claude isn’t one thing, but family of various models and version.
tl;dr We generate training slop from a plural set of LLMs.
There is at least one example of this in the wild. Paneru (2026) build a 25,140-pair AI→human corpus and fine-tune BART and Mistral-7B (QLoRA) , arguing that “using two generators rather than one reduces the risk that a trained humanizer learns to undo one model’s idiosyncratic habits rather than AI-style writing more broadly”. They also target multiple human-target domains for the same reason, which I cbf doing so I hope that part is not important. They don’t run a single-source vs. multi-source ablation, so the value of this multiple model approach is unknown. Maybe I can publish that ablation if I find time? Paneru (2026) also observe some interesting failure modes — for example we can learn the correct style transform, but overcorrect, which looks great on metrics but probably not on human inspection.
3.2 Where to train and serve
I (plan to) train on Tinker and serve on Fireworks, with local MLX as the fallback when I want it off the cloud entirely.
Tinker is Thinking Machines’ managed-training API: I write a CPU-side training loop and they run the GPUs. Its API exposes forward_backward, optim_step and sample as low-level primitives, so the same platform that runs my vanilla SFT can later shape an RL or DPO loop if I preference-tune the styler. The trained adapter downloads to Hugging Face format, which keeps out options open for local inference.
Serving the fine-tuned model, now… Fireworks deployments scale to zero and accept an uploaded LoRA which is what we want for this rarely run model. When I want it fully local I merge the adapter into the base, convert to MLX, and serve it off my own machine.1
4 Evaluating
How do we measure the quality of the styler on the held-out set? I audition four options.
- Vale the “style linter” catches mechanical slop (e.g. banned words)
- a few-shot LLM judge assesses whether ths output resembles other things I have written
- we can optionally incorporate a statistical detector
- I also eyeball the output myself
There are a few options for statistically classifying text as dan-like-or-not.
One option might be to use a general-purpose AI-text detector, and see if the output of the styler is less AI-like than the input.2
However, a moment of introspection reveals this is a dumb idea. Evading slop-detection is a non-goal, so this is not really the right tool for the job. A lot of things are not slop but also not Dan, for example,
In the beginning was the Word, and the Word was with God, and the Word was God.
We could imagine this signal being helpful? But it is at best a surrogate for what we care about.
4.1 Scoring Dan-ness directly
We can directly estimate whether text is more slop-like or dan-like by directly training a classifier on the (slop, Dan) pairs we have, via an appropriate embedding model. Some care is needed in choosing the embedding. I am more familiar with content embedding models as made famous by ai_search. Semantic embedders, it turns out, do capture style a little bit (Icard et al. 2025) but it is not their strong suit.
However, style-embeddings are a thing (Wegmann, Schraagen, and Nguyen 2022) and seem to do okay at ignoring content. StyleDistance (Patel2025StyleDistance?) is one trained on data generated with a synthetic corpus generated in a manner very much reminiscent of this blog, which suggests we can use it here too. cf LUAR (Rivera-Soto et al. 2021) which attempts to classify authorship. As a bonus Soto et al. (2024) show that these style vectors pick out machine-generated text from human text few-shot, with no AI-detector training at all.
These things are all plug and play, so we can audition ion the standard way we evaluate classifiers.
AnnaWegmann/Style-EmbeddingStyleDistance/styledistancerrivera1849/LUAR-MUD
I ran a logistic regression using each embedder as a feature extractor, scored on pairs held out by source document. StyleDistance won, at 0.78 pairwise accuracy and 0.72 AUC, ahead of LUAR (0.76 / 0.71), Wegmann’s CISR (0.73 / 0.67), and the semantic embedder baseline (0.75 / 0.62). Interestingly the semantic embedder is not terrible at pairwise accuracy, but it fails to generalize, getting a poor AUC. The quality of the StyleDistance-based classified is good enough that I probably won’t bother with more elaborate pipelines such as fine-tuning my own style encoder.
4.2 Where we are now
Phase 1 has shipped. There is a manual pair-logger (uv run ai-style-log) wired in such that every time I rewrite an AI draft into my own voice, the (before, after) diff is captured at paragraph level into a JSONL file. So even before we run a single training step, the corpus accumulates with each notebook I clean up. To make the corpus more representative, I have also stripped the automatic slop-removal pass out of the /dan-voice skill: drafts Claude generates in my voice now leak realistic AI tics, which is exactly what we want as training input. Manual cleanup is now via the separate /slop-hunter skill, invoked when I want a clean draft rather than a training-data candidate.
5 Appendix: Wiring it into VS Code
Typing uv run ai-style-log open notebook/foo.qmd over and over is friction. VS Code already knows which file is currently focused; we can lean on that via its task system, which substitutes the editor’s path into a shell command at run time.
Drop a .vscode/tasks.json in the workspace:
{
"version": "2.0.0",
"tasks": [
{
"label": "ai-style-log: open current file",
"type": "shell",
"command": "uv run ai-style-log open '${relativeFile}'",
"presentation": { "reveal": "always", "panel": "shared", "clear": true },
"problemMatcher": []
},
{
"label": "ai-style-log: save current file",
"type": "shell",
"command": "uv run ai-style-log save '${relativeFile}'",
"presentation": { "reveal": "always", "panel": "shared", "clear": true },
"problemMatcher": []
},
{
"label": "ai-style-log: save --keep-open current file",
"type": "shell",
"command": "uv run ai-style-log save --keep-open '${relativeFile}'",
"presentation": { "reveal": "always", "panel": "shared", "clear": true },
"problemMatcher": []
},
{
"label": "ai-style-log: drop current file",
"type": "shell",
"command": "uv run ai-style-log drop '${relativeFile}'",
"presentation": { "reveal": "always", "panel": "shared", "clear": true },
"problemMatcher": []
},
{
"label": "ai-style-log: list",
"type": "shell",
"command": "uv run ai-style-log list",
"presentation": { "reveal": "always", "panel": "shared", "clear": true },
"problemMatcher": []
}
]
}Then in (user-level) keybindings.json:
[
{ "key": "cmd+k cmd+o", "command": "workbench.action.tasks.runTask",
"args": "ai-style-log: open current file",
},
{ "key": "cmd+k cmd+s", "command": "workbench.action.tasks.runTask",
"args": "ai-style-log: save current file",
},
{ "key": "cmd+k cmd+i", "command": "workbench.action.tasks.runTask",
"args": "ai-style-log: save --keep-open current file",
},
{ "key": "cmd+k cmd+d", "command": "workbench.action.tasks.runTask",
"args": "ai-style-log: drop current file"
},
{ "key": "cmd+k cmd+l", "command": "workbench.action.tasks.runTask",
"args": "ai-style-log: list"
}
]${relativeFile} is the editor’s path relative to the workspace root. Cmd+K is VS Code’s chord prefix, already used for Cmd+K Z and similar, so chord-style bindings do not collide with single-key editing shortcuts. Mnemonics: O for open, S for save, I for interim (keep-open), D for drop, L for list. We could scope these using a clause like ”when”: “resourceLangId == ‘quarto’ || resourceLangId == ‘markdown’”. This would restrict them to qmd or markdown buffers so I cannot accidentally open a session on a Python file. But I like living dangerously.
The workflow becomes: edit a draft, Cmd+K Cmd+O to begin a session, edit more, Cmd+K Cmd+S to save and close. No copying paths, no terminal context switch.
6 Appendix: You see the problem?
Why am I doing this? Because I recently had the following conversation with Claude:
USER: This document is crammed with overwrought, rambling, almost schizophrenically-dense, confusing prose. I need you to help me tidy it up. I will show you one example of how the text started out (“original”) and how I tidied it up (“tidied”)
original:
The one thing maths rewards that ordinary agentic work does not is what makes the cloud earn its keep. A single solve is irreducibly sequential — each code block depends on the last, and no hardware shortens that chain. The parallelism is across samples and problems: the maj@k draws are independent, a problem set’s entries are independent, and a prover’s Pass@32 is thirty-two independent chains. That is the axis to fan out along, and the binding constraint is almost never the GPU. The three roles parallelize unevenly: the model server already batches many chains against one card (vLLM, SGLang), the orchestrator is just \(k\) async loops, and the laggard is the executor — a hand-rolled loop holds one kernel, a bare lean-repl compiles one proof at a time. So the question a workflow answers is not “how big a GPU” but “how do we run many executors cheaply”.
tidied:
One weird trick to make provers go better is to sample several independent attempts at solving the same problem, and choose the most popular solution. This is the so called
maj@k-trick. Provers have an equivalent one, called Pass@k. Either way you can run a lot of these fuckers at once. The LLM tokens are delivered over the network and as such are parallel. So our agentic loop handles \(k\) conversations with the model. The executors may as well be run in parallel too, ideally on \(k\) different machines running whatever tool is needed for that chain.Do you see what I mean? The first one was full of baffling unclarity, introducing things in needlessly complicated, even incoherent ways, and ultimately after reading it I felt much stupider than before. The second version omits needless bullshit and communicates, in context, what the reader needs to understand. Do you think you can go through the notebook section-by-section and edit each bit so it sound less batshit raving insane? This is a long document, and you do have a tendency for, let us say, prolixity, so, don’t worry about matching my voice or whatever right now, forget all that shit. Just do your best to turn this ululating turd-burger into something humans can read in order to become elucidated thereby. Delete info that doesn’t help them. Drop useless crap. Don’t forget they can look up things in the attached git repo. Here is not a time for falling in the info latrine while info dumping. Rather, it is a time to stop, take a breath, and consider what we need to say.
Do you think you can do that?
The answer was ofc that Claude thought he could do that. Oh Claude. However, Claude managed to stay tidy for about three paragraphs before descending once again into shoggothy litany.
7 References
Footnotes
Alternatively, Together trains a LoRA cheaply and makes it easy to extract, but hosts a fine-tune only as a dedicated endpoint — $6.49/hr for an H100, i.e. ~$4,000/month if I forget to turn it off — so, no good for me. Unsloth is another DIY fine-tuner but it seems that tinker gives me all I need.↩︎
Pangram is the flagship (Emi and Spero 2024; Jabarian and Imas 2025); Binoculars, Ghostbuster and RADAR are open-weight and much cheaper than Pangram’s princely fees.↩︎
