Building Production AI Systems: A Practical Engineering Playbook
Shipping an AI demo is easy. Shipping an AI feature that stays correct, fast, and affordable under real traffic is a different sport. This playbook covers the patterns I use when building production systems — from retrieval to evaluation to ops.
Why most AI features fail in production
Demos hide the hard parts: messy data, latency budgets, cost spikes, prompt drift, and silent quality regressions. Treat the model as one component in a larger system, not the product itself.
[!NOTE] If you cannot explain how you will measure quality next week, you are not ready to ship.
The production stack at a glance
Layer | Job | Typical tools |
|---|---|---|
Interface | Collect user intent | Next.js, mobile clients |
Orchestration | Route, tool-call, retry | NestJS, workers, queues |
Retrieval | Ground answers in your data | Vector DB, hybrid search |
Model | Generate / classify / extract | Hosted LLMs or local |
Eval + Obs | Catch regressions early | Traces, golden sets, alerts |
Architecture overview
Core workflow
Ingest documents with clean chunking and metadata
Retrieve with hybrid search (keyword + vectors)
Generate with a tight system prompt and citations
Evaluate against a golden set before every release
Observe latency, cost, and failure modes in production
Checklist before you ship
Prompt version pinned in code/config
Retrieval returns citations the UI can show
Latency budget agreed with product (p95)
Cost alert threshold configured
Rollback path tested (feature flag)
Retrieval that does not hallucinate as much
Bad chunking creates confident nonsense. Prefer semantic chunks with overlap, keep source URLs/ids, and refuse to answer when retrieval confidence is low.
[!TIP] Log the retrieved chunks with every answer. When users complain, you debug retrieval first — not the model.
Example retrieval contract (TypeScript)
type RetrievedChunk = {
id: string
text: string
score: number
sourceUrl: string
}
type RagAnswer = {
answer: string
citations: RetrievedChunk[]
refused: boolean
}
async function answerQuestion(query: string): Promise<RagAnswer> {
const chunks = await retrieve(query, { topK: 6 })
if (chunks.length === 0 || chunks[0].score < 0.35) {
return {
answer: 'I do not have enough grounded context to answer that.',
citations: [],
refused: true,
}
}
return generateWithCitations(query, chunks)
}Prompting with guardrails
Keep prompts short, explicit, and versioned. Prefer structured outputs when the UI needs predictable fields.
[!WARNING] Never put secrets, raw PII, or untrusted HTML into the prompt without scrubbing and size limits.
A minimal system prompt pattern
You are a technical assistant for Daily AI Pulse.
Answer ONLY using the provided context.
If context is insufficient, say you do not know.
Cite sources using [#] markers that map to context ids.Evaluation is not optional
Ship a golden set of 30–100 questions with expected behaviors: must-cite, must-refuse, must-include key facts.
Eval type | Catches | Cadence |
|---|---|---|
Offline golden set | Prompt/retrieval regressions | Every PR |
Spot checks | Weird edge cases | Weekly |
Online feedback | Real user pain | Continuous |
[!SUCCESS] Even a small golden set beats “vibes-based” shipping. Automate it in CI.
Observability you will actually use
Track:
p50 / p95 latency for retrieve + generate
Token cost per request
Refusal rate and empty-retrieval rate
User thumbs down with linked traces
NestJS sketch for the orchestrator
@Controller('ai')
export class AiController {
constructor(private readonly rag: RagService) {}
@Post('ask')
async ask(@Body() body: { query: string }) {
const result = await this.rag.answer(body.query)
return {
answer: result.answer,
citations: result.citations.map((c) => ({
id: c.id,
url: c.sourceUrl,
score: c.score,
})),
refused: result.refused,
}
}
}Formatting showcase (editor features)
This section exists so you can see every TipTap feature after Import Markdown:
Text styles
This sentence has bold, italic, strikethrough, and inline code.
Chemical-ish demo: H2O uses subscript ideas; E = mc^2^ uses superscript ideas (adjust in toolbar after import if needed).
[!INFO] After import, select a phrase and try Highlight colors from the toolbar.
Lists
Unordered:
Ground the model
Measure quality
Cap cost and latency
Ordered:
Build retrieval
Add evaluations
Add observability
Ship behind a flag
Blockquote
“The model is not the product. The system around the model is the product.”
Horizontal rule
Comparison table
Approach | Speed to ship | Production readiness |
|---|---|---|
Prompt-only chatbot | Fast | Low |
RAG + citations | Medium | Medium–High |
RAG + eval + flags | Slower | High |
Common failure modes
[!DANGER] Shipping without a kill switch. Always wrap model calls in a feature flag you can flip in minutes.
Context window bloat — too many chunks, slow + expensive answers
Silent prompt edits — someone changes prod prompt without a version bump
No refusal path — model invents answers when retrieval is empty
One giant monolith prompt — impossible to test or reason about
What to do this week
Write 20 golden questions for your domain
Add citation UI on the frontend
Log retrieve + generate latency separately
Put the feature behind a flag
Document the rollback steps in one page
Closing
Production AI is systems engineering with a probabilistic core. Prefer boring infrastructure, measurable quality, and reversible releases. That is how Daily AI Pulse — and any serious product — stays trustworthy.
Written for builders shipping real AI features. Import this Markdown into the TipTap editor, polish highlights/alignment if you want, then publish.
Comments
0No comments yet. Be the first to share your thoughts!