Skip to content
Beyond Chatbots: Building Production-Grade AI Contract Review in Java

Beyond Chatbots: Building Production-Grade AI Contract Review in Java

This article details a complete Java architecture for AI-powered contract review, covering PDF parsing, text normalization, structured LLM output with enums and records, chunking long documents, async task processing with retries, business rule validation, human-in-the-loop workflows, and feedback capture for continuous improvement.

LuTiao Programming
LuTiao Programming
LuTiao Programming

The author addresses a practical question: beyond chatbots, what real business value can Java projects gain from AI integration? The answer is embedding AI into existing workflows — specifically contract review — where AI handles semantic understanding while Java manages deterministic business logic, state machines, and reliability.

Don't Send the Entire PDF Directly to the Model

A naive implementation uploads the PDF, converts to Base64, and sends the whole file to the LLM. This fails in production due to variable contract lengths (5–80 pages), context limits, cost, latency, and inconsistent natural-language outputs (e.g., "Risk Level: High" vs. "Comprehensive judgment indicates high risk"). Java cannot reliably parse such free-form responses.

Instead, decompose the pipeline:

File Upload → Text Extraction → Text Preprocessing → AI Structured Analysis → Business Rule Enrichment → Save Results → Human Confirmation

AI only handles what it excels at: text understanding + information extraction + semantic risk judgment . State transitions remain in Java.

Convert Contracts to Clean Text First

Controller saves the file, creates a Contract record with status PARSING, then extracts text. For text-based PDFs, use PDFBox:

try (PDDocument document = Loader.loadPDF(fileBytes)) {
    PDFTextStripper stripper = new PDFTextStripper();
    String text = stripper.getText(document);
}

Scanned PDFs require OCR. The author recommends handling text PDFs and scanned PDFs separately because garbled extraction (e.g., "甲 方 应 在 合 同 签 订 后") degrades model performance and increases cost. Normalize raw text: collapse whitespace, strip headers/footers, remove duplicate page numbers, merge abnormal line breaks.

String normalizedText = contractTextNormalizer.normalize(rawText);

Define Java Return Types — Not Free-Form Strings

Define the result structure upfront using Java records and enums:

public record ContractAnalysis(
    String contractType,
    RiskLevel overallRisk,
    List<ClauseAnalysis> clauses,
    List<RiskItem> risks,
    String summary
) {}
public record ClauseAnalysis(ClauseType type, String originalText, String conclusion) {}
public record RiskItem(RiskType type, RiskLevel level, String clause, String reason, String suggestion) {}
public enum RiskLevel { LOW, MEDIUM, HIGH }
public enum ClauseType { PAYMENT, TERMINATION, AUTO_RENEWAL, LIABILITY, INTELLECTUAL_PROPERTY, CONFIDENTIALITY, DISPUTE_RESOLUTION, OTHER }

OpenAI Structured Outputs (preferred over legacy JSON mode) constrain the model to this schema, so the backend receives a typed ContractAnalysis object, not a narrative.

Prompts Must Define Task Boundaries Explicitly

A vague prompt ("Review this contract for risks") yields unpredictable output. A production prompt specifies:

Role: "Enterprise contract review assistant"

Exact extraction targets: payment terms, contract duration, auto-renewal, early termination, liability, liability cap, IP, confidentiality, dispute resolution

Allowed risk levels: LOW, MEDIUM, HIGH

No hallucination: mark missing clauses as NOT_FOUND Every risk must cite the original contract text

Citation enables human reviewers to verify instantly (e.g., "Liability has no cap" + original text: "Party B shall compensate all losses caused by breach") instead of hunting through 40 pages.

Don't Let AI Decide Deterministic Business Rules

If company policy says payment terms > 60 days → at least MEDIUM risk, encode that in Java:

if (paymentDays > 60) {
    risks.add(new RiskItem(RiskType.LONG_PAYMENT_TERM, RiskLevel.MEDIUM, paymentClause,
        "Payment term exceeds company standard", "Adjust to within 60 days"));
}

Similarly, auto-renewal → mandatory manual review. AI handles semantic classification ("Is this an auto-renewal clause? Does this liability clause have a cap?"); Java enforces deterministic rules. Mixing them causes instability — a prompt tweak could flip HIGH to MEDIUM overnight.

Chunk Long Contracts for Scalable Analysis

For contracts of dozens of pages, split into chunks (~3,000–5,000 chars) by headings, sections, page numbers, or length:

public record ContractChunk(int index, int pageStart, int pageEnd, String content) {}

Two-pass process: 1. Extraction pass — each chunk independently identifies candidate clauses. 2. Synthesis pass — aggregate candidates and run comprehensive risk analysis.

Benefits: smaller context per call, independent retry per chunk, and preserved pageStart / pageEnd / originalText for UI deep-linking ("Risk on page 17, click to view").

AI Calls Must Be Background Tasks

Synchronous HTTP (upload → wait 30s → response) fails due to parsing, chunking, and multi-round model latency. Return immediately: { "contractId": 10086, "status": "ANALYZING" } Persist a ContractAnalysisTask; a worker processes asynchronously:

public void process(ContractAnalysisTask task) {
    try {
        parse(task);
        analyze(task);
        applyRules(task);
        saveResult(task);
        markCompleted(task);
    } catch (Exception e) {
        markFailed(task, e);
    }
}

Status enum:

UPLOADED, PARSING, ANALYZING, REVIEW_REQUIRED, APPROVED, REJECTED, FAILED

. UI polls or uses SSE. Treating AI as a business task (not a simple RPC) enables handling timeouts, rate limits, parse failures, and manual compensation.

Granular Retry — Don't Re-Process Successful Chunks

If a 12-chunk contract fails on chunk 11, retry only that chunk. Persist per-chunk state:

contract_analysis_chunk: contract_id, chunk_index, status, result, retry_count

Aggregate only when all chunks are COMPLETED. Model calls must be designed as retryable tasks , not assumed to always succeed.

Bounded Retries with Backoff and Error Classification

No infinite while(true) retry. Limit to maxRetries = 3 with exponential backoff (2s, 10s, 30s). Classify failures:

MODEL_TIMEOUT, RATE_LIMIT, INVALID_OUTPUT, FILE_PARSE_FAILED, CONTENT_TOO_LARGE

. Each type dictates different recovery: auto-retry, switch model, re-parse, or escalate to human.

Validate AI Output in Java After Structured Parsing

Even with Structured Output, schema validity ≠ business validity. Example: overallRisk: HIGH but risks: []. Add a validator:

if (result.overallRisk() == RiskLevel.HIGH && result.risks().isEmpty()) {
    throw new InvalidAnalysisException("HIGH risk must contain at least one risk item");
}

Also verify: cited text exists in contract, page numbers are valid, risk types match clauses, counts within limits. Principle: Structured Output ensures format correctness; business validation ensures semantic correctness.

AI Produces Recommendations, Not Decisions

High-risk actions (reject contract, freeze customer, cancel payment, send legal notice) must not execute automatically. AI emits a ReviewRecommendation:

public record ReviewRecommendation(RiskLevel risk, RecommendedAction action, String reason) {}

System state stays REVIEW_REQUIRED. Legal clicks "Confirm Reject"; then Java executes contract.reject(currentUserId). Aligns with Spring AI 2.1.0-RC1's tool-call confirmation: distinguish actions that can auto-execute from those requiring human confirmation.

Gradual Automation by Action Risk Level

Classify actions: public enum ActionRisk { LOW, MEDIUM, HIGH } LOW (extract contract name, parties, dates) → auto-fill form.

MEDIUM → execute with undo capability.

HIGH (auto-pay, auto-reject, auto-delete, notify external parties) → mandatory human confirmation.

This tiered approach is more production-ready than a blanket "AI Agent executes everything."

Structured Review UI — Not a Chat Summary

Final page shows: - Top-level risk badge (HIGH) - Category table: Payment LOW, Auto-Renewal MEDIUM, Liability HIGH, IP HIGH, Dispute LOW - Drill-down: original text, AI judgment, suggestion - Actions: Accept Risk, Modify Level, Ignore, Add Comment AI becomes a business workbench component , not a chat widget.

Persist Human Corrections as Training Data

When legal changes AI's AUTO_RENEWAL HIGH to MEDIUM with rationale ("Long-term partner, auto-renewal acceptable"), store: AI original judgment, Human final judgment, Modification reason in contract_review_feedback . This yields a real-world feedback dataset to analyze: which risks AI misclassifies, which clauses humans frequently adjust, which prompt/model versions perform best. The most valuable data is what the model predicted vs. what experts corrected .

Version Every AI Execution

Record metadata per analysis: <code>public record AiExecutionMetadata(String model, String promptVersion, String schemaVersion, Instant executedAt) {}</code> When a user asks "Why was this HIGH last month?", you can trace the exact model, prompt version, and rule set. Code + Model + Prompt + Schema jointly determine behavior — all must be versioned. Overall Architecture Recap <code>User Upload → File Storage → Create Contract → Background Parse → Chunk → AI Clause Extraction → Structured Output → Java Business Rules → Risk Result → REVIEW_REQUIRED → Human Confirm → APPROVED/REJECTED</code> AI occupies only the middle steps. The surrounding scaffolding — Controller, Service, Task Table, State Machine, Database, Retry Logic, Permissions, Audit Logs, Human Approval — is familiar Java engineering. The insight: don't replace the system with AI; insert AI into the most labor-intensive step of an existing workflow. Java + AI Direction: Complete Work, Not Answer Questions Chat windows have no moat — any generic AI product does that. Embedding AI into contract review, support tickets, invoice OCR, resume screening, status reports, procurement, anomaly detection, and content moderation creates differentiated value. Users don't need to know AI is involved; they just see that uploading a contract now yields clauses, risk locations, citations, and recommendations in minutes instead of waiting 30 minutes for legal. AI shifts from "answering questions" to "completing work." The hardest code remains classic backend engineering: reliable task execution, timeout/retry handling, result structuring, action authorization, failure recovery, auditability. AI is merely a new, non-deterministic execution node. Making it production-grade is still a Java backend discipline.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Javaprompt engineeringAI IntegrationPDF parsingasync processinghuman-in-the-loopstructured outputcontract review
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.