Skip to main content

Applied AI and ML in production: from model output to trustworthy product

00:10:03:29

Demos make applied machine learning look deceptively simple: fire a prompt at an API endpoint, stream back some tokens, render markdown in a card. Production is where that illusion collapses.

In the wild, language models drift, hallucinate with calm authority, ignore system prompts when context windows get crowded, and return malformed payloads on Friday afternoon. If you treat a model as an oracle, your product will eventually mislead someone.

The only sustainable approach is to treat the model as an untrusted, probabilistic calculation engine operating inside a deterministic software harness. The application—not the model—must own data hygiene, hybrid retrieval, schema validation, confidence telemetry, and the fallback path when an answer fails verification.

text
Raw Ingestion (PDF / HTML / Markdown / Tables)
  │
  ▼
Structural parsing & text normalization
  │
  ▼
Semantic chunking with parent-document metadata
  │
  ├───────────────────────────────────┐
  ▼                                   ▼
Dense Index (Vector embeddings)    Sparse Index (BM25 inverted index)
  │                                   │
  └─────────────────┬─────────────────┘
                    ▼
          Hybrid Retrieval (RRF, k=60)
                    │
                    ▼
          Cross-Encoder Reranking (top-K joint self-attention)
                    │
                    ▼
          Grammar-Constrained Generation (Strict JSON Schema decoding)
                    │
                    ▼
          Citation Alignment & Factuality Verification
                    │
                    ▼
          UI Presentation (Confidence telemetry, inline sources, review controls)
                    │
                    ▼
          Continuous Evaluation Harness (RAG Triad & human edits)

Skipping steps in this pipeline produces flashy prototypes that quickly fail in real workloads. Retrieval, validation, and evaluation are not decorative polish; they are the boundary conditions that make probabilistic software dependable.

Enforce strict output contracts at the token level

Free-form text is a nightmare for client-side rendering. Asking a model to "reply in JSON" in the prompt text is a polite request that the model will periodically disregard whenever attention drifts or temperature fluctuates.

Production systems enforce schema contracts through grammar-constrained decoding (such as JSON Schema mode or context-free grammar token masking), then validate the payload immediately with a runtime validator like Zod.

ts
import { z } from 'zod';

export const EvidenceCitation = z.object({
  sourceId: z.string().describe('Unique identifier of the retrieved source document'),
  chunkId: z.string().describe('Deterministic hash of the specific text chunk'),
  verbatimQuote: z.string().min(12).max(400).describe('Direct, unedited quote from the source chunk'),
  relevanceScore: z.number().min(0).max(1),
});

export const SynthesizedInsight = z.object({
  headline: z.string().min(4).max(120),
  statement: z.string().min(10).max(800),
  citations: z.array(EvidenceCitation).min(1).max(6),
  limitations: z.array(z.string().max(200)).max(4),
  escalateToReviewer: z.boolean(),
});

export type SynthesizedInsight = z.infer<typeof SynthesizedInsight>;

A schema does not guarantee that the content is true. It guarantees that the response is structurally inspectable. If the model fails to find evidence, returns a claim that violates character bounds, or outputs an unsupported enum, your application layer catches the contract failure before it ever renders on a user screen.

Retrieval is a multi-stage ranking problem

Vector embeddings excel at fuzzy semantic concepts. They will effortlessly match "engine overheating" to "coolant temperature spike." But vector search frequently stumbles on the exact things enterprise software depends on: serial numbers, exact SKU codes, invoice identifiers, and boolean permission filters.

A single vector lookup is not a search architecture. Production retrieval requires a multi-stage funnel:

  1. Hybrid Retrieval: Run dense vector similarity and sparse BM25 lexical search in parallel.
  2. Reciprocal Rank Fusion (RRF): Merge the result lists based on rank position rather than raw scores, avoiding the trap of comparing incompatible cosine similarities and unbounded BM25 scores.
  3. Cross-Encoder Reranking: Pass the top candidates through a cross-encoder model that scores query-document token interactions simultaneously.
ts
type SearchHit = {
  id: string;
  sourceId: string;
  text: string;
  score: number;
  metadata: {
    pageNumber: number;
    sectionTitle: string;
    updatedAt: string;
  };
};

export async function retrieveEvidence(
  query: string,
  filter: { tenantId: string }
): Promise<SearchHit[]> {
  // Step 1: Parallel dense and sparse candidate generation
  const [vectorHits, keywordHits] = await Promise.all([
    vectorIndex.search(query, { filter, limit: 40 }),
    lexicalIndex.search(query, { filter, limit: 40 }),
  ]);

  // Step 2: Combine candidates without normalizing raw score distributions
  // RRF score = sum(1 / (k + rank)) where k = 60
  const candidates = reciprocalRankFusion(vectorHits, keywordHits, { k: 60 });

  // Step 3: Run expensive cross-attention reranking on top 25 candidates
  const reranked = await crossEncoder.rank(query, candidates.slice(0, 25));

  // Step 4: Return top-k highest quality chunks with preserved sentence boundaries
  return reranked.slice(0, 6);
}

Why the cross-encoder? Bi-encoders compress an entire document chunk into a single vector embedding ahead of time. When querying, the vector database computes a cosine dot product in milliseconds, but tokens never interact directly. A cross-encoder feeds both the query and candidate chunk into transformer self-attention layers together ([CLS] query [SEP] chunk [SEP]). It is computationally heavier, but applying it to the top 20 or 30 candidates gives you pinpoint accuracy without blowing your latency budget.

Chunking strategy determines your ceiling. Fixed 500-token slicing inevitably shears sentences in half. Use semantic chunking anchored to document structure—markdown headers, HTML section tags, or paragraph breaks—and prepend parent document breadcrumbs to every chunk so context survives isolation.

Generate against evidence, not general knowledge

When constructing prompt context, your system instructions should explicitly constrain the model's hypothesis space. The model must behave as an analytical summarizer over the provided payload, not a creative writer drawing on training weights.

ts
const systemInstruction = `
You are a precision research assistant operating strictly on provided retrieval context.

Rules of operation:
1. Ground every claim exclusively in the provided evidence blocks.
2. Each factual assertion must cite the exact chunkId and include a verbatim quote.
3. If the retrieved evidence is incomplete, ambiguous, or contradictory, state this explicitly in the "limitations" field.
4. Set "escalateToReviewer" to true if evidence confidence is marginal or sources disagree.
5. Never extrapolate beyond the clear statements in the context.
`;

export async function generateInsight(query: string, tenantId: string) {
  const evidence = await retrieveEvidence(query, { tenantId });

  if (evidence.length === 0) {
    return {
      type: 'insufficient_evidence' as const,
      message: 'No authoritative records matched your query within the access boundary.',
    };
  }

  const modelOutput = await inferenceClient.generate({
    system: systemInstruction,
    prompt: query,
    context: evidence.map(item => ({
      chunkId: item.id,
      documentTitle: item.metadata.sectionTitle,
      content: item.text,
    })),
    // Enforce constrained sampling against your schema
    responseSchema: SynthesizedInsight,
    temperature: 0.05,
  });

  const parsed = SynthesizedInsight.parse(modelOutput);
  return verifyCitations(parsed, evidence);
}

A temperature of 0.05 curbs token sampling entropy, but low temperature is not a magic shield against hallucination. If bad or contradictory chunks enter the prompt, the model will generate a confident, low-temperature error. Retrieval precision is what protects truth.

Never trust model citations without automated verification

Language models are notoriously prone to citation fabrication: they will generate an insightful paragraph, create an evidence tag, and append a quote that never existed in the source text.

Before storing or displaying a citation, verify it deterministically:

ts
export function verifyCitations(
  insight: SynthesizedInsight,
  retrievedChunks: SearchHit[]
): { insight: SynthesizedInsight; verified: boolean; discrepancies: string[] } {
  const chunkMap = new Map(retrievedChunks.map(c => [c.id, c.text]));
  const discrepancies: string[] = [];

  for (const citation of insight.citations) {
    const rawText = chunkMap.get(citation.chunkId);

    if (!rawText) {
      discrepancies.push(`Hallucinated chunk reference: ${citation.chunkId}`);
      continue;
    }

    // Normalize unicode, collapse whitespace, and strip decorative punctuation
    const cleanSource = normalizeText(rawText);
    const cleanQuote = normalizeText(citation.verbatimQuote);

    if (!cleanSource.includes(cleanQuote)) {
      // Fallback check: token overlap ratio to catch trivial whitespace drifts
      const similarity = calculateTokenOverlap(cleanQuote, cleanSource);
      if (similarity < 0.92) {
        discrepancies.push(`Quote mismatch in chunk ${citation.chunkId}: citation drifted from source.`);
      }
    }
  }

  const hasIssues = discrepancies.length > 0;

  return {
    insight: {
      ...insight,
      escalateToReviewer: insight.escalateToReviewer || hasIssues,
    },
    verified: !hasIssues,
    discrepancies,
  };
}

function normalizeText(str: string): string {
  return str.normalize('NFKC').toLowerCase().replace(/\s+/g, ' ').trim();
}

If citation verification fails, resist the urge to silently strip the bad citation and present the text anyway. Flag the answer as requiring review or degrade the response gracefully. Silent suppression trains users to rely on unchecked text.

Confidence is an engineering metric, not a model logit

A raw model logit or self-reported "confidence: 0.98" is not a statistical probability. Deep neural networks are notoriously overconfident, and language models will enthusiastically assign 99% confidence to complete fabrications.

True production confidence is a composite score synthesized from deterministic signals that your product can explain:

ts
type ReliabilityFactors = {
  retrieverAgreementRatio: number; // Did BM25 and vector search identify the same source documents?
  rerankerTopScore: number;        // Cross-encoder relevance confidence
  citationVerificationRate: number; // Ratio of citations verified verbatim against raw chunks
  semanticEntropy: number;         // Variance across multiple low-temp token rollouts
};

export function calculateConfidence(factors: ReliabilityFactors): number {
  if (factors.citationVerificationRate < 1.0) {
    // Unverified quotes immediately cap confidence
    return Math.min(0.4, factors.rerankerTopScore);
  }

  const score =
    factors.rerankerTopScore * 0.45 +
    factors.retrieverAgreementRatio * 0.25 +
    (1 - factors.semanticEntropy) * 0.3;

  return Math.max(0, Math.min(1, Math.round(score * 100) / 100));
}

When communicating this to the user, present the signals honestly. "Confidence 96%" is opaque; "Grounding: 3 corroborating engineering records, 100% citation match" gives the user actionable context to make an informed decision.

Decouple ingestion through idempotent background queues

Processing documents—parsing messy PDF tables, calculating token embeddings, indexing chunks into vector stores—is asynchronous, I/O-intensive work. Never tie ingestion to an open HTTP request.

Run ingestion through an idempotent task queue with deterministic hashing:

ts
import { createHash } from 'crypto';

export async function enqueueDocumentIngestion(doc: { id: string; buffer: Buffer; tenantId: string }) {
  // Hash content + ingestion pipeline version to prevent redundant compute
  const contentHash = createHash('sha256').update(doc.buffer).digest('hex');
  const pipelineVersion = 'v3.2';
  const idempotencyKey = `ingest:${doc.tenantId}:${doc.id}:${contentHash}:${pipelineVersion}`;

  const existingJob = await taskQueue.findJobByKey(idempotencyKey);
  if (existingJob) {
    return { jobId: existingJob.id, status: existingJob.status };
  }

  return taskQueue.add(
    'process-document',
    { documentId: doc.id, tenantId: doc.tenantId, contentHash },
    { idempotencyKey, attempts: 3, backoff: { type: 'exponential', delay: 2000 } }
  );
}

Idempotency guarantees that retries after worker crashes don't pollute your vector space with duplicate chunks. If a document hasn't changed, the pipeline exits early, saving GPU time and indexing cost.

Build an evaluation harness before tweaking system prompts

The most common trap in AI development is the prompt-tuning loop: you change three words in a prompt, test it on the query you just thought of, see that it works, and push to production—unaware that you broke four other edge cases.

Systematic evaluation requires a frozen golden test set covering your real query distribution:

  • Happy-path queries: Standard domain requests with clear answers.
  • Out-of-domain queries: Queries where the retriever should find nothing and the model must cleanly abstain.
  • Adversarial / injection queries: Attempts to bypass boundaries or leak underlying prompts.
  • Ambiguous queries: Prompts where sources present conflicting evidence.

Measure your pipeline using the RAG Triad:

  1. Context Relevance: Are the retrieved chunks strictly pertinent to the query, or is noise cluttering the context?
  2. Groundedness (Faithfulness): Can every single claim in the generated output be mathematically mapped to a retrieved chunk?
  3. Answer Relevance: Does the generated text directly resolve the user's intent without wandering?

Run this regression suite on every pull request that alters prompt templates, embedding models, chunk sizes, or reranker thresholds.

Design user interfaces that respect uncertainty

Production AI is fundamentally about collaboration between human expertise and automated synthesis. The frontend interface should make evidence easily auditable:

  • Render citations as interactive elements that highlight the exact corresponding text span in the original document preview.
  • Surface limitations and assumptions prominently, not tucked away behind collapsed disclosures.
  • Provide a clear correction mechanism that captures the user feedback along with the full trace: the user query, retrieved chunk IDs, prompt version, model identifier, and generated output.

The production reliability checklist

Before releasing an AI feature to users, make sure you can answer yes to these questions:

  1. Can every claim be traced to an immutable source chunk?
  2. Does the system cleanly abstain when retrieved evidence is absent or contradictory?
  3. Are vector searches backed by lexical fallback to catch exact identifiers and terms?
  4. Is prompt injection defended against at both input validation and schema decoding levels?
  5. Does the ingestion pipeline deduplicate content and operate idempotently?
  6. Do you track regression metrics (citation precision, faithfulness, abstention rate) against a fixed evaluation dataset?
  7. Can your support team inspect the exact prompt, retrieved context, and model parameters for any past user interaction?

Building reliable AI software is not about convincing yourself that a model will never make a mistake. It is about engineering a system where mistakes are constrained, verified, and safely corrected.