agents101 · Agent Engineering

Preface: From LLM to Agent — A Complete Engineering Path

Large Language Models (LLMs) have transformed the way we build software. But simply calling an API and getting text back is only the first step. The real engineering challenge is: how do you evolve a model from “being able to chat” to “being able to get things done” — capable of calling external tools, autonomously planning tasks, collaborating in teams, remembering user preferences, and ultimately running reliably in production environments.

This path covers eight core domains:

  1. LLM Fundamentals: Understanding Tokenization, Transformer architecture, context windows, and prompt engineering
  2. RAG Principles and Practice: Mastering Retrieval-Augmented Generation to give models access to private knowledge
  3. Agent Tool Use: Function Calling, the ReAct loop, and the MCP protocol
  4. Agent Planning & Execution: Reflection mechanisms, Plan & Execute, workflow orchestration
  5. Multi-Agent Collaboration: Hierarchical collaboration, blackboard patterns, collaboration methodology
  6. Memory & Skill: Short-term/long-term memory management, Skill system design
  7. Agent Evaluation: End-to-end evaluation, white-box evaluation, evaluation-driven iteration
  8. Production Deployment: Model deployment, inference optimization, safety guardrails, Harness Engineering

This guide is aimed at developers who want to systematically master Agent engineering, providing a panoramic technical reference from foundational principles to production practices.


Tokenization Principles

Why Tokenization is Necessary

Computers cannot directly understand human text. The first step an LLM takes when processing text is to convert natural language into a numerical format that machines can compute. This process is called Tokenization.

A token is the basic unit obtained after a tokenizer encodes text; each token corresponds to an integer ID in the vocabulary. A key insight: a token is typically a sub-word fragment or character fragment — it is not necessarily a complete “word” and may not carry independent semantic meaning.

Input text: "ACP is a very"
          ↓ Tokenizer
Token sequence: [347, 1186, 374, 1134]

In English, words may be split into roots and suffixes; Chinese may be segmented by character or common phrase; spaces and punctuation may also be encoded into tokens. Tokenizers differ greatly between models — the GPT family uses BPE (Byte Pair Encoding), LLaMA uses SentencePiece BPE, and some Chinese models are specially optimized for Chinese corpora.

Token Vectorization and Positional Encoding

The integer ID value itself carries no semantic meaning. An ID of 500 does not mean it is “more important” than ID 50. Therefore, these discrete IDs need to be mapped to dense vectors through an Embedding matrix.

Token ID → Embedding Matrix Lookup → d-dimensional vector
  1186   →   [0.023, -0.451, 0.789, ..., -0.312]  (d=4096 or larger)

At the same time, word order is critical — “I help you” and “you help me” are entirely different. Since the Transformer’s Self-Attention mechanism inherently lacks sequence awareness, additional Positional Encoding is needed. Modern models typically use Rotary Position Embedding (RoPE), which introduces relative position information into the attention computation, enabling the model to naturally handle long sequences.

Decoding Strategies: From Probabilities to Output

After model inference, logits (score vectors) are obtained and converted via softmax into a probability distribution P(next_token | context). A decoding strategy is then used to select the output token:

StrategyPrincipleSuitable Scenarios
Greedy DecodingSelect the token with the highest probability each timeTasks requiring deterministic output (e.g., code generation, structured extraction)
Beam SearchMaintain multiple candidate paths and select the sequence with the highest overall probabilityTranslation, summarization, and other tasks requiring a global optimum
Top-k SamplingRandomly sample from the k tokens with the highest probabilityCreative writing, dialogue generation
Top-p (Nucleus)Sample from the smallest token set whose cumulative probability exceeds pGeneral conversation, balancing diversity and quality

Two core parameters control output randomness:

Temperature: Controls the “sharpness” of the softmax probability distribution.

# temperature effect comparison
# raw logits → softmax(logits / temperature)
# T=0.1: probability is highly concentrated, nearly deterministic output
# T=0.7: moderate, preserving reasonable diversity
# T=1.5: probability tends toward uniform, output highly random

Top_p (nucleus sampling threshold): Controls the range of candidate tokens that participate in sampling. For example, top_p=0.9 means sampling only from the smallest token set whose cumulative probability reaches 90%.

# typical configuration example
response = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "Write a poem about artificial intelligence"}],
    temperature=0.8,   # moderate creativity
    top_p=0.9,         # nucleus sampling
    max_tokens=200
)

Autoregressive Generation and Stop Conditions

The model uses Autoregressive Generation: each time a new token is generated, it is appended to the end of the input, and then the next token is predicted based on the new sequence.

"ACP is a very" → predicts "informative"
"ACP is a very informative" → predicts "course"
"ACP is a very informative course" → ... → predicts <EOS> → stops

Stop conditions include:

  • Generation of a special end-of-sequence token (EOS token)
  • Reaching the preset max_tokens limit
  • Generating a user-specified stop word sequence

Different model families have different stop tokens:

ModelStop Token
GPT family<|endoftext|>
LLaMA/Mistral</s>
DeepSeek<|end▁of▁sentence|>
Some Chinese LLMs<|im_end|>

Streaming output is essentially the server decoding and incrementally sending each token or small group of tokens as soon as they are generated, rather than waiting for all tokens to be generated before returning.


Transformer Architecture & Attention Mechanism

Architectural Overview

The core of modern large language models is the Transformer architecture. Although the complete Transformer contains both an encoder and a decoder, today’s mainstream autoregressive LLMs (GPT, LLaMA, etc.) use only the Decoder-only architecture.

Transformer decoder architecture

Causal Self-Attention

The core formula of the Attention mechanism:

Attention(Q, K, V) = softmax(QK^T / √d_k) · V

Here Q (Query), K (Key), and V (Value) are all derived from the same input sequence through linear transformations. Key design elements:

(1) Scaling Factor √d_k: Prevents the dot product from becoming too large, which would cause the softmax gradient to vanish. When the vector dimension d_k is large, the variance of the dot product also increases; dividing by √d_k normalizes the variance.

(2) Causal Mask: In autoregressive models, each token can only “see” the tokens before it, and cannot “peek” at future content. This is achieved by filling the upper triangular positions with -∞:

Input: ["ACP", "is", "a", "very"]
Attention matrix (after causal masking):
        ACP   is    a   very
ACP   0.8    -∞   -∞    -∞
is    0.3   0.7   -∞    -∞
a     0.2   0.3  0.5    -∞
very  0.1   0.2  0.3   0.4

(3) Multi-Head Attention: Rather than performing a single attention computation, multiple sets of Q/K/V projections are executed in parallel, each set focusing on different semantic relationships (grammatical structure, coreference, semantic similarity, etc.), and the outputs of all heads are concatenated at the end.

# pseudo-code for multi-head attention
def multi_head_attention(x, num_heads=8, d_model=512):
    d_head = d_model // num_heads  # dimension per head
    outputs = []
    for h in range(num_heads):
        Q = linear_projection(x, d_head)
        K = linear_projection(x, d_head)
        V = linear_projection(x, d_head)
        attn_out = softmax(Q @ K.T / sqrt(d_head)) @ V
        outputs.append(attn_out)
    return concat(outputs)  # concatenate all heads

Feed-Forward Network (FFN)

Each Attention layer is immediately followed by an FFN:

FFN(x) = GELU(x·W₁ + b₁) · W₂ + b₂

The FFN typically first expands the hidden dimension (e.g., 4x), then compresses it back to the original dimension. This “expand-compress” structure provides the model with nonlinear transformation capability, and is a key component for storing and applying knowledge.

Residual Connections & Layer Normalization

Each sub-layer (Attention and FFN) is combined with its input via a residual connection:

output = LayerNorm(x + Sublayer(x))

Residual connections allow gradients to propagate directly to shallow layers, solving the training difficulties of deep networks. Modern architectures typically use a Pre-Norm layout for LayerNorm (normalization before the sub-layer), which is more stable than the original Post-Norm.


Context Window & Token Budget

The Essence of the Context Window

The place where a large model receives input is called the Context Window. Think of it as a computer’s memory (RAM) — capacity is limited, and directly impacts performance.

Context window composition

Modern models have greatly expanded context windows:

  • GPT-4 Turbo: 128K tokens
  • Claude 3: 200K tokens
  • Gemini 1.5 Pro: 1M+ tokens
  • Open-source models (LLaMA 3, some Chinese models, etc.): 32K–128K tokens

But a larger window does not mean you can abuse it. Research shows a “Lost in the Middle” effect: the model’s ability to process information in the middle portion of the context significantly degrades — it pays more attention to the beginning (primacy effect) and the end (recency effect).

Token Budget Management

In production, you need to manage context like you manage memory. Here are the core strategies:

(1) Accurately Calculate Token Consumption

import tiktoken

def count_tokens(text: str, model: str = "gpt-4") -> int:
    encoding = tiktoken.encoding_for_model(model)
    return len(encoding.encode(text))

# Example: count total tokens for messages
def count_message_tokens(messages):
    encoding = tiktoken.encoding_for_model("gpt-4")
    total = 0
    for msg in messages:
        # each message has a fixed overhead (~4 tokens)
        total += 4
        total += len(encoding.encode(msg["content"]))
    total += 2  # priming for the reply
    return total

(2) Context Window Allocation Strategy

┌──────────────────────────────────────────────┐
│ Token Budget Allocation (128K window example)  │
├──────────────────────────────────────────────┤
│ System Prompt:      2-5K  (role definition, rules) │
│ RAG Retrieval:      3-8K  (relevant knowledge chunks) │
│ Chat History:       10-20K (most recent N turns)     │
│ Current User Input: 1-3K                             │
│ Reserved Response:  4-8K                             │
│ Buffer Margin:      remaining (~80K)                 │
└──────────────────────────────────────────────┘

(3) Context Engineering

Context Engineering is the practice of systematically designing, building, and optimizing context. It goes beyond simply “stuffing information into a prompt” and encompasses four core techniques:

TechniqueProblem SolvedCore Method
RAGInsufficient private-domain knowledgeRetrieve relevant information from external knowledge bases and inject into context
Prompt EngineeringImprecise instructionsGuide model behavior through carefully designed instructions
Tool UseModel cannot execute operationsEmpower models with the ability to call external tools
MemoryForgetting across sessionsEstablish long- and short-term memory mechanisms

Many failures of LLM applications are not due to the model lacking intelligence, but rather failures of “context.” Context Engineering is precisely the key to unlocking the potential of large models.


Prompt Engineering Methodology

System Prompt Design

The System Prompt is the model’s “constitution” — it defines the role’s behavioral boundaries, response style, and task constraints. A good System Prompt should include:

# System Prompt structure template
Role Definition: |
  You are a senior Python technical documentation reviewer,
  focused on code correctness and pedagogical effectiveness.

Behavioral Guidelines:
  - Do not modify variable names or API version numbers in the code
  - When you find issues, provide the specific location and fix suggestions
  - If there is insufficient information to judge, explicitly say "unsure"

Output Format:
  ## Review Report
  ### Key Issues
  - **[Line N]**: Issue description
    - Severity: Critical|General|Minor
    - Fix Suggestion: specific suggestion

Constraints:
  - No playful remarks or unnecessary scene-setting
  - Terminology must be explained on first occurrence
  - Code blocks must include necessary import statements

Few-Shot & Structured Output

Few-Shot Examples: Provide input-output exemplars for the model to imitate format and style.

examples = [
    {
        "input": "Explain what a Python decorator is",
        "output": "### Pain Point Introduction\nHave you ever wanted to add the same logging logic across multiple functions?..."
    },
    {
        "input": "Explain what a list comprehension is",
        "output": "### Pain Point Introduction\nHave you ever written code like this — 5 lines of a for loop just to filter out even numbers from a list?..."
    }
]

prompt = f"""
Please answer user questions in the style of the following examples:

{examples}

User question: {user_question}
"""

Structured Output: Constrain output format through JSON Schema or Pydantic models.

from pydantic import BaseModel
from typing import List, Optional

class CodeReview(BaseModel):
    file_name: str
    issues: List[dict]
    overall_score: int  # 1-5
    requires_rewrite: bool

# Attach the Schema in the prompt
prompt = f"""
Please output the review results according to the following JSON Schema:

{CodeReview.model_json_schema()}

Code to review:
{code}
"""

Chain-of-Thought (CoT)

For complex tasks requiring multi-step reasoning, guiding the model to “speak its thought process” can significantly improve accuracy.

# ❌ Direct request (lower accuracy)
prompt_simple = "Calculate: A class has 30 students. There are 4 more boys than girls. How many boys are there?"

# ✅ CoT prompt (higher accuracy)
prompt_cot = """
Calculate: A class has 30 students. There are 4 more boys than girls. How many boys are there?

Please reason step by step:
Step 1: Let the number of girls be x, then the number of boys is x + 4
Step 2: Total students = x + (x + 4) = 30
Step 3: Solve 2x + 4 = 30, so x = 13
Step 4: Number of boys = x + 4 = 17
Answer: 17 boys
"""

CoT variants also include:

  • ToT (Tree of Thoughts): Explore multiple reasoning paths simultaneously and choose the best
  • GoT (Graph of Thoughts): Represent reasoning as a directed graph, supporting more complex reasoning topologies
  • Self-Consistency: Sample multiple CoT paths and take a majority vote

Meta Prompting: Let the Model Optimize Its Own Prompts

Writing a perfect prompt in one shot is nearly impossible. The core idea of Meta Prompting is: have a large model play the role of “prompt review expert,” helping you analyze and optimize the prompt itself.

meta_prompt = """
You are a prompt engineering expert. Please analyze the flaws in the following prompt and generate an optimized version.

Current prompt:
{current_prompt}

Output of this prompt:
{current_output}

Desired output:
{desired_output}

Please analyze the gap and output the optimized prompt.
"""

# This loop can be automated: Generate → Evaluate → Optimize → Regenerate

A complete Meta Prompting workflow can also introduce “reference answers” and quantitative scoring:

  1. Set Reference Answers: Define the ideal output
  2. Analyze the Gap: Have an “evaluator” model compare generated results against reference answers
  3. Optimize the Prompt: Rewrite the prompt based on the gap analysis report
  4. Quantitative Verification: Use a Grader to score multiple versions

Embedding & Vector Retrieval

How Embedding Models Work

Embedding models convert text into high-dimensional vectors so that semantically similar texts are close together in vector space.

"I like eating apples"   →  [0.12, -0.34, 0.56, ..., 0.78]  (1024 dimensions)
"I love eating apples"   →  [0.11, -0.33, 0.55, ..., 0.79]  ← very close
"Car repair guide"       →  [-0.78, 0.45, -0.23, ..., 0.01] ← very far

Embedding model training typically includes a Contrastive Learning phase: the input consists of many text pairs labeled as relevant/irrelevant, and the training objective is to maximize the vector similarity of relevant texts and minimize that of irrelevant ones.

# Calculate cosine similarity between two text vectors
import numpy as np

def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

# Example
query_vec = embedding_model.encode("How do I apply for annual leave?")
doc_vec = embedding_model.encode("Employee annual leave application procedure")

similarity = cosine_similarity(query_vec, doc_vec)
print(f"Similarity: {similarity:.4f}")  # 0.92 — highly relevant

Vector Database Selection

Vector databases are the core infrastructure of RAG systems. Choosing requires trade-offs:

SolutionRepresentative ProductsAdvantagesDisadvantagesSuitable Scenarios
In-Memory StorageLlamaIndex built-inZero-config, rapid prototypingData not persistent, memory-constrainedDevelopment & testing
Local Vector DBMilvus, Qdrant, ChromaFull-featured, data under your controlRequires self-deployment and maintenanceSmall-to-medium applications
Managed ServicesPinecone, Weaviate CloudNo operations, auto-scalingHigher cost, data offsiteProduction, elastic demand
Existing DB ExtensionsPostgreSQL + pgvector, ElasticsearchLeverage existing infrastructureVector performance not as good as dedicated DBsTeams already using that database
# Example using Chroma (lightweight local vector DB)
import chromadb
from chromadb.utils import embedding_functions

client = chromadb.PersistentClient(path="./chroma_db")
collection = client.create_collection(
    name="company_docs",
    embedding_function=embedding_functions.OpenAIEmbeddingFunction(
        api_key="your-api-key",
        model_name="text-embedding-3-small"
    )
)

# Add documents
collection.add(
    documents=["Employee annual leave application procedure...", "Travel reimbursement standards..."],
    metadatas=[{"source": "hr_policy.pdf"}, {"source": "finance_policy.pdf"}],
    ids=["doc_1", "doc_2"]
)

# Retrieve
results = collection.query(
    query_texts=["How do I apply for annual leave?"],
    n_results=3
)

Document Chunking Strategies

The Fundamental Tension of Chunking

The retrieval effectiveness of a RAG system heavily depends on document chunking quality. The core tension is:

Chunks too large → too much noise introduced during retrieval, model attention is diluted
Chunks too small → key information may be truncated, context is lost

There is no single “universally optimal” chunking strategy. You need to choose based on document type, retrieval scenario, and model capability.

Five Mainstream Chunking Methods

Token Chunking

Splits by a fixed number of tokens. Suitable for scenarios requiring precise token consumption control.

from llama_index.core.node_parser import TokenTextSplitter

splitter = TokenTextSplitter(
    chunk_size=256,     # tokens per chunk
    chunk_overlap=30    # overlap tokens between adjacent chunks
)

nodes = splitter.get_nodes_from_documents(documents)

Advantages: Precisely controls context size, suitable for models with smaller context windows. Disadvantages: May cut off mid-sentence, breaking semantic integrity.

Sentence Chunking

A splitting method that preserves sentence integrity; the default choice for most scenarios.

from llama_index.core.node_parser import SentenceSplitter

splitter = SentenceSplitter(
    chunk_size=512,
    chunk_overlap=50
)

Advantages: Maintains the semantic unit integrity of natural language. Disadvantages: Unaware of document structure, may split related paragraphs into different chunks.

Sentence Window Chunking

Uses different granularities for indexing and retrieval: small granularity for indexing for precise matching, and when returning retrieval results, includes adjacent context windows.

from llama_index.core.node_parser import SentenceWindowNodeParser

parser = SentenceWindowNodeParser(
    window_size=3,          # expand by 3 adjacent sentences during retrieval
    window_metadata_key="window",
    original_text_metadata_key="original"
)

Core advantage: Balances retrieval precision and context completeness.

Semantic Chunking

Adaptively selects split points based on semantic relevance, preserving the semantic continuity of documents.

from llama_index.core.node_parser import SemanticSplitterNodeParser

splitter = SemanticSplitterNodeParser(
    buffer_size=1,
    breakpoint_percentile_threshold=95,  # split when similarity falls below this threshold
    embed_model=embed_model
)

Suitable scenarios: Long documents with good logical structure and specialized content.

Markdown Chunking

Specifically optimized for Markdown structured documents, splitting by heading hierarchy.

from llama_index.core.node_parser import MarkdownNodeParser

parser = MarkdownNodeParser()
# Automatically recognizes #, ##, ### heading levels, splits at each heading paragraph

Best practice: Convert documents from PDF/Word to Markdown before chunking, leveraging heading structure to improve retrieval accuracy.

Chunking Strategy Selection Guide

Document TypeRecommended StrategyReason
Technical manuals (clear structure)Markdown chunkingLeverages heading hierarchy to preserve structure
Legal contracts (tight logic)Semantic chunkingPreserves the semantic integrity of clauses
Conversation transcriptsSentence window chunkingNeeds surrounding context for semantic understanding
Code documentationToken chunking + Semantic chunkingNeeds precise length control
News/Blog postsSentence chunkingWeak inter-paragraph connections

Retrieval-Augmented Generation Pipeline

The Two-Phase RAG Architecture

RAG (Retrieval-Augmented Generation) is the core architecture for solving the “knowledge deficiency” problem of large models. It divides the process into two phases:

Phase One: Build Index

Retrieval-augmented generation pipeline

  1. Parse Documents: Parse formats like PDF, Word, Markdown into plain text
  2. Chunk Text: Split documents into paragraphs according to the chosen strategy
  3. Embed Vectors: Convert each chunk into a vector using an Embedding model
  4. Store Index: Store vectors in a vector database and build an index

Phase Two: Retrieve and Generate

┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│  Query    │ → │  Retrieve │ → │  Augment  │ → │  Generate │
│  User Q   │    │  Vectors  │    │  Prompt   │    │  Model    │
└──────────┘    └──────────┘    └──────────┘    └──────────┘
  1. Receive Query: Accept the user’s question
  2. Vector Retrieval: Vectorize the question and retrieve the most similar chunks from the vector database
  3. Prompt Assembly: Assemble retrieved knowledge chunks + original question + instructions into a complete prompt
  4. Model Generation: The LLM generates an answer based on the augmented context

Complete RAG Pipeline Example

from openai import OpenAI
import numpy as np

client = OpenAI()

class SimpleRAG:
    def __init__(self, embed_model="text-embedding-3-small"):
        self.embed_model = embed_model
        self.documents = []      # store document texts
        self.embeddings = []     # store document vectors

    def add_documents(self, docs: list[str]):
        """Build index: vectorize and store documents"""
        for doc in docs:
            vec = self._embed(doc)
            self.documents.append(doc)
            self.embeddings.append(vec)

    def _embed(self, text: str) -> np.ndarray:
        resp = client.embeddings.create(
            model=self.embed_model,
            input=text
        )
        return np.array(resp.data[0].embedding)

    def retrieve(self, query: str, top_k: int = 3) -> list[str]:
        """Retrieve the most relevant document chunks"""
        query_vec = self._embed(query)
        similarities = [
            np.dot(query_vec, doc_vec) /
            (np.linalg.norm(query_vec) * np.linalg.norm(doc_vec))
            for doc_vec in self.embeddings
        ]
        top_indices = np.argsort(similarities)[-top_k:][::-1]
        return [self.documents[i] for i in top_indices]

    def query(self, question: str) -> str:
        """Complete RAG query"""
        contexts = self.retrieve(question)
        prompt = f"""Please answer the question based on the following reference information:

Reference Information:
{' '.join(contexts)}

Question: {question}

If the reference information is insufficient to answer the question, please state so explicitly."""

        resp = client.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": prompt}]
        )
        return resp.choices[0].message.content

Query Rewriting in Multi-Turn RAG Conversations

Implementing multi-turn conversation in a RAG scenario presents unique challenges. If the user’s second turn says “Who is his manager?”, retrieving directly with this sentence would completely fail — the system doesn’t know who “he” refers to.

Solution: Query Rewriting

def rewrite_query(conversation_history: list, current_query: str) -> str:
    """Use an LLM to rewrite a context-dependent question into a standalone question"""
    rewrite_prompt = f"""
    Based on the conversation history, rewrite the current question into a standalone question that does not depend on context.

    Conversation History:
    {format_history(conversation_history)}

    Current Question: {current_query}

    Rewritten Question:"""

    resp = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": rewrite_prompt}],
        temperature=0.1
    )
    return resp.choices[0].message.content

# Example
# History: User asked "Where is Zhang San's desk?" Assistant replied "5th floor, Building A"
# Current: "Who is his manager?"
# Rewritten: "Who is Zhang San's manager?"

Advanced RAG Patterns

Advanced RAG

HyDE (Hypothetical Document Embeddings)

The core idea of HyDE: first have the model “fabricate” a hypothetical answer, then use that hypothetical answer for retrieval instead of the original question. The intuition is — the hypothetical answer is semantically closer to real documents than the question itself.

def hyde_retrieve(query: str, top_k: int = 3) -> list[str]:
    """Retrieve using the HyDE method"""
    # Step 1: Generate a hypothetical answer
    hyde_prompt = f"""
    Question: {query}
    Please write a passage that answers this question.
    Passage:"""

    hyde_resp = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": hyde_prompt}]
    )
    hypothetical_doc = hyde_resp.choices[0].message.content

    # Step 2: Use the hypothetical answer for retrieval instead of the original question
    query_vec = embed(hypothetical_doc)
    results = vector_db.search(query_vec, top_k=top_k)
    return results

HyDE is particularly suitable for scenarios where the original query is very short but semantically complex, because the hypothetical answer can provide more semantic clues.

Re-Ranking

Initial vector retrieval (coarse ranking) is fast but has limited precision. A re-ranking model (fine ranking) can be introduced to perform a second sort on candidate chunks:

from sentence_transformers import CrossEncoder

reranker = CrossEncoder('BAAI/bge-reranker-v2-m3')

def rerank(query: str, candidates: list[str], top_k: int = 3):
    """Re-rank candidate chunks"""
    pairs = [(query, doc) for doc in candidates]
    scores = reranker.predict(pairs)

    # Sort by relevance score
    ranked = sorted(
        zip(candidates, scores),
        key=lambda x: x[1],
        reverse=True
    )
    return [doc for doc, _ in ranked[:top_k]]

Retrieval Strategy Optimization Panorama

TimingImprovement StrategyDescription
Pre-RetrievalQuery RewritingRewrite context-dependent questions into standalone questions
Pre-RetrievalQuery ExpansionAdd more semantic information to improve recall
Pre-RetrievalTag ExtractionFilter by tags first, then use vector retrieval
Pre-RetrievalMulti-Step Query DecompositionBreak complex questions into multiple sub-queries
Post-RetrievalReRankUse a more precise model for a second sort
Post-RetrievalSliding WindowAfter retrieving a chunk, supplement with adjacent chunks

RAG Document Preparation Strategy

The key to building a high-quality RAG system is document preparation. You need to understand the relationship between “intent space” and “knowledge space”:

Intent Space (what users may ask)    Knowledge Space (what the knowledge base covers)
     ┌─────────┐
     │ Overlap │ ← RAG can answer these
     └─────────┘
     ↑            ↑
  Uncovered        Unused
  intents →        knowledge →
  supplement       optimize
  knowledge        recall

Core principles:

  • Before optimizing algorithms, first supplement missing knowledge
  • Before improving recall, first improve document quality
  • Continuously collect user intent, forming a closed loop of “data collection → knowledge updates → expert verification”

Function Calling Protocol

AutoGen Studio — multi-agent workflow builder UI AutoGen Studio — Microsoft’s no-code multi-agent workflow builder — via microsoft/autogen

DSPy — declarative LLM programming DSPy — program (not prompt) LLMs declaratively; used by Hermes’s self-evolution pipeline — via stanfordnlp/dspy

What is Function Calling

Function Calling (also called Tool Calling) is a standard capability provided by LLM APIs. It allows the model to output structured tool invocation instructions when needed, rather than plain text replies.

The workflow is as follows:

Function Calling protocol

JSON Schema Tool Definition

# Define tool list
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_knowledge_base",
            "description": "Search the company internal knowledge base for policy documents and operation guides",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "Search keywords or question"
                    },
                    "category": {
                        "type": "string",
                        "enum": ["hr", "it", "finance", "general"],
                        "description": "Knowledge category"
                    }
                },
                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "send_email",
            "description": "Send an email",
            "parameters": {
                "type": "object",
                "properties": {
                    "to": {
                        "type": "string",
                        "description": "Recipient email address"
                    },
                    "subject": {
                        "type": "string",
                        "description": "Email subject"
                    },
                    "body": {
                        "type": "string",
                        "description": "Email body"
                    }
                },
                "required": ["to", "subject", "body"]
            }
        }
    }
]

Complete Function Calling Loop

from openai import OpenAI
import json

client = OpenAI()

def execute_function_call(tool_call):
    """Execute a tool call and return the result"""
    func_name = tool_call.function.name
    args = json.loads(tool_call.function.arguments)

    if func_name == "search_knowledge_base":
        # actual search logic
        result = knowledge_base.search(args["query"])
        return json.dumps(result)
    elif func_name == "send_email":
        # actual email sending logic
        result = email_service.send(
            to=args["to"],
            subject=args["subject"],
            body=args["body"]
        )
        return json.dumps({"status": "sent" if result else "failed"})
    else:
        return json.dumps({"error": f"Unknown function: {func_name}"})

def chat_with_tools(user_message: str, messages: list = None):
    """Conversation with Function Calling support"""
    if messages is None:
        messages = [
            {"role": "system", "content": "You are an enterprise assistant that can search the knowledge base and send emails."}
        ]

    messages.append({"role": "user", "content": user_message})

    # First call: model decides whether to use tools
    response = client.chat.completions.create(
        model="gpt-4",
        messages=messages,
        tools=tools
    )

    assistant_msg = response.choices[0].message

    # If the model wants to call tools
    if assistant_msg.tool_calls:
        messages.append(assistant_msg)

        for tool_call in assistant_msg.tool_calls:
            # Execute the tool
            result = execute_function_call(tool_call)
            # Return the result to the model
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result
            })

        # Second call: model generates final response based on tool results
        final_response = client.chat.completions.create(
            model="gpt-4",
            messages=messages
        )
        return final_response.choices[0].message.content

    # If the model responds directly
    return assistant_msg.content

Tool Definition Best Practices

  1. Descriptions must be precise: The model decides when to call a tool based on description; vague descriptions lead to incorrect invocations
  2. Parameters should have constraints: Use enum, required, and type constraints to reduce parameter errors by the model
  3. Functions should have a single responsibility: Don’t pack multiple operations into one function
  4. Return structured results: Tool returns should be easy for the model to understand; JSON is recommended
# ❌ Poor tool description
{"name": "do_stuff", "description": "Perform an operation", "parameters": {...}}

# ✅ Precise tool description
{
    "name": "cancel_meeting",
    "description": "Cancel a specified meeting; requires a meeting ID and cancellation reason",
    "parameters": {
        "properties": {
            "meeting_id": {"type": "string", "description": "Unique identifier of the meeting"},
            "reason": {"type": "string", "description": "Cancellation reason, will be notified to all participants"}
        },
        "required": ["meeting_id"]
    }
}

The ReAct Reasoning-Action Loop

The Core Idea of ReAct

ReAct (Reasoning + Acting) is a pattern that allows the model to alternate between Thought and Action. It does not generate a final answer in one shot; instead, it solves problems through a cycle of “Think → Act → Observe → Think…”

ReAct reasoning-action loop

A typical ReAct execution process:

User: "Look up Zhang San's department, then send an email to his manager"

Thought 1: I need to first find Zhang San's department information
Action 1: search_knowledge_base(query="Zhang San department")
Observation 1: "Zhang San belongs to the Teaching & Research Department, manager is Li Si ([email protected])"

Thought 2: Got the information; now I need to write an email to Li Si
Action 2: send_email(to="[email protected]", subject="Regarding Zhang San",
                      body="...")
Observation 2: {"status": "sent"}

Thought 3: Task complete
Final Answer: "Zhang San is in the Teaching & Research Department. I have sent an email to his manager Li Si."

Manually Implementing a ReAct Agent

class ReActAgent:
    def __init__(self, tools: dict, max_iterations: int = 10):
        self.tools = tools
        self.max_iterations = max_iterations

    def run(self, task: str) -> str:
        messages = [
            {"role": "system", "content": self._build_system_prompt()},
            {"role": "user", "content": task}
        ]

        for i in range(self.max_iterations):
            response = client.chat.completions.create(
                model="gpt-4",
                messages=messages,
                tools=self._format_tools()
            )

            msg = response.choices[0].message

            if msg.content and not msg.tool_calls:
                # Model gave a final answer
                return msg.content

            if msg.tool_calls:
                # Add the assistant's tool calls to history
                messages.append(msg)

                for tc in msg.tool_calls:
                    tool_name = tc.function.name
                    args = json.loads(tc.function.arguments)

                    # Execute the tool
                    result = self.tools[tool_name](**args)

                    # Add the observation result to history
                    messages.append({
                        "role": "tool",
                        "tool_call_id": tc.id,
                        "content": json.dumps(result)
                    })
                    print(f"  [Tool: {tool_name}({args}) → {result}]")

        return "ReAct loop reached maximum iterations"

    def _build_system_prompt(self) -> str:
        return """You are an intelligent assistant capable of using tools.
Follow the ReAct pattern: think first, then act, observe the result, then decide the next step.
If the task is complete, directly give the final answer."""

    def _format_tools(self) -> list:
        return [
            {
                "type": "function",
                "function": {
                    "name": name,
                    "description": func.__doc__ or "",
                    "parameters": get_schema(func)
                }
            }
            for name, func in self.tools.items()
        ]

Advantages and Limitations of ReAct

Advantages:

  • Observable: Every step of thought and action is traceable
  • Self-correcting: Strategies can be adjusted after observing erroneous results
  • Composable: Automatically combines multiple tools into a solution

Limitations:

  • Unpredictable number of iterations (may loop infinitely)
  • Multiple API calls increase latency and cost
  • Dependent on the quality of observations returned by tools

MCP Protocol & Tool Ecosystem

MCP protocol and tool ecosystem

Why MCP is Needed

Function Calling has a fundamental problem: tool definition and consumption are coupled. Every Agent developer needs to hard-code the tool’s JSON Schema in their own code. When a tool API is upgraded, all Agents that integrate that tool need manual updates.

Traditional Function Calling pattern:
  Agent A ──hard-coded Schema──→ web_search v1
  Agent B ──hard-coded Schema──→ web_search v1  ← duplicated definition
  Agent C ──hard-coded Schema──→ web_search v1  ← duplicated definition

MCP pattern:
  Agent A ──┐
  Agent B ──┼── MCP Client ──→ MCP Server (web_search)
  Agent C ──┘                    ↑
                          Tool provider defines Schema

The core idea of MCP (Model Context Protocol) is “whoever provides the tool defines the tool.” It shifts the responsibility for tool definition from the Agent (consumer) to the tool service (provider).

MCP Architectural Roles

RoleResponsibilityAnalogy
MCP ServerDeclares tools (name, description, parameters), executes tool logicUSB device
MCP ClientConnects to MCP Server, fetches tool definitions, sends invocation requestsUSB host controller
AgentUses MCP Client to get the tool list, decides which to invokeApplication

Building MCP Server & Client

MCP Server Example:

from mcp.server import Server, stdio_server
from mcp.types import Tool, TextContent

app = Server("web-search")

@app.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="web_search",
            description="Search the internet to get the latest information",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "Search keywords"},
                    "num_results": {"type": "integer", "default": 5}
                },
                "required": ["query"]
            }
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    if name == "web_search":
        results = search_engine.search(
            arguments["query"],
            num=arguments.get("num_results", 5)
        )
        return [TextContent(type="text", text=json.dumps(results))]
    raise ValueError(f"Unknown tool: {name}")

# Start Server via stdio
async def main():
    async with stdio_server() as streams:
        await app.run(streams[0], streams[1], app.create_initialization_options())

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

MCP Client Integration Example:

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def run_with_mcp_tools(user_query: str):
    server_params = StdioServerParameters(
        command="python",
        args=["web_search_server.py"]
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # Fetch tool definitions from MCP Server
            tools_result = await session.list_tools()
            tools = tools_result.tools

            # Convert to OpenAI-format tools parameter
            openai_tools = [
                {
                    "type": "function",
                    "function": {
                        "name": tool.name,
                        "description": tool.description,
                        "parameters": tool.inputSchema
                    }
                }
                for tool in tools
            ]

            # Standard Function Calling flow
            response = client.chat.completions.create(
                model="gpt-4",
                messages=[{"role": "user", "content": user_query}],
                tools=openai_tools
            )

            # If the model wants to call tools, execute via MCP Client
            if response.choices[0].message.tool_calls:
                for tc in response.choices[0].message.tool_calls:
                    result = await session.call_tool(
                        tc.function.name,
                        json.loads(tc.function.arguments)
                    )
                    # ... return result to the model

Engineering Value of MCP

  • Decoupling: Tool definition and service implementation are separated, allowing independent iteration
  • Dynamic Discovery: Agent automatically pulls the latest tool list at startup, zero maintenance cost
  • Ecosystem Effect: Third parties can provide standardized MCP Servers; Agent developers only need to integrate an MCP Client
  • Multiple Transport Protocols: Supports stdio (local process communication) and HTTP/SSE (remote communication)

Reflection & Self-Correction

Why Reflection is Needed

LLM-generated content is not always usable. It may:

  • Silently “correct” variable names in code causing runtime errors
  • Continue reasoning based on incorrect “facts,” producing cascading errors
  • Forget earlier constraints somewhere in a very long output

The core idea of Reflection is: give the model an opportunity to review and evaluate the complete content it has already generated, so it can discover and correct errors.

Two Patterns of Self-Feedback

Pattern 1: Single-Step Instruction Reflection

In a single call, instruct the model via prompt to generate an answer and reflect at the same time:

prompt_with_reflection = """
## Task
1. Polish the language expression of the following course content, output the full polished content.
2. Reflect on the polished content:
   - Does it comply with writing standards
   - Apart from language expression, was any other content accidentally modified
   Output the reflection results and modification suggestions.
3. Revise the course content based on the suggestions, output the revised full content.

## Course Draft
{original_content}
"""

Advantages: Simple implementation, completes in one call. Disadvantages: The model is prone to self-verification with the same thinking bias, falling into “self-justification.”

Pattern 2: Two-Step “Generate-Review”

Separate generation and review into two independent calls:

def generate_and_review(content: str) -> str:
    # Step 1: Generate
    draft_resp = client.chat.completions.create(
        model="gpt-4",
        messages=[{
            "role": "system",
            "content": "You are a course writer. Polish the following content to make it more engaging."
        }, {
            "role": "user",
            "content": content
        }]
    )
    draft = draft_resp.choices[0].message.content

    # Step 2: Review (use a different system prompt!)
    review_resp = client.chat.completions.create(
        model="gpt-4",
        messages=[{
            "role": "system",
            "content": """You are a strict technical reviewer.
Please compare the [Original Content] and [Polished Content]:
- If only wording was modified and technical content (like code) is fully identical → reply "Pass"
- If technical content was modified → reply "Fail", indicate the specific location"""
        }, {
            "role": "user",
            "content": f"Original Content:\n{content}\n\nPolished Content:\n{draft}"
        }]
    )

    # Step 3: If not passed, feed the review result back to the writing Agent for correction
    review = review_resp.choices[0].message.content
    if "Fail" in review:
        # ... feed the review back to the writing Agent for correction
        pass

    return draft

Core advantage: The reviewer Agent’s perspective differs from the writer Agent’s, avoiding role bias. You can even set up multiple specialized reviewer Agents — fact review, logic review, style review, safety review.

External Feedback

Self-feedback has inherent limitations: the model cannot verify the correctness of content in a real environment. The idea of external feedback is to execute the generated results in a real environment and use objective facts to validate.

def generate_code_and_validate(spec: str) -> str:
    # Step 1: Generate code
    code_resp = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": f"Write Python code based on the following requirements:\n{spec}"}]
    )
    code = extract_code(code_resp.choices[0].message.content)

    # Step 2: External execution validation
    import subprocess, tempfile
    with tempfile.NamedTemporaryFile(suffix=".py", mode="w") as f:
        f.write(code)
        f.flush()
        result = subprocess.run(
            ["python", f.name],
            capture_output=True,
            text=True,
            timeout=30
        )

    # Step 3: Feed errors back to the model for correction
    if result.returncode != 0:
        fix_prompt = f"""The following code produced an error when executed:

Code:
{code}

Error:
{result.stderr}

Please fix the code and output the complete corrected version."""
        fix_resp = client.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": fix_prompt}]
        )
        return fix_resp.choices[0].message.content

    return code

Application scenarios for external feedback:

  • Code execution validation: Run code with a code interpreter, catch runtime errors
  • JSON Schema validation: Use libraries like Pydantic to validate structured output
  • Numerical computation verification: Use calculator tools to verify mathematical results
  • Visualization rendering verification: After generating a chart, let the model “see” the rendered result for visual inspection

Plan & Execute Pattern

Why Explicit Planning is Needed

When having an Agent directly execute complex tasks, common problems are:

  • Forgetting: “Forgetting” earlier constraints when processing later steps
  • Cascading errors: Early errors become the foundation for subsequent reasoning, with error amplification
  • Structural oversight: The model tends toward linear processing and fails to identify parallel/dependency relationships in tasks

The core idea of the Plan & Execute pattern: Plan first, then execute — first formulate a complete action plan, confirm it through review, then execute step by step.

Plan Mode Implementation

from typing import List
from pydantic import BaseModel

class PlanStep(BaseModel):
    step_id: int
    description: str
    dependencies: List[int] = []  # IDs of dependent steps
    tool: str = ""               # tool to use
    expected_output: str = ""    # expected output

class ExecutionPlan(BaseModel):
    goal: str
    steps: List[PlanStep]

def plan_and_execute(task: str) -> str:
    # Phase 1: Formulate the plan
    plan_prompt = f"""
    You are a project planning expert. Please create a detailed execution plan for the following task.

    Requirements:
    1. Decompose the task into concrete steps
    2. Mark dependencies between steps
    3. State the expected output for each step

    Task: {task}

    Please output the plan in JSON format."""

    plan_resp = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": plan_prompt}],
        response_format={"type": "json_object"}
    )
    plan = ExecutionPlan.model_validate_json(
        plan_resp.choices[0].message.content
    )

    # Phase 2: Execute based on dependencies
    results = {}
    executed = set()

    while len(executed) < len(plan.steps):
        for step in plan.steps:
            if step.step_id in executed:
                continue
            # Check if all dependencies have been executed
            if all(dep in executed for dep in step.dependencies):
                # Execute the step
                result = execute_step(step, results)
                results[step.step_id] = result
                executed.add(step.step_id)

    # Phase 3: Summarize results
    return summarize_results(plan, results)

Fixed Workflows: Pipeline Pattern

When a task’s steps are deterministic and repeatable, they should be solidified into a pipeline:

Input → Step 1 → Step 2 → Step 3 → ... → Output
class Pipeline:
    """Fixed pipeline: each step's output is the next step's input"""

    def __init__(self):
        self.steps = []

    def add_step(self, name: str, func):
        self.steps.append({"name": name, "func": func})

    def run(self, input_data):
        result = input_data
        for step in self.steps:
            print(f"  [Execute] {step['name']}")
            result = step["func"](result)
        return result

# Example: Document processing pipeline
pipeline = Pipeline()
pipeline.add_step("Parse PDF", parse_pdf_to_text)
pipeline.add_step("Clean Text", clean_text)
pipeline.add_step("Split Sections", split_sections)
pipeline.add_step("Vectorize", vectorize_chunks)
pipeline.add_step("Store Index", store_to_vectordb)

pipeline.run("document.pdf")

Workflow Orchestration Patterns

Five Core Workflow Patterns

Complex tasks require organizing Agent nodes according to specific topologies. Here are five core patterns:

Branching (Router)

Judge the task type at the entry node and route to different processing paths.

Workflow orchestration patterns

def router_agent(user_input: str):
    """Route to different processing pipelines based on intent"""
    classify_prompt = f"""
    Analyze the type of the following user request and reply with a single word:
    - code_review: check/validate code
    - style_review: polish/optimize language
    - fact_check: verify factual/conceptual accuracy

    Request: {user_input}
    Type:"""

    intent = client.chat.completions.create(
        model="gpt-4o-mini",  # use a lightweight model to save cost
        messages=[{"role": "user", "content": classify_prompt}],
        temperature=0
    ).choices[0].message.content.strip()

    pipelines = {
        "code_review": code_review_pipeline,
        "style_review": style_review_pipeline,
        "fact_check": fact_check_pipeline
    }
    return pipelines.get(intent, default_pipeline)(user_input)

Parallel Execution

Simultaneously dispatch mutually independent subtasks, then aggregate at the end.

              ┌→ Code Check ──┐
User Input → Split ─┼→ Fact Check ──┼→ Merge → Summary Report
              └→ Style Check ─┘
import asyncio

async def parallel_review(notebook_content: str):
    """Run three reviews on course content in parallel"""
    tasks = [
        asyncio.create_task(check_code(notebook_content)),
        asyncio.create_task(check_facts(notebook_content)),
        asyncio.create_task(check_style(notebook_content))
    ]

    code_result, fact_result, style_result = await asyncio.gather(*tasks)

    # Summarize
    return generate_summary_report(code_result, fact_result, style_result)

Mixture-of-Agents (MoA)

Multiple different models process the same task, and an aggregator synthesizes the best result.

             ┌→ Model A (good at reasoning) ──┐
User Question → Split ─┼→ Model B (good at creativity) ──┼→ Aggregator → Best Answer
             └→ Model C (good at accuracy) ──┘

The core finding of MoA is model “Collaborativeness”: when a model can reference other models’ outputs, it often generates higher-quality responses.

def mixture_of_agents(task: str):
    """MoA implementation: multiple models + aggregation"""
    # Layer 1: Proposers generate in parallel
    proposers = ["gpt-4", "claude-3-opus", "gemini-pro"]
    proposals = []

    for model in proposers:
        resp = client.chat.completions.create(
            model=model,
            messages=[{"role": "user", "content": task}]
        )
        proposals.append(resp.choices[0].message.content)

    # Layer 2: Aggregator synthesizes
    aggregator_prompt = f"""
    Below are {len(proposals)} responses to the same question. Please synthesize their strengths
    and produce a single optimal answer.

    {format_proposals(proposals)}

    Synthesized answer:"""

    final = client.chat.completions.create(
        model="gpt-4",  # use the strongest model for aggregation
        messages=[{"role": "user", "content": aggregator_prompt}]
    )
    return final.choices[0].message.content

Human-in-the-Loop (HITL)

Introduce human review at key nodes, forming an “AI executes → human approves → AI continues” loop.

def hitl_workflow(task: str):
    """Human-in-the-loop workflow"""
    plan = generate_plan(task)
    print(f"Execution Plan:\n{format_plan(plan)}")

    approval = input("Approve this plan? (y/n): ")
    if approval.lower() != 'y':
        return "Task cancelled"

    for step in plan.steps:
        result = execute_step(step)
        print(f"Step {step.step_id} complete: {result['summary']}")

        if step.get("requires_review"):
            review = input(f"Please review the step result (approve/modify/reject): ")
            if review == "reject":
                print("Step rejected, re-executing...")
                result = execute_step(step, feedback=review)

    return generate_final_output()

Pattern Selection Methodology

PatternSuitable ScenariosUnsuitable Scenarios
PipelineFixed processes, linear stepsTasks requiring dynamic decisions
BranchingMultiple input types needing different processingTasks requiring simultaneous handling of multiple aspects
ParallelMutually independent subtasks, pursuing efficiencyTasks with dependency chains
MoAHigh-quality requirements, creative tasksCost-sensitive routine tasks
HITLHigh-risk decisions, compliance requirementsReal-time systems with low-latency requirements
Plan & ExecuteVariable processes, new tasks requiring explorationHighly repetitive, deterministic tasks

Best practice is the “explore-solidify” hybrid approach: first use Plan & Execute to discover the optimal solution, then solidify it into a Pipeline for mass production.


Hierarchical Collaboration Pattern

Leader-Worker Architecture

Hierarchical collaboration (Hierarchical/Team Leader Pattern) is the most intuitive multi-Agent collaboration pattern. It mimics the organizational structure of “project manager + team members”:

Hierarchical collaboration pattern

The Leader Agent is responsible for:

  1. Receiving and understanding the top-level task
  2. Decomposing it into subtasks and assigning them to appropriate Workers
  3. Tracking overall progress
  4. Aggregating Worker outputs

Worker Agents each have domain-specific expertise and focus on executing their assigned subtasks.

Implementing Hierarchical Collaboration via Handoff

class LeaderWorkerSystem:
    """Hierarchical collaboration system implementation"""

    def __init__(self):
        self.workers = {
            "instructional_designer": self._create_worker(
                "You are an instructional designer, skilled at designing course outlines and learning paths."
            ),
            "data_scientist": self._create_worker(
                "You are a data scientist, skilled at writing Python data analysis code and case studies."
            ),
            "content_writer": self._create_worker(
                "You are a content writer, skilled at transforming technical content into engaging course scripts."
            )
        }
        self.leader = self._create_leader()

    def _create_leader(self):
        return {
            "system_prompt": """You are a course project manager. Your responsibilities are:
1. Analyze requirements and decompose them into subtasks
2. Assign tasks to the appropriate experts
3. Integrate each expert's output into a complete course
Available expert team: instructional_designer, data_scientist, content_writer""",
            "tools": [
                {
                    "type": "function",
                    "function": {
                        "name": "delegate_to_worker",
                        "description": "Delegate a subtask to a specified expert",
                        "parameters": {
                            "type": "object",
                            "properties": {
                                "worker": {
                                    "type": "string",
                                    "enum": list(self.workers.keys()),
                                    "description": "The expert receiving the task"
                                },
                                "task": {
                                    "type": "string",
                                    "description": "Specific subtask description"
                                }
                            },
                            "required": ["worker", "task"]
                        }
                    }
                }
            ]
        }

    def run(self, project_brief: str) -> str:
        """Execute a complete course development project"""
        messages = [
            {"role": "system", "content": self.leader["system_prompt"]},
            {"role": "user", "content": project_brief}
        ]

        # Leader loop
        while True:
            response = client.chat.completions.create(
                model="gpt-4",
                messages=messages,
                tools=self.leader["tools"]
            )
            msg = response.choices[0].message

            if msg.content and not msg.tool_calls:
                return msg.content  # Final aggregated output

            if msg.tool_calls:
                messages.append(msg)
                for tc in msg.tool_calls:
                    if tc.function.name == "delegate_to_worker":
                        args = json.loads(tc.function.arguments)
                        # Call Worker to execute the subtask
                        worker_result = self._run_worker(
                            args["worker"], args["task"]
                        )
                        messages.append({
                            "role": "tool",
                            "tool_call_id": tc.id,
                            "content": worker_result
                        })

    def _run_worker(self, worker_name: str, task: str) -> str:
        worker = self.workers[worker_name]
        resp = client.chat.completions.create(
            model="gpt-4",
            messages=[
                {"role": "system", "content": worker},
                {"role": "user", "content": task}
            ]
        )
        return resp.choices[0].message.content

Pros and Cons of Hierarchical Collaboration

Advantages:

  • Clear structure, each Agent has well-defined responsibilities
  • The Leader controls the big picture, preventing deviation from the goal
  • Each Worker has an independent context window, enabling more focus
  • Supports parallel task delegation

Disadvantages:

  • Workers do not communicate directly with each other; information transfer has delay/distortion
  • The Leader becomes a single point of bottleneck
  • The final assembled output may lack a sense of overall coherence

Blackboard Collaboration Pattern

Decentralized Co-Creation

The Blackboard pattern (Blackboard/Co-creation Pattern) mimics the working style of “experts gathered around a whiteboard brainstorming.” It has no centralized coordinator; all Agents equally read from and write to a shared space:

Blackboard collaboration pattern

Blackboard Pattern Implementation

class BlackboardSystem:
    """Blackboard collaboration system"""

    def __init__(self, agents: dict, max_rounds: int = 3):
        self.agents = agents
        self.max_rounds = max_rounds
        self.blackboard = []  # shared space

    def run(self, problem: str) -> str:
        # Write the problem to the blackboard
        self.blackboard.append({"source": "user", "content": problem})

        for round_num in range(self.max_rounds):
            print(f"\n=== Round {round_num + 1} ===")
            new_contributions = []

            # All Agents read the blackboard in parallel and contribute
            for name, agent_config in self.agents.items():
                contribution = self._agent_contribute(
                    name, agent_config, self.blackboard
                )
                if contribution:
                    new_contributions.append({
                        "source": name,
                        "content": contribution
                    })

            # Write new contributions to the blackboard
            self.blackboard.extend(new_contributions)

            # Check if consensus has been reached
            if self._check_consensus():
                break

        return self._synthesize_final_answer()

    def _agent_contribute(self, name, config, blackboard):
        """Each Agent reads the blackboard and contributes their thoughts"""
        board_text = self._format_blackboard(blackboard)

        prompt = f"""You are a {config['role']}.

Current content on the shared blackboard:
{board_text}

Based on the existing discussion, offer your insights, additions, challenges, or new ideas.
If an existing proposal is already well-developed, you may agree and explain why."""

        resp = client.chat.completions.create(
            model="gpt-4",
            messages=[
                {"role": "system", "content": config["system_prompt"]},
                {"role": "user", "content": prompt}
            ]
        )
        return resp.choices[0].message.content

    def _format_blackboard(self, blackboard):
        return "\n\n".join([
            f"[{entry['source']}]: {entry['content']}"
            for entry in blackboard
        ])

    def _check_consensus(self):
        """Check whether the latest blackboard contributions have formed a consensus"""
        # Implement consensus detection logic
        pass

    def _synthesize_final_answer(self):
        """Synthesize the final solution from blackboard content"""
        pass

Blackboard Pattern vs. Hierarchical Pattern

DimensionHierarchical PatternBlackboard Pattern
Control ModeCentralized (Leader controls)Decentralized (equal participation)
CommunicationStar (Leader↔Worker)Fully connected (All Agents↔Blackboard)
Decision MechanismLeader decidesEmergent consensus
Suitable TasksClear goals, decomposableOpen exploration, need collective intelligence
EfficiencyHigh (parallel + controllable)Lower (multiple rounds of discussion)
CreativityLimited (constrained by Leader’s perspective)High (collision of ideas produces new ones)
CostMediumHigh (all Agents participate every round)

Collaboration Pattern Selection Methodology

Learning from the Real World

Excellent multi-Agent system design comes from observing and distilling real-world team collaboration. Rather than memorizing abstract pattern names, go into the business and observe how human expert teams accomplish similar tasks.

Three dimensions of observation:

Collaboration pattern selection methodology

Hybrid Pattern Design

Real projects rarely use a single pattern. Hybrid designs are common:

                    ┌─────────────┐
                    │   Leader    │  ← Hierarchical pattern
                    └──────┬──────┘
           ┌───────────────┼───────────────┐
           ▼               ▼               ▼
    ┌──────────┐    ┌──────────┐    ┌──────────┐
    │Instructional│  │ Content   │    │ Review    │
    │ Designer   │  │ Writer    │    │ Leader    │
    │  Worker    │  │  Worker   │    └─────┬────┘
    └──────────┘    └──────────┘           │
                                  ┌────────┼────────┐
                                  ▼        ▼        ▼
                             ┌──────┐ ┌──────┐ ┌──────┐
                             │ Code │ │ Fact │ │Style │  ← Parallel pattern
                             │Check │ │Check │ │Check │
                             └──────┘ └──────┘ └──────┘
                                 │        │        │
                                 └────────┼────────┘

                                    ┌──────────┐
                                    │ Summary  │
                                    │ Report   │
                                    └──────────┘

Cost Awareness

The token consumption of multi-Agent systems is typically 3-5x that of a single Agent. Trade-offs need to be considered during design:

def estimate_cost(num_agents: int, avg_tokens_per_agent: int,
                  rounds: int = 1, price_per_1k: float = 0.01):
    """Estimate token cost for a multi-Agent system"""
    total_tokens = num_agents * avg_tokens_per_agent * rounds
    return total_tokens * price_per_1k / 1000

# Example: 5 Agents, 2000 tokens each, 3 rounds of blackboard discussion
cost = estimate_cost(5, 2000, 3)
print(f"Estimated cost: ${cost:.2f}")
# Actual numbers may be higher because blackboard content is also retransmitted

Short-Term Memory Management

Statelessness: The Root of the Problem

Large language models are fundamentally stateless. Each API call is independent — it won’t remember the content of the previous conversation, your preferences, or previously reached consensus.

# These two calls are completely independent of each other
response1 = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "My name is John"}]
)
# Response: "Hello John! How can I help you?"

response2 = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "What's my name?"}]
)
# Response: "Sorry, I don't know your name because we haven't spoken before."

The solution: maintain a conversation history list and send the complete history each time.

class ConversationBuffer:
    """Simplest short-term memory: save the full conversation history"""

    def __init__(self, system_prompt: str = ""):
        self.messages = []
        if system_prompt:
            self.messages.append({"role": "system", "content": system_prompt})

    def chat(self, user_input: str) -> str:
        self.messages.append({"role": "user", "content": user_input})
        response = client.chat.completions.create(
            model="gpt-4",
            messages=self.messages
        )
        reply = response.choices[0].message.content
        self.messages.append({"role": "assistant", "content": reply})
        return reply

Context Window Pressure

As conversation turns increase, the full-history approach faces three fatal problems:

  1. Exceeding the context window: History length exceeds model limits → program error
  2. Cost spiral: Re-sending the entire history every call → linear growth in token consumption
  3. Attention dilution: In long contexts, the model’s ability to process information in the middle portion significantly degrades

Three Memory Management Strategies

Strategy 1: Fixed Window Truncation (Context Truncation)

Keep only the most recent N conversation turns or N tokens.

class TruncationMemory:
    def __init__(self, max_tokens: int = 4000):
        self.max_tokens = max_tokens
        self.messages = []

    def add_and_truncate(self, role: str, content: str):
        self.messages.append({"role": role, "content": content})

        # Delete the oldest messages until total tokens are within the limit
        while self._total_tokens() > self.max_tokens:
            self.messages.pop(0)  # delete the oldest non-system message

    def _total_tokens(self):
        return sum(count_tokens(m["content"]) for m in self.messages)

Advantages: Extremely simple implementation, low computational overhead. Disadvantages: If key information was in early conversations, the Agent will “forget” it after truncation.

Strategy 2: Rolling Summary

Before forgetting, first extract the key points.

Conversation History: [msg1, msg2, msg3, msg4, msg5, msg6, msg7, msg8]
                          ↓ compress the first half
         [Summary(m1-m4), msg5, msg6, msg7, msg8]
                          ↓ continue compressing
         [Summary(m1-m6), msg7, msg8]
class RollingSummaryMemory:
    def __init__(self, summary_trigger_tokens: int = 3000):
        self.summary_trigger = summary_trigger_tokens
        self.messages = []
        self.summary = ""

    def add_message(self, role: str, content: str):
        self.messages.append({"role": role, "content": content})

        if self._total_tokens() > self.summary_trigger:
            self._compress()

    def _compress(self):
        """Compress the first half of the conversation into a summary"""
        split_point = len(self.messages) // 2
        to_compress = self.messages[:split_point]
        remaining = self.messages[split_point:]

        compress_prompt = f"""
        Please summarize the following conversation history into a concise paragraph,
        preserving key information:

        Conversation:
        {format_messages(to_compress)}

        Summary:"""

        resp = client.chat.completions.create(
            model="gpt-4o-mini",  # use a lightweight model for summarization
            messages=[{"role": "user", "content": compress_prompt}]
        )
        self.summary = resp.choices[0].message.content

        # Replace compressed messages with the summary
        self.messages = [
            {"role": "system", "content": f"Conversation History Summary:\n{self.summary}"}
        ] + remaining

    def _total_tokens(self):
        return sum(count_tokens(m["content"]) for m in self.messages)

Advantages: Compresses length while preserving core information, maintaining long-term coherence. Disadvantages: Extra API call cost; summary quality directly affects subsequent conversations.

Strategy 3: Vector-Based Retrieval

The most intelligent approach: store conversation history in a vector database and retrieve the most relevant memories on demand.

class VectorBasedMemory:
    def __init__(self):
        self.conversations = []  # full conversation records
        self.embeddings = []     # vector for each conversation turn
        self.embed_model = "text-embedding-3-small"

    def store_conversation(self, user_msg: str, assistant_msg: str):
        """Store a conversation turn and vectorize it"""
        conversation_text = f"User: {user_msg}\nAssistant: {assistant_msg}"
        self.conversations.append(conversation_text)

        vec = client.embeddings.create(
            model=self.embed_model,
            input=conversation_text
        )
        self.embeddings.append(vec.data[0].embedding)

    def retrieve_relevant(self, current_query: str, top_k: int = 5):
        """Retrieve the history conversations most relevant to the current query"""
        query_vec = client.embeddings.create(
            model=self.embed_model,
            input=current_query
        ).data[0].embedding

        # Calculate similarity
        similarities = [
            np.dot(query_vec, mem_vec) /
            (np.linalg.norm(query_vec) * np.linalg.norm(mem_vec))
            for mem_vec in self.embeddings
        ]

        top_indices = np.argsort(similarities)[-top_k:][::-1]
        return [self.conversations[i] for i in top_indices]

Advantages: Fundamentally breaks free from context window length limits, semantically precise matching. Disadvantages: Highest system complexity, introduces Embedding models and vector databases.

Strategy Selection Guide

ScenarioRecommended Strategy
ChatbotsFixed window truncation (simple and effective)
Customer service Q&A (information value decays quickly over time)Fixed window truncation
Long-form content creation / project planningRolling summary
Personalized assistant / long-term interactionVector-based retrieval
Best practiceHybrid: summary + vector retrieval

Long-Term Memory & Vector Storage

From Passive Context to Active Memory Management

A truly intelligent Agent should not merely passively receive pre-processed context; it should be able to actively manage its own memory — deciding for itself when to remember something and when to recall something.

This requires providing the Agent with two core tools:

# Memory management tools available to the Agent
memory_tools = [
    {
        "type": "function",
        "function": {
            "name": "record_to_memory",
            "description": "Store important information in long-term memory. Use when the user explicitly states a preference, provides key information, or completes an important decision.",
            "parameters": {
                "type": "object",
                "properties": {
                    "content": {
                        "type": "string",
                        "description": "The content to remember"
                    },
                    "category": {
                        "type": "string",
                        "enum": ["preference", "fact", "decision", "context"],
                        "description": "Memory category"
                    }
                },
                "required": ["content"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "retrieve_from_memory",
            "description": "Retrieve relevant information from long-term memory. Use when needing to recall user preferences, historical decisions, or previously discussed content.",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "Retrieval query"
                    },
                    "category": {
                        "type": "string",
                        "description": "Optional, limit retrieval to a specific memory category"
                    }
                },
                "required": ["query"]
            }
        }
    }
]

Short-Term Memory vs. Long-Term Memory

┌─────────────────────────────────────────────────────┐
│               Memory System Architecture              │
├─────────────────────────────────────────────────────┤
│                                                     │
│  Short-term Memory                                   │
│  ┌───────────────────────────────────────────────┐  │
│  │ Storage: Conversation buffer (messages list)  │  │
│  │ Management: Truncation / Summarization        │  │
│  │ Lifecycle: Current session                    │  │
│  │ Responsibility: Maintain conversational        │  │
│  │ coherence, remember "what we were just          │  │
│  │ talking about"                                 │  │
│  └───────────────────────────────────────────────┘  │
│                                                     │
│  Long-term Memory                                    │
│  ┌───────────────────────────────────────────────┐  │
│  │ Storage: Vector DB + metadata store           │  │
│  │ Management: Vector retrieval + tool-based     │  │
│  │ invocation                                    │  │
│  │ Lifecycle: Cross-session                      │  │
│  │ Responsibility: Persist key information,       │  │
│  │ support intelligent cross-session retrieval    │  │
│  └───────────────────────────────────────────────┘  │
│                                                     │
└─────────────────────────────────────────────────────┘

Memory Management Best Practices

  1. Be selective about what to remember: More memory is not always better. Low-value information interferes with subsequent retrieval. Establish a write admission mechanism — only write when the user explicitly requests it or the information’s importance exceeds a threshold.

  2. Continuous governance: Memory is a dynamic data asset. Regularly clean up outdated information, merge duplicate entries, and verify factual accuracy. Provide users with an interface to manage their memory (view, modify, delete).

  3. Contextualized application: Different scenarios have different memory needs. In a course documentation workflow, personalized preferences should not be recorded; for product fact information (API parameters, feature limitations, etc.), it should be recorded and periodically reviewed for validity.


Skill System Design

Evolution from Prompt to Skill

A carefully crafted Prompt has high value, but it only works within the current session. When the session ends, the knowledge is scattered. Skill elevates Prompts into reusable, version-managed, team-shareable professional knowledge modules.

Evolution path:

Ad-hoc Prompt ("Help me review this course")
    ↓ Problem: have to rewrite every time, inconsistent standards
Fixed Prompt (stored in chat history)
    ↓ Problem: scattered, hard to find, impossible to collaborate
Standalone File (course-review.md)
    ↓ Problem: file bloat, hard to maintain
Knowledge Base Directory (course-review/)
    ↓ Problem: still need to manually tell the Agent what to do
Skill (SKILL.md + resource files + scripts)

Skill Structure

A standard Skill consists of YAML frontmatter + Markdown body:

---
name: course-review
description: |
  Review course content for technical accuracy, code correctness, and pedagogical quality.
  Use this skill when the user requests reviewing, auditing, or evaluating existing courses or training materials.
---

# Course Review Skill

## Review Process
1. Extract the Notebook directory structure to understand the overall chapter layout
2. Review each section chapter by chapter, checking against the following directions:
   - Code executability (see [code-quality.md](code-quality.md))
   - Content accuracy (see [content-accuracy.md](content-accuracy.md))
   - Teaching style (see [style-guide.md](style-guide.md))
   - Outdated APIs (see [outdated-api.md](outdated-api.md))
3. Aggregate review results and generate a report in the output format

## Anti-Pattern Checklist
- ❌ Do not modify variable names or API version numbers in code
- ❌ Do not skip any check item
- ❌ Do not introduce new technical concepts during the review

## Output Format
For each check, provide: Pass/Fail/Needs Human Review + location + modification suggestion

Directory structure:

course-review/
├── SKILL.md              # Main instruction entry point
├── code-quality.md       # Code executability check items
├── content-accuracy.md   # Factual accuracy check items
├── style-guide.md        # Teaching style positive and negative examples
├── outdated-api.md       # Outdated API reference table
└── scripts/
    ├── extract_toc.py    # Extract Notebook table of contents
    └── validate_code.py  # Automatically execute code validation

Skill vs RAG

Many people confuse Skill and RAG. Their core distinction is:

RAGSkill
Problem Solved”The model doesn’t know a certain fact""The model doesn’t know how to do something”
Information TypeFactual knowledge (document content, product parameters)Procedural knowledge (processes, standards, judgment rules)
Trigger MethodRetrieved and injected into contextSelected and expanded (Agent decides whether to activate)
Loading MethodOne-shot injection of retrieval resultsProgressive disclosure (load sub-files on demand step by step)
LifecycleEach query is independently retrievedCross-session persistent, version-manageable

Five-Step Method for Writing High-Quality Skills

Step 1: Determine if it's worth doing
  ├─ Does this task involve "expert intuition"? (Experts do it well but beginners easily miss edge cases)
  ├─ Is this task sufficiently complex? (If it can be done in 3 clicks in a GUI, skip it)
  └─ Will this task be executed repeatedly? (If only once, skip it)

Step 2: Extract what to write
  ├─ Extract the expert's decision tree, not just steps
  ├─ Inject anti-pattern checks ("what pitfalls must absolutely be avoided")
  ├─ Template pattern: provide standardized output templates
  └─ Examples pattern: use examples instead of text descriptions

Step 3: Write good instructions
  ├─ Concise: every sentence should be worth its token cost
  ├─ Freedom matching: constraint level matches task risk
  │   ├─ Low freedom (database migration): precise scripts
  │   ├─ Medium freedom (report generation): pseudo-code / parameterized
  │   └─ High freedom (code review): text instructions
  └─ Progressive disclosure: keep the main file lean, load details on demand

Step 4: Equip with proper tools
  ├─ Workflow: traceable Checklists
  ├─ Feedback loop: Run → Check → Fix → Repeat
  ├─ High-risk operations: verify the plan before executing
  └─ AI-friendly scripts: structured state + fix hints + graceful degradation + idempotent safety

Step 5: Verify and iterate
  ├─ Phase 1: Establish an evaluation baseline (have evaluation before writing the Skill)
  ├─ Phase 2: Extract the Skill (use AI to summarize repeatedly provided information)
  └─ Phase 3: Dual-Agent testing iteration (designer vs. user)

Skill as Code

Treat Skills as code and enjoy the methodology of code engineering:

  • Version management: Skill directories are checked into Git; every modification has a history
  • Code review: New/modified Skills require PR review
  • CI/CD: Skill changes trigger evaluation pipelines to ensure no regression
  • Community sharing: Share and reuse Skills like open-source libraries

Progressive Disclosure & Skill as Code

The Design Philosophy of Progressive Disclosure

The core contradiction faced by traditional Prompt engineering: too much information → context crowding, attention dilution; too little information → Agent lacks sufficient knowledge to support decisions.

Progressive Disclosure is a design pattern that resolves this contradiction:

Layer 1: Skill list (visible when Agent starts)
  ↓ Agent chooses to activate a Skill
Layer 2: SKILL.md (loaded after Skill activation)
  ↓ Agent executes a certain step
Layer 3: Sub-files (load corresponding resources on demand)
  ↓ Need to verify a specific technical detail
Layer 4: Script execution (provides deterministic results)

Design principles:

  • Maintain a flat structure, avoid deep nested references
  • SKILL.md directly links all resource files, ensuring “one step” reachability
  • Don’t stuff everything into SKILL.md — it’s just the entry point

AI-Friendly Script Design

The quality of tool script output directly impacts Agent performance. Four key principles:

1. Structured status feedback — Output JSON instead of free text:

# ❌ Poor output
print("Error: exit code 1")
print("Could not find module 'openpyxl'")

# ✅ AI-friendly output
print(json.dumps({
    "status": "failed",
    "error_code": "MODULE_NOT_FOUND",
    "missing_module": "openpyxl",
    "fix_hint": "Run pip install openpyxl to install the missing dependency",
    "fallback_available": True,
    "affected_cells": [42, 43, 44]
}))

2. Errors include fix hints — Tell the Agent “how to fix” rather than just “what went wrong.”

3. Graceful degradation over crashing — Provide defaults and continue where possible.

4. Idempotent and safe — Support repeated execution without side effects:

ScenarioNon-idempotent (Dangerous)Idempotent (Safe)
File writeAppend content each timeClear first, then write
Database operationINSERT each timeUse UPSERT
API callCreate a new resource each timeUse idempotency keys

Evaluation Framework Design

Evaluation framework design

Why “Feels Good Enough” is Unreliable

Optimizing an Agent based purely on subjective feeling leads to:

  • Hard to quantify: “Feels better” cannot serve as an engineering decision basis
  • Lack of standards: Evaluation criteria may drift between different testers or different time points
  • Cannot reproduce: Cannot systematically regression test to ensure new changes haven’t broken old functionality

Evaluation-Driven Development elevates evaluation from the end of the development cycle to the core:

         ┌──────────────────┐
         │  Evaluation =     │
         │  Quality Ruler    │
         └────────┬─────────┘

    ┌─────────────┼─────────────┐
    ▼             ▼             ▼
What can be    The faster and  Determines the
measured can   more accurate    upper bound of
be improved    the feedback,    product capability
               the more efficient
               the improvement

End-to-End Evaluation

End-to-end evaluation focuses on the final output, answering “Is this Agent good for users?”

Evaluation metrics fall into two categories:

TypeDescriptionExample
Objective metricsCan be directly judged by code rulesCan the code run, does the format match Schema, is the word count within range
Subjective metricsInvolve semantic and quality judgmentsContent accuracy, pedagogical effectiveness, whether language style complies with rules

White-box Evaluation

When Agent processes become complex, end-to-end evaluation cannot pinpoint specific issues. White-box evaluation advocates going deep inside the system, designing separate evaluation systems for key components.

End-to-End Evaluation (only looks at final output):
  Agent overall → Score 4.2/5 → but doesn't know where the problem is

White-box Evaluation (examines intermediate stages):
  ┌────────────┐    ┌────────────┐    ┌────────────┐
  │ Concept     │    │ Code Gen   │    │ Style       │
  │ Explanation │    │            │    │ Adjustment  │
  │ Score 3.1/5 │    │ Score 4.8/5│    │ Score 4.5/5 │
  └────────────┘    └────────────┘    └────────────┘
       ↑ Bottleneck is here!

Core advantages of white-box evaluation:

  • Clear signal: Uninterfered improvement signal, focus on the real bottleneck
  • Fast iteration: Only need to test a single component, no need to run the entire process
  • Precise optimization: Each change’s effect can be precisely measured

LLM-as-Judge

Using LLMs as Evaluators

Have another LLM play the role of “evaluation expert,” automatically scoring based on defined metrics and scoring rubrics.

def llm_judge_evaluate(generated_output: str, criteria: dict):
    """Use an LLM as an evaluator"""
    judge_prompt = f"""
    You are a professional course quality evaluator. Please score according to the following criteria:

    Evaluation Criteria:
    {json.dumps(criteria, indent=2, ensure_ascii=False)}

    Content to Evaluate:
    {generated_output}

    Please output the scoring result in JSON format:
    {{
        "scores": {{
            "accuracy": <1-5>,
            "clarity": <1-5>,
            "engagement": <1-5>
        }},
        "overall": <1-5>,
        "comments": "Overall evaluation"
    }}"""

    resp = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": judge_prompt}],
        response_format={"type": "json_object"}
    )
    return json.loads(resp.choices[0].message.content)

Biases of LLM Evaluators

When using LLMs as evaluators, you must be alert to their inherent biases:

Bias TypeManifestationMitigation
Style BiasPrefers a certain code/writing styleExplicit scoring criteria, don’t rely on “taste”
Length BiasThinks longer responses are “more complete”Include “conciseness” as a scoring dimension
”Yes-Man” BiasTends to give positive evaluationsUse comparative evaluation (A vs B, which is better)
Position BiasTends to select content at specific positions in a listRandomly shuffle the order of evaluated content

Best practice: In the early stages, use human experts to establish a “golden test set” and use it to calibrate the LLM evaluator. Periodically verify the consistency of automated evaluation with manual spot checks.

Evaluation Metric Decomposition

Decompose vague evaluation goals into specific, individually checkable rubrics:

# Content quality evaluation decomposition example
Evaluation Dimension: "Content Quality"
Rubrics:
  - id: "pain_point"
    description: "Does it open with a specific pain point?"
    type: "boolean"
  - id: "theory_depth"
    description: "Does it clearly point out the limitations of the initial solution and introduce the core theory?"
    type: "boolean"
  - id: "code_relevance"
    description: "Are the code examples closely related to the theory being explained and sufficiently simplified?"
    type: "boolean"
  - id: "anti_pattern_check"
    description: "Does it avoid playful remarks like 'congratulations on unlocking a new skill'?"
    type: "boolean"

Evaluation-Driven Iteration

The Evaluation Closed Loop

Evaluation is not a one-time event, but an engine that continuously drives improvement:

    ┌──────────────────────────────────┐
    │                                  │
    ▼                                  │
┌─────────┐   ┌──────────┐   ┌─────────┐
│ Build   │ → │ Discover │ → │ Extract │
│ MVP     │   │ Issues   │   │ Metrics │
└─────────┘   └──────────┘   └─────────┘


┌─────────┐   ┌──────────┐   ┌─────────┐
│ Deploy  │ ← │ Regression│ ← │ Optimize│
│ to Prod │   │ Test     │   │ & Improve│
└─────────┘   └──────────┘   └─────────┘

                                  └────→ (loop)

Business Experts Lead Evaluation Standards

Evaluation metrics (especially subjective ones) must be led by the most senior business experts:

  1. Mobilize participation with business goals: Don’t say “help us define evaluation metrics”; say “this Agent will help you shorten the course production cycle from 2 weeks to 3 days while maintaining 90%+ user satisfaction.”

  2. Provide structured tools to lower the barrier: Scoring rubric templates, case annotation tools, guiding questions like “If you could only look at three metrics to judge whether a course is good, which three would you choose?”

  3. Establish a continuous collaboration mechanism: Weekly review meetings where experts look at data and the tech team adjusts the system, making decisions together.

The Efficiency Leverage of Evaluation

Not all evaluation needs to be fully automated and comprehensive. Start with the simplest method:

Level 1: Manual spot check → "Copy the code and run it"
Level 2: Automated scripts → "Write a script for batch execution"
Level 3: Integrated evaluation pipeline → CI/CD integration, automatically triggered on every PR
Level 4: Continuous monitoring → Real-time monitoring of key metrics in production

Each level has a different ROI. In the early stages, Level 1 has the highest ROI — fastest problem discovery, lowest implementation cost. As the system matures, gradually evolve toward higher levels.


Model Deployment Strategy

Business Requirements Analysis Framework

The first step in deploying an LLM application to production is not technology selection, but requirements analysis:

┌────────────────────────────────────────────┐
│        Business Requirements Matrix         │
├────────────────────────────────────────────┤
│                                            │
│  Functional Requirements (What to do):       │
│  ├─ Natural Language Processing → General LLM│
│  ├─ Code Generation → Code-optimized LLM    │
│  ├─ Mathematical Reasoning → Math-fine-tuned LLM│
│  ├─ Visual Understanding → Multimodal Model │
│  └─ Speech Processing → Speech Model        │
│                                            │
│  Non-Functional Requirements (How to do it):│
│  ├─ Performance: TTFT < 500ms, TPOT < 50ms │
│  ├─ Cost: per call < $0.01                 │
│  ├─ Stability: 99.9% availability          │
│  ├─ Security: Content filtering, privacy    │
│  │   protection                            │
│  └─ Compliance: Industry regulatory         │
│      requirements                          │
│                                            │
└────────────────────────────────────────────┘

Model Selection Strategy

Not every scenario requires the largest model. Model selection follows the “minimum viable” principle:

Task Complexity

    │  ┌──────────────────────────┐
    │  │ Large Models (GPT-4, Claude)│
    │  │ - Complex reasoning       │
    │  │ - Multi-step planning     │
    │  │ - Creative generation     │
    │  └──────────────────────────┘
    │  ┌──────────────────────────┐
    │  │ Medium Models (GPT-4o-mini)│
    │  │ - Intent recognition      │
    │  │ - Structured extraction   │
    │  │ - Summary generation      │
    │  └──────────────────────────┘
    │  ┌──────────────────────────┐
    │  │ Small Models / Distilled  │
    │  │ - Text classification     │
    │  │ - Keyword matching        │
    │  │ - Format validation       │
    │  └──────────────────────────┘
    └─────────────────────────────────→ Call Frequency

Distillation: Giving Small Models Professional Capabilities

The core idea of distillation: “copy” the judgment capability of a large model to a small model.

Teacher Model (GPT-4)          Student Model (0.6B params)
      │                         │
      │  Generate labeled data    │
      ├─────────────────────────→│
      │  "Understand request      │  Learn the teacher's
      │   intent"                │  behavior patterns
      │  [Input → Output pairs]  │
      │                         │
      │  Result: Small model     │
      │  approaches teacher      │
      │  performance on          │
      │  specific tasks          │

Distillation vs. Fine-tuning:

Fine-tuningDistillation
Data sourceHuman annotationTeacher model generation
Data costHighLow (API call cost)
Data scaleLimitedCan be generated at scale
Quality ceilingDepends on annotatorsDepends on teacher model

Three distillation paths:

PathResources NeededSuitable Scenarios
Data Synthesis Distillation (Black-box)Only API access neededStructured tasks, commercial API teachers
Knowledge Distillation KD (White-box)Teacher model weightsOpen-source teachers, needs higher precision
Inference CompressionTeacher inference trajectoriesMulti-step reasoning tasks (e.g., DeepSeek-R1)

Inference Optimization

Performance Optimization Framework

LLM inference optimization can be divided into four directions:

Process Requests Faster

  • Model miniaturization: Choose model variants with fewer parameters
  • Quantization: INT4/INT8/FP16 quantization reduces computational resource demands
  • Pruning: Remove redundant weights, reducing model complexity
  • Knowledge distillation: Train small models using large model data
Quantization Precision Comparison:
  FP32 (full precision) → Baseline performance, highest computational overhead
  FP16 (half precision) → ~2x speedup, almost no precision loss
  INT8                  → ~4x speedup, minor precision loss
  INT4                  → ~8x speedup, precision impact needs careful evaluation

Reduce the Number of Requests to Process

  • Context Cache: Cache the common prefix of multi-turn conversations to reduce redundant computation
  • Batching: Combine multiple requests into one batch to improve hardware utilization
  • Result caching: Directly return cached results for high-frequency identical queries
# Typical application of context caching
# In multi-turn conversations, System Prompt + historical knowledge documents are the common prefix
# First turn: full computation (full price)
# Subsequent turns: cache-hit portion billed at 20% of the price

Reduce Token Input/Output

  • Input side: Streamline input, remove redundant information, generate summaries for long documents first
  • Output side: Guide concise answers through prompts, set reasonable max_tokens

The design philosophy of max_tokens: it is a safety valve rather than a content control mechanism. Semantically complete, concise replies should be guided by prompts; max_tokens is more suitable as the last line of defense for cost control.

Parallel Processing

LLM inference is fundamentally large-scale matrix computation. Understand the difference between CPU and GPU:

CPUGPU
Core countFew powerful cores (8-64)Massive simple cores (thousands)
Suitable tasksComplex logic, serialLarge-scale parallel matrix computation

GPU parallelization strategies:

  • Data parallelism: Distribute data shards across multiple GPUs
  • Model parallelism: Distribute different model layers across different devices
  • Pipeline parallelism: Divide the computation process into stages executed sequentially

Don’t Default to Large Models

In many scenarios, simpler approaches are actually more efficient:

ScenarioAlternative
Standard confirmation messagesHard-coded templates + random variant selection
Responses with limited optionsPre-compute all possible results, match by input
Data displayUse charts, tables, and other traditional UI instead of LLM-generated descriptive text
Keyword matchingFilter by keywords first in the intent recognition stage, call LLM only when necessary

Safety Guardrails

Security Threats Facing LLMs

LLM applications face multi-layered security threats requiring systematic defense strategies:

Safety guardrails

Defense Strategy Matrix

Attack TypeAttack MethodDefense Measure
Prompt InjectionInducing the model to override system instructionsBuilt-in safety guard detection + strict isolation of user input from system instructions
Command InjectionEmbedding malicious code in requestsPre-execution audit + least privilege
Prompt LeakingInducing the model to output its own System PromptSafety guard identification of probing patterns
Knowledge Base PoisoningUploading documents with incorrect informationKnowledge entry approval process + content pre-scanning
Model TheftCollecting training data through massive API callsAPI rate limiting + bot traffic identification
Malicious Tool UseInducing the Agent to perform dangerous operationsPre-tool-call audit + circuit breaker mechanism

Engineering Implementation of Safety Guardrails

class SafetyGuard:
    """Multi-layer safety guardrail implementation"""

    def __init__(self):
        self.blocked_keywords = set()    # custom sensitive words
        self.rate_limits = {}            # rate limit records
        self.max_tool_calls = 10         # max Agent tool calls
        self.dangerous_commands = {      # dangerous command blacklist
            "rm -rf", "DROP TABLE", "DELETE FROM",
            "os.system", "subprocess.call", "eval("
        }

    def check_input(self, user_input: str) -> tuple[bool, str]:
        """Input security check"""
        # 1. Sensitive word detection
        for keyword in self.blocked_keywords:
            if keyword in user_input.lower():
                return False, f"Input contains sensitive word: {keyword}"

        # 2. Command injection detection
        for dangerous in self.dangerous_commands:
            if dangerous.lower() in user_input.lower():
                return False, f"Potential dangerous command detected: {dangerous}"

        return True, "Passed"

    def check_tool_call(self, tool_name: str, args: dict) -> tuple[bool, str]:
        """Pre-tool-call audit"""
        # 1. Check call frequency
        if self.rate_limits.get(tool_name, 0) >= self.max_tool_calls:
            return False, f"Tool {tool_name} call limit exceeded"

        # 2. Check parameter safety
        args_str = json.dumps(args).lower()
        for dangerous in self.dangerous_commands:
            if dangerous.lower() in args_str:
                return False, f"Tool parameter contains dangerous command: {dangerous}"

        self.rate_limits[tool_name] = self.rate_limits.get(tool_name, 0) + 1
        return True, "Passed"

    def check_output(self, output: str) -> tuple[bool, str]:
        """Output content review"""
        # Detect whether sensitive information patterns appear in output
        sensitive_patterns = [
            r'\b\d{17}[\dXx]\b',           # ID card number
            r'\b1[3-9]\d{9}\b',            # mobile phone number
            r'[Pp]assword\s*[:=]\s*\S+',  # password pattern
        ]

        for pattern in sensitive_patterns:
            if re.search(pattern, output):
                return False, f"Output may contain sensitive information: {pattern}"

        return True, "Passed"

Circuit Breaker Mechanism

Set clear resource limits for each Agent task:

class CircuitBreaker:
    """Agent circuit breaker: prevent runaway loops from causing massive losses"""

    def __init__(self,
                 max_api_calls: int = 10,      # max API calls
                 max_wall_time: int = 300,      # max execution time (seconds)
                 max_cost: float = 0.50):       # max cost (USD)
        self.max_api_calls = max_api_calls
        self.max_wall_time = max_wall_time
        self.max_cost = max_cost
        self.reset()

    def reset(self):
        self.api_calls = 0
        self.start_time = time.time()
        self.total_cost = 0.0

    def check(self) -> tuple[bool, str]:
        """Check whether the circuit should break"""
        self.api_calls += 1
        elapsed = time.time() - self.start_time

        if self.api_calls > self.max_api_calls:
            return False, f"API call limit exceeded ({self.api_calls}/{self.max_api_calls})"
        if elapsed > self.max_wall_time:
            return False, f"Execution time limit exceeded ({elapsed:.0f}s/{self.max_wall_time}s)"
        if self.total_cost > self.max_cost:
            return False, f"Cost limit exceeded (${self.total_cost:.2f}/${self.max_cost:.2f})"

        return True, "Normal"

Harness Engineering

The Overall Blueprint for Production

Harness Engineering is the complete set of engineering practices that ensure an Agent system runs stably from development to production. It includes:

Harness Engineering

Observability

Use the OpenTelemetry standard to establish three types of data collection:

  • Metrics: Token consumption, latency distribution, error rate
  • Traces: Every stage a single request goes through and its duration
  • Logs: Input/output at each stage, error stacks, audit information
# Instrument Agent calls with OpenTelemetry
from opentelemetry import trace
from opentelemetry.instrumentation.openai import OpenAIInstrumentor

# Automatically instrument OpenAI API calls
OpenAIInstrumentor().instrument()

tracer = trace.get_tracer(__name__)

@tracer.start_as_current_span("agent_task")
def run_agent_task(task: str):
    # span automatically records duration and context
    with tracer.start_as_current_span("llm_call") as span:
        span.set_attribute("task", task)
        result = agent.run(task)
        span.set_attribute("tokens_used", result.usage.total_tokens)
        return result

Pre-Deployment Checklist

Before pushing an Agent to production, complete the following checks:

☐ SLO Definition
  ├─ TTFT (Time to First Token) target: _____ ms
  ├─ TPOT (Time per Output Token) target: _____ ms
  └─ Availability target: _____%

☐ Cost Control
  ├─ Per-call budget cap: $_____
  ├─ Daily token consumption cap: _____ tokens
  └─ Alert thresholds configured

☐ Security Protection
  ├─ Input security check enabled
  ├─ Output content review enabled
  ├─ Agent behavior circuit breaker configured
  └─ Security monitoring alerts configured

☐ Disaster Recovery Plan
  ├─ Model degradation path defined
  ├─ Critical path fallback logic verified
  └─ Failure recovery drill completed

☐ Evaluation Baseline
  ├─ End-to-end evaluation score: _____
  ├─ Component-level evaluation score: _____
  └─ Regression test pipeline integrated

☐ Observability
  ├─ OpenTelemetry integrated
  ├─ Key metrics Dashboard established
  └─ Alert rules configured and verified

Progressive Rollout Strategy

Don’t go all-in at once. Adopt a progressive strategy:

Phase 1: Internal Testing (1-2 weeks)
  └─ Team members use it, collect initial feedback

Phase 2: Small-Scale Canary (5% of users)
  └─ Compare evaluation metrics between old and new systems

Phase 3: Expanded Rollout (25% → 50% → 100%)
  └─ Stay at each stage for 3-5 days to observe metrics

Phase 4: Full Rollout
  └─ Maintain monitoring, establish a continuous optimization loop

Production Operations Best Practices

Evaluation Baseline Management:

  • Set the current production version as the baseline; any new version must surpass the baseline
  • Periodically (weekly) retest both baseline and candidate versions with the latest data
  • Integrate baseline checks into the deployment pipeline; versions that don’t meet the standard are automatically blocked

Layered Degradation Strategy:

  1. Primary model unavailable → Switch to backup model
  2. Backup model also unavailable → Use cached common responses
  3. Cache miss → Return preset degradation response templates

Cost Governance:

  • Analyze token consumption by model, by user, by task type dimensions
  • Identify abnormal cost spikes and set alerts
  • Regular review: are there unnecessary long contexts, redundant System Prompts

Model Distillation: Teaching Small Models Domain Expertise

Why Distillation Matters

Large models (GPT-4, Claude, Qwen-72B) deliver excellent quality but come with high inference costs and latency. For production systems handling thousands of requests per minute, the token cost can become prohibitive. Model distillation offers a practical solution: use a large model’s outputs as training data to teach a smaller model (7B-14B) to replicate the same behavior at a fraction of the cost.

Teacher Model (GPT-4, 175B params)

  ├── Generate high-quality responses for domain tasks


Training Data (input → teacher_output pairs)

  ├── Fine-tune student model


Student Model (7B-14B params)

  ├── Same quality, 10-50x lower cost
  └── 5-10x lower latency

Distillation Pipeline

Step 1: Define the Task Scope

Distillation works best when the task is well-defined and repetitive. Identify the specific domains where you need the small model to perform:

# Example: Customer support intent classification
task_definitions = [
    {
        "name": "intent_classification",
        "input_schema": {"user_message": "string", "context": "string"},
        "output_schema": {"intent": "string", "confidence": "float", "reasoning": "string"},
    },
    {
        "name": "response_generation",
        "input_schema": {"intent": "string", "knowledge": "string", "tone": "string"},
        "output_schema": {"response": "string", "sources": ["string"]},
    },
]

Step 2: Generate Training Data with Teacher Model

Use the large model to generate high-quality examples for your domain:

from openai import OpenAI

teacher = OpenAI(api_key="...")  # GPT-4 or similar
student = OpenAI(base_url="http://localhost:8000/v1")  # Your small model

def generate_training_examples(task_def: dict, n_examples: int = 1000) -> list:
    examples = []
    for i in range(n_examples):
        # Generate diverse inputs
        input_prompt = f"Generate a realistic input for task '{task_def['name']}'. Vary the complexity and edge cases."
        input_resp = teacher.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": input_prompt}],
        )
        user_input = input_resp.choices[0].message.content

        # Generate teacher output
        teacher_resp = teacher.chat.completions.create(
            model="gpt-4",
            messages=[
                {"role": "system", "content": f"You are an expert at {task_def['name']}. Follow this output schema: {task_def['output_schema']}"},
                {"role": "user", "content": user_input},
            ],
        )
        teacher_output = teacher_resp.choices[0].message.content

        examples.append({"input": user_input, "output": teacher_output})

    return examples

training_data = generate_training_examples(task_definitions[0], n_examples=2000)

Step 3: Fine-tune the Student Model

Use LoRA or full fine-tuning to train the small model on the teacher’s outputs:

# Using Hugging Face Transformers + PEFT for LoRA fine-tuning
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model

model_name = "meta-llama/Llama-2-7b-hf"
model = AutoModelForCausalLM.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)

# Configure LoRA for efficient fine-tuning
lora_config = LoraConfig(
    r=16,  # LoRA rank
    lora_alpha=32,
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# Output: trainable params: 4,194,304 || all params: 6,742,609,920 || trainable%: 0.0622

# Train on the distillation data
# ... (standard training loop)

Step 4: Evaluate and Iterate

Compare the student model’s outputs against the teacher’s on a held-out test set:

def evaluate_student_vs_teacher(test_set: list, teacher_client, student_client) -> dict:
    results = {"exact_match": 0, "semantic_similarity": 0, "total": len(test_set)}

    for example in test_set:
        student_resp = student_client.chat.completions.create(
            model="student-7b",
            messages=[{"role": "user", "content": example["input"]}],
        )
        student_output = student_resp.choices[0].message.content
        teacher_output = example["output"]

        # Exact match check
        if student_output.strip() == teacher_output.strip():
            results["exact_match"] += 1

        # Semantic similarity (using embeddings)
        # ... (compute cosine similarity between outputs)

    results["exact_match_rate"] = results["exact_match"] / results["total"]
    return results

# Target: >85% semantic similarity with teacher

When to Distill

ScenarioRecommendation
High-volume, repetitive tasks (classification, extraction)Distill — cost savings are massive
Creative, open-ended generationKeep teacher — quality matters more than cost
Latency-sensitive applications (real-time chat)Distill — smaller models are 5-10x faster
Rare, complex reasoning tasksKeep teacher — small models struggle with novel reasoning
Hybrid: simple tasks + complex edge casesRoute — small model handles 80%, escalate to teacher for 20%

Production Best Practices: Monitoring, Canary Releases, A/B Testing

Observability Stack

Production Agent systems need comprehensive observability. Set up the following monitoring layers:

1. Application-Level Metrics

import time
from prometheus_client import Counter, Histogram, Gauge

# Request metrics
request_counter = Counter('agent_requests_total', 'Total requests', ['model', 'intent', 'status'])
request_latency = Histogram('agent_request_duration_seconds', 'Request latency', ['model', 'intent'])
active_sessions = Gauge('agent_active_sessions', 'Active conversation sessions')

# Token usage metrics
token_usage = Counter('agent_tokens_total', 'Token usage', ['model', 'type'])  # type: input/output

# Quality metrics
hallucination_rate = Gauge('agent_hallucination_rate', 'Estimated hallucination rate')
user_satisfaction = Histogram('agent_user_satisfaction', 'User satisfaction score', buckets=[1, 2, 3, 4, 5])

def track_request(model: str, intent: str, status: str, latency: float, tokens_in: int, tokens_out: int):
    request_counter.labels(model=model, intent=intent, status=status).inc()
    request_latency.labels(model=model, intent=intent).observe(latency)
    token_usage.labels(model=model, type='input').inc(tokens_in)
    token_usage.labels(model=model, type='output').inc(tokens_out)

2. Distributed Tracing

Use OpenTelemetry to trace requests across your Agent pipeline:

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

trace.set_tracer_provider(TracerProvider())
tracer = trace.get_tracer(__name__)

def process_user_request(user_input: str):
    with tracer.start_as_current_span("process_request") as span:
        span.set_attribute("user.input_length", len(user_input))

        with tracer.start_as_current_span("retrieve_context") as retrieve_span:
            context = retrieve_relevant_docs(user_input)
            retrieve_span.set_attribute("docs.retrieved", len(context))

        with tracer.start_as_current_span("llm_generate") as llm_span:
            llm_span.set_attribute("model", "gpt-4")
            response = call_llm(user_input, context)
            llm_span.set_attribute("tokens.used", response.usage.total_tokens)

        return response

3. Logging Strategy

import structlog

logger = structlog.get_logger()

def log_agent_event(event_type: str, **kwargs):
    logger.info(
        event_type,
        session_id=kwargs.get("session_id"),
        user_id=kwargs.get("user_id"),
        model=kwargs.get("model"),
        intent=kwargs.get("intent"),
        latency_ms=kwargs.get("latency_ms"),
        tokens_in=kwargs.get("tokens_in"),
        tokens_out=kwargs.get("tokens_out"),
        error=kwargs.get("error"),
    )

# Example usage
log_agent_event(
    "request_completed",
    session_id="abc123",
    user_id="user_456",
    model="gpt-4",
    intent="faq_answer",
    latency_ms=1234,
    tokens_in=500,
    tokens_out=200,
)

Canary Release Strategy

Roll out changes gradually to minimize risk:

Phase 1: Canary (5% of traffic)
  └─ Run new model/config on small subset
  └─ Monitor error rates, latency, satisfaction
  └─ Duration: 1-3 days

Phase 2: Expanded (25% → 50%)
  └─ Increase traffic gradually
  └─ Compare metrics against baseline
  └─ Duration: 3-5 days per stage

Phase 3: Full Rollout (100%)
  └─ All traffic on new version
  └─ Continue monitoring
  └─ Keep old version available for quick rollback
# Simple canary routing based on user ID hash
import hashlib

def route_to_version(user_id: str, canary_percent: int = 5) -> str:
    hash_val = int(hashlib.md5(user_id.encode()).hexdigest(), 16)
    bucket = hash_val % 100
    return "canary" if bucket < canary_percent else "stable"

# In your request handler
def handle_request(user_id: str, user_input: str):
    version = route_to_version(user_id, canary_percent=5)

    if version == "canary":
        response = call_new_model(user_input)
    else:
        response = call_stable_model(user_input)

    # Track metrics per version
    track_request(model=version, ...)
    return response

A/B Testing Framework

Compare two versions head-to-head to measure impact:

from dataclasses import dataclass
from enum import Enum

class Variant(Enum):
    CONTROL = "control"
    TREATMENT = "treatment"

@dataclass
class ABTestResult:
    variant: Variant
    total_requests: int
    avg_latency_ms: float
    success_rate: float
    user_satisfaction: float  # 1-5 scale

def run_ab_test(user_id: str, user_input: str) -> tuple[str, str]:
    """Returns (variant, response)"""
    # Consistent assignment based on user ID
    hash_val = int(hashlib.md5(user_id.encode()).hexdigest(), 16)
    variant = Variant.CONTROL if hash_val % 2 == 0 else Variant.TREATMENT

    if variant == Variant.CONTROL:
        response = call_control_model(user_input)
    else:
        response = call_treatment_model(user_input)

    return variant.value, response

def analyze_ab_results(results_a: list, results_b: list) -> dict:
    """Compare metrics between control and treatment"""
    import statistics

    def compute_metrics(results):
        return {
            "count": len(results),
            "avg_latency": statistics.mean([r["latency_ms"] for r in results]),
            "success_rate": sum(1 for r in results if r["success"]) / len(results),
        }

    metrics_a = compute_metrics(results_a)
    metrics_b = compute_metrics(results_b)

    return {
        "control": metrics_a,
        "treatment": metrics_b,
        "latency_improvement": (metrics_a["avg_latency"] - metrics_b["avg_latency"]) / metrics_a["avg_latency"] * 100,
        "success_rate_delta": (metrics_b["success_rate"] - metrics_a["success_rate"]) * 100,
    }

Alerting Rules

Set up alerts for critical conditions:

# Prometheus alerting rules
groups:
  - name: agent_alerts
    rules:
      - alert: HighErrorRate
        expr: rate(agent_requests_total{status="error"}[5m]) / rate(agent_requests_total[5m]) > 0.05
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Agent error rate above 5%"

      - alert: HighLatency
        expr: histogram_quantile(0.95, rate(agent_request_duration_seconds_bucket[5m])) > 10
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "P95 latency above 10 seconds"

      - alert: TokenCostSpike
        expr: rate(agent_tokens_total[1h]) > 100000
        for: 30m
        labels:
          severity: warning
        annotations:
          summary: "Token usage spike detected"

RIDE Methodology: A Framework for AI-Driven Business Impact

The Challenge

Organizations rushing to adopt AI often fall into one of two traps:

  1. Solution looking for a problem: Building impressive AI demos that don’t address real business needs
  2. Analysis paralysis: Endless evaluation of AI tools without shipping anything

The RIDE methodology provides a structured approach to selecting, implementing, and measuring AI initiatives that deliver real business value.

RIDE: Research → Implement → Deliver → Enhance

┌─────────────────────────────────────────────────────────────┐
│                      RIDE Cycle                              │
│                                                              │
│   ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────┐ │
│   │ Research │───▶│Implement │───▶│ Deliver  │───▶│Enhance│ │
│   │          │    │          │    │          │    │      │ │
│   └──────────┘    └──────────┘    └──────────┘    └──────┘ │
│        ▲                                              │     │
│        └──────────────────────────────────────────────┘     │
│                        (iterate)                             │
└─────────────────────────────────────────────────────────────┘

Phase 1: Research — Select the Right Problem

Goal: Identify high-impact, feasible AI use cases aligned with business priorities.

Key Activities:

  1. Map business pain points: Interview stakeholders, analyze support tickets, review process bottlenecks
  2. Score opportunities using the Impact-Feasibility matrix:
@dataclass
class UseCase:
    name: str
    description: str
    business_impact: int      # 1-10: revenue impact, cost savings, customer satisfaction
    technical_feasibility: int # 1-10: data availability, model capability, integration complexity
    time_to_value: int         # weeks to MVP
    stakeholders: list[str]

def score_use_case(uc: UseCase) -> float:
    """Higher score = better candidate for AI initiative"""
    return (uc.business_impact * 0.4 + uc.technical_feasibility * 0.3 +
            (10 - uc.time_to_value / 4) * 0.3)  # Normalize time to 1-10 scale

# Example scoring
use_cases = [
    UseCase("FAQ Bot", "Automate customer FAQ responses", 7, 9, 4, ["Support", "Engineering"]),
    UseCase("Code Review", "AI-assisted code review", 6, 7, 8, ["Engineering"]),
    UseCase("Demand Forecast", "Predict product demand", 9, 5, 12, ["Product", "Supply Chain"]),
]

for uc in sorted(use_cases, key=score_use_case, reverse=True):
    print(f"{uc.name}: score={score_use_case(uc):.1f}, impact={uc.business_impact}, feasibility={uc.technical_feasibility}")
# Output:
# FAQ Bot: score=8.2, impact=7, feasibility=9
# Code Review: score=6.7, impact=6, feasibility=7
# Demand Forecast: score=6.1, impact=9, feasibility=5
  1. Validate with stakeholders: Present top candidates, get buy-in, define success criteria

Deliverable: Prioritized list of 2-3 use cases with clear success metrics.

Phase 2: Implement — Build the MVP

Goal: Ship a working prototype quickly, focusing on core functionality.

Key Principles:

  • Start with the simplest approach: Rule-based → RAG → Fine-tuned model (only escalate if needed)
  • Use existing tools: Don’t build infrastructure unless necessary
  • Measure from day one: Instrument the MVP with the observability stack from the previous section

Implementation Checklist:

Week 1-2: Foundation
  □ Set up development environment
  □ Define data sources and access
  □ Build basic prompt templates
  □ Create evaluation dataset (50-100 examples)

Week 3-4: Core Functionality
  □ Implement retrieval pipeline (if RAG)
  □ Build Agent loop (if tool use)
  □ Integrate with existing systems (API, database)
  □ Add basic error handling and fallbacks

Week 5-6: Quality & Testing
  □ Run evaluation suite, iterate on prompts
  □ Add safety guardrails (content filtering, PII detection)
  □ Conduct user testing with 5-10 internal users
  □ Fix critical issues

Week 7-8: Deployment Prep
  □ Set up monitoring and alerting
  □ Document runbooks for common issues
  □ Prepare rollback plan
  □ Deploy to staging, run load tests

Phase 3: Deliver — Measure Business Impact

Goal: Quantify the real-world impact against the success criteria defined in Research.

Key Metrics by Use Case Type:

Use Case TypePrimary MetricsSecondary Metrics
Customer Support BotTicket deflection rate, resolution timeCustomer satisfaction (CSAT), cost per ticket
Code Review AssistantReview turnaround time, defect escape rateDeveloper satisfaction, code quality scores
Content GenerationContent production time, engagement metricsBrand consistency scores, editorial review pass rate
Data Analysis AgentAnalysis turnaround time, insight qualityStakeholder satisfaction, decision velocity

Impact Calculation Example:

# Before/after comparison for FAQ Bot
before = {
    "monthly_tickets": 5000,
    "avg_resolution_time_hours": 24,
    "cost_per_ticket": 15,  # human agent cost
    "csat_score": 3.2,
}

after = {
    "monthly_tickets": 5000,
    "bot_deflection_rate": 0.65,  # 65% handled by bot
    "avg_resolution_time_hours": 0.5,  # for bot-handled
    "cost_per_ticket_bot": 0.50,  # API cost
    "cost_per_ticket_human": 15,  # for escalated
    "csat_score": 4.1,
}

# Calculate impact
bot_handled = after["monthly_tickets"] * after["bot_deflection_rate"]
human_handled = after["monthly_tickets"] - bot_handled

monthly_cost_before = before["monthly_tickets"] * before["cost_per_ticket"]
monthly_cost_after = (bot_handled * after["cost_per_ticket_bot"] +
                      human_handled * after["cost_per_ticket_human"])

monthly_savings = monthly_cost_before - monthly_cost_after
annual_savings = monthly_savings * 12

print(f"Monthly cost before: ${monthly_cost_before:,.0f}")
print(f"Monthly cost after: ${monthly_cost_after:,.0f}")
print(f"Annual savings: ${annual_savings:,.0f}")
print(f"CSAT improvement: {after['csat_score'] - before['csat_score']:.1f} points")
# Output:
# Monthly cost before: $75,000
# Monthly cost after: $29,875
# Annual savings: $541,500
# CSAT improvement: 0.9 points

Phase 4: Enhance — Iterate and Expand

Goal: Continuously improve the system based on data and feedback.

Enhancement Strategies:

  1. Prompt Optimization: Use evaluation data to refine prompts (see Evaluation chapter)
  2. RAG Improvements: Add more documents, improve chunking, add reranking
  3. Model Upgrades: Distill to smaller models for cost savings (see Distillation section)
  4. Feature Expansion: Add new capabilities based on user feedback
  5. Process Integration: Deepen integration with existing workflows

Iteration Cadence:

Weekly:
  - Review error logs and user feedback
  - Update evaluation dataset with new examples
  - Fix critical bugs

Monthly:
  - Run full evaluation suite
  - Analyze trends in metrics
  - Plan next iteration

Quarterly:
  - Review business impact against goals
  - Assess model/provider landscape for upgrades
  - Plan strategic enhancements

RIDE in Practice: Common Pitfalls

PitfallHow to Avoid
Skipping Research, jumping straight to implementationAlways start with stakeholder interviews and use case scoring
Building for months before deliveringSet 8-week MVP deadline; ship something measurable
Measuring only technical metrics (latency, accuracy)Define business metrics upfront; track cost savings, time saved
Treating AI as a one-time projectPlan for continuous iteration; AI systems need ongoing maintenance
Ignoring safety and complianceBuild guardrails from day one; don’t bolt them on later

Appendix: Key Concepts Quick Reference

ConceptOne-Line Explanation
TokenizationConverting text into a sequence of numeric IDs the model can process
EmbeddingMapping discrete IDs to dense vectors containing semantic information
AttentionAllowing the model to attend to all relevant positions in the sequence when processing each token
RAGFirst retrieve relevant knowledge, then let the model generate an answer based on that knowledge
ReActHaving the model alternate between Reasoning and Action in a loop
Function CallingThe model outputs structured tool invocation instructions instead of plain text replies
MCPAnthropic’s proposed tool standardization protocol, decoupling tool definition from usage
Plan & ExecuteFirst formulate a complete action plan, review and approve it, then execute step by step
HyDEFirst generate a hypothetical answer, then use that hypothetical answer for retrieval instead of the original question
Lost in the MiddleThe model’s ability to process information in the middle portion of a long context significantly degrades
Mixture-of-AgentsMultiple different models process the same task, and an aggregator synthesizes the best result
LoRAEfficient fine-tuning by training low-rank matrix adapters
DistillationUsing a large model’s outputs as training data to teach a small model
SkillEncapsulating domain expertise into reusable, modular functional units
LLM-as-JudgeUsing a large model as an evaluator to automatically score outputs
SLOService Level Objective, such as TTFT, TPOT, availability, etc.

This guide is based on best practices in Agent engineering, covering the complete path from LLM fundamentals to production deployment. Technology evolves rapidly; it is recommended to stay tuned to community developments and combine the methodologies in this guide with the latest tools.

Capstone Project: Building an Intelligent Q&A Agent

Project Narrative: Throughout Parts 1-5, you learned the building blocks of Agent engineering in isolation. Now it’s time to put them all together. You will build a New Employee Q&A Agent — a system that starts as a simple API call and evolves into a production-grade Agent with RAG, tool use, memory, skills, evaluation, and deployment. Each stage mirrors a real engineering milestone.


6.1 Environment Setup & Basic Conversation

The Starting Point

Your company has a problem: new employees keep asking the same questions about onboarding, benefits, tools, and processes. HR spends hours repeating answers. Your goal: build an AI agent that can answer these questions accurately.

Stage 1 Goal: Get a basic LLM conversation working.

Project Setup

# Create project directory
mkdir qa-agent && cd qa-agent

# Set up Python environment
python -m venv .venv
source .venv/bin/activate

# Install dependencies
pip install openai python-dotenv
# .env
OPENAI_API_KEY=sk-your-key-here

First Conversation

# chat.py
import os
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

def chat(user_message: str) -> str:
    response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": "You are a helpful onboarding assistant for new employees."},
            {"role": "user", "content": user_message},
        ],
    )
    return response.choices[0].message.content

# Test it
print(chat("What's the dress code?"))
# Output: "Our dress code is business casual..."

Multi-Turn Conversation

A single-turn chat isn’t enough. Employees ask follow-up questions. Let’s add conversation history:

# multi_turn_chat.py
conversation_history = [
    {"role": "system", "content": "You are a helpful onboarding assistant for new employees at Acme Corp."}
]

def chat_with_history(user_message: str) -> str:
    conversation_history.append({"role": "user", "content": user_message})

    response = client.chat.completions.create(
        model="gpt-4",
        messages=conversation_history,
    )
    assistant_reply = response.choices[0].message.content
    conversation_history.append({"role": "assistant", "content": assistant_reply})

    return assistant_reply

# Test multi-turn
print(chat_with_history("What's the dress code?"))
print(chat_with_history("What about on Fridays?"))  # Follow-up question
print(chat_with_history("And for client meetings?"))  # Another follow-up

Token Budget Awareness

# token_tracker.py
import tiktoken

def count_tokens(messages: list, model: str = "gpt-4") -> int:
    encoding = tiktoken.encoding_for_model(model)
    total = 0
    for msg in messages:
        total += len(encoding.encode(msg["content"])) + 4  # overhead per message
    return total

def chat_within_budget(user_message: str, max_tokens: int = 4000) -> str:
    conversation_history.append({"role": "user", "content": user_message})

    # Trim history if over budget
    while count_tokens(conversation_history) > max_tokens:
        # Remove oldest non-system message pair
        non_system = [m for m in conversation_history if m["role"] != "system"]
        if len(non_system) >= 2:
            conversation_history.remove(non_system[0])
            conversation_history.remove(non_system[1])
        else:
            break

    response = client.chat.completions.create(
        model="gpt-4",
        messages=conversation_history,
    )
    reply = response.choices[0].message.content
    conversation_history.append({"role": "assistant", "content": reply})
    return reply

Stage 1 Complete: You have a basic multi-turn chatbot. But it only knows what the LLM was trained on — it doesn’t know your company’s specific policies.


6.2 RAG: Connecting Enterprise Knowledge

The Problem

print(chat_with_history("What's the parental leave policy?"))
# Output: "I don't have specific information about Acme Corp's parental leave policy..."

The LLM doesn’t know your company’s internal documents. You need Retrieval-Augmented Generation (RAG).

Step 1: Prepare Your Knowledge Base

# knowledge_base.py
import os
from pathlib import Path

# Sample company documents (in practice, load from your document store)
documents = [
    {
        "id": "doc_001",
        "title": "Employee Handbook - Leave Policies",
        "content": """Acme Corp provides the following leave benefits:
- Annual Leave: 20 days per year, prorated for partial years
- Sick Leave: 10 days per year
- Parental Leave: 16 weeks paid leave for primary caregivers, 8 weeks for secondary caregivers
- Bereavement Leave: 5 days for immediate family members
All leave requests must be submitted through the HR portal at least 2 weeks in advance, except for sick leave which can be reported same-day."""
    },
    {
        "id": "doc_002",
        "title": "Employee Handbook - Dress Code",
        "content": """Acme Corp Dress Code:
- Regular days: Business casual (collared shirts, slacks, closed-toe shoes)
- Casual Fridays: Jeans and casual wear allowed, but no flip-flops or gym clothes
- Client meetings: Business formal (suit and tie for men, business suit or dress for women)
- Remote work days: No dress code, but camera-on for meetings
When in doubt, err on the side of being more formal."""
    },
    # ... more documents
]

Step 2: Build the Retrieval Pipeline

# rag_pipeline.py
from openai import OpenAI
import numpy as np

client = OpenAI()

def get_embedding(text: str) -> list[float]:
    response = client.embeddings.create(
        model="text-embedding-3-small",
        input=text,
    )
    return response.data[0].embedding

def build_vector_store(docs: list) -> dict:
    """Embed all documents and store as a simple vector index"""
    vector_store = {}
    for doc in docs:
        embedding = get_embedding(doc["content"])
        vector_store[doc["id"]] = {
            "embedding": embedding,
            "content": doc["content"],
            "title": doc["title"],
        }
    return vector_store

def search(query: str, vector_store: dict, top_k: int = 3) -> list[dict]:
    """Retrieve top-k most relevant documents"""
    query_embedding = get_embedding(query)

    results = []
    for doc_id, doc_data in vector_store.items():
        similarity = cosine_similarity(query_embedding, doc_data["embedding"])
        results.append({
            "doc_id": doc_id,
            "title": doc_data["title"],
            "content": doc_data["content"],
            "score": similarity,
        })

    results.sort(key=lambda x: x["score"], reverse=True)
    return results[:top_k]

def cosine_similarity(a: list, b: list) -> float:
    a_arr = np.array(a)
    b_arr = np.array(b)
    return np.dot(a_arr, b_arr) / (np.linalg.norm(a_arr) * np.linalg.norm(b_arr))

Step 3: Augment Generation with Retrieved Context

# rag_chat.py
def rag_chat(user_message: str, vector_store: dict) -> str:
    # Step 1: Retrieve relevant documents
    relevant_docs = search(user_message, vector_store, top_k=3)

    # Step 2: Build context from retrieved documents
    context = "\n\n".join([
        f"[Source: {doc['title']}]\n{doc['content']}"
        for doc in relevant_docs
    ])

    # Step 3: Generate answer with context
    system_prompt = f"""You are a helpful onboarding assistant for Acme Corp.
Use the following company documents to answer questions. If the answer is not in the documents, say so honestly.
Always cite which document you're referencing.

--- Company Documents ---
{context}
--- End Documents ---"""

    response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_message},
        ],
    )
    return response.choices[0].message.content

# Test it
vector_store = build_vector_store(documents)
print(rag_chat("What's the parental leave policy?", vector_store))
# Output: "According to the Employee Handbook - Leave Policies, Acme Corp provides:
# - 16 weeks paid leave for primary caregivers
# - 8 weeks for secondary caregivers
# All requests must be submitted through the HR portal at least 2 weeks in advance."

Stage 2 Complete: Your bot now answers from company documents. But it can only talk — it can’t do things like submit a leave request or check calendar availability.


6.3 Agent Tool Calling & Planning

The Problem

An employee asks: “Can you submit a leave request for me next Monday to Wednesday?”

Your bot can explain the policy, but it can’t actually submit the request. You need tool calling.

Step 1: Define Tools

# tools.py
import json

tools = [
    {
        "type": "function",
        "function": {
            "name": "submit_leave_request",
            "description": "Submit a leave request to the HR system",
            "parameters": {
                "type": "object",
                "properties": {
                    "leave_type": {
                        "type": "string",
                        "enum": ["annual", "sick", "parental", "bereavement"],
                        "description": "Type of leave"
                    },
                    "start_date": {"type": "string", "description": "Start date (YYYY-MM-DD)"},
                    "end_date": {"type": "string", "description": "End date (YYYY-MM-DD)"},
                    "reason": {"type": "string", "description": "Reason for leave"},
                },
                "required": ["leave_type", "start_date", "end_date"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "check_leave_balance",
            "description": "Check remaining leave balance for an employee",
            "parameters": {
                "type": "object",
                "properties": {
                    "employee_id": {"type": "string", "description": "Employee ID"},
                    "leave_type": {"type": "string", "description": "Type of leave to check"},
                },
                "required": ["employee_id"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "search_knowledge_base",
            "description": "Search the company knowledge base for policies and procedures",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "Search query"},
                },
                "required": ["query"],
            },
        },
    },
]

# Mock implementations
def submit_leave_request(leave_type: str, start_date: str, end_date: str, reason: str = "") -> dict:
    # In production, this calls your HR API
    return {"status": "submitted", "request_id": "LR-2024-001", "leave_type": leave_type, "dates": f"{start_date} to {end_date}"}

def check_leave_balance(employee_id: str, leave_type: str = None) -> dict:
    # Mock data
    balances = {"annual": 15, "sick": 8, "parental": 0, "bereavement": 5}
    if leave_type:
        return {"employee_id": employee_id, "leave_type": leave_type, "remaining_days": balances.get(leave_type, 0)}
    return {"employee_id": employee_id, "balances": balances}

def search_knowledge_base(query: str) -> str:
    # Reuse RAG search from previous section
    results = search(query, vector_store, top_k=2)
    return "\n\n".join([f"[{r['title']}]: {r['content'][:200]}..." for r in results])

TOOL_IMPLEMENTATIONS = {
    "submit_leave_request": submit_leave_request,
    "check_leave_balance": check_leave_balance,
    "search_knowledge_base": search_knowledge_base,
}

Step 2: Implement the ReAct Loop

# agent.py
def agent_chat(user_message: str, max_iterations: int = 5) -> str:
    messages = [
        {"role": "system", "content": """You are an onboarding assistant for Acme Corp.
You can use tools to help answer questions and perform actions.
Always think step by step. If you need information, use search_knowledge_base.
If the user wants to perform an action, use the appropriate tool.
After getting tool results, provide a clear summary to the user."""},
        {"role": "user", "content": user_message},
    ]

    for i in range(max_iterations):
        response = client.chat.completions.create(
            model="gpt-4",
            messages=messages,
            tools=tools,
            tool_choice="auto",
        )

        message = response.choices[0].message

        # If no tool calls, return the final answer
        if not message.tool_calls:
            return message.content

        # Process tool calls
        messages.append(message)  # Add assistant message with tool calls

        for tool_call in message.tool_calls:
            func_name = tool_call.function.name
            func_args = json.loads(tool_call.function.arguments)

            print(f"  [Tool Call] {func_name}({func_args})")

            # Execute the tool
            result = TOOL_IMPLEMENTATIONS[func_name](**func_args)

            # Add tool result to messages
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(result),
            })

    return "I wasn't able to complete the task within the allowed steps."

# Test it
print(agent_chat("How many annual leave days do I have left? My employee ID is EMP-042."))
# Output:
#   [Tool Call] check_leave_balance({'employee_id': 'EMP-042', 'leave_type': 'annual'})
# "You have 15 annual leave days remaining."

print(agent_chat("Can you submit annual leave for me from Dec 23 to Dec 27?"))
# Output:
#   [Tool Call] submit_leave_request({'leave_type': 'annual', 'start_date': '2024-12-23', 'end_date': '2024-12-27'})
# "Your leave request has been submitted! Request ID: LR-2024-001, covering Dec 23-27, 2024."

Step 3: Add Planning for Complex Tasks

For multi-step tasks, the Agent needs to plan before executing:

# planner.py
def plan_and_execute(user_request: str) -> str:
    # Step 1: Generate a plan
    plan_response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": """You are a task planner. Break down the user's request into concrete steps.
For each step, specify which tool to use and what arguments to pass.
Output a JSON array of steps."""},
            {"role": "user", "content": user_request},
        ],
    )

    plan_text = plan_response.choices[0].message.content
    print(f"Plan: {plan_text}")

    # Step 2: Execute each step using the agent
    # (In production, you'd parse the plan and execute step by step with validation)
    return agent_chat(user_request)

Stage 3 Complete: Your bot can now call tools and perform actions. But every conversation starts from scratch — it doesn’t remember previous interactions.


6.4 Memory & Skill: Making the Agent Smarter Over Time

The Problem

An employee has three separate conversations:

  1. “I’m starting next Monday, what should I bring?”
  2. “Thanks! By the way, my employee ID is EMP-042.”
  3. “Can you check my leave balance?”

The Agent has no idea what their employee ID is. Each conversation is isolated.

Step 1: Short-Term Memory (Conversation Buffer)

# memory.py
from dataclasses import dataclass, field

@dataclass
class ConversationBuffer:
    max_tokens: int = 4000
    messages: list = field(default_factory=list)
    summary: str = ""

    def add_message(self, role: str, content: str):
        self.messages.append({"role": role, "content": content})
        self._trim_if_needed()

    def _trim_if_needed(self):
        token_count = count_tokens(self.messages)
        if token_count > self.max_tokens:
            # Summarize old messages and keep recent ones
            old_messages = self.messages[:len(self.messages)//2]
            self.summary = self._summarize(old_messages)
            self.messages = self.messages[len(self.messages)//2:]

    def _summarize(self, messages: list) -> str:
        response = client.chat.completions.create(
            model="gpt-4",
            messages=[
                {"role": "system", "content": "Summarize this conversation in 2-3 sentences, focusing on key facts and decisions."},
                *messages,
            ],
        )
        return response.choices[0].message.content

    def get_context(self) -> list:
        context = []
        if self.summary:
            context.append({"role": "system", "content": f"Previous conversation summary: {self.summary}"})
        context.extend(self.messages)
        return context

Step 2: Long-Term Memory (User Profile Store)

# long_term_memory.py
import json
from pathlib import Path

USER_PROFILES_DIR = Path("user_profiles")
USER_PROFILES_DIR.mkdir(exist_ok=True)

def save_user_fact(user_id: str, fact: str):
    """Save a fact about a user to their long-term profile"""
    profile_path = USER_PROFILES_DIR / f"{user_id}.json"
    profile = {}
    if profile_path.exists():
        profile = json.loads(profile_path.read_text())

    if "facts" not in profile:
        profile["facts"] = []
    profile["facts"].append(fact)
    profile_path.write_text(json.dumps(profile, indent=2))

def get_user_facts(user_id: str) -> list[str]:
    """Retrieve all known facts about a user"""
    profile_path = USER_PROFILES_DIR / f"{user_id}.json"
    if not profile_path.exists():
        return []
    profile = json.loads(profile_path.read_text())
    return profile.get("facts", [])

def extract_and_save_facts(user_id: str, messages: list):
    """Use LLM to extract important facts from conversation and save them"""
    response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": """Extract important facts about the user from this conversation.
Focus on: name, employee ID, department, preferences, upcoming events, action items.
Output a JSON array of fact strings. If no important facts, output []."""},
            *messages,
        ],
    )
    facts = json.loads(response.choices[0].message.content)
    for fact in facts:
        save_user_fact(user_id, fact)

Step 3: Skills — Reusable Workflows

# skills/onboarding_guide.md
"""
---
name: onboarding_guide
description: Guide new employees through their first week
triggers: new employee, first day, onboarding, getting started
---

# Onboarding Guide Skill

## Day 1 Checklist
1. Verify IT setup (laptop, accounts, VPN)
2. Introduce to team via Slack
3. Share key documents: handbook, org chart, tools guide
4. Schedule 1:1 with manager for week overview

## Week 1 Priorities
- Complete mandatory training modules (compliance, security)
- Set up development environment (if engineer)
- Attend team standup meetings
- Read team's project documentation

## Common First-Week Questions
- "How do I submit expenses?" → Use Concur, submit within 30 days
- "What's the wifi password?" → Provided on IT setup sheet
- "Who do I talk about benefits?" → HR portal or email [email protected]
"""

# skill_loader.py
from pathlib import Path

def load_skill(skill_name: str) -> str:
    skill_path = Path(f"skills/{skill_name}.md")
    if not skill_path.exists():
        return ""
    return skill_path.read_text()

def find_relevant_skill(query: str, available_skills: list[str]) -> str | None:
    """Determine which skill to activate based on the query"""
    skills_info = []
    for skill_name in available_skills:
        content = load_skill(skill_name)
        # Extract description from frontmatter
        skills_info.append(f"- {skill_name}: {content[:200]}")

    response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": "Given the user query, which skill should be activated? Output just the skill name, or 'none' if no skill is relevant."},
            {"role": "user", "content": f"Query: {query}\n\nAvailable skills:\n" + "\n".join(skills_info)},
        ],
    )
    result = response.choices[0].message.content.strip()
    return result if result != "none" else None

Putting It Together: Memory-Aware Agent

# memory_agent.py
def memory_aware_agent_chat(user_id: str, user_message: str) -> str:
    # Load user's long-term facts
    user_facts = get_user_facts(user_id)
    facts_context = "\n".join(user_facts) if user_facts else "No previous facts known."

    # Check if a skill should be activated
    skill_name = find_relevant_skill(user_message, ["onboarding_guide"])
    skill_context = load_skill(skill_name) if skill_name else ""

    system_prompt = f"""You are an onboarding assistant for Acme Corp.

Known facts about this user:
{facts_context}

{f'Active skill: {skill_context}' if skill_context else ''}

Use the user's known facts to personalize responses.
If the user shares new important information, note it for future reference."""

    messages = [{"role": "system", "content": system_prompt}]
    messages.extend(conversation_buffer.get_context())
    messages.append({"role": "user", "content": user_message})

    response = client.chat.completions.create(
        model="gpt-4",
        messages=messages,
        tools=tools,
        tool_choice="auto",
    )

    reply = response.choices[0].message.content

    # Update memory
    conversation_buffer.add_message("user", user_message)
    conversation_buffer.add_message("assistant", reply)
    extract_and_save_facts(user_id, messages)

    return reply

Stage 4 Complete: Your Agent now remembers users, activates skills, and provides personalized responses. But how do you know it’s actually giving good answers?


6.5 Evaluation & Iterative Optimization

The Problem

You’ve built a lot, but you have no idea how well it works. Is it giving correct answers? Is it hallucinating? Is it missing important context?

Step 1: Build an Evaluation Dataset

# eval_dataset.py
eval_cases = [
    {
        "id": "eval_001",
        "input": "What's the parental leave policy?",
        "expected_output": "16 weeks for primary caregivers, 8 weeks for secondary caregivers",
        "required_sources": ["Employee Handbook - Leave Policies"],
        "category": "factual_recall",
    },
    {
        "id": "eval_002",
        "input": "How do I submit a leave request?",
        "expected_output": "Through the HR portal, at least 2 weeks in advance",
        "required_sources": ["Employee Handbook - Leave Policies"],
        "category": "procedural",
    },
    {
        "id": "eval_003",
        "input": "What should I wear to a client meeting?",
        "expected_output": "Business formal: suit and tie for men, business suit or dress for women",
        "required_sources": ["Employee Handbook - Dress Code"],
        "category": "factual_recall",
    },
    {
        "id": "eval_004",
        "input": "Can you submit a sick leave request for me today?",
        "expected_behavior": "Should call submit_leave_request tool with leave_type='sick'",
        "category": "tool_use",
    },
    {
        "id": "eval_005",
        "input": "What's the meaning of life?",
        "expected_behavior": "Should politely decline or redirect to onboarding topics",
        "category": "boundary",
    },
]

Step 2: Automated Evaluation

# evaluator.py
def evaluate_factual_recall(agent_fn, case: dict) -> dict:
    """Evaluate if the agent correctly recalls facts from documents"""
    response = agent_fn(case["input"])

    # Check 1: Does the response contain the expected information?
    expected_keywords = case["expected_output"].lower().split()
    response_lower = response.lower()
    keyword_hits = sum(1 for kw in expected_keywords if kw in response_lower)
    recall_score = keyword_hits / len(expected_keywords)

    # Check 2: Did it cite the right source?
    source_cited = any(src.lower() in response.lower() for src in case["required_sources"])

    return {
        "case_id": case["id"],
        "category": case["category"],
        "recall_score": recall_score,
        "source_cited": source_cited,
        "passed": recall_score > 0.7 and source_cited,
        "response": response[:200],
    }

def evaluate_tool_use(agent_fn, case: dict) -> dict:
    """Evaluate if the agent correctly uses tools"""
    # Capture tool calls during execution
    tool_calls_made = []
    original_implementations = {}

    # Wrap tools to capture calls
    for name, impl in TOOL_IMPLEMENTATIONS.items():
        original_implementations[name] = impl
        def make_wrapper(n):
            def wrapper(*args, **kwargs):
                tool_calls_made.append({"name": n, "args": kwargs})
                return original_implementations[n](*args, **kwargs)
            return wrapper
        TOOL_IMPLEMENTATIONS[name] = make_wrapper(name)

    try:
        response = agent_fn(case["input"])
        expected_tool = case["expected_behavior"].split("'")[1] if "'" in case["expected_behavior"] else ""
        correct_tool_called = any(tc["name"] == expected_tool for tc in tool_calls_made)

        return {
            "case_id": case["id"],
            "category": case["category"],
            "correct_tool_called": correct_tool_called,
            "tools_called": [tc["name"] for tc in tool_calls_made],
            "passed": correct_tool_called,
        }
    finally:
        # Restore original implementations
        for name, impl in original_implementations.items():
            TOOL_IMPLEMENTATIONS[name] = impl

def run_evaluation(agent_fn) -> dict:
    results = []
    for case in eval_cases:
        if case["category"] in ("factual_recall", "procedural"):
            results.append(evaluate_factual_recall(agent_fn, case))
        elif case["category"] == "tool_use":
            results.append(evaluate_tool_use(agent_fn, case))

    total = len(results)
    passed = sum(1 for r in results if r["passed"])

    return {
        "total_cases": total,
        "passed": passed,
        "pass_rate": passed / total if total > 0 else 0,
        "results": results,
    }

Step 3: LLM-as-Judge

# llm_judge.py
def llm_judge_evaluation(case: dict, agent_response: str) -> dict:
    """Use GPT-4 as a judge to evaluate response quality"""
    judge_prompt = f"""You are an expert evaluator for an onboarding assistant.

User Question: {case['input']}
Expected Answer: {case['expected_output']}
Agent Response: {agent_response}

Rate the response on these dimensions (1-5 scale):
1. Accuracy: Is the information correct?
2. Completeness: Does it cover all key points?
3. Helpfulness: Would a new employee find this useful?
4. Tone: Is it professional and friendly?

Output a JSON object with scores and a brief explanation."""

    response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": judge_prompt},
            {"role": "user", "content": "Evaluate the response."},
        ],
    )

    return json.loads(response.choices[0].message.content)

Step 4: Iterate Based on Results

# iteration.py
def run_eval_improve_loop(max_iterations: int = 3):
    for iteration in range(max_iterations):
        print(f"\n=== Iteration {iteration + 1} ===")

        # Run evaluation
        results = run_evaluation(memory_aware_agent_chat)
        print(f"Pass rate: {results['pass_rate']:.0%}")

        # Analyze failures
        failures = [r for r in results["results"] if not r["passed"]]
        for f in failures:
            print(f"  FAIL [{f['case_id']}]: {f.get('response', '')[:100]}")

        if results["pass_rate"] >= 0.9:
            print("Target reached!")
            break

        # Iterate: improve prompts, add documents, fix tools
        print("  → Improving system prompt and adding more documents...")
        # (In practice, you'd modify your prompts, add documents, etc.)

Stage 5 Complete: You now have a measurable, iteratively improving Agent system. One last step: making it production-ready.


6.6 Distillation & Deployment

The Problem

Your Agent works great with GPT-4, but at scale the costs are unsustainable:

  • 500 employees × 10 queries/day × 30 days = 150,000 queries/month
  • At ~2000 tokens per query (input + output), that’s 300M tokens/month
  • GPT-4 cost: ~$3,000/month

You need a cheaper model that performs just as well for your specific domain.

Step 1: Distill to a Smaller Model

# distillation.py
# Generate training data using GPT-4 as teacher
def generate_training_data(n_examples: int = 1000) -> list[dict]:
    training_data = []

    for case in eval_cases * (n_examples // len(eval_cases)):
        # Add variations to create more diverse data
        variations = [
            case["input"],
            f"Hey, {case['input'].lower()}",
            f"Quick question: {case['input']}",
        ]

        for variant in variations:
            response = client.chat.completions.create(
                model="gpt-4",
                messages=[
                    {"role": "system", "content": "You are an onboarding assistant for Acme Corp..."},
                    {"role": "user", "content": variant},
                ],
            )
            training_data.append({
                "input": variant,
                "output": response.choices[0].message.content,
            })

    return training_data

# Fine-tune a small model (e.g., Llama 2 7B) using the training data
# (See Production chapter for detailed fine-tuning code)

Step 2: Set Up Monitoring

# monitoring.py
from prometheus_client import Counter, Histogram, start_http_server

request_counter = Counter('qa_agent_requests_total', 'Total requests', ['intent', 'status'])
request_latency = Histogram('qa_agent_latency_seconds', 'Request latency')
token_counter = Counter('qa_agent_tokens_total', 'Token usage', ['type'])

# Start Prometheus metrics server
start_http_server(8000)

def tracked_agent_chat(user_id: str, user_message: str) -> str:
    import time
    start = time.time()

    try:
        response = memory_aware_agent_chat(user_id, user_message)
        request_counter.labels(intent="general", status="success").inc()
        return response
    except Exception as e:
        request_counter.labels(intent="general", status="error").inc()
        raise
    finally:
        request_latency.observe(time.time() - start)

Step 3: Deploy with Canary Release

# deployment.py
def route_request(user_id: str, user_message: str) -> str:
    """Route 5% of traffic to the new distilled model"""
    import hashlib
    bucket = int(hashlib.md5(user_id.encode()).hexdigest(), 16) % 100

    if bucket < 5:
        # Canary: use distilled model
        return distilled_model_chat(user_id, user_message)
    else:
        # Stable: use GPT-4
        return memory_aware_agent_chat(user_id, user_message)

Final Architecture

┌─────────────────────────────────────────────────────────┐
│                    Q&A Agent System                       │
│                                                          │
│  User ──▶ Router ──▶ Agent Core ──▶ LLM (GPT-4/7B)     │
│              │            │                               │
│              │            ├── RAG Pipeline (Vector DB)    │
│              │            ├── Tool Registry (HR API)      │
│              │            ├── Memory Store (User Profile) │
│              │            └── Skill Loader (Onboarding)   │
│              │                                            │
│              └── Monitoring (Prometheus + Grafana)        │
│                                                          │
│  Canary: 5% → Distilled Model (7B, fine-tuned)          │
│  Stable: 95% → GPT-4                                    │
└─────────────────────────────────────────────────────────┘

What You’ve Built

Starting from a single API call, you’ve progressively built:

StageCapabilityKnowledge Applied
6.1Basic conversationLLM Fundamentals (Tokens, Context Window)
6.2Document-grounded answersRAG (Embedding, Chunking, Retrieval)
6.3Tool use & planningAgent Core (Function Calling, ReAct, MCP)
6.4Memory & personalized responsesMemory & Skill Systems
6.5Measurable qualityEvaluation Framework
6.6Cost-effective production systemDistillation, Monitoring, Canary Release

This is the complete Agent engineering path — from “Hello World” to production.