AI document review: checking policies against a template
How I approach AI document review on a compliance platform: OCR uploaded policies, compare them to a template section by section, and explain each pass or fail.
By Hamza Ghouri
One of the features I have been building on a compliance and risk platform I work on is AI document review. An organisation uploads a policy document, and the system evaluates it against the template that policy is supposed to follow. The output is a pass or fail per requirement, and every result comes with the reason: which part of the document satisfied the requirement, or what was missing.
That last part matters more than the verdict. A compliance team will never trust a bare "failed". They need to see why, so they can fix the document or challenge the analysis. This post covers how I think about the pipeline: getting clean text out of the upload, structuring the comparison, and making the result explainable.
Start with the template as data
The template is the contract. Before any model sees a document, the template should exist as structured data: a list of required sections and, inside each, the specific requirements a policy must meet.
Each requirement gets an id, a short statement, and guidance on what counts as satisfying it. Turning a template document into this structure is worth doing carefully and reviewing by hand, because every analysis downstream depends on it. A vague requirement produces vague verdicts.
On a multi-tenant platform this also means deciding who owns the template. In this platform every organisation gets its own copy of the default compliance frameworks, which keeps tenants isolated from each other's changes. The same thinking applies to templates.
Getting clean text: OCR in two tiers
Uploaded policies arrive in every shape: born-digital PDFs, scanned printouts, Word exports with tables for roles and responsibilities. The review is only as good as the text it reads.
The approach on this platform runs OCR on a local engine first. Pages the local engine had to assemble from tables, which is where extraction tends to go wrong, are sent to a hosted engine. The idea is to spend on the stronger engine only for the pages that need it.
The general pattern I'd recommend:
- Run the cheap, local path first.
- Detect pages where that path is likely to be unreliable (tables, low-confidence regions, layout it had to reconstruct).
- Send only those pages to the stronger engine.
- Keep page numbers attached to every chunk of text so verdicts can cite where they came from.
Comparing requirement by requirement
The tempting design is one big prompt: here is the template, here is the document, tell me what is wrong. It is quick to demo and hard to trust. Long documents push the model to skim, and a single blob of output is hard to validate.
I'd structure it as one evaluation per requirement (or per small group of related requirements):
- Retrieve the parts of the document relevant to the requirement.
- Ask the model for a verdict, the evidence it relied on, and its reasoning.
- Validate the response against a schema before storing it.
A verdict schema I'd use looks like this:
import { z } from "zod";
export const RequirementResult = z.object({
requirementId: z.string(),
status: z.enum(["pass", "fail", "unclear"]),
reason: z.string().min(1),
evidence: z.array(
z.object({
page: z.number().int().positive(),
quote: z.string().max(500),
})
),
});
export type RequirementResult = z.infer<typeof RequirementResult>;
Two details in that shape do a lot of work. The unclear status gives the model a way to say "I cannot tell" instead of forcing a guess. And evidence requires quotes with page numbers, which lets the system check that each quote actually appears in the extracted text. A verdict citing text that does not exist in the document gets rejected. For more on that kind of validation, see how to validate LLM output before it reaches users.
Explaining pass and fail
The reason field is the product. A good reason:
- Names the requirement in plain language.
- Quotes or points to the section that satisfies it, with a page number.
- For a fail, says what is missing or contradicts the template, specifically enough that someone could fix it.
In the UI, I'd show the overall result with each requirement expandable to its reason and evidence, so reviewers can jump straight to the cited page. When they disagree, they can see exactly which reasoning to challenge.
Keep a human in the loop
Compliance decisions carry consequences, so the AI analysis is input to a person's decision. The same principle runs through other parts of this platform: an AI analyst reads KRI (key risk indicator) breaches and proposes reassessment conclusions, and a human accepts them. For policy review, the pattern is the same. The system drafts the analysis; a person signs off.
That framing also makes unclear results useful. They become a short list of items for a human to look at, instead of silent errors. I go deeper on this pattern in human-in-the-loop AI design.
Practical lessons
- Check permissions before the work starts. On this platform, the agent permission gate is checked before the database transaction opens, so an unauthorised request fails early and cheaply.
- Store raw inputs and outputs. Keep the extracted text, the prompt version, and the model's response for every requirement. When someone disputes a verdict, you need to reproduce it.
- Version the template. A policy that passed against version 3 might fail against version 4. Store which version each analysis used.
- Build an evaluation set early. Take a handful of real policies, have a domain expert mark each requirement, and re-run the pipeline against them whenever the prompt or model changes. Evaluating LLM features before shipping covers how.
The short version
AI document review works when the template is structured data, the text extraction is reliable, and each requirement is checked separately with a validated verdict, a reason and cited evidence. Run cheap local OCR first and send only the hard pages to a stronger engine. Explain every pass and fail, and keep a person making the final call.