Integer to Roman: The Greedy Mapping That Always Shows Up in Interviews

From the first user query to the response stored in the database. A complete guide to layers, tools, request flow, learning roadmap, and real-world production implementation โ including the newly released Google ADK.
From the first user query to the response stored in the database. A complete guide to layers, tools, request flow, learning roadmap, and real-world production implementation โ including the newly released Google ADK.
What makes an AI system production-grade isn't the model quality โ it's the quality of the architecture surrounding the model. Models can be swapped anytime. Good architecture remains solid.
LLMs can only do one thing: text in โ text out. They cannot remember previous conversations, access databases, execute code, or make multi-step decisions. All the "sophistication" you see in production AI isn't because the model is smart on its own โ it's because there's an architecture around the model coordinating all those capabilities.
Understanding this architecture gives you the ability to: choose the right tool for the right problem, design AI systems that are maintainable, and debug when something doesn't work as expected.
A modern AI system consists of 8 layers working together. Click each layer below to see detailed explanations and their tools.
How users interact with the AI system. There are three paths: via API (developer), UI (end user), or CLI (developer tooling). All of them boil down to the same HTTP request โ only the presentation layer differs.
Developer consumes via SDK or HTTP POST to endpoint
Next.js, Flutter โ render streaming token to UI
Claude Code, Aider, custom script
All these paths ultimately send the same thing: a messages[] array to the LLM. The only difference is how the stream is handled and rendered to the user.
Abstraction layer above all LLM providers. One interface, swap models without changing code. Handles retry, fallback to other providers when down, centralized cost tracking, and load balancing across API keys.
Unified API to 100+ models. Drop-in OpenAI replacement.
GPT-4o, o1, embeddings. De facto standard.
Claude Sonnet, Haiku, Opus. Best for coding.
Gemini 2.0 Flash. Native for Google ADK.
Local LLM. Privacy, no API cost, offline.
# Without gateway โ different code per provider
client.chat.completions.create(model="gpt-4o", ...) # OpenAI
client.messages.create(model="claude-sonnet-4-6", ...) # Anthropic
# With LiteLLM โ one interface for all providers
litellm.completion(model="gpt-4o", messages=[...])
litellm.completion(model="claude-sonnet-4-6", messages=[...])
litellm.completion(model="gemini/gemini-2.0-flash", messages=[...])
# Automatic fallback when provider is down
litellm.completion(
model="gpt-4o",
fallbacks=["claude-sonnet-4-6", "gemini/gemini-2.0-flash"]
)The brain of the AI system. Coordinates tools, memory, RAG, and multi-step reasoning. This is the layer you need to study most. The fundamental pattern is the ReAct Loop: Reason โ Act โ Observe โ repeat until done.
Graph-based stateful agent. Checkpointing, human-in-the-loop.
Complete RAG pipeline. Data connectors, indexing, retrieval.
Native multi-agent from Google. A2A protocol.
Role-based multi-agent. Intuitive for prototyping.
Structured output + Pydantic. Auto-retry.
Prompts as learnable parameters.
Type-safe agent framework, minimal.
Standard tool protocol from Anthropic.
Ingest documents โ parse โ chunk โ embed โ store โ retrieve. The foundation of RAG. This pipeline runs offline (indexing) and online (retrieval per request). Quality at this step determines the quality of AI answers.
Parse PDF/DOCX with layout. Best currently available.
Multi-format: PDF, DOCX, HTML, Excel, Email.
Embeddings + reranking. Multilingual.
Production-grade reranking. Top-100 โ top-5.
Vector search in PostgreSQL. Pragmatic.
Dedicated vector DB. High-perf, self-hosted.
Tools the agent can call: search the web, read/write databases, execute code, access external APIs. The more high-quality tools available, the more powerful the agent. Tool calling isn't magic โ the LLM outputs a JSON descriptor, the orchestration layer executes the function.
Search API for agents. Returns structured data, not HTML.
LLM controls browser. Navigate, click, extract.
Text-to-SQL with RAG. Train on schema.
Open-source NL2SQL. Can self-host via Ollama.
Speech-to-text. Supports Indonesian language.
Browser automation. Can serve as agent tool.
# Tool calling: not magic, but a simple mechanism
# 1. Register tool with the agent
@tool
def check_inventory(product_id: str) -> dict:
"""Check product stock from database"""
return db.query("SELECT stock FROM products WHERE id = ?", product_id)
# 2. LLM OUTPUT (not direct execution):
# { "tool_call": { "name": "check_inventory", "args": {"product_id": "A"} } }
# 3. Orchestration layer executes the function, injects result into context
# 4. LLM is called again with the enriched contextLLMs are stateless by default โ each request stands alone. The memory layer gives the agent the ability to "remember" across sessions. There are 4 types: in-context (conversation history), external DB (Redis), episodic (what happened), semantic (what is known).
Long-term agent memory. Auto-extract facts from conversations.
Memory + knowledge graph. Entity & relationship extraction.
Self-editing memory agent. Manages own context window.
Session cache, queue. Fast, every AI app needs this.
Without observability, you're blind in production. Trace every LLM call, measure latency/cost/output quality, detect prompt regressions. LangFuse is mandatory for every AI feature going to production.
Trace + eval. Open-source, self-hosted. Mandatory.
RAG-specific eval. Faithfulness, relevancy, precision.
Unit tests for LLM output. CI/CD friendly.
Red-team prompts before deploying to production.
Serve your own model (vLLM) or use managed APIs. K8s for container orchestration. FastAPI as the API layer. LGTM stack for infra observability. LangFuse for AI-layer observability.
High-throughput inference. 10โ20ร faster than naive serving.
Async Python REST framework. Best for AI services.
Container orchestration. GPU node pool for model serving.
Loki, Grafana, Tempo, VictoriaMetrics.
Below is the complete journey of a single user query from start to response received. No step happens by magic โ everything is explicit and traceable.
Before diving into specific frameworks, these are 6 concepts you must thoroughly understand. All frameworks โ LangGraph, Google ADK, CrewAI โ are built on top of these concepts.
ReAct (Reason + Act) is the fundamental pattern used by all agentic frameworks. The agent loop repeats until the agent is confident the answer is sufficient.
Tool calling is not magic. The LLM outputs a JSON descriptor {"name": "check_stock", "args": {...}}. The orchestration layer executes the Python function, injects the result back into the context, then the LLM is called again. That's one ReAct cycle.
An LLM is a mathematical function: . No memory, no state. Every call, it reads the entire context from scratch. All of the LLM's "knowledge" in a single request lives within this context window.
| Component | Content | Token Budget | Source |
|---|---|---|---|
| System prompt | Identity, rules, output format, constraints | 200โ2,000 | hardcoded by developer |
| Retrieved docs | Relevant chunks from vector DB | 500โ4,000 | RAG pipeline |
| Memory | Facts from previous sessions | 100โ500 | Mem0 / Redis |
| Tool schema | JSON descriptors of all available tools | 200โ800 | tool registry |
| History | Previous conversation turns | 500โ8,000 | conversation state |
| User query | Current query | 50โ200 | user input |
| Total | โ | ~4kโ15k | โ |
LLMs pay more "attention" to the beginning and end of the context. Place important instructions in the system prompt (beginning) and the user query right before generation (end). Retrieved chunks in the middle are prone to being ignored if there are too many.
RAG consists of two separate phases: Indexing (offline, one-time) and Retrieval (online, per request).
INDEXING (Offline โ One-Time Run)
๐ฅ Load documents ๐ Parse (Docling) โ๏ธ Chunk (~512 token, 50 overlap) ๐งฎ Embed (OpenAI / Jina) ๐พ Store (pgvector / Qdrant)
RETRIEVAL (Online โ Every Request)
๐ฃ๏ธ User query ๐งฎ Embed query ๐ Similarity search (top-100) ๐ Rerank (Cohere) top-5 ๐ Inject into context
| Situation | Use RAG? | Reason |
|---|---|---|
| Q&A over internal documents (SOPs, catalogs) | Yes (Green) | Data not in LLM training set |
| Real-time data (prices, current stock) | Tool call (Yellow) | Need direct DB query, not RAG |
| General questions (how to use Python) | No (Gray) | LLM already knows |
| Compliance & audit trail | Yes (Green) | Must be able to cite specific sources |
| Inventory forecasting + live data | Hybrid (Blue) | RAG for historical docs, tool call for live data |
This is what enables LLMs to interact with the real world. Not magic โ it's an explicit, deterministic sequence from the orchestration side.
# Step 1: Define tool as a regular Python function
@tool
def check_inventory(product_id: str) -> dict:
"""Check product stock from database. Args: product_id (str)"""
return db.query("SELECT * FROM products WHERE id = %s", product_id)
# Step 2: LLM receives tool schema in context
# {"name": "check_inventory", "description": "Check stock...", "parameters": {...}}
# Step 3: LLM output (not execution!) โ JSON tool call
# {"type": "tool_call", "name": "check_inventory", "args": {"product_id": "A"}}
# Step 4: Orchestration layer executes the Python function
result = check_inventory(product_id="A") # {"stock": 142, "safety_stock": 200}
# Step 5: Inject result as tool_result into context
# {"type": "tool_result", "content": {"stock": 142, "safety_stock": 200}}
# Step 6: LLM is called AGAIN with complete context + tool result
# This time the LLM can generate the final answer based on real dataWithout structured output, the LLM is a text source. With structured output, the LLM becomes a callable function like an API โ its output can be directly processed by code without manual parsing.
import instructor
from pydantic import BaseModel, Field
from typing import Literal
# 1. Define the desired schema
class InventoryAlert(BaseModel):
product_id: str
current_stock: int = Field(ge=0)
status: Literal["ok", "low", "critical", "stockout"]
reorder_suggested: bool
suggested_quantity: int | None = None
reason: str = Field(max_length=200)
# 2. Instructor wraps the LLM client
client = instructor.from_anthropic(Anthropic())
# 3. Response is directly a Python object โ not a string
alert = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=500,
response_model=InventoryAlert, # inject schema into tool calling
messages=[{"role": "user", "content": context}]
)
# alert.status == "low" โ type-safe, use directly
if alert.reorder_suggested:
trigger_purchase_order(alert.product_id, alert.suggested_quantity)JSON mode only guarantees the output is valid JSON โ schema fields can still be missing or have wrong types. Instructor injects the Pydantic schema into the tool calling mechanism, validates the output, and auto-retries 3x if the output doesn't match the schema. That's what makes it reliable in production.
LangFuse is a dedicated AI observability layer โ analogous to Grafana for infra, but for LLM calls. Its data hierarchy: Trace (one end-to-end request) Span (unit of work) Generation (one LLM call) Score (evaluation).
# Simplest approach: LiteLLM integration (1 line)
import os
os.environ["LANGFUSE_SECRET_KEY"] = "sk-lf-..."
os.environ["LANGFUSE_PUBLIC_KEY"] = "pk-lf-..."
litellm.success_callback = ["langfuse"] # all LLM calls auto-traced
# Or via decorator for custom spans
from langfuse.decorators import observe
@observe(name="inventory-analysis")
def analyze_inventory(query: str) -> str:
chunks = retrieve_docs(query) # becomes a child span
answer = generate_answer(query, chunks) # becomes a child span
return answer
# What gets captured per generation:
# latency, input/output tokens, cost, faithfulness score, user feedbackOpen-source framework from Google to build, test, and deploy AI agents. Native Gemini, but supports other models. Designed for multi-agent production use cases with the A2A (Agent-to-Agent) protocol.
| Component | Function | LangGraph Analogy |
|---|---|---|
| Agent | Basic unit. LlmAgent (reasoning) or WorkflowAgent (deterministic) | Node in a graph |
| Tools | Python function callable by the agent. Auto-converts to function calling schema | Tool in LangGraph |
| Runner | Engine that executes the agent loop | graph.invoke() |
| Session | Per-user conversation state, persistent | Checkpointer |
| A2A Protocol | Agent-to-Agent communication. Sub-agent can be called as a tool | Subgraph |
| Deployment | Local dev, Vertex AI (managed), or self-hosted ADK server | LangGraph Cloud / self-host |
from google.adk.agents import LlmAgent
from google.adk.tools import tool
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
# 1. Define tool
@tool
def check_stock(product_id: str) -> dict:
"""Check product stock from inventory database"""
return db.get_stock(product_id)
# 2. Create agent
inventory_agent = LlmAgent(
name="inventory-analyst",
model="gemini-2.0-flash",
tools=[check_stock],
instruction="You are an inventory analyst. Help users understand stock conditions."
)
# 3. Run
session_service = InMemorySessionService()
runner = Runner(agent=inventory_agent, session_service=session_service)
response = runner.run(
user_id="user_123",
session_id="session_abc",
message="What is the current stock of product A?"
)| Framework | Strengths | Trade-offs | Choose when |
|---|---|---|---|
| Google ADK | Native multi-agent, Vertex AI, A2A protocol | Python only, small community | Need production-grade multi-agent on Google Cloud |
| LangGraph | Mature, graph state machine, checkpointing | Thick abstraction, LangChain dep | Complex stateful agent, human-in-the-loop |
| CrewAI | Intuitive, role-based, fast prototyping | Less flexible for complex flows | Quick prototyping, workshops |
| Pydantic AI | Type-safe, minimal, Pythonic | Limited ecosystem | Simple agent with strict type safety |
A good response format isn't about aesthetics โ it's about predictability, parsability, and a contract that doesn't change when the model is updated.
For data pipelines, background jobs, document analysis. Fully type-safe via Instructor + Pydantic.
class InventoryInsight(BaseModel):
product_id: str
action: Literal["reorder", "hold", "liquidate"]
urgency: Literal["immediate", "this_week", "this_month"]
confidence: float = Field(ge=0.0, le=1.0)
suggested_quantity: int | None = None
insight = client.messages.create(
response_model=InventoryInsight,
messages=[{"role": "user", "content": context}]
)
# insight.action == "reorder" โ use directly, no parsingFor chat interfaces. SSE (Server-Sent Events) via FastAPI. TTFT (Time to First Token) < 500ms for good perceived performance.
@app.post("/chat/stream")
async def chat_stream(request: ChatRequest):
async def generate():
with client.messages.stream(
model="claude-sonnet-4-6",
messages=request.messages
) as stream:
for text in stream.text_stream:
yield f"data: {json.dumps({'token': text})}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(generate(), media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})Metadata wrapper around every AI response. Important for observability, audit trail, and production debugging.
class AIResponse[T](BaseModel):
request_id: str # for tracing
trace_id: str | None # LangFuse trace ID
model: str
latency_ms: int
input_tokens: int
output_tokens: int
cost_usd: float
data: T # main payload (InventoryInsight, etc.)
confidence: float
fallback_used: bool = False
retry_count: int = 0
context_sources: list[str] = [] # chunk ID from RAG (audit trail)
generated_at: datetimeAI endpoints fail differently from regular APIs. Needs a special error taxonomy with a retryable flag so the frontend can handle it correctly.
class AIErrorCode(str, Enum):
VALIDATION_FAILED = "validation_failed" # Instructor retries exhausted
CONTEXT_TOO_LONG = "context_too_long" # exceeds context window
MODEL_UNAVAILABLE = "model_unavailable" # all providers down
RATE_LIMITED = "rate_limited" # quota exhausted
TIMEOUT = "timeout" # inference too long
class AIErrorResponse(BaseModel):
error_code: AIErrorCode
retryable: bool # True = rate_limited/timeout; False = validation
retry_after_seconds: int | None
request_id: strThe optimal learning sequence based on concept dependencies. No shortcuts โ each week builds the foundation for the next.
Understand how to call LLMs via API, model differences, token pricing. Master Instructor + Pydantic โ this is the foundation of all other AI tools. Without this, you won't be able to debug issues at the layers above.
Wrap LLM calls in a REST API using FastAPI. Add LiteLLM for multi-provider routing. Implement a streaming endpoint. Deploy via Docker. This skill goes straight onto your resume and can be used in real projects immediately.
Build a document Q&A system. Ingest PDF chunk embed store in pgvector retrieve rerank inject into prompt. LlamaIndex handles the orchestration pipeline. This is the most sought-after skill in AI job postings.
Build an agent that can reason, call tools (search, SQL, API), and loop until the task is complete. LangGraph is a graph state machine for complex agent workflows. Learn checkpointing and human-in-the-loop patterns.
Integrate LangFuse into all LLM calls. Track latency, token cost, and output quality per request. Set up RAGAS for RAG pipeline evaluation. Without this, you're blind in production. Self-hosted for free, great UI, integrates with K8s.
Learn Google ADK for multi-agent orchestration on top of Gemini. Concepts: sub-agent, tool registry, session management, A2A protocol. Great for your portfolio since it's the newest and increasingly sought after by employers using Google Cloud.
Combine all concepts into a single demo-ready project. Example: inventory Q&A agent โ user asks in natural language agent RAG + query DB + generate insight stream response. Deploy to K8s with LangFuse tracing active.
FastAPI + LiteLLM + LlamaIndex + LangGraph + pgvector + LangFuse + Instructor. This covers 90% of mid-level AI engineer job requirements. Master these 7 tools to production depth before expanding to others.
Use this before deploying any AI feature to production. Every missed item is a potential incident.
dict or AnyOptional / Nonege=0, le=1.0, etc.)Literal or Enum for fields with limited valuesCache-Control: no-cache, X-Accel-Buffering: noretryable flagrequest_id present in every response for tracingtrace_id linking to LangFuse in every responselatency_ms logged per requestcontext_sources (chunk IDs from RAG) logged for audit trail


