Skip to main content
C carlos.enredando.me CTO · Advisor · Builder
Mastering Agentic AI: The Evaluation and Monitoring Pattern
Numbers on a screen — photo by Martin Sanchez on Unsplash.

Mastering Agentic AI: The Evaluation and Monitoring Pattern

·1416 words·7 mins
Carlos Prados
Author
Carlos Prados
Telecommunications Engineer, Entrepreneur, CTO & CIO, Team Leader & Manager, IoT-M2M-Big Data Consultant, Pre-sales Engineer, Product-Service Manager & Strategist.

In my previous post, we looked at Guardrails / Safety Patterns — the rails that keep an agent from doing something stupid or dangerous in the moment. Guardrails answer “is this single action safe right now?” But they don’t answer a different, harder question: is this agent actually any good?

You can ship an agent that never violates a guardrail and still be slow, expensive, and wrong half the time. And the brutal part is that with a non-deterministic system, “wrong half the time” is invisible until you measure it. There’s no stack trace for a mediocre answer.

That’s where the Evaluation and Monitoring pattern comes in. It’s the least glamorous pattern in the book and the one that separates a demo from a product.

Pattern #19: Evaluation and Monitoring
#

The Problem
#

Traditional software has a comforting property: given the same input, you get the same output. You write a unit test, it passes, it keeps passing. Agentic systems break that contract on day one. The same prompt yields different responses. Quality is a distribution, not a boolean.

So how do you know if a change made things better or worse? How do you catch the slow regression where your agent’s answers quietly degrade after a model update or a prompt tweak? How do you bound the cost when every interaction burns tokens you pay for?

You need two things working together:

  • Evaluation — offline and online scoring of output quality: is the answer correct, relevant, complete, unbiased?
  • Monitoring — continuous tracking of operational health: latency, token usage, cost, error rates.

Neither one is optional. An agent that gives great answers in 40 seconds at a euro per call is a failed product. An agent that’s blazing fast and cheap but wrong is worse. You have to watch both axes at once.

The Solution
#

Start with the cheap, deterministic signals. Not everything needs an LLM. Exact-match accuracy, latency, and token counting are dumb, fast, and free — and they catch a surprising amount.

import time
from typing import Callable, Any

def evaluate_response_accuracy(agent_output: str, expected_output: str) -> float:
    """Calculates a simple binary accuracy score."""
    is_correct = agent_output.strip().lower() == expected_output.strip().lower()
    return 1.0 if is_correct else 0.0

def timed_agent_action(agent_function: Callable, *args, **kwargs):
    """Measures execution time of an agent action in milliseconds."""
    start_time = time.perf_counter()
    result = agent_function(*args, **kwargs)
    end_time = time.perf_counter()
    latency_ms = (end_time - start_time) * 1000
    return result, latency_ms

For operational health, the same minimalism applies. You don’t need a vendor dashboard on day one — you need a place to accumulate the numbers that decide whether you can afford to keep the lights on:

class LLMInteractionMonitor:
    """Conceptual monitor for tracking token usage across interactions."""

    def __init__(self):
        self.total_input_tokens = 0
        self.total_output_tokens = 0

    def record_interaction(self, prompt: str, response: str):
        # Placeholder splitting; use tiktoken or similar for real projects
        input_tokens = len(prompt.split())
        output_tokens = len(response.split())
        self.total_input_tokens += input_tokens
        self.total_output_tokens += output_tokens
        print(f"Metrics: In={input_tokens}, Out={output_tokens}")

    def get_summary(self):
        return {
            "total_input": self.total_input_tokens,
            "total_output": self.total_output_tokens,
        }

The word-split tokenizer is deliberately a placeholder — swap in tiktoken (or your provider’s usage metadata) for real numbers. The point is the shape: every interaction flows through one chokepoint that records what it cost.

But exact-match accuracy collapses the moment your agent produces open-ended prose. “Paris” == “Paris” works for a trivia bot. It’s useless for “summarize the risks of AGI.” There’s no string to match against. This is where the real workhorse of agentic evaluation comes in: LLM-as-a-Judge.

The idea: use a second LLM, given an explicit rubric, to score the first one’s output. You’re not asking it for vibes — you’re handing it criteria and demanding structured, machine-readable scores back.

EVALUATION_RUBRIC = """
Evaluation Criteria (score each 1-5):
1. **Clarity & Precision**: Is the response clear, unambiguous, and well-structured?
2. **Neutrality & Bias**: Is the response balanced and free from bias?
3. **Relevance**: Does the response directly address the question asked?
4. **Completeness**: Does the response cover all important aspects of the topic?
"""


class LLMJudge:
    """Evaluates text outputs against a rubric using LLM-based scoring."""

    def __init__(self):
        self.llm = get_llm(temperature=0)

    def evaluate(self, question: str, response: str) -> dict:
        prompt = ChatPromptTemplate.from_messages([
            ("system",
             "You are an expert evaluator. Assess the following response against the rubric.\n\n"
             "Rubric:\n{rubric}\n\n"
             "Output your evaluation as valid JSON with these fields:\n"
             '{{"overall_score": <1-5>, "clarity": <1-5>, "neutrality": <1-5>, '
             '"relevance": <1-5>, "completeness": <1-5>, '
             '"rationale": "<brief explanation>", '
             '"recommended_action": "<accept/revise/reject>"}}'),
            ("user",
             "Question: {question}\n\nResponse to evaluate:\n{response}")
        ])
        chain = prompt | self.llm | StrOutputParser()
        raw = chain.invoke({
            "rubric": EVALUATION_RUBRIC,
            "question": question,
            "response": response,
        })

        try:
            cleaned = raw.strip()
            if cleaned.startswith("```"):
                cleaned = "\n".join(cleaned.split("\n")[1:-1])
            return json.loads(cleaned)
        except json.JSONDecodeError:
            return {"raw_evaluation": raw, "parse_error": True}

Two details that matter more than they look. First, temperature=0 on the judge: you want the scoring to be as reproducible as possible, even if the thing being scored is not. Second, the defensive JSON parsing — stripping markdown fences, falling back to a parse_error flag instead of crashing. The judge is itself an LLM, which means it will occasionally wrap its JSON in ```json or hallucinate a trailing comment. An evaluation harness that dies when the evaluator misbehaves is worse than no harness. Treat the judge’s output as untrusted input, because it is.

The recommended_action field — accept / revise / reject — is the bridge from evaluation to action. A score is data; an action is a decision. That single field is what lets you wire the judge into a Reflection loop (revise) or a Guardrail (reject) instead of just logging a number nobody reads.

Why This Matters
#

Concretely, this pattern shows up in three places in any serious agent:

  • CI for agents (offline eval): keep a golden dataset of question/expected pairs. Run the judge on every prompt or model change. If the average score drops, the build fails — same discipline as a failing unit test, applied to a fuzzy system.
  • Production monitoring (online eval): sample a slice of live traffic, score it with the judge, alert when quality drifts. Pair it with the latency and token monitor so you see quality, speed, and cost on one dashboard.
  • Self-improvement loops: feed recommended_action: revise straight back into the agent, closing the loop with Reflection.

The honest trade-offs:

  • The judge has its own cost and latency. Every evaluation is another LLM call. You can’t judge 100% of production traffic — sample it. Offline, you can afford to be thorough; online, you’re buying signal, not certainty.
  • LLM judges are biased. They favor longer answers, their own writing style, and the first option in a pairwise comparison. Calibrate against human labels on a sample before you trust the scores, and pin the judge model version so your baseline doesn’t shift under you.
  • A rubric is a product decision, not a technical one. “Good” for a legal assistant is not “good” for a creative writing tool. The four criteria above are a starting template — the real work is defining what quality means for your use case, and that work doesn’t have an API.

Rule of thumb: start with the cheap deterministic metrics, add LLM-as-a-Judge only where there’s no ground-truth string to match, and never run an agent in production you can’t measure. If you can’t tell whether yesterday’s deploy made the agent better or worse, you don’t have a product — you have a slot machine.


The Bigger Picture
#

This is post #19 in my series documenting Antonio Gulli’s Agentic Design Patterns. All credit for the conceptual work goes to him — I’m focused on producing clean, runnable Python implementations that you can clone, modify, and ship.

Evaluation and Monitoring is the pattern that makes every other pattern in this series trustworthy. Reflection needs a critic — that’s a judge. Guardrails need a definition of “unsafe” — that’s a rubric. Learning and Adaptation needs a reward signal — that’s a score. This chapter is the measurement substrate the rest of the patterns quietly depend on.

All the code from this post lives in my repository: carlosprados/Agentic_Design_Patterns, specifically under 19_Evaluation_and_Monitoring/. The examples run with uv run and use the shared get_llm() abstraction, so they work with Gemini and Ollama out of the box.


What’s Next
#

In the next post we’ll tackle Prioritization — how an agent decides what to do first when it has more tasks than time, conflicting goals, and finite resources. Evaluation tells you whether your agent is good; prioritization tells it where to spend the effort. It’s the difference between an agent that’s busy and an agent that’s effective.

Stay tuned.