Emma Sofia 用 Tinker 微調 Qwen/Qwen3.5-4B,打造 30 秒內識別不公平服務條款的 Didn't Read
Everyone Clicks 'I Agree.' No One Reads Them: I Fine-Tuned a 4B Model with Tinker to Catch Unfair Terms of Service in 30 Seconds
Emma Sofia 推出 Didn't Read,使用 Tinker 微調的 Qwen/Qwen3.5-4B 逐句辨識服務條款中的 8 類不公平條款。在 422 句評測切片上,Macro F1 由零樣本基礎模型的 0.707 升至微調版本的 0.874(+23.7%),仲裁條款 F1 則由 0.318 升至 0.923。
The average consumer Terms of Service contract is 9,000 words long.
Reading one carefully takes about 35 minutes of agonizing focus through dense legalese, nested definitions, and cross-referenced indemnity clauses. An active software engineer or web user signs roughly 30 to 40 of these agreements every single year. That’s more than 20 hours of reading just to avoid giving away your legal rights.
Naturally, nobody reads them. We scroll directly to the bottom, click the cheerful blue "I agree to the Terms of Service" button, and move on with our lives.
And in doing so, we quietly surrender our constitutional rights to a court trial, agree to binding private arbitration in jurisdictions thousands of miles away, grant companies blanket authority to delete our content without recourse, and waive liability even when critical cloud data is lost.
For the DEV Community Hacktoberfest Challenge: Slow and Steady, the theme is rewarding laziness through open-weight AI. I asked myself: What is the ultimate developer act of constructive laziness?
It isn’t generating another pull request description or summarizing a 200-word email. It’s refusing to read 35 pages of contract boilerplate, and having an open-weight 4B model fine-tuned for this exact task catch every predatory clause in under 30 seconds.
This is Didn't Read.
What I Built
Didn't Read is a specialized contract reading tool built like a lawyer’s physical proofing sheet.
You provide any Terms of Service URL (or paste raw contract text). The system extracts the legal prose, splits the document into individual legal sentences using a rule-based sentence boundary disambiguator (pysbd), and classifies each sentence across the 8 statutory unfair consumer clause categories defined by European and international consumer protection standards.
Instead of serving an AI hallucination or a vague summary, Didn't Read renders the full document in place, highlighting offending clauses in distinct pastel marker tints with 2px solid underlines. Hovering or tapping any flagged clause displays a margin popover with the official legal definition of the rights waived and the model's classification tag.
┌─────────────────────────────────────────────────────────────┐
│ 1. Document Extraction & NLP Segmentation │
│ URL / Text ──> trafilatura ──> pysbd (Legal boundaries) │
└──────────────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. Tinker Fine-Tuned Qwen-4B LoRA Inference │
│ Remote Sampling on Thinking Machines Cloud │
│ Checkpoint: wasnt-read-unfair-tos-lora │
└──────────────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. Clean Editorial Markup Sheet │
│ • Left Verdict: Flagged counts & category jump links │
│ • Center Sheet: Source Serif 4 document with highlights │
│ • Margin Notes: Statutory definitions (Zero hallucinations)│
│ • Keyboard Nav: 'J' / 'K' quick clause hopping │
└─────────────────────────────────────────────────────────────┘

The Didn't Read landing interface with real Terms of Service example quick-chips and URL / raw text scanning options.
Live Demo & Code Repository
- Live Web Application: https://didnt-read.onrender.com
- Tinker Playground (Try the Checkpoint Directly): Tinker Checkpoint Interactive Playground
- Open-Source GitHub Repository: https://github.com/EmmaSofiaDev/didnt-read
- Auditable Evaluation Dashboard: https://didnt-read.onrender.com/eval
Why Fine-Tuning with Tinker Mattered: The "Clear Improvement" Evidence
The judging rubric for Best Use of Tinker states:
"Use Tinker to fine-tune a model for a specific task, and show a clear improvement in performance, latency, or cost over a baseline."
General large language models are notorious for failing at contract clause classification. When prompted zero-shot, base models produce massive false-positive rates on generic warranty phrases (like "Software provided AS-IS"), while frequently missing binding arbitration waivers embedded deep inside indemnification headers.
To solve this, I used Tinker to fine-tune Qwen/Qwen3.5-4B with rank-16 LoRA on the academic LexGLUE UNFAIR-ToS benchmark (CLAUDETTE project: 50 consumer Terms of Service annotated sentence-by-sentence by legal scholars).
The Audited Benchmark (Test Split, Scored Once)
I evaluated both the base zero-shot model (Qwen/Qwen3.5-4B) and our Tinker fine-tuned LoRA checkpoint across a frozen test slice (all 172 unfair sentences + 250 fair sentences = 422 sentences) using standard multi-label precision, recall, and F1 metrics:
| Clause Category | Base Model Zero-Shot F1 | Tinker Fine-Tuned LoRA F1 | F1 Delta (Gain) | Fine-Tune Precision | Fine-Tune Recall | Published LexGLUE SOTA |
|---|---|---|---|---|---|---|
| Arbitration | 0.318 | 0.923 | +0.605 (+190%) | 1.000 | 0.857 | 0.880 |
| Limitation of liability | 0.667 | 0.873 | +0.206 (+30.9%) | 0.939 | 0.816 | 0.830 |
| Contract by using | 0.732 | 0.878 | +0.146 (+20.0%) | 1.000 | 0.783 | 0.750 |
| Choice of law | 0.813 | 0.960 | +0.147 (+18.1%) | 1.000 | 0.923 | 0.860 |
| Unilateral change | 0.660 | 0.763 | +0.103 (+15.6%) | 0.763 | 0.763 | 0.810 |
| Content removal | 0.765 | 0.833 | +0.068 (+8.9%) | 0.909 | 0.769 | 0.780 |
| Jurisdiction | 0.914 | 0.968 | +0.054 (+5.9%) | 1.000 | 0.938 | 0.840 |
| Unilateral termination | 0.786 | 0.794 | +0.008 (+1.0%) | 0.900 | 0.711 | 0.850 |
| Macro Average F1 | 0.707 | 0.874 | +0.167 (+23.7%) | 0.939 | 0.820 | 0.810 |
| Micro Average F1 | 0.695 | 0.848 | +0.153 (+22.0%) | 0.924 | 0.783 | 0.840 |
Key Findings from the Data
- The Arbitration Breakthrough (+190% F1): In the zero-shot base model, precision on Arbitration was a dismal 0.189—it flagged almost any mention of "disputes" or "claims" as forced arbitration. After fine-tuning with Tinker, precision reached 1.000 with an F1 of 0.923.
- Surpassing Published BERT Baselines: The fine-tuned 4B model achieved a Macro F1 of 0.874, exceeding the published LexGLUE benchmark scores for Legal-BERT (0.810) and RoBERTa-base (0.790).
-
Consistent Convergence: Training ran across 196 optimization steps on Thinking Machines' GPU infrastructure, with cross-entropy loss dropping smoothly from
0.4080at Step 1 to0.0075at Step 185.

Live Evaluation Dashboard (/eval) displaying the Macro F1 comparison (+23.7% gain), per-label performance breakdown, and honest limitation analysis.
{
"checkpoint_name": "didnt-read-unfair-tos-lora",
"checkpoint_path": "tinker://d4884b70-cb6f-51d0-b797-77f851c1b8cc:train:0/sampler_weights/didnt-read-unfair-tos-lora",
"base_model": "Qwen/Qwen3.5-4B",
"rank": 16,
"total_steps": 196,
"macro_f1_improvement": "+0.1673 (+23.7%)",
"micro_f1_improvement": "+0.1528 (+22.0%)"
}
How I Built It: Training & Runtime Architecture
1. Data Preparation (prep.py)
I extracted the official coastalcph/lex_glue unfair_tos split using Hugging Face datasets:
- Train split: 5,532 sentences (630 unfair, 4,902 fair).
- Validation split: 2,275 sentences.
- Test split: 1,607 sentences (frozen, never seen during training).
Because real-world ToS documents are heavily imbalanced (~90% fair), training naively produces models that guess "Fair" on everything. In prep.py, I created a balanced training split of 1,575 sentences (all 630 unfair sentences + 945 fair sentences), formatted into structured conversational turns.
2. Fine-Tuning with Tinker SDK (train.py)
Tinker eliminates the need for expensive local GPU rigs by running training operations directly on Thinking Machines' cloud:
import tinker
service_client = tinker.ServiceClient(display_name="didnt-read-sft")
training_client = await service_client.create_lora_training_client_async(
base_model="Qwen/Qwen3.5-4B", rank=16
)
# Training loop with pipelined forward-backward and Adam optimizer steps
for step, batch in enumerate(batches):
fwdbwd_future = await training_client.forward_backward_async(batch, "cross_entropy")
optim_future = await training_client.optim_step_async(tinker.AdamParams(learning_rate=2e-4))
fwdbwd_result = await fwdbwd_future.result_async()
await optim_future.result_async()
# Save adapter checkpoint directly for sampling
save_future = await training_client.save_weights_for_sampler_async(name="didnt-read-unfair-tos-lora")
checkpoint = await save_future.result_async()
3. Real-Time Inference & Extraction (app.py)
The web application is built with FastAPI, Jinja2, and Vanilla CSS:
-
trafilaturaextracts clean legal prose from live URLs, stripping headers, footers, and cookie banners. -
pysbd(Python Sentence Boundary Disambiguation) splits legal clauses cleanly without breaking on abbreviations like "e.g.", "i.e.", or "Section 4(a)". - An asynchronous semaphore (
asyncio.Semaphore(15)) batches clause requests to the TinkerSamplingClient, allowing 300+ sentences to be evaluated in ~25 seconds. - Every flagged clause maps to a curated, static legal definition from the CLAUDETTE corpus, guaranteeing zero hallucinated explanations.
Design Principles: Why Didn't Read Doesn't Look Like an AI Cockpit
Most AI hackathon projects look like science-fiction spaceship dashboards: dark backgrounds glowing with neon cyan buttons, bouncing particle animations, and fake telemetry widgets displaying meaningless numbers.
Contracts are read on paper. Lawyers mark up contracts with physical highlighters.
Following our strict UI standards, Didn't Read looks and feels like an editorial legal sheet:
-
Warm Paper Palette: Background set to warm archival paper (
#FBFAF7) with crisp white document sheets (#FFFFFF) and charcoal ink (#1C1B19). -
Typography Built for Reading: Document text is rendered in
Source Serif 4at 18px with 1.75 line-height, constrained to a comfortable reading measure of 68 characters. UI elements useInter, and benchmark numbers useJetBrains Mono. - Accessible Color Coding: Every clause category pairs a low-saturation pastel marker tint with a solid 2px underline in a darker tone, accompanied by high-contrast badges for full WCAG AA compliance.
-
Ergonomic Keyboard Navigation: Reading should be effortless. Tapping
JorKsmoothly scrolls and centers the next or previous flagged clause in the viewport.
The Lazy Payoff
Here is what Didn't Read produces on actual production Terms of Service agreements:
- Spotify Terms of Use: Extracted 276 sentences in 23.9 seconds. Immediately highlighted Section 24.1 (Mandatory Individual Arbitration and Class Action Waiver in New York) and Section 14 (Limitation of Liability for streaming interruptions).
- GitHub Terms of Service: Scanned in 28 seconds. Flagged Section E (Responsibility for User-Generated Content and Content Removal Rights) and Section L (Termination without cause upon 30 days notice).
- Discord Terms: Scanned in 19 seconds. Identified Section 16 (Settling Disputes and Informal Resolution Protocol before mandatory arbitration).
Instead of spending 35 minutes squinting at microscopic typography on a mobile screen, you get the five sentences that actually determine your legal recourse before you sign.

The Document Review Sheet (/review): Flagged clauses highlighted with pastel tints and underlines, sticky left verdict count, and interactive margin notes.
Why Open Innovation Matters
Consumer contracts represent one of the most asymmetrical relationships on the modern internet. Multi-billion dollar platforms employ teams of corporate attorneys to draft agreements that maximize corporate protection while systematically extinguishing consumer rights.
Proprietary models behind closed APIs cannot solve this problem long-term: they can change without notice, censor legal analysis under vague safety guidelines, or log private enterprise contracts for retraining.
Open-weight models like Qwen 4B, combined with democratized training infrastructure like Tinker, level the playing field. They enable developers, non-profits, and consumer protection advocates to fine-tune compact, high-precision models that can run anywhere, audit corporate agreements transparently, and publish auditable benchmark results.
Open innovation ensures that the tools protecting our digital rights are as accessible and accountable as the open web itself.
Wrapping Up
Building Didn't Read proved that fine-tuning small, specialized models on high-quality domain datasets dramatically outperforms prompting gigantic general models. In contract classification, domain specificity beats parameter count every single time.
I’d love to hear how you handle digital agreements in your own workflow. Have you ever actually read a full Terms of Service from start to finish? Which clauses bother you the most—mandatory arbitration, unilateral terms changes, or unlimited content licenses?
Give the live app a spin with any service you signed up for this week, or check out the weights in the Tinker Playground. The code is completely open-source on GitHub, and issues/contributions are always welcome!
來源:DEV Community · dev.to