I'm Siddharth Notani — an Applied AI Engineer at Apple. I built this for the AI Deployment Engineer, Startups role at OpenAI in London.
I built a legal-AI contract-review agent, then broke it on purpose. Here's what failed, why, and the fix.
correct answers on amended contract terms, after one retrieval fix
questions per eval run, generated from a seeded synthetic corpus
real runs committed to the repo — every number here is generated from them
The setup
I reimplemented the workflow archetype behind legal-AI products — retrieval-augmented contract review, the category Harvey defined (OpenAI's Startup Fund backed their seed, and they co-developed a custom case-law model with OpenAI). My version is the honest month-three implementation a startup actually ships — BM25 retrieval over clause chunks, an LLM (called through the OpenAI SDK) that must cite the clause it relied on — not a strawman built to fail. I generated a synthetic contract corpus with three traps built in, wrote one programmatic grader per failure mode, and measured the failures a naive implementation of this archetype exhibits — the ones mature platforms like Harvey have had to engineer away. Every eval ran twice: once against a frontier model, once against the cost-tier model a startup watching its margins would reach for.
OpenAI SDK harness · exact model strings pinned in the committed results JSON · 204 questions per run · corpus seed 42 · graders are deterministic string checks, no LLM judges · both models score 98% on 65 control questions, so the failures below aren't noise · every number on this page is rendered at build time from committed results JSON
1 · Stale-amendment retrieval reproduced on both models
When a later amendment supersedes a clause, the agent answers from the original — retrieval surfaces both versions and nothing tells the model which one is law.
This failure survived the frontier model, because it isn't a model failure — it's a context-construction bug. The fix is in the index: apply amendments at ingestion so retrieval only ever sees the effective state of each contract.
- chunks = every clause of every document, indexed independently
+ for each amendment: parse "Clause N ... deleted and replaced",
+ substitute the amended text into the base clause, and index
+ only the effective clauses (agent/index.py, resolve_amendments)2 · Citation grounding did not reproduce — but the eval nearly lied to me
Every contract contains near-duplicate clause pairs differing in one material term — a general liability cap next to a data-protection one — to bait the agent into citing a clause that doesn't support its answer. My first grader scored the cost-tier model at 68% on these (49/72) — an apparent reproduction. It was the grader that was broken: the model cited real, correct clauses in a slightly different format ("[MSA-01 Clause 13] Confidentiality" instead of "MSA-01 Clause 13"), and my exact-match citation resolver scored every one as ungrounded. With citation matching made fair, the true rates are 66/72 (cost-tier) and 68/72 (frontier), and not one of the remaining misses is an ungrounded citation — they're retrieval recall, where the model correctly declined to answer. I'm reporting that instead of shipping the fake failure.
That's the deployment lesson: the eval is part of the system. A grader artifact can manufacture a failure mode as convincingly as the model can, and the first job is telling them apart — which is why every grade in the repo is a deterministic check you can re-run against the committed raw output (python -m evals regrade).
3 · False presence did not reproduce
I expected confident answers about clauses that don't exist, and made the trap as hard as I honestly could: questions presupposing a clause that's absent from the target contract but present, near-verbatim, in other contracts in the corpus — so retrieval returns a plausible clause from the wrong agreement. Both models declined to take the bait on 43 absent-clause questions each (frontier 42/43 — the single miss was an empty response, not a fabricated clause — cost-tier 43/43), without any abstention instruction in the prompt. I'm reporting that rather than weakening the baseline until it broke. Where I'd probe next: multi-hop questions, and corpora where the distractor shares the party names rather than just the clause language.
What I'd do in month one, embedded with a startup shipping this
First, sit with their actual traffic and find the five question shapes that matter commercially, because failure modes are properties of the corpus and the questions, not just the model. Then build exactly this harness around their workflow: seeded data that encodes their hard cases, one programmatic grader per failure mode, a smoke run cheap enough to run on every prompt change. Fix the worst mode the way I did here — in retrieval and context construction before touching the prompt, because that's where the durable bugs live. Leave the eval running in CI so the next model swap is a measured decision instead of a vibe. And write the one-pager that lets their team argue about numbers rather than anecdotes.
Clone the repo— synthetic corpus, agent, evals and raw results. Every number here traces to a committed run.·Page 2 →