I have been working on a pipeline that takes messy PDF documents, extracts structured data with an LLM, validates the result, and writes it to a database.
The first version was mostly about getting the extraction right. I give the model a document, describe the fields I need, get JSON back, validate it, and save it.
That worked, but after manually checking enough results, I started finding that the model could return perfectly valid data and still be wrong.
For example, a field could contain an allowed value but still be the wrong classification for what the document actually said. A document with several repeated sections could produce clean JSON while quietly missing one. A date could be valid but belong to the wrong part of the document.
None of those failures necessarily break schema validation. That's where I added another layer to the pipeline: an LLM judge.
Valid JSON was not enough
A lot of the early checks were easy to automate.
If a field is required, I can check whether it exists. If category must be one of a fixed set of values, I can validate the enum. Dates can be parsed, identifiers can be normalized, and calculations can be recomputed from their underlying entries instead of trusting whatever aggregate the model returned.
I also stopped relying on the model for things I could determine independently. For example, I can check whether a PDF has a usable text layer from the file itself rather than asking the model whether it thinks the document was scanned.
The same applies to numerical values. If a document contains several line items, I would rather recompute the total and weighted average in code than trust an aggregate simply because the model returned one.
Over time, the pipeline started looking less like:

and more like:

The problem was that deterministic validation could only answer questions with deterministic answers.
Consider something like this:
{
"role": "Regional Operations Director",
"category": "employee"
}
employee may be a perfectly valid value according to the schema, but whether employee is the correct classification for that role is a different question.
That requires understanding what the source document actually says.
Code was already doing some of the judging
This was one thing I found useful when thinking about LLM-as-a-judge.
I did not want another model deciding everything. Code was already acting as a judge for anything it could prove. It could answer questions like:
- Is this value allowed?
- Is this date parseable?
- Are all required fields present?
- Do these line items add up?
- Is this identifier valid?
- Have I already processed this document?
I trust normal code more for those questions because there is no reason to introduce model uncertainty where I already have an exact rule.
The LLM judge was for a narrower set of problems like:
- Does this classification actually match the role in the document?
- Were all repeated sections captured?
- Does this date belong to the event or to the document itself?
- Did the extraction infer something the source never states?
- Is an important piece of information present in the document but missing from the output?
Those are harder to express as if statements, so I kept the deterministic checks and added the judge after them.
Where the judge fits
The pipeline became:

The key point is that the second model isn't asked to extract the document again.
The first model is trying to answer: what data is in this document? While the judge answers: does this proposed data actually match the document?
That difference sounds small, but it changes the task quite a bit.
Instead of asking the judge something vague like: is this extraction correct?
I give it specific things to check. For example:
- Does the extracted name match the source?
- Does the extracted role match what the document says?
- Is the category consistent with that role?
- Were all relevant sections captured?
- Do the dates correspond to the correct records?
- Was anything added that cannot be supported by the document?
I found this much more useful than asking for a confidence score. A model saying it is 93% confident does not tell me much.
A model saying, "Category is unsupported because the source says 'Regional Operations Director' while the extraction classified the person as an employee" gives me something I can actually inspect.
I made the judge return evidence
I also wanted the judge output to be structured. Something like:
{
"verdict": "review",
"checks": [
{
"field": "category",
"status": "unsupported",
"extracted": "employee",
"suggested": "director",
"evidence": "Regional Operations Director",
"reason": "The extracted category does not match the role stated in the document."
}
]
}
The important field for me was evidence. If the judge disagrees with the extraction, I want it to show me what in the source caused that disagreement.
That makes the output easier to audit and makes human review much faster.
Instead of opening a document and checking every extracted field again, I can focus on the exact place where the two models disagree.
The judge was not allowed to silently fix the data
One tempting approach would have been to let the judge correct anything it thinks is wrong.
I did not want that. If the extractor says:
{
"category": "employee"
}
and the judge thinks it should be:
{
"category": "director"
}
I do not automatically overwrite the value. The disagreement becomes a review signal.
The judge is still an LLM, so introducing a second model does not suddenly give me ground truth. It can misunderstand the source too.
This is one reason I think LLM-as-a-judge works better as part of a larger validation system than as the final authority.
The application still decides what happens next.
Conceptually, it looks more like:
if (!deterministicChecksPassed) {
sendToReview();
} else if (judge.verdict === "review") {
sendToReview();
} else {
approve();
}
The model reports what it found and the application controls the workflow.
unclear was important too
The original pipeline already had a review path for cases where I did not want automation to make the final decision.
Scanned documents, invalid extractions, unusual classifications, and other edge cases could all be routed for review.
I kept the same idea for the judge.
A field can be:
supportedunsupportedunclear
I think unclear is one of the most important options. If a source is ambiguous, forcing the model to choose between correct and incorrect only hides that ambiguity behind another confident answer.
I would rather have ten records waiting for review than ten wrong records that passed because every stage of the pipeline was required to produce a definite answer.
I had already learned this from the earlier version of the system. Automation works better when uncertainty has somewhere to go.
The bigger change was separating extraction from trust
There is a difference between "we extracted this" and "we trust this enough to use".
Those should not be the same state.
Earlier versions of the pipeline were more focused on whether a document had been processed. Once a structured row existed, the system could treat the job as done, even if the row had been flagged for review.
That gets uncomfortable once you have multiple review layers.
I would rather make the state explicit:
pending
↓
extracted
↓
validated
↓
approved
with uncertain cases going to:
needs_review
Anything consuming the data should only see approved records.
That seems obvious in hindsight, but AI pipelines make it very easy to confuse "the model returned an answer" with "the answer is ready to use."
They are different events.
What I ended up with
I now think about the pipeline in three layers.
Normal code handles parts that can be calculated or validated exactly, while the LLM judge handles questions that require reading and interpretation. Humans step in when the source is ambiguous, the judge disagrees with the extraction, or the cost of getting the decision wrong is too high.

The useful part of adding an LLM judge was not having another model call in the pipeline.
It was being more deliberate about which decisions should belong to code, which ones require language understanding, and which ones I still don't want automation making on its own.




