Two-stage LLM pipeline: turning discovery notes into specs
How a two-stage LLM pipeline turns messy client discovery notes into an internal spec first, then into client, design and dev handoff documents.
By Hamza Ghouri
Every client project starts with a pile of raw discovery material: call notes, a transcript, a forwarded email thread, a feature list someone typed into a spreadsheet at midnight. Somebody has to turn that into documents a client can sign off on, a designer can work from and a developer can build from. I built REQ Agent, a small CLI, to do the first pass of that work with a two-stage LLM pipeline. The shape of that pipeline is the useful part, so that is what this post covers.
Why one prompt falls apart
The obvious first attempt is a single prompt. Paste in the notes and ask for a client proposal, a design brief and a dev handoff in one go. You get three documents. You also get three documents that quietly disagree with each other, because the model is inventing structure and writing prose at the same moment, and nothing forces the outputs to share a single source of truth.
One screen gets called "Dashboard" in the design brief and "Home" in the handoff. A feature mentioned once in the notes shows up in the client document and vanishes from the dev tasks. Each output reads fine alone. Together they are a mess that a human has to reconcile line by line.
The fix that works for me is to separate two jobs: deciding what the product is, and writing it up for different readers.
Stage one: raw notes to an internal spec
The first LLM call reads the raw discovery material and produces two things: an internal Spec-Driven Development document and a designer brief. The client never sees the internal spec. Its job is to be the source of truth for everything downstream: features, user roles, key flows, the data the product has to hold, open questions and acceptance criteria.
This mirrors how I run Spec-Driven Development with my team. The spec is the artifact everything else gets judged against. REQ Agent productizes that habit. The model does the tedious extraction, and the result lands in the same shape a person on my team would write by hand.
A few rules make stage one worth trusting:
- Structure first, prose later. Features, roles, flows and open questions come out as discrete items with stable names. Nice sentences can wait for stage two.
- Gaps stay visible. If the notes never mention payments, the spec says "payments: not discussed" under open questions. A model that fills gaps with plausible guesses produces a spec that looks complete and lies.
- One pass, two outputs. The designer brief comes out of the same call as the spec, so screens and flows use the same feature names from day one.
Stage two: spec to final documents
The second call takes the stage-one outputs as its only input. The raw notes stay out. From the internal spec and the designer brief, it writes the final client document, design document and developer handoff.
Leaving the raw notes out of this stage is the most important decision in the whole design. Raw notes are noisy: tangents, contradictions, a feature the client floated and then dropped ten minutes later. By the time the second call runs, those decisions are already made and written down. The writer stage works from a clean, decided input, and the three outputs agree with each other because they share one parent.
Here is the skeleton, using a small callModel() wrapper around whatever model API you use:
type Stage1 = { spec: string; designerBrief: string };
type FinalDocs = { client: string; design: string; devHandoff: string };
async function callModel(system: string, input: string): Promise<string> {
const res = await fetch(process.env.LLM_API_URL!, {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${process.env.LLM_API_KEY}`,
},
body: JSON.stringify({ system, input }),
});
if (!res.ok) throw new Error(`Model call failed with ${res.status}`);
const data = await res.json();
return data.output as string;
}
export async function runPipeline(rawNotes: string, prompts: { stage1: string; stage2: string }) {
const stage1: Stage1 = JSON.parse(await callModel(prompts.stage1, rawNotes));
// Stage two never sees rawNotes, so it can't resurrect ideas stage one dropped.
const finalDocs: FinalDocs = JSON.parse(
await callModel(prompts.stage2, JSON.stringify(stage1))
);
return { stage1, finalDocs };
}
In a real build, those JSON.parse calls get wrapped in schema validation with a retry. That pattern deserves its own post, and it is the difference between a demo and a tool you can run every day.
Validate before anything touches disk
The step I would never skip: REQ Agent validates the generated documents before it writes a single file. It runs three kinds of checks.
- Structure. Every required section exists and has content.
- Feature coverage. Every feature in the internal spec appears in the dev handoff. A feature that silently disappears between stages is the most common failure in a pipeline like this, and the hardest one for a tired reviewer to spot.
- Design-to-dev token consistency. The names used in the design document (screens, components, states) match the names in the dev handoff, so the designer and the developer are talking about the same thing.
If a check fails, nothing gets written. A half-right document on disk tends to get forwarded before anyone reads it closely. I go deeper on these checks in how to validate LLM output before it reaches users.
How many stages is enough
Every extra call adds latency, cost and another place for meaning to drift. Two stages map cleanly onto the two real jobs: understand the input, then write for each audience. I would add a third stage only when a step genuinely needs different input than the ones around it, for example a pricing estimate that needs a rate card the other stages never see.
The same reasoning applies outside document generation. If you are deciding whether to split a task across calls, the question to ask is the same: does this step need different input, or a checkpoint you can validate?
Where I'd start
Write the intermediate spec format by hand first, then teach stage one to produce it. Keep raw input out of the writer stage. Add validation before you add features. And keep a person in the loop: the pipeline produces a strong first draft, and someone who understands the client still decides what ships. If you want a pipeline like this built for your own workflow, that is the kind of work I take on through my services.