AI Best Practices for Production
Building an AI prototype is relatively easy. Building an AI application that behaves predictably under real production traffic is much harder. Production systems must handle model failures, hallucinations, changing data, long contexts, tool execution, security boundaries, traffic spikes, latency requirements, and costs that can grow directly with usage.
The most important production principle is to treat AI as one component of a larger software system rather than as the system itself. Language models are powerful at understanding and generating language, extracting information, reasoning over context, and choosing actions. Databases, APIs, queues, authorization systems, validators, and conventional application code remain better at maintaining authoritative state and enforcing deterministic rules.
This article brings together practical patterns for building reliable AI applications: use the right model for each task, keep authoritative data outside the model, control context, ground factual answers, validate model output, restrict tool permissions, bound autonomous workflows, design for failures, monitor quality and cost, and evaluate every important change before production.
Table of Contents
- Treat the Model as a Component
- Use the Simplest AI Architecture That Works
- Keep Authoritative Data Outside the Model
- Control Prompts and Context
- Validate Model Output
- Design Safe Tool Calling
- Bound AI Agents
- Design for Failures
- Optimize Latency and Cost
- Secure the Complete AI Pipeline
- Evaluate Before Deployment
- Production Design Example
- Common Production Mistakes
- Production Checklist
- Conclusion
Treat the Model as a Component
A common early AI architecture looks like this:
User → Prompt → LLM → Response
This can be enough for prototypes and simple generation tasks. Production applications usually need stronger boundaries:
User
↓
Application
↓
AI Orchestrator
↓
Context + Tools + Model
↓
Validation
↓
Application
↓
User
The application remains responsible for authentication, authorization, persistent state, business rules, validation, retries, rate limits, and observability.
The model handles tasks where probabilistic behavior is useful:
- understanding natural-language requests;
- classifying or extracting information;
- summarizing content;
- reasoning over supplied information;
- generating natural-language responses;
- selecting from explicitly available tools.
For example, a model can recognize that:
My package still hasn't arrived. Can you check it?
requires shipment information.
The model should not invent the shipment state. Application code can retrieve it from the authoritative service:
shipment = shipping_service.get(
tracking_number=tracking_number
)
response = model.generate(
question=user_message,
shipment=shipment,
)
This separation makes AI behavior easier to test, secure, replace, and operate.
The complete component design is covered in AI Application Architecture.
Use the Simplest AI Architecture That Works
Production AI systems can include prompt chains, RAG, vector databases, rerankers, agents, tool registries, memory systems, multiple models, and complex orchestration. None of these components should be added simply because they are associated with modern AI.
Start with the simplest architecture that satisfies the product requirement.
Can One Model Call Solve the Task?
│
├─ Yes → Use One Model Call
│
└─ No
↓
Is External Knowledge Required?
│
├─ Yes → Add Retrieval
│
└─ No
↓
Are External Actions Required?
│
├─ Yes → Add Tools
│
└─ No
↓
Are Steps Dynamic at Runtime?
│
├─ Yes → Consider an Agent
│
└─ No → Use a Fixed Workflow
Suppose an application always performs these steps:
Extract Invoice
↓
Validate Fields
↓
Look Up Customer
↓
Store Invoice
An autonomous agent does not provide much value because the workflow is already known. Conventional orchestration is easier to reason about and test.
An agent becomes more useful when the required sequence genuinely depends on intermediate results and cannot be predetermined easily.
Every additional AI component increases operational complexity. RAG introduces indexing and retrieval quality. Agents introduce variable execution paths. Multiple models introduce routing and compatibility concerns. Memory introduces lifecycle and privacy questions.
Complexity should solve a measured problem.
Keep Authoritative Data Outside the Model
Models should not be treated as databases for current or exact application facts.
Different information belongs in different systems:
| Information | Preferred Source |
|---|---|
| Account balance | Transactional database / API |
| Current inventory | Inventory service |
| Shipment status | Carrier or shipping service |
| Company documentation | Search / RAG |
| Conversation state | Application database |
| General language generation | LLM |
If an order ID is already known, an exact database or API lookup is normally better than semantic retrieval:
order = order_service.get(
order_id="ORD-18492"
)
RAG is appropriate when the application needs to find relevant information across large collections of documents.
Question
↓
Search
↓
Relevant Evidence
↓
LLM
↓
Grounded Answer
Fine-tuning is also not a replacement for frequently changing application data. Training product inventory or current policies into model parameters creates a synchronization problem.
The distinction between retrieval and parameter adaptation is explained in RAG vs Fine-Tuning.
Control Prompts and Context
Model context should be treated as a limited production resource. Larger contexts increase token consumption and can increase latency without necessarily improving quality.
Keep Context Relevant
A long-running conversation might contain 100,000 tokens of history, but only a small portion may matter for the current request.
Instead of:
System Prompt
+
Entire Conversation
+
Every Retrieved Document
+
Current Request
construct context deliberately:
System Instructions
+
Relevant Conversation Summary
+
Recent Messages
+
Relevant Retrieved Evidence
+
Current Request
A simple context builder could look like:
def build_context(conversation, request):
return {
"summary": conversation.summary,
"recent_messages": conversation.last(10),
"documents": retrieve_relevant_documents(
request
),
"request": request,
}
More context is not automatically better. Irrelevant information can distract the model and make important evidence harder to identify.
Token and context behavior is covered in AI Tokens and Context Windows.
Version Prompts
Prompts are production configuration and should be versioned accordingly.
Instead of silently changing a prompt in application code, identify it explicitly:
{
"prompt_name": "support-assistant",
"prompt_version": "v18",
"model": "model-v7"
}
This makes production behavior traceable.
If quality changes after deployment, traces can identify which prompt version generated each response.
Prompt changes should also run through evaluation before deployment. A small wording change can improve one class of requests while degrading another.
Practical prompt design is covered in AI Prompt Engineering.
Validate Model Output
Model output is untrusted input, even when it was generated using a carefully designed prompt.
For structured outputs, use schemas:
{
"intent": "refund",
"order_id": "ORD-18492",
"amount": 149.00
}
Schema validation can verify that fields exist and have correct types:
class RefundRequest:
intent: Literal["refund"]
order_id: str
amount: Decimal
But schema validity is only the first layer.
The application must still verify that:
Order Exists?
↓
Belongs to User?
↓
Payment Exists?
↓
Refund Allowed?
↓
Amount Valid?
↓
Already Refunded?
↓
Execute
A model can generate a perfectly valid JSON object containing a nonexistent order ID or unauthorized amount.
The same principle applies to generated HTML, SQL, URLs, file paths, commands, and code. Output must be validated according to where it will be used.
Hallucinations are impossible to eliminate completely, so production systems should be designed to prevent unsupported generation from silently becoming trusted application state. See AI Hallucinations.
Design Safe Tool Calling
Tools allow models to interact with real systems, which makes the tool boundary one of the most important parts of production AI architecture.
Prefer narrow capabilities:
get_order(order_id)
get_tracking(tracking_number)
create_return(order_id, reason)
over broad capabilities such as:
execute_sql(query)
execute_shell(command)
call_any_api(url, body)
A tool executor should validate every model-generated request:
def execute_tool(user, tool_call):
tool = registry.get(tool_call.name)
if tool is None:
raise UnknownToolError()
arguments = tool.validate(
tool_call.arguments
)
authorize(
user=user,
tool=tool,
arguments=arguments,
)
enforce_business_rules(
user=user,
tool=tool,
arguments=arguments,
)
return tool.execute(**arguments)
The authenticated identity should come from the application, not from the model.
For side effects such as payments, refunds, emails, deployments, or destructive operations, use stronger controls when appropriate:
Model Proposal
↓
Validation
↓
Authorization
↓
Risk Check
↓
Approval if Required
↓
Execution
Retries for side-effecting tools should also use idempotency where possible to prevent duplicate operations.
Tool execution patterns are covered in AI Tool Calling.
Bound AI Agents
An agent can transform one user request into multiple model calls and tool operations. Without limits, this creates unpredictable latency, cost, and risk.
Every production agent should have explicit execution budgets.
limits = {
"max_steps": 8,
"max_model_calls": 6,
"max_tool_calls": 8,
"max_total_tokens": 50_000,
"max_duration_seconds": 30,
}
The workflow should stop when a limit is reached rather than continuing indefinitely.
Tool access should also be task-specific.
A shipment-support task may need:
get_order
get_tracking
create_support_ticket
It probably does not need:
delete_customer
change_permissions
deploy_application
Reducing available tools decreases both decision complexity and security blast radius.
Long-running agents should usually execute asynchronously so they do not consume interactive request capacity while waiting on multiple external operations.
The decision to use an agent at all should be deliberate. If the required workflow is known beforehand, deterministic orchestration is usually simpler and more reliable.
Agent design is covered in AI Agents.
Design for Failures
AI applications are distributed systems. A request can depend on model providers, embedding services, vector databases, search infrastructure, external APIs, internal tools, and ordinary application databases.
Any dependency can fail.
Use explicit timeouts:
response = await model.generate(
prompt=prompt,
timeout=15,
)
Use bounded retries only for appropriate transient failures:
for attempt in range(3):
try:
return await model.generate(prompt)
except TemporaryModelError:
if attempt == 2:
raise
await asyncio.sleep(2 ** attempt)
Retries should use jitter in production to avoid many workers retrying simultaneously.
Fallback models can improve availability, but they are not automatically equivalent:
Primary Model Fails
↓
Compatible Fallback Available?
/ \
Yes No
↓ ↓
Fallback Model Graceful Failure
The fallback model should be evaluated with the same prompts and tools because different models can behave differently.
Applications should also degrade gracefully when optional components fail.
For example, if recommendation retrieval is unavailable, a support assistant may still be able to answer basic account questions. If authoritative order data is unavailable, however, the assistant should not invent an order status.
Graceful degradation means reducing functionality safely, not fabricating missing information.
Optimize Latency and Cost
AI cost and latency are often strongly influenced by model choice, token volume, and the number of model calls per task.
Measure them explicitly:
Cost per Task
≈
Input Tokens
+
Output Tokens
+
Model Calls
+
Embedding / Retrieval
+
Tool Infrastructure
Use smaller models for tasks where evaluation proves they are sufficient:
Classification → Small Model
Simple Extraction → Small Model
General Generation → General Model
Complex Reasoning → Stronger Model
A routing layer can select models according to task requirements.
Streaming can improve perceived latency for interactive generation because the user receives tokens before the complete response is ready.
Long-running work should move to asynchronous queues:
Client → API → Queue → Worker → AI Pipeline → Result
Caching can reduce repeated computation. Good candidates include embeddings for unchanged text, parsed documents, stable retrieval results, and carefully selected model outputs.
Caches must include the versions and security scope that affect the result. A response generated for one tenant should never be returned to another simply because the prompt text matches.
Scaling patterns, model routing, queues, concurrency control, and capacity planning are covered in Scaling AI Applications.
Secure the Complete AI Pipeline
AI security is not limited to filtering the user's prompt.
A production request can process several untrusted inputs:
User Input
Retrieved Documents
External Web Content
Tool Responses
Model Output
Generated Tool Arguments
Retrieved content can contain prompt injection. Model output can contain fabricated identifiers. Generated HTML can contain unsafe markup. Tool arguments can request unauthorized resources.
Security should therefore be enforced by deterministic boundaries.
For RAG, authorization should happen before documents enter model context:
documents = search(
query=query,
filters={
"tenant_id": user.tenant_id,
"classification": allowed_classification,
},
)
The model should not receive documents the authenticated user cannot access.
Secrets should remain outside prompts unless the model genuinely requires them. API keys, database credentials, signing keys, and access tokens should not be hidden inside system instructions and assumed to be safe.
Observability also needs security controls. Prompts and traces can contain personal data, internal documents, and tool results. Logging should apply redaction, access control, and retention policies.
Prompt injection, least privilege, RAG authorization, output security, and agent permissions are covered in AI Security.
Evaluate Before Deployment
AI changes should be evaluated like other production changes, but the tests need to measure behavior rather than only code execution.
A versioned evaluation dataset can contain:
{
"input": "Can I return an item after 45 days?",
"context": "Returns are accepted within 30 days.",
"expected": {
"eligible": false
}
}
Evaluation should include:
- common production requests;
- edge cases;
- ambiguous input;
- missing-information cases;
- previous production failures;
- tool failures;
- security-sensitive requests;
- long-context requests.
A candidate model or prompt should be compared against a baseline:
| Metric | Current | Candidate |
|---|---|---|
| Task success | 94.1% | 95.0% |
| Groundedness | 96.3% | 96.5% |
| Invalid tool calls | 0.7% | 0.4% |
| Average input tokens | 4,800 | 3,900 |
| p95 latency | 2.4 s | 2.1 s |
Production rollout can then use a canary:
Production Traffic
↓
Router
/ \
95% 5%
↓ ↓
Current Candidate
\ /
Metrics
If the candidate performs well, traffic can increase gradually. If quality regresses, rollback should be straightforward.
Production failures should become new regression cases after review. This creates a continuous feedback loop between real traffic and offline evaluation.
The complete approach is covered in AI Monitoring and Evaluation.
Production Design Example
Consider a production customer-support assistant that answers policy questions, checks orders, investigates shipments, and creates support tickets.
A practical architecture might be:
Client
↓
API Gateway
↓
Authentication + Rate Limits
↓
Assistant Service
↓
AI Orchestrator
├──────────┬──────────┐
↓ ↓ ↓
RAG Tools Model Router
↓ ↓ ↓
Docs Business Models
Index APIs
└──────────┼──────────┘
↓
Context Builder
↓
LLM
↓
Output Validation
↓
Response
Suppose the user asks:
My order ORD-18492 is late. Can I cancel it?
The system does not ask the model to invent the answer from general knowledge.
The authenticated user identity comes from the application. The order tool retrieves current state:
{
"order_id": "ORD-18492",
"customer_id": "C-481",
"status": "shipped",
"tracking_number": "TRK-8112"
}
The shipping tool retrieves current tracking:
{
"tracking_number": "TRK-8112",
"status": "delayed",
"estimated_delivery": null
}
RAG retrieves the relevant cancellation policy:
Orders cannot be canceled after shipment.
Customers may request a return after delivery.
The model receives only the information necessary to answer the question:
User Question
+
Verified Order State
+
Verified Shipment State
+
Relevant Cancellation Policy
It can then explain that the shipment is delayed but cancellation is unavailable because the order has already shipped.
If the user asks:
Create a support ticket.
the model can request:
{
"tool": "create_support_ticket",
"arguments": {
"order_id": "ORD-18492",
"reason": "delayed_shipment"
}
}
The tool executor validates the order, confirms ownership, checks the schema, and creates the ticket using an idempotency key.
The complete request trace might record:
{
"request_id": "req-9182",
"model": "model-v7",
"prompt_version": "support-v18",
"retrieval_version": "rag-v11",
"model_calls": 2,
"tool_calls": 3,
"input_tokens": 4280,
"output_tokens": 480,
"total_latency_ms": 2310
}
This architecture gives the model enough flexibility to understand and explain the request while keeping identity, permissions, current data, business rules, and side effects under deterministic application control.
Common Production Mistakes
Building an agent when a fixed workflow is enough adds unpredictable behavior, latency, and cost without solving a real problem.
Using the model as the source of truth leads to stale or fabricated facts. Current application data should come from authoritative systems.
Sending all available context increases token consumption and can reduce answer quality. Context should be selected deliberately.
Trusting valid JSON confuses schema correctness with business correctness. Generated values still require authorization and domain validation.
Putting business rules only in prompts makes critical behavior probabilistic. Important invariants belong in application code.
Giving agents broad tools increases security blast radius. Use least privilege and expose only capabilities needed for the current task.
Retrying every failure can duplicate side effects or amplify overload. Retries should be bounded and appropriate for the operation.
Using the largest model everywhere wastes cost and capacity when smaller models perform simpler tasks sufficiently well.
Monitoring only latency and HTTP errors misses the most important AI failures: incorrect, unsupported, or ineffective responses.
Changing prompts or models without evaluation can silently introduce regressions even when the new version appears better in a few manual tests.
Production Checklist
Before deploying an AI workflow, verify that the architecture provides clear answers to the following questions:
- Is the model being used only where probabilistic behavior provides value?
- Do current and exact facts come from authoritative systems?
- Is the context limited to relevant information?
- Are prompts, models, retrieval settings, and tool schemas versioned?
- Are structured outputs validated before use?
- Are authorization and business rules enforced outside the model?
- Do tools follow least privilege?
- Are side-effecting operations protected against duplicate execution?
- Do agents have limits for steps, tool calls, tokens, duration, and cost?
- Do model and external-service calls have explicit timeouts?
- Are retries bounded and limited to appropriate failures?
- Can long-running work move to asynchronous processing?
- Are token usage, latency, model calls, tool calls, and cost observable?
- Are AI quality metrics measured in addition to infrastructure metrics?
- Are prompts, traces, retrieved documents, and model outputs protected as potentially sensitive data?
- Is there a representative evaluation dataset?
- Can model and prompt changes be canaried and rolled back?
Not every AI application needs every technique. A simple summarization service may need only a model call, request validation, timeouts, basic evaluation, and observability. A multi-tenant agent that accesses business systems requires much stronger authorization, tool isolation, execution limits, and security testing.
The goal is not maximum architectural complexity. The goal is enough control to make the application's behavior predictable for its actual risk and workload.
Conclusion
Production AI engineering is less about connecting an application to a model and more about designing the systems around that model. Reliable applications combine probabilistic AI capabilities with deterministic software boundaries.
Authoritative data should remain in databases and APIs. Unstructured knowledge can be supplied through retrieval. Model outputs should be validated. Tool calls should pass through authorization and business rules. Agents should operate with bounded steps, permissions, time, tokens, and cost.
AI workloads also need traditional distributed-systems engineering: timeouts, bounded retries, idempotency, queues, caching, rate limits, graceful degradation, horizontal scaling, and observability.
Quality must be measured explicitly. Evaluation datasets, production traces, task-success metrics, groundedness checks, canary deployments, and regression tests provide the feedback needed to improve an AI system safely over time.
Security should assume that user input can be adversarial, retrieved content can contain malicious instructions, and model output can be wrong. Critical permissions and business invariants should never depend entirely on the model behaving correctly.
The best production AI architecture is not the one with the most models, agents, or AI infrastructure. It is the simplest architecture that delivers the required quality while keeping data, permissions, failures, latency, cost, and model uncertainty under explicit control.
Comments (0)