--- name: ai-engineer description: Use when building production LLM applications — designing RAG pipelines, choosing vector databases, implementing agent orchestration, optimizing cost, or adding AI safety guardrails. --- # AI Engineering Patterns for building production-grade LLM applications, RAG systems, and intelligent agents. ## When to Activate - Building or improving RAG systems, LLM features, or AI agent workflows - Selecting models, vector databases, or embedding strategies - Optimizing retrieval quality, latency, or inference cost - Implementing AI safety guardrails, content moderation, or PII handling - Integrating multimodal inputs (images, audio, documents) into AI pipelines - Designing multi-agent coordination or agentic tool-use loops - Setting up AI observability, evaluation, or A/B testing ## Model Selection | Model | Best For | Relative Cost | |-------|----------|---------------| | `claude-opus-4-6` | Complex reasoning, architecture, research | High | | `claude-sonnet-4-6` | Balanced coding, most development tasks | Medium | | `claude-haiku-4-5` | Classification, extraction, high-volume tasks | Low | | GPT-4o | OpenAI tool ecosystem, function calling | Medium-High | | Llama 3.1 70B (local) | Air-gapped, cost-sensitive, no PII risk | None (infra cost) | Default to Sonnet-class models for development. Use Haiku/mini variants for high-throughput steps. Reserve Opus/GPT-4o for reasoning-heavy tasks. ## RAG Architecture ### Chunking Strategy ```python # BAD: Fixed-size splits break semantic units text_splitter = CharacterTextSplitter(chunk_size=500) # GOOD: Semantic chunking preserves context from langchain.text_splitter import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=512, chunk_overlap=64, separators=["\n\n", "\n", ". ", " ", ""] ) ``` | Strategy | Use When | |----------|----------| | Recursive character | General prose, code | | Semantic (sentence-transformers) | Mixed-length documents | | Document-structure aware | PDFs, HTML, Markdown | | Sliding window | Dense technical content | ### Vector Database Selection | DB | Hosted | Self-Hosted | Hybrid Search | Notes | |----|--------|-------------|---------------|-------| | Pinecone | Yes | No | Yes | Managed, serverless | | Qdrant | Yes | Yes | Yes | Rust core, fast filtering | | Weaviate | Yes | Yes | Yes | GraphQL API | | pgvector | Via Supabase | Yes | With tsvector | Great if already on Postgres | | Chroma | No | Yes | No | Local dev only | ### Hybrid Search (Vector + Keyword) ```python from qdrant_client import QdrantClient from qdrant_client.models import SparseVector, NamedSparseVector # Dense vector (semantic) + sparse vector (BM25) results = client.query_points( collection_name="docs", prefetch=[ models.Prefetch(query=dense_embedding, using="dense", limit=20), models.Prefetch(query=SparseVector(indices=bm25_indices, values=bm25_values), using="sparse", limit=20), ], query=models.FusionQuery(fusion=models.Fusion.RRF), # Reciprocal Rank Fusion limit=10, ) ``` ### Reranking ```python # BAD: Return top-k by vector similarity alone results = index.query(vector=embedding, top_k=5) # GOOD: Over-fetch then rerank for precision candidates = index.query(vector=embedding, top_k=20) import cohere co = cohere.Client() reranked = co.rerank( model="rerank-english-v3.0", query=user_query, documents=[r.metadata["text"] for r in candidates.matches], top_n=5, ) ``` ### RAG Pipeline Patterns | Pattern | What It Solves | |---------|---------------| | HyDE (Hypothetical Document Embeddings) | Query/document embedding mismatch | | RAG-Fusion | Single query too narrow — runs multiple query variants | | Self-RAG | Model decides when retrieval is needed | | GraphRAG | Multi-hop reasoning across connected entities | | Contextual compression | Retrieved chunks too noisy; extract relevant spans only | ```python # HyDE: generate a hypothetical answer, embed it, retrieve similar docs hyde_prompt = f"Write a paragraph that would answer: {query}" hypothetical_doc = llm.invoke(hyde_prompt) hyde_embedding = embedder.embed(hypothetical_doc) results = vector_store.similarity_search_by_vector(hyde_embedding) ``` ## Agent Orchestration ### Agentic Loop (LangGraph) ```python from langgraph.graph import StateGraph, END from typing import TypedDict, Annotated import operator class AgentState(TypedDict): messages: Annotated[list, operator.add] tool_calls_remaining: int def agent_node(state: AgentState): response = llm.invoke(state["messages"]) return {"messages": [response]} def tool_node(state: AgentState): last_message = state["messages"][-1] results = execute_tools(last_message.tool_calls) return {"messages": results, "tool_calls_remaining": state["tool_calls_remaining"] - 1} def should_continue(state: AgentState): last = state["messages"][-1] if not last.tool_calls or state["tool_calls_remaining"] <= 0: return END return "tools" graph = StateGraph(AgentState) graph.add_node("agent", agent_node) graph.add_node("tools", tool_node) graph.add_edge("tools", "agent") graph.add_conditional_edges("agent", should_continue) ``` ### Agent Memory Patterns | Type | Storage | Use For | |------|---------|---------| | Short-term | In-memory messages list | Current conversation context | | Long-term | Vector store + summary | Facts, preferences across sessions | | Episodic | Structured DB | Past task outcomes | | Procedural | Prompt / tool definitions | Skills, workflows | ```python # Summarize + compress conversation memory from langchain.memory import ConversationSummaryBufferMemory memory = ConversationSummaryBufferMemory( llm=llm, max_token_limit=1000, # summarize once buffer exceeds limit return_messages=True, ) ``` ### Multi-Agent Pattern (CrewAI) ```python from crewai import Agent, Task, Crew researcher = Agent( role="Senior Researcher", goal="Find accurate information", tools=[search_tool, browse_tool], llm=llm, ) writer = Agent( role="Technical Writer", goal="Synthesize research into clear prose", llm=llm, ) research_task = Task(description="Research {topic}", agent=researcher) write_task = Task(description="Write a report based on research", agent=writer) crew = Crew(agents=[researcher, writer], tasks=[research_task, write_task]) result = crew.kickoff(inputs={"topic": "vector databases"}) ``` ## Prompt Engineering ### Structured Output ```python # TypeScript: Zod schema → structured output import Anthropic from "@anthropic-ai/sdk"; import { z } from "zod"; import zodToJsonSchema from "zod-to-json-schema"; const ExtractedData = z.object({ entities: z.array(z.object({ name: z.string(), type: z.string() })), summary: z.string(), }); const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, tools: [{ name: "extract_data", description: "Extract structured data from text", input_schema: zodToJsonSchema(ExtractedData), }], tool_choice: { type: "tool", name: "extract_data" }, messages: [{ role: "user", content: `Extract from: ${text}` }], }); ``` ### Prompting Techniques | Technique | When to Use | |-----------|-------------| | Chain-of-thought (`Think step by step`) | Multi-step reasoning, math, logic | | Tree-of-thoughts | Exploring multiple solution paths | | Self-consistency | Sample multiple outputs, majority vote | | Few-shot examples | Consistent formatting, specialized tasks | | Constitutional self-critique | Safety checks, tone alignment | ```python # BAD: Vague instruction "Summarize this document" # GOOD: Structured, constrained prompt """Summarize the following document in exactly 3 bullet points. Each bullet must start with a verb and be under 20 words. Focus only on actionable findings. Document: {document}""" ``` ## Production Patterns ### Semantic Caching ```python from semantic_router.encoders import OpenAIEncoder from semantic_router.layer import RouteLayer # Cache responses for semantically similar queries cache = RedisSemanticCache( redis_url="redis://localhost:6379", embedding=OpenAIEmbeddings(), score_threshold=0.95, # cosine similarity threshold ) @cache def get_answer(query: str) -> str: return llm.invoke(query) ``` ### Streaming with FastAPI ```python from fastapi import FastAPI from fastapi.responses import StreamingResponse import anthropic app = FastAPI() client = anthropic.Anthropic() @app.post("/chat") async def chat(query: str): async def generate(): with client.messages.stream( model="claude-sonnet-4-6", max_tokens=1024, messages=[{"role": "user", "content": query}], ) as stream: for text in stream.text_stream: yield f"data: {text}\n\n" yield "data: [DONE]\n\n" return StreamingResponse(generate(), media_type="text/event-stream") ``` ### Cost Controls ```python # Estimate cost before execution def estimate_cost(prompt: str, model: str = "claude-sonnet-4-6") -> float: input_tokens = len(prompt) // 4 # rough estimate # Sonnet 4.6: $3/M input, $15/M output return (input_tokens / 1_000_000) * 3.0 # Hard cap: reject if estimated cost exceeds threshold if estimate_cost(prompt) > 0.10: raise ValueError("Prompt too large for single request — chunk it") ``` ## AI Safety & Guardrails ### Prompt Injection Detection ```python INJECTION_PATTERNS = [ r"ignore (previous|above|all) instructions", r"you are now", r"disregard your", r"new persona", r"act as (if you are|a)?", ] def detect_injection(user_input: str) -> bool: import re return any(re.search(p, user_input, re.IGNORECASE) for p in INJECTION_PATTERNS) # Wrap all user inputs if detect_injection(user_message): return {"error": "Input rejected"} ``` ### PII Redaction ```python import re PII_PATTERNS = { "email": r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "ssn": r"\b\d{3}-\d{2}-\d{4}\b", "credit_card": r"\b(?:\d{4}[- ]?){3}\d{4}\b", "phone": r"\b\+?1?\s?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}\b", } def redact_pii(text: str) -> str: for label, pattern in PII_PATTERNS.items(): text = re.sub(pattern, f"[{label.upper()}_REDACTED]", text) return text ``` ### Content Moderation ```python # Use OpenAI Moderation API as a pre-filter (free) import openai def is_safe(text: str) -> bool: result = openai.moderations.create(input=text) return not result.results[0].flagged # Gate all user inputs if not is_safe(user_message): return {"error": "Message violates content policy"} ``` ## AI Observability ### LangSmith Tracing ```python import os os.environ["LANGCHAIN_TRACING_V2"] = "true" os.environ["LANGCHAIN_API_KEY"] = "your-key" os.environ["LANGCHAIN_PROJECT"] = "my-rag-app" # All LangChain calls now auto-traced — no code changes needed chain = prompt | llm | output_parser result = chain.invoke({"query": user_query}) ``` ### Custom Metrics (Prometheus) ```python from prometheus_client import Counter, Histogram llm_requests = Counter("llm_requests_total", "Total LLM API calls", ["model", "status"]) llm_latency = Histogram("llm_latency_seconds", "LLM response latency", ["model"]) retrieval_score = Histogram("retrieval_relevance_score", "RAG retrieval scores") with llm_latency.labels(model="claude-sonnet-4-6").time(): response = client.messages.create(...) llm_requests.labels(model="claude-sonnet-4-6", status="success").inc() ``` ### RAG Evaluation | Metric | Tool | Measures | |--------|------|---------| | Context Precision | RAGAS | Are retrieved chunks relevant? | | Context Recall | RAGAS | Did retrieval miss needed chunks? | | Answer Faithfulness | RAGAS | Does answer match retrieved context? | | Answer Relevance | RAGAS | Does answer address the question? | ```python from ragas import evaluate from ragas.metrics import faithfulness, answer_relevancy, context_precision dataset = Dataset.from_dict({ "question": questions, "answer": answers, "contexts": retrieved_contexts, "ground_truth": expected_answers, }) scores = evaluate(dataset, metrics=[faithfulness, answer_relevancy, context_precision]) ``` ## Red Flags - **RAG without evaluating retrieval quality** — high embedding similarity doesn't mean the retrieved chunks answer the question; evaluate retrieval precision/recall separately from generation quality - **Chunk size chosen arbitrarily** — too large floods context with irrelevant content, too small loses coherence; benchmark chunk sizes against real queries before committing - **No evals before deploying prompt changes** — changing a prompt in production without a regression suite is the AI equivalent of deploying untested code; build evals first - **Unlimited agent loops** — an agent without a max-turn ceiling can loop indefinitely on ambiguous tasks; always set a hard limit and define graceful stopping behavior - **User-provided text injected directly into system prompts** — prompt injection via user content can override instructions; sanitize input and keep it in the `user` message role, never the `system` role - **Cost estimation deferred until after launch** — LLM costs scale with tokens × requests; estimate per-request cost at the architecture stage, not post-launch when it's too expensive to change - **Single embedding model for all content types** — code, prose, and tables have different semantic spaces; benchmark domain-appropriate models or separate indexes per content type ## Checklist Before shipping an AI feature: - [ ] Model pinned to specific version, not `latest` - [ ] `max_tokens` set explicitly; generation budget validated against cost - [ ] Prompt injection detection applied to all user-controlled inputs - [ ] PII redaction runs before sending data to external models - [ ] Content moderation gate in place for user-facing endpoints - [ ] Retrieval quality measured (context precision/recall via RAGAS or equivalent) - [ ] Semantic caching enabled for repeated or near-duplicate queries - [ ] Streaming used for responses >200 tokens to avoid client timeouts - [ ] Retry with exponential backoff on rate-limit and transient errors - [ ] LLM calls traced (LangSmith, Phoenix, or custom spans) - [ ] Latency and token-usage metrics emitted to monitoring stack - [ ] Fallback model or graceful degradation path defined - [ ] Chunking strategy validated on representative documents - [ ] Reranker in place if retrieval corpus exceeds 10K chunks > See also: `claude-api`, `observability`, `security`