← Back to Table of Contents

Chapter 12 β€” Deployment & Production

β€œA demo is not a product. The gap between β€˜it works on my laptop’ and β€˜it works for 10,000 users’ is where most agent projects die.”

The Production Gap

Your agent works great in development. Now ship it. Here’s what that actually requires:

Development vs. Production
Development
  • Single user (you)
  • Best-case inputs
  • Cost doesn't matter
  • Errors β†’ print() and fix
  • No latency requirements
Production
  • Thousands of concurrent users
  • Adversarial and edge-case inputs
  • Every token costs money
  • Errors β†’ graceful degradation + alerts
  • Sub-second response time expected

The Production Stack

Agent Production Architecture
πŸ‘€ User Interface Chat UI, API client, Slack bot, CLI
🌐 API Layer FastAPI / Express.js β€” authentication, rate limiting, request routing
πŸ€– Agent Runtime LangGraph / OpenAI SDK β€” the agent loop, tool execution, state management
πŸ”§ Tool Layer MCP servers, API integrations, databases, search
πŸ‘οΈ Observability LangSmith, Arize Phoenix, Langfuse β€” tracing, logging, evaluation
πŸ’Ύ Persistence PostgreSQL, Redis, vector DB β€” conversation history, agent state, memory

Serving Agents via API

Wrap your agent in a FastAPI server:

from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
import asyncio

app = FastAPI()

class ChatRequest(BaseModel):
    message: str
    thread_id: str = "default"

class ChatResponse(BaseModel):
    response: str
    tool_calls: list[dict] = []

@app.post("/chat", response_model=ChatResponse)
async def chat(req: ChatRequest):
    try:
        result = await agent.ainvoke(
            {"messages": [("user", req.message)]},
            config={"configurable": {"thread_id": req.thread_id}},
        )
        return ChatResponse(
            response=result["messages"][-1].content,
            tool_calls=[],
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/chat/stream")
async def chat_stream(req: ChatRequest):
    """Stream agent responses for real-time UX."""
    async def generate():
        async for event in agent.astream_events(
            {"messages": [("user", req.message)]},
            config={"configurable": {"thread_id": req.thread_id}},
            version="v2",
        ):
            if event["event"] == "on_chat_model_stream":
                chunk = event["data"]["chunk"]
                if chunk.content:
                    yield f"data: {chunk.content}\n\n"
        yield "data: [DONE]\n\n"
    
    return StreamingResponse(generate(), media_type="text/event-stream")

Observability: Seeing What Your Agent Does

In production, you must be able to trace every decision your agent makes. Key tools:

Tool What It Does Best For
LangSmith Full trace visualization, evaluation, datasets LangGraph users
Langfuse Open-source tracing, prompt management Self-hosted / privacy
Arize Phoenix Traces + evaluation + embeddings analysis ML teams
Braintrust Evaluation, logging, prompt playground Evaluation-focused

Adding Tracing (LangSmith)

# Just set environment variables β€” LangGraph auto-traces
export LANGCHAIN_TRACING_V2=true
export LANGCHAIN_API_KEY="ls__..."
export LANGCHAIN_PROJECT="my-research-agent"

# Every agent run now shows in LangSmith:
# - Full message trace (user β†’ LLM β†’ tool β†’ LLM β†’ ...)
# - Token counts and costs per step
# - Latency breakdown
# - Tool inputs/outputs

Custom Logging

import logging
import time

logger = logging.getLogger("agent")

class AgentLogger:
    def on_llm_start(self, prompt, **kwargs):
        logger.info(f"LLM call started | model={kwargs.get('model')}")
        self.start_time = time.time()
    
    def on_llm_end(self, response, **kwargs):
        duration = time.time() - self.start_time
        tokens = response.usage
        logger.info(
            f"LLM call completed | "
            f"duration={duration:.2f}s | "
            f"input_tokens={tokens.prompt_tokens} | "
            f"output_tokens={tokens.completion_tokens}"
        )
    
    def on_tool_start(self, tool_name, tool_input, **kwargs):
        logger.info(f"Tool call: {tool_name}({tool_input})")
    
    def on_tool_error(self, error, **kwargs):
        logger.error(f"Tool error: {error}")

Cost Management

Agents can be expensive. A complex task might make 10–20 LLM calls with large context windows.

Cost Estimation

Model Input (per 1M tokens) Output (per 1M tokens) Typical agent task
GPT-4o $2.50 $10.00 ~$0.05–$0.50
GPT-4o-mini $0.15 $0.60 ~$0.005–$0.05
Claude 3.5 Sonnet $3.00 $15.00 ~$0.06–$0.60
Llama 3.3 (local) Free Free Electricity only

Cost Control Strategies

# 1. Use cheaper models for simple routing/classification
router_llm = ChatOpenAI(model="gpt-4o-mini")  # Cheap for classification
worker_llm = ChatOpenAI(model="gpt-4o")        # Powerful for complex work

# 2. Set token budgets
MAX_TOKENS_PER_REQUEST = 50_000
MAX_COST_PER_REQUEST = 1.00  # dollars

# 3. Cache tool results
from functools import lru_cache

@lru_cache(maxsize=1000)
def cached_search(query: str) -> str:
    return tavily.search(query)

# 4. Summarize long tool outputs before injecting into context
def truncate_tool_output(output: str, max_chars: int = 2000) -> str:
    if len(output) > max_chars:
        return output[:max_chars] + "\n...[truncated]"
    return output

Error Handling

Agents fail in unique ways. Plan for all of them:

πŸ”Œ
API Errors
Rate limits, timeouts, model downtime. β†’ Retry with exponential backoff
πŸ”§
Tool Failures
External API returns error. β†’ Let the LLM know and try an alternative
πŸ”„
Infinite Loops
Agent repeats the same action. β†’ Detect and break with max iterations
🀯
Hallucination
Agent invents facts or fake tool names. β†’ Validate outputs, structured responses
import tenacity

@tenacity.retry(
    stop=tenacity.stop_after_attempt(3),
    wait=tenacity.wait_exponential(multiplier=1, min=1, max=10),
    retry=tenacity.retry_if_exception_type((RateLimitError, TimeoutError)),
)
async def call_llm_with_retry(messages, tools):
    return await client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        tools=tools,
        timeout=30,
    )

Deployment Options

Option Complexity Best For
FastAPI + Docker Medium Custom deployments, full control
LangServe Low LangGraph apps, quick deployment
Modal / Fly.io Low Serverless, auto-scaling
AWS Lambda + API Gateway Medium Event-driven, pay-per-use
Kubernetes High Enterprise, multi-region, high availability

Docker Deployment

FROM python:3.12-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# docker-compose.yml
services:
  agent:
    build: .
    ports:
      - "8000:8000"
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - TAVILY_API_KEY=${TAVILY_API_KEY}
    restart: unless-stopped

Safety in Production

Production Safety Checklist
πŸ”
Auth & Authorization
API keys, JWT tokens, role-based access. Never expose raw LLM access.
🚦
Rate Limiting
Per-user and global limits. Prevent abuse and cost spikes.
🧹
Input Sanitization
Detect prompt injection. Validate all user inputs. Block PII leaks.
πŸ“Š
Monitoring & Alerts
Track error rates, latency, cost per user. Alert on anomalies.

What’s Next

You know how to deploy. Now let’s explore the open source ecosystem β€” the projects, tools, and communities that make the agent world tick.

Next: Chapter 13 β€” Open Source Landscape β†’


← Previous: Chapter 11 β€” Agentic Design Patterns Β· Next: Chapter 13 β€” Open Source Landscape β†’

Last updated: April 2026