MCP, RAG & CAG

Augmenting LLMs with external knowledge and tools

Beyond the Training Data

LLMs are incredibly powerful, but they have a fundamental limitation: they only know what was in their training data, which has a cutoff date and doesn't include your private data. MCP (Model Context Protocol), RAG (Retrieval-Augmented Generation), and CAG (Context-Augmented Generation) are techniques to overcome this limitation by connecting LLMs to external knowledge sources, databases, APIs, and real-time information. These approaches transform static models into dynamic, context-aware systems.

RAG: Retrieval-Augmented Generation

RAG is the most common technique for grounding LLM responses in external knowledge. Instead of relying solely on the model's training data, RAG retrieves relevant information from external sources and includes it in the prompt.

How RAG Works

Definition: RAG (Retrieval-Augmented Generation) is a pipeline that combines semantic search with LLM generation. It converts queries to vectors, searches a knowledge base for relevant documents, and includes them as context in the LLM prompt to ground responses in factual information.
┌─────────────────────────────────────────────────────────────────┐
│                        RAG Pipeline Flow                        │
└─────────────────────────────────────────────────────────────────┘

    ┌─────────────────┐
    │   User Query    │  "What is our vacation policy?"
    └────────┬────────┘
             │
             ▼
    ┌─────────────────┐
    │ 1. Embedding    │  Convert query → vector representation
    │    Model        │  [0.234, -0.891, 0.456, ..., 0.123]
    └────────┬────────┘
             │
             ▼
    ┌─────────────────┐
    │ 2. Vector       │  Similarity search in vector database
    │    Search       │  (cosine similarity, dot product, etc.)
    └────────┬────────┘
             │
             ▼
    ┌─────────────────┐
    │ 3. Retrieve     │  Get top K most relevant documents
    │    Top K Docs   │  (K = 3-5 typically)
    └────────┬────────┘
             │
             ▼
    ┌─────────────────┐
    │ 4. Build        │  Query + Retrieved Docs → Prompt
    │    Context      │  Format for LLM consumption
    └────────┬────────┘
             │
             ▼
    ┌─────────────────┐
    │ 5. LLM          │  Generate answer using context
    │    Generation   │  (Temperature: 0.0-0.3 for factual)
    └────────┬────────┘
             │
             ▼
    ┌─────────────────┐
    │ 6. Response     │  Return answer + source citations
    │    with Sources │  to user
    └─────────────────┘
Detailed Example: Vacation Policy Query
User Query:
"What is our company's vacation policy?"

Step 1 - Embedding:
  Input:  "What is our company's vacation policy?"
  Output: [0.234, -0.891, 0.456, 0.123, ..., -0.567]  (384 dimensions)

Step 2 - Vector Search:
  Search ChromaDB for similar document embeddings
  Method: Cosine similarity

Step 3 - Retrieve Top Documents:
  ✓ Document 1: "Employee Handbook - Section 5.2"
    Similarity: 0.94
    Preview: "Vacation Policy: Full-time employees receive 20 days..."

  ✓ Document 2: "HR Policy Update 2024"
    Similarity: 0.87
    Preview: "Updated vacation rollover policy effective Jan 2024..."

  ✓ Document 3: "Benefits Guide"
    Similarity: 0.82
    Preview: "Comprehensive benefits including vacation, health..."

Step 4 - Build Context:
  Prompt = """
  Context:
  [Document 1 content]
  [Document 2 content]
  [Document 3 content]

  Question: What is our company's vacation policy?
  Answer based only on the context above.
  """

Step 5 - LLM Generation:
  Model: claude-sonnet-4-20250514
  Temperature: 0.3 (factual responses)
  Max tokens: 1000

  Output: "Based on the Employee Handbook Section 5.2, full-time
          employees receive 20 days of paid vacation per year,
          accruing at 1.67 days per month. According to the 2024
          HR Policy Update, unused vacation can roll over up to
          10 days annually..."

Step 6 - Return Response:
  Answer: [Generated text above]
  Sources:
    - Employee Handbook - Section 5.2 (94% match)
    - HR Policy Update 2024 (87% match)
    - Benefits Guide (82% match)

  Total latency: ~800ms
    - Embedding: 50ms
    - Vector search: 100ms
    - LLM generation: 650ms
Why RAG is Powerful
  • Access to private/proprietary data
  • Always up-to-date information
  • Reduces hallucinations
  • Attributable sources
  • No model retraining needed
Key Components
  • Document corpus/knowledge base
  • Embedding model
  • Vector database
  • Retrieval algorithm
  • LLM for generation

Complete RAG Implementation

Implementation Guide: This production-ready RAG system uses Sentence Transformers for embeddings, ChromaDB for vector storage, and Claude for generation. It handles document ingestion, semantic retrieval, and answer generation with source attribution.
# rag_system.py - Complete RAG implementation

from typing import List, Dict
import numpy as np
from sentence_transformers import SentenceTransformer
import chromadb
from anthropic import Anthropic

class RAGSystem:
    """
    Production RAG system with embeddings, vector search, and LLM generation.
    """

    def __init__(
        self,
        embedding_model: str = "all-MiniLM-L6-v2",
        llm_model: str = "claude-sonnet-4-20250514",
        api_key: str = None
    ):
        # Embedding model for converting text to vectors
        self.embedder = SentenceTransformer(embedding_model)

        # Vector database for similarity search
        self.chroma_client = chromadb.Client()
        self.collection = self.chroma_client.create_collection(
            name="documents",
            metadata={"hnsw:space": "cosine"}
        )

        # LLM for generation
        self.llm = Anthropic(api_key=api_key)
        self.llm_model = llm_model

        self.doc_count = 0

    def add_documents(self, documents: List[Dict[str, str]]):
        """
        Add documents to the RAG system.

        Args:
            documents: List of dicts with 'text', 'metadata' (title, source, etc.)
        """
        texts = [doc['text'] for doc in documents]
        metadatas = [doc.get('metadata', {}) for doc in documents]

        # Generate embeddings
        embeddings = self.embedder.encode(texts, show_progress_bar=True)

        # Add to vector database
        ids = [f"doc_{self.doc_count + i}" for i in range(len(documents))]
        self.collection.add(
            embeddings=embeddings.tolist(),
            documents=texts,
            metadatas=metadatas,
            ids=ids
        )

        self.doc_count += len(documents)
        print(f"Added {len(documents)} documents. Total: {self.doc_count}")

    def retrieve(self, query: str, top_k: int = 3) -> List[Dict]:
        """
        Retrieve most relevant documents for a query.

        Args:
            query: User question
            top_k: Number of documents to retrieve

        Returns:
            List of retrieved documents with metadata and scores
        """
        # Embed query
        query_embedding = self.embedder.encode([query])[0]

        # Search vector database
        results = self.collection.query(
            query_embeddings=[query_embedding.tolist()],
            n_results=top_k
        )

        # Format results
        retrieved_docs = []
        for i in range(len(results['ids'][0])):
            retrieved_docs.append({
                'id': results['ids'][0][i],
                'text': results['documents'][0][i],
                'metadata': results['metadatas'][0][i],
                'similarity': 1 - results['distances'][0][i]  # Convert distance to similarity
            })

        return retrieved_docs

    def generate_answer(
        self,
        query: str,
        retrieved_docs: List[Dict],
        include_sources: bool = True
    ) -> Dict:
        """
        Generate answer using retrieved context.

        Args:
            query: User question
            retrieved_docs: Documents from retrieve()
            include_sources: Include source citations

        Returns:
            Answer with metadata
        """
        # Build context from retrieved documents
        context = "\n\n".join([
            f"Source {i+1} ({doc['metadata'].get('title', 'Unknown')}):\n{doc['text']}"
            for i, doc in enumerate(retrieved_docs)
        ])

        # Create prompt with context
        system_prompt = """You are a helpful assistant that answers questions based on the provided context.

Guidelines:
1. Answer ONLY using information from the provided context
2. If the context doesn't contain enough information, say so
3. Cite sources when possible (Source 1, Source 2, etc.)
4. Be concise but complete"""

        user_prompt = f"""Context:
{context}

Question: {query}

Answer the question based on the context above."""

        # Call LLM
        response = self.llm.messages.create(
            model=self.llm_model,
            max_tokens=1000,
            temperature=0.3,  # Lower temperature for factual responses
            system=system_prompt,
            messages=[{"role": "user", "content": user_prompt}]
        )

        answer = response.content[0].text

        # Build result
        result = {
            'answer': answer,
            'query': query,
            'sources': [
                {
                    'title': doc['metadata'].get('title', 'Unknown'),
                    'similarity': doc['similarity'],
                    'excerpt': doc['text'][:200] + '...'
                }
                for doc in retrieved_docs
            ] if include_sources else []
        }

        return result

    def query(self, question: str, top_k: int = 3) -> Dict:
        """
        Complete RAG pipeline: retrieve + generate.

        Args:
            question: User question
            top_k: Number of documents to retrieve

        Returns:
            Answer with sources
        """
        # Step 1: Retrieve relevant documents
        retrieved_docs = self.retrieve(question, top_k=top_k)

        # Step 2: Generate answer
        result = self.generate_answer(question, retrieved_docs)

        return result


# Example Usage
if __name__ == "__main__":
    import os

    # Initialize RAG system
    rag = RAGSystem(api_key=os.getenv("ANTHROPIC_API_KEY"))

    # Add company documents
    documents = [
        {
            "text": """Vacation Policy: Full-time employees receive 20 days of paid vacation per year.
            Vacation accrues monthly at 1.67 days per month. Unused vacation rolls over up to 10 days.
            Employees must request vacation at least 2 weeks in advance except for emergencies.""",
            "metadata": {"title": "Employee Handbook - Section 5.2", "source": "HR"}
        },
        {
            "text": """Remote Work Policy: Employees may work remotely up to 3 days per week.
            Remote work requires manager approval and must be scheduled in advance.
            Employees must be available during core hours (10 AM - 3 PM) regardless of location.""",
            "metadata": {"title": "Remote Work Guidelines", "source": "HR"}
        },
        {
            "text": """Health Benefits: Company provides comprehensive health insurance through BlueCross.
            Coverage starts on the first day of the month following hire date.
            Premium is $150/month for individuals, $350/month for families.
            Annual deductible: $1,000 individual, $2,000 family.""",
            "metadata": {"title": "Benefits Guide 2024", "source": "Benefits"}
        },
        {
            "text": """Performance Review Process: Reviews conducted annually in December.
            Employees receive feedback on goals, competencies, and overall performance.
            Reviews determine merit increases and promotion eligibility.
            Self-assessment due by December 1st, manager review by December 15th.""",
            "metadata": {"title": "Performance Management", "source": "HR"}
        }
    ]

    rag.add_documents(documents)

    # Ask questions
    questions = [
        "How many vacation days do I get?",
        "What is the remote work policy?",
        "When are performance reviews?",
        "How much does health insurance cost?"
    ]

    for question in questions:
        print(f"\n{'='*60}")
        print(f"Q: {question}")
        print('='*60)

        result = rag.query(question, top_k=2)

        print(f"\nA: {result['answer']}")
        print(f"\nSources:")
        for i, source in enumerate(result['sources'], 1):
            print(f"  {i}. {source['title']} (similarity: {source['similarity']:.2f})")

# Output example:
# Q: How many vacation days do I get?
# A: Full-time employees receive 20 days of paid vacation per year,
#    which accrues monthly at 1.67 days per month (Source 1).
# Sources:
#   1. Employee Handbook - Section 5.2 (similarity: 0.94)
#   2. Benefits Guide 2024 (similarity: 0.45)
Key Implementation Details: This system uses Sentence Transformers for fast embeddings, ChromaDB for vector search, and temperature=0.3 for factual responses. The modular design allows easy swapping of components (different embedding models, vector databases, or LLMs).

Common RAG Challenges

1. Retrieval Quality

Problem: Retrieved docs aren't relevant

Solution: Better embeddings, hybrid search, query rewriting

2. Context Length

Problem: Too many docs exceed LLM context window

Solution: Document chunking, re-ranking, summarization

3. Stale Information

Problem: Documents outdated

Solution: Regular re-indexing, metadata timestamps

4. Multi-Hop Reasoning

Problem: Answer requires multiple docs

Solution: Iterative retrieval, knowledge graphs

MCP: Model Context Protocol

MCP is Anthropic's open protocol for connecting AI assistants to external data sources and tools. Think of it as a standardized way to give LLMs "plugins", whether that's accessing databases, APIs, file systems, or custom tools.

What is MCP?

Definition: MCP (Model Context Protocol) is Anthropic's open, standardized protocol for connecting AI assistants to external data sources and tools. It defines a universal interface for exposing resources (read-only data) and tools (actions) to any MCP-compatible AI system.
MCP Architecture:

┌─────────────────────────────────────────────────────────┐
│                      AI Application                     │
│              (Claude, custom chatbot, etc.)             │
└────────────────────┬────────────────────────────────────┘
                     │ MCP Protocol
                     │ (Standardized communication)
                     │
     ┌───────────────┼───────────────┐
     │               │               │
     ▼               ▼               ▼
┌─────────┐    ┌─────────┐    ┌─────────┐
│  MCP    │    │  MCP    │    │  MCP    │
│ Server  │    │ Server  │    │ Server  │
│ (DB)    │    │ (Files) │    │ (API)   │
└─────────┘    └─────────┘    └─────────┘
     │               │               │
     ▼               ▼               ▼
┌─────────┐    ┌─────────┐    ┌─────────┐
│Database │    │File     │    │External │
│         │    │System   │    │API      │
└─────────┘    └─────────┘    └─────────┘

Key Concepts:

1. Resources: Read-only data sources (files, DB records, API responses)
2. Tools: Actions the AI can take (create, update, delete, execute)
3. Prompts: Pre-built prompt templates with variables
4. Sampling: Request AI to generate content (for complex workflows)

Benefits:
✓ Standardized protocol (not vendor-specific)
✓ Composable (multiple MCP servers work together)
✓ Secure (fine-grained access control)
✓ Extensible (easy to add new integrations)

Building an MCP Server

Implementation Guide: This MCP server exposes a company database to AI assistants through standardized resources and tools. Resources provide read access to tables, while tools enable SQL queries and schema inspection with safety constraints (SELECT-only queries).
# mcp_server.py - MCP server for database access
# Uses the official MCP Python SDK (pip install mcp)

from mcp.server.fastmcp import FastMCP
import sqlite3
import json

# Initialize MCP server with the high-level FastMCP API
mcp = FastMCP("company-db-server")

# Database connection
DB_PATH = "company.db"


def get_connection():
    return sqlite3.connect(DB_PATH)


@mcp.resource("db://tables")
def list_tables() -> str:
    """List all available database tables."""
    conn = get_connection()
    cursor = conn.cursor()
    cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
    tables = [row[0] for row in cursor.fetchall()]
    conn.close()
    return json.dumps({"tables": tables}, indent=2)


@mcp.resource("db://tables/{table_name}")
def read_table(table_name: str) -> str:
    """Read data from a specific table (limited to 100 rows)."""
    conn = get_connection()
    cursor = conn.cursor()
    # Use parameterized allowlist to prevent SQL injection
    cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
    if not cursor.fetchone():
        conn.close()
        return json.dumps({"error": f"Table '{table_name}' not found"})

    cursor.execute(f"SELECT * FROM [{table_name}] LIMIT 100")
    columns = [description[0] for description in cursor.description]
    rows = cursor.fetchall()
    conn.close()

    data = [dict(zip(columns, row)) for row in rows]
    return json.dumps(data, indent=2)


@mcp.tool()
def query_database(query: str) -> str:
    """
    Execute a read-only SQL query on the database.

    Args:
        query: SQL SELECT query to execute
    """
    # Safety: only allow SELECT queries
    if not query.strip().upper().startswith("SELECT"):
        return json.dumps({"error": "Only SELECT queries are allowed"})

    conn = get_connection()
    try:
        cursor = conn.cursor()
        cursor.execute(query)
        columns = [description[0] for description in cursor.description]
        rows = cursor.fetchall()

        data = [dict(zip(columns, row)) for row in rows]
        return json.dumps({
            "query": query,
            "row_count": len(data),
            "results": data
        }, indent=2)

    except Exception as e:
        return json.dumps({"error": f"Query failed: {str(e)}"})
    finally:
        conn.close()


@mcp.tool()
def get_schema(table_name: str) -> str:
    """
    Get the schema (columns, types) of a database table.

    Args:
        table_name: Name of the table to inspect
    """
    conn = get_connection()
    cursor = conn.cursor()
    # Validate table exists first
    cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
    if not cursor.fetchone():
        conn.close()
        return json.dumps({"error": f"Table '{table_name}' not found"})

    cursor.execute(f"PRAGMA table_info([{table_name}])")
    schema = cursor.fetchall()
    conn.close()

    columns = []
    for col in schema:
        columns.append({
            "name": col[1],
            "type": col[2],
            "nullable": not col[3],
            "primary_key": bool(col[5])
        })

    return json.dumps({"table": table_name, "columns": columns}, indent=2)


# Run the server (uses stdio transport by default)
if __name__ == "__main__":
    mcp.run()

# Claude can now:
# 1. List resources: "Show me all tables"       → reads db://tables
# 2. Read resources: "Get employees data"        → reads db://tables/employees
# 3. Call tools: "Query engineering department"   → calls query_database()
# 4. Inspect schema: "What columns in users?"    → calls get_schema()
#
# Example conversation:
# User: "How many employees in each department?"
# Claude (internally):
#   1. Calls get_schema("employees") to understand structure
#   2. Calls query_database("SELECT department, COUNT(*) FROM employees GROUP BY department")
#   3. Returns formatted answer to user
#
# Response: "Based on the database:
#   - Engineering: 45 employees
#   - Sales: 23 employees
#   - Marketing: 12 employees"
#
# Works with any MCP-compatible AI system (Claude Desktop, Claude Code, etc.)
MCP Benefits: Standardized protocol means you write one MCP server and it works with any MCP-compatible AI system. No custom APIs, consistent interface, composable servers, and fine-grained security controls for sensitive operations.

When to Use MCP

  • Database access: Read and write with tools, schema inspection
  • File system access: Read files, search, create documents
  • API integrations: Slack, GitHub, Jira, external services
  • Custom business logic: Pricing calculations, workflow automation
  • Real-time data: Stock prices, weather, news feeds

MCP vs RAG: When to Use Which

✓ Use RAG when:
  • You have static documents/knowledge base
  • Need semantic search over large corpus
  • Documents don't change frequently
  • Read-only access is sufficient
  • Example: Company handbook Q&A
✓ Use MCP when:
  • Need real-time data access
  • AI needs to perform actions (not just read)
  • Multiple data sources/tools
  • Dynamic data (databases, APIs)
  • Example: AI assistant that updates CRM
Best Practice: Use both together! RAG excels at knowledge retrieval from static documents, while MCP handles actions and real-time data. They're complementary, not mutually exclusive.

CAG: Context-Augmented Generation

CAG is a broader concept that encompasses various techniques for augmenting LLM context with relevant information. While RAG is a specific implementation focused on retrieval, CAG includes any method of enhancing the context window with useful information.

CAG Techniques

1. Prompt-Based Context
Definition: The simplest form of CAG - directly including relevant context (user profile, current date, system state) in the prompt without complex retrieval mechanisms. Best for small, structured context that doesn't require search.
# Simplest form of CAG
prompt = f"""Context: Today is {current_date}.
User is: {user_name}, role: {user_role}, department: {user_department}

Current sprint: Sprint 42, ends {sprint_end_date}
Team velocity: 32 story points
Outstanding tickets: 12

User question: {user_question}

Answer based on the context above."""
2. Conversation History
Definition: Maintaining multi-turn conversation state by including previous messages in the context. Enables the AI to reference earlier parts of the conversation and maintain coherent, context-aware responses across multiple exchanges.
class ConversationContext:
    def __init__(self, max_history: int = 10):
        self.history = []
        self.max_history = max_history

    def add_turn(self, user_msg: str, assistant_msg: str):
        self.history.append({"user": user_msg, "assistant": assistant_msg})
        # Keep only recent history
        if len(self.history) > self.max_history:
            self.history = self.history[-self.max_history:]

    def build_messages(self, new_user_msg: str):
        messages = []
        # Add history
        for turn in self.history:
            messages.append({"role": "user", "content": turn["user"]})
            messages.append({"role": "assistant", "content": turn["assistant"]})
        # Add new message
        messages.append({"role": "user", "content": new_user_msg})
        return messages
3. Dynamic Context Assembly
Definition: Intelligently selecting and combining context from multiple sources based on query analysis. The system determines what context is needed (user data, documents, real-time info) and assembles it dynamically rather than including everything.
class DynamicContextBuilder:
    """Intelligently build context based on query type."""

    def build_context(self, query: str, user: User) -> str:
        context_parts = []

        # Always include: current date/time
        context_parts.append(f"Current date: {datetime.now()}")

        # Query-specific context
        if self._is_about_user_data(query):
            context_parts.append(f"User profile: {user.profile}")
            context_parts.append(f"Recent activity: {user.recent_activity}")

        if self._mentions_project(query):
            projects = self._get_user_projects(user)
            context_parts.append(f"Active projects: {projects}")

        if self._needs_company_data(query):
            context_parts.append(self._get_company_context())

        # RAG retrieval if needed
        if self._needs_document_search(query):
            docs = self.rag_system.retrieve(query, top_k=3)
            context_parts.append(f"Relevant documents: {docs}")

        return "\n\n".join(context_parts)
4. Hierarchical Context
Definition: Organizing context into tiers of importance (critical, primary, secondary, background) to help the LLM prioritize information. More important context appears first, ensuring it receives more attention even in long contexts.
def build_hierarchical_context(query: str):
    return f"""
CRITICAL CONTEXT (always consider):
- User: {user.name}, role: {user.role}
- Security level: {user.security_level}
- Current time: {now}

PRIMARY CONTEXT (highly relevant):
{retrieve_most_relevant_docs(query, top_k=3)}

SECONDARY CONTEXT (potentially useful):
{get_related_information(query)}

BACKGROUND CONTEXT (for reference):
{get_general_company_info()}
"""

Complete CAG System

Implementation Guide: This production CAG system combines multiple techniques - temporal context, user profiles, conversation history, domain-specific data, RAG retrieval, and real-time information - into a comprehensive context augmentation framework.
# cag_system.py - Comprehensive Context-Augmented Generation

from typing import Dict, List, Optional
from datetime import datetime
import anthropic

class CAGSystem:
    """
    Complete CAG system with multiple context sources.
    """

    def __init__(self, api_key: str):
        self.llm = anthropic.Anthropic(api_key=api_key)
        self.conversation_history = {}  # Per-user history

    def generate_response(
        self,
        user_id: str,
        query: str,
        user_profile: Dict,
        include_history: bool = True
    ) -> str:
        """
        Generate response with full context augmentation.

        Args:
            user_id: User identifier
            query: User question
            user_profile: User metadata (name, role, preferences)
            include_history: Include conversation history

        Returns:
            AI response with context
        """
        # Build comprehensive context
        context = self._build_context(user_id, query, user_profile)

        # Build messages with history
        messages = []

        if include_history and user_id in self.conversation_history:
            messages.extend(self.conversation_history[user_id])

        messages.append({
            "role": "user",
            "content": f"{context}\n\nUser question: {query}"
        })

        # Generate response
        response = self.llm.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=2000,
            temperature=0.7,
            system=self._build_system_prompt(user_profile),
            messages=messages
        )

        assistant_message = response.content[0].text

        # Update conversation history
        self._update_history(user_id, query, assistant_message)

        return assistant_message

    def _build_context(
        self,
        user_id: str,
        query: str,
        user_profile: Dict
    ) -> str:
        """
        Intelligently build context from multiple sources.
        """
        context_sections = []

        # 1. Temporal context
        context_sections.append(f"""
=== TEMPORAL CONTEXT ===
Current date/time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
Day of week: {datetime.now().strftime('%A')}
""")

        # 2. User context
        context_sections.append(f"""
=== USER CONTEXT ===
Name: {user_profile.get('name')}
Role: {user_profile.get('role')}
Department: {user_profile.get('department')}
Preferences: {user_profile.get('preferences', {})}
""")

        # 3. Domain-specific context (example: project management)
        if 'current_project' in user_profile:
            context_sections.append(f"""
=== PROJECT CONTEXT ===
Active project: {user_profile['current_project']['name']}
Status: {user_profile['current_project']['status']}
Team members: {', '.join(user_profile['current_project']['team'])}
Deadline: {user_profile['current_project']['deadline']}
""")

        # 4. Retrieved knowledge (RAG-style)
        if self._needs_knowledge_retrieval(query):
            relevant_docs = self._retrieve_knowledge(query)
            context_sections.append(f"""
=== RELEVANT KNOWLEDGE ===
{relevant_docs}
""")

        # 5. Real-time data (if applicable)
        if self._needs_realtime_data(query):
            realtime_data = self._fetch_realtime_data(query)
            context_sections.append(f"""
=== REAL-TIME DATA ===
{realtime_data}
""")

        return "\n".join(context_sections)

    def _build_system_prompt(self, user_profile: Dict) -> str:
        """Build personalized system prompt."""
        return f"""You are a helpful AI assistant for {user_profile.get('name')}.

Your role:
- Provide accurate, helpful responses
- Use the provided context to ground your answers
- Be concise but complete
- Cite sources when using retrieved information
- Respect user's role and permissions

User's role: {user_profile.get('role')}
Tone: {user_profile.get('preferences', {}).get('tone', 'professional')}"""

    def _update_history(self, user_id: str, user_msg: str, assistant_msg: str):
        """Maintain conversation history."""
        if user_id not in self.conversation_history:
            self.conversation_history[user_id] = []

        self.conversation_history[user_id].extend([
            {"role": "user", "content": user_msg},
            {"role": "assistant", "content": assistant_msg}
        ])

        # Keep only last 10 turns
        if len(self.conversation_history[user_id]) > 20:  # 20 messages = 10 turns
            self.conversation_history[user_id] = self.conversation_history[user_id][-20:]

    def _needs_knowledge_retrieval(self, query: str) -> bool:
        """Determine if query needs knowledge base search."""
        knowledge_keywords = ['policy', 'procedure', 'guideline', 'documentation', 'how to']
        return any(kw in query.lower() for kw in knowledge_keywords)

    def _retrieve_knowledge(self, query: str) -> str:
        """Retrieve relevant knowledge (simplified)."""
        # In production: use RAG system
        return "[Knowledge base results would go here]"

    def _needs_realtime_data(self, query: str) -> bool:
        """Determine if query needs real-time data."""
        realtime_keywords = ['current', 'latest', 'now', 'today', 'status']
        return any(kw in query.lower() for kw in realtime_keywords)

    def _fetch_realtime_data(self, query: str) -> str:
        """Fetch real-time data (simplified)."""
        # In production: call MCP servers or APIs
        return "[Real-time data would go here]"


# Example Usage
if __name__ == "__main__":
    import os

    cag = CAGSystem(api_key=os.getenv("ANTHROPIC_API_KEY"))

    # User profile
    user_profile = {
        "name": "Alice Johnson",
        "role": "Senior Engineer",
        "department": "Engineering",
        "current_project": {
            "name": "API Redesign",
            "status": "In Progress",
            "team": ["Alice", "Bob", "Carol"],
            "deadline": "2024-03-15"
        },
        "preferences": {
            "tone": "professional"
        }
    }

    # Example conversation
    queries = [
        "What's my current project status?",
        "Who's on my team?",
        "When is the deadline?"
    ]

    for query in queries:
        print(f"\nUser: {query}")
        response = cag.generate_response(
            user_id="alice_123",
            query=query,
            user_profile=user_profile
        )
        print(f"AI: {response}")

# Output:
# User: What's my current project status?
# AI: Your current project "API Redesign" is In Progress. The deadline
#     is March 15, 2024, and you're working with Bob and Carol.
#
# User: Who's on my team?
# AI: Your team for the API Redesign project includes: Alice (you),
#     Bob, and Carol.
#
# User: When is the deadline?
# AI: The deadline for your API Redesign project is March 15, 2024.
CAG Best Practices: Organize context hierarchically (most important first), include only relevant information to avoid wasting tokens, use clear structured formatting, combine multiple sources intelligently, and monitor token usage to stay within context window limits.

RAG vs MCP vs CAG: Comparison

AspectRAGMCPCAG
PurposeRetrieve relevant docs for contextConnect AI to tools and data sourcesAugment context from any source
Primary UseKnowledge base Q&AAI agents with actionsGeneral context enhancement
Data AccessRead-only (documents)Read & Write (tools)Read-only (any source)
Real-timeNo (pre-indexed)Yes (dynamic queries)Yes (if configured)
ComplexityMedium (embeddings, vector DB)High (protocol implementation)Low to Medium
StandardizationNo standard (many implementations)Yes (Anthropic's MCP protocol)No standard
Best ForStatic knowledge basesDynamic data + actionsMulti-source context
Combined Approach: Most production systems use a combination. CAG as the overall framework, RAG for knowledge retrieval, and MCP for tool access. They're complementary, not mutually exclusive.

Best Practices

1. Start Simple, Add Complexity as Needed

Begin with simple prompt-based CAG. Add RAG when you need semantic search. Implement MCP when you need actions/real-time data. Don't over-engineer from day one, let actual requirements drive complexity.

2. Monitor Context Token Usage

Context is not free. Track token usage and costs. Prioritize most relevant context. Use summarization for lengthy documents. Stay within context window limits to avoid truncation or errors.

3. Implement Context Caching

Cache frequently used context (user profiles, company info, common docs) to reduce costs and latency. Use prompt caching features when available. Invalidate caches appropriately when data changes.

4. Validate Retrieved Context

Check relevance of retrieved documents. Low similarity scores indicate poor retrieval. Re-rank results. Filter out irrelevant context before sending to LLM to improve response quality and reduce token waste.

5. Keep Context Fresh

Re-index documents regularly. Add timestamps to context. Invalidate caches when data changes. Stale context is worse than no context, it leads to incorrect, outdated answers that erode user trust.

6. Provide Source Attribution

Always cite where information came from. Include document titles, timestamps, URLs. Enables users to verify information. Builds trust in the system and allows users to dive deeper into sources.

Key Takeaways

  • RAG retrieves knowledge - Semantic search over documents, best for static knowledge bases
  • MCP connects to tools - Standardized protocol for AI to access systems and take actions
  • CAG is the umbrella - Any technique for augmenting LLM context with useful information
  • Use them together - Complementary approaches that combine well in production
  • Context is expensive - Monitor token usage, prioritize relevance, cache when possible
  • Embeddings are key - Quality of embeddings determines RAG success
  • MCP is standardized - One protocol for all integrations, not custom APIs
  • Start simple - Begin with basic prompts, add complexity as needed
  • Validate everything - Check relevance, freshness, and accuracy of context
  • Cite sources - Always provide attribution for transparency and trust
Software Engineering in AI EraLesson 8