AI EngineeringZero to ProductionHome·About·What’s new·Contact
Interoperability & Agent Ops · Chapter I4

Evaluation and Tracing with LangSmith

O4 taught you to trace and evaluate agents by hand. LangSmith is the productized version: a platform that captures every step of every run, turns them into datasets, runs evals against them, and lets you compare versions — closing the LLMOps loop with a real tool instead of print statements.

⏱️ ~50 min📊 Hands-on🎯 Intermediate
⚙️ To run this for realThe code here is complete and correct as written. To actually execute it you'll need a LangSmith API key (LANGSMITH_API_KEY) plus LANGSMITH_TRACING=true and pip install langsmith, and an API key for whichever model vendor you follow (toggle the Claude | OpenAI tabs on the code below):Either way, keep the key in a .env file (never in code) and let the zero-argument client pick it up from the environment.Reading and learning works without any of this — run when you're ready. IDs/ARNs in examples are placeholders; swap in your own.

Learning objectives

  • Explain what LangSmith adds over hand-rolled tracing (O4) and evals (Ch 5).
  • Instrument an app to send traces, framework or not.
  • Turn real traces into datasets and run offline evaluations.
  • Use LLM-as-judge and custom evaluators inside the platform.
  • Compare prompt/model versions and wire an eval gate into CI (O3).
📋 Before you startThis page builds on: Evaluation & testing. Jumping in? Skim those first — or open the recap.
30-second recap
  • Evaluation & testing: A golden set of real questions plus cheap deterministic checks and an LLM judge, run as a regression gate before every change ships.
This is O4 + Ch 5, productizedYou already built the concepts: traces as the request's step-tree (O4), golden sets and LLM-judge evals (Ch 5), the eval gate before rollout (O3). LangSmith is one popular tool that packages all of it. Framework-agnostic (works with the raw SDK, LangChain, or anything), and vendor tools change — learn what job each feature does, not exact UI.

Why a platform beats print statements advanced

Picture the 2am page you don't want. Your agent gave a customer a confidently wrong answer, the ticket's been escalated, and all you have to reconstruct what happened is a wall of print() output interleaved from a dozen concurrent requests. You grep for the user's question, find three matching lines, and realize you never logged which tool the agent called or what the retriever returned. The run is gone. That helpless feeling — I can't see what my own system did — is the exact problem an observability platform exists to kill.

Here's the honest framing, because it's easy to feel like reaching for a platform is admitting your O4 code wasn't good enough. It was good enough — for learning. Rolling your own span() helper in O4 was the right way to understand tracing, the same way building a stack from scratch teaches you what a stack is. But hand-rolled logging hits a wall the moment you have real traffic: thousands of runs you need to search, two prompt versions you need to compare across a hundred cases, a product manager who wants to inspect a failure without reading Python. Those aren't code problems you can out-clever — they're storage, search, and UI problems, and building that yourself means building a second product alongside your actual one.

The analogy that makes this click: think of the difference between writing print(timestamp) in your code versus wiring up a real logging service like Datadog or CloudWatch. Both record events — but one is a string in a terminal that scrolls away, and the other is durable, indexed, queryable, and shareable across a team. LangSmith is that same jump, applied specifically to LLM runs. It doesn't invent a new idea; every concept — the trace tree, the span, the golden set, the LLM judge — you already met in O4 and Chapter 5. What it adds is the boring, essential infrastructure that turns those concepts from "a thing you did once by hand" into "a thing your whole team does continuously at scale."

The common beginner mistake is treating tracing as an afterthought — something you bolt on when a bug appears. By then it's too late: the run you needed to see already happened untraced. The discipline is to trace from the start, so the data is already sitting there when the incident lands. The other half of the mistake is thinking tracing and evaluation are separate chores. They aren't — and that unity is the platform's real trick. The traces you capture become your evaluation dataset, and your evaluations run over that same trace data. Capture is the foundation everything else stacks on; read the table below as a job-by-job map from O4/Ch 5 to what the platform hands you, but keep that one-loop picture in mind rather than seeing five disconnected features.

Your O4 span() helper works — until you have thousands of runs, want to search them, compare two prompt versions across a hundred cases, or let a non-engineer inspect a failure. Hand-rolled logging doesn't scale to those. A tracing/eval platform does: it stores runs durably, makes them searchable and visual, and connects tracing to evaluation so the two reinforce each other.

By hand (print) With a platform print(run 1) print(run 2) scrolls away · grep-only · gone durable store searchable + visual shared with team indexed · queryable · replayable The same runs, two fates. A print() is a string in a terminal that scrolls away and can only be grepped; a platform stores the same run durably, indexed and shareable — the storage/search/UI problem you don't want to build yourself.
🗺️ How to read this diagram

Read it as two panels side by side: the same LLM runs, handled two ways. Left is what you get with bare print(); right is what a platform hands you.

  • The left (red) panel stacks two print(run) lines that flow down a dashed arrow into "scrolls away · grep-only · gone" — the run isn't stored, so once the terminal scrolls it's lost, and search means grepping if you happened to log the right field.
  • The right (green) panel shows the same runs landing in a durable store that is searchable, visual, and shared across the team — indexed, queryable, and replayable later.
  • The payoff is the contrast: the platform doesn't add a new idea, it solves the boring storage, search, and UI problem that hand-rolled logging never can at scale.

In short: print = a string that scrolls away; a platform = the same run made durable, searchable, and shareable.

Job (from O4/Ch 5)By handWith LangSmith
See one run's stepsRead log lines, reconstruct the treeVisual trace tree, timed & costed per step
Search runsgrep, if you logged the right fieldsFilter/search over all runs
Build a golden setCopy inputs into a file manuallyPromote real traces into a dataset in a click
Run evalsWrite a harness (Ch 5)Run evaluators over a dataset; store scores
Compare versionsEyeball two outputsSide-by-side diff across the whole dataset

Lab I4.1 · Tracing an app advanced

Everything in this chapter is built on one primitive, so it's worth dwelling on: the trace. If you skip capturing runs, you have nothing to search, nothing to promote into a dataset, nothing to score, and nothing to monitor. That's why this is Lab I4.1 and not an afterthought — get capture right and the rest of the platform simply falls out of it. A trace is just a faithful recording of what one run of your code did: the input it received, the output it produced, how long each step took, how much it cost in tokens, and where — if anywhere — it blew up.

Think of it like the black box flight recorder on an aircraft. Nobody wants the plane to crash, but if it does, the recorder is the difference between "we have no idea what happened" and "engine two failed at 04:12, here's the exact sequence." Your LLM app will produce surprising outputs — that's the nature of a probabilistic system — and the trace is the recorder that lets you replay the surprise instead of shrugging at it. The tempting shortcut is to reach for tracing only after something breaks; the discipline, as with a flight recorder, is that it's already running before anyone needs it.

The one idea that trips people up here: a trace is not a single log line, it's a nested tree of spans. A span is one timed step — the model call, the retriever lookup, a tool invocation — and spans nest inside each other the way function calls do. Your top-level answer() is the root; the retrieve step and the model call are its children; a tool the model triggers is a grandchild. This is exactly the step-tree you built by hand in O4, and it's what lets you answer "where did the time go?" and "which step failed?" instead of just "the run was slow" or "the run errored." A flat log can't do that; a tree can.

Here's the part that feels almost too easy: you get that whole tree from a single decorator. LangSmith auto-instruments LangChain and LangGraph with zero code changes (just environment variables), and for everything else — including the raw-SDK apps you wrote back in C2 — it gives you @traceable, a one-line wrapper you place above any function. Decorate your model call, your retriever, and each tool, and the platform assembles the nested tree for you and stores it durably. The lab below is the smallest possible version: one decorated function, one call, one fully-captured run.

The foundation is capturing runs. LangSmith auto-instruments LangChain/LangGraph, and offers a decorator/wrapper for arbitrary code — so even your raw-SDK apps (C2) get traced.

answer()root span · 255 ms retrievechild · 40 ms model callchild · 210 ms tool callgrandchild · 12 ms A trace is a tree, not a log line. Each span is one timed step and nests the way function calls do — so you can answer "which step failed?" and "where did the time go?", which a flat log can't.
🗺️ How to read this diagram

Read it top to bottom, the way calls nest: the box at the top is the whole run, and everything below it is a step that happened inside.

  • The top box, answer(), is the root span — the entire run, timed at 255 ms.
  • The two arrows down to retrieve (40 ms) and model call (210 ms) are its children — the timed steps it ran, exactly the step-tree from O4.
  • The arrow from model call down to tool call (12 ms) is a grandchild: a tool the model triggered, nested one level deeper.
  • The payoff is the per-step timing: because the tree carries each span's duration, you see the 210 ms model call is where the time went — a flat log would only tell you "the run took 255 ms."

In short: spans nest like function calls, so the trace tells you which step was slow or failed — not just that the run was.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Lab I4.1
shellpip install langsmith
export LANGSMITH_API_KEY=...          # plus LANGSMITH_TRACING=true

A traced model call — in Claude or OpenAI

LangSmith's @traceable decorator wraps any function, so the tracing scaffolding is identical for both vendors — only the model call inside changes. Claude uses client.messages.create(..., messages=[…]) and reads the first text block; OpenAI uses client.responses.create(..., input=…) and reads r.output_text. Toggle the tab to switch SDK.

trace.pyfrom langsmith import traceable
from anthropic import Anthropic
client = Anthropic()

@traceable                              # captures inputs, outputs, timing, errors
def answer(question: str) -> str:
    r = client.messages.create(
        model="claude-opus-4-8", max_tokens=512,
        messages=[{"role":"user","content": question}],
    )
    return next(b.text for b in r.content if b.type == "text")

answer("What is prompt caching?")   # run appears in LangSmith, fully traced
▶ How this works

This is the smallest possible traced app. "Tracing" means recording what your code did on each run — the inputs it got, the output it produced, how long it took, and any error — so you can inspect it later instead of guessing. Here a single decorator turns an ordinary function that calls the model into one whose every run is captured by LangSmith.

  1. The two import lines bring in traceable (LangSmith's recorder) and the model client — Anthropic for Claude, OpenAI for OpenAI (toggle the tab above). The client is built once so the function can reuse it.
  2. @traceable is a decorator — a line placed directly above a function that wraps it with extra behaviour. This one records a span (one timed step) every time answer() runs: what question came in, what text went out, how many milliseconds it took, and whether it crashed. You did not have to write any logging.
  3. Inside, the model call (client.messages.create(...) on Claude, client.responses.create(...) on OpenAI) is the actual request — it names the model, caps the reply length, and passes the user's question.
  4. The return line reads the answer out of the reply — the first text block via next(b.text ...) on Claude, or r.output_text on OpenAI. That returned string is what LangSmith records as this span's output.
  5. The last line, answer("What is prompt caching?"), simply calls the function once. Because it is decorated, that single call now shows up as a full trace in the LangSmith UI.

What the output means: Nothing dramatic prints locally — the payoff is remote: a new run appears in LangSmith showing the input question, the model's answer, the timing, and the token cost, all captured automatically.

Try this: Add a second @traceable function (say a fake retrieve() step) and call it from inside answer(). LangSmith will nest the two into a tree — parent answer with retrieve as a child — which is the whole trace-tree idea from O4, built for you.

@traceable nests — that's the whole trace treeDecorate your model call, your retriever, and each tool, and LangSmith assembles the nested tree from O4 automatically: which step ran, how long, what it cost, what errored. For a LangChain/LangGraph app you often get this with zero code changes — just the env vars. It's your hand-rolled span(), but it builds the tree for you and stores it durably.
Tracing sends your prompts & outputs to a service — that's a data decisionTraces contain full inputs and outputs, which may include PII, secrets, or confidential content (O4, T1). Sending them to a hosted platform is exactly the governance decision from O4: redact sensitive fields, control who can view traces, check data-handling/residency requirements, and consider a self-hosted/enterprise deployment where regulation demands it. Observability that leaks data is a governance failure.

Lab I4.2 · From traces to datasets advanced

Where do the test cases for an LLM app actually come from? This is the question that quietly sinks most evaluation efforts. The instinct is to sit down and invent a golden set — dream up twenty questions you think users will ask, write down what you think the right answers are, and call it a test suite. It feels productive. It's also a trap, because you're testing the app against your imagination of how it's used, not how it's actually used. The cases that break in production are almost always the ones you'd never have thought to write: the weirdly-phrased question, the edge-case document, the input in a language you forgot to handle.

LangSmith's answer — and the single most important habit in this chapter — is to build your evaluation set from real traces instead of from guesses. Because you're already capturing every run (Lab I4.1), you have a growing pile of real inputs and real outputs. When a run is interesting — a genuinely great answer worth locking in, or a production failure worth never repeating — you promote it into a dataset with one click. The reference answer you attach is the correct output, which for a failure means the answer it should have given. Your golden set is now grown from the field, not the whiteboard.

The mental model to hold is a flywheel, not a pipeline. A pipeline runs once, start to finish, and stops. A flywheel is a loop that gets heavier and harder to knock over every time it turns: traces feed the dataset, the dataset feeds evaluations, and the failures those evaluations surface feed back into the dataset as new permanent cases. Each turn, your test suite gets a little more comprehensive and your app a little harder to break. This is the productized form of the O4 habit "every incident becomes a test case" — LangSmith just makes promoting a trace to a golden case a button instead of a copy-paste chore.

The common mistake worth naming is treating the dataset as a one-time artifact you build at the start of the project and never touch again. A static golden set rots: your app changes, your users change, new failure modes appear, and a test suite frozen in month one stops catching anything by month six. The whole point of the loop below is that the dataset is living — every real failure you find becomes a case that can never silently regress again. Read the diagram as exactly that: a circle, with the dashed arrow closing the loop being the part that matters most.

The platform's superpower is the loop between the two halves: real traces become your evaluation dataset. When a run is interesting — a great answer, or a production failure — you promote it into a dataset. Over time your golden set (Ch 5) is built from real usage, not guesses.

prod tracesreal runs datasetgolden set evaluationscored failures found → promoted to the dataset → can't regress again The flywheel that makes a system reliable. Real runs feed a dataset; the dataset drives evals; eval failures (and the O4 incident runbook) feed more cases back into the dataset. This is exactly the "every incident becomes a test case" habit from O4 — LangSmith is the machinery that makes promoting a trace to a golden case a one-click action.
🗺️ How to read this diagram

This diagram shows the flywheel at the heart of LangSmith: real runs feed a dataset, the dataset feeds evaluations, and the failures those evaluations find get promoted back into the dataset — so the system keeps getting harder to break.

  • Read it left to right. The first green box, prod traces, is the real runs your live app produced (captured by the tracing from Lab I4.1).
  • The arrow into dataset (the "golden set") means you promote interesting runs — great answers, or bad ones — into a saved collection of test cases. This is your evaluation data, built from real usage instead of guesswork.
  • The next arrow into evaluation ("scored") means the evaluators from Lab I4.3 run over that dataset and produce scores.
  • The curved dashed arrow looping back from evaluation to the first box is the key part: when an eval surfaces a failure, you add that case to the dataset. The red caption spells it out — failures found → promoted to the dataset → can't regress again.

In short: It is a loop, not a line: trace → collect → evaluate → feed failures back. Every bug you find becomes a permanent test, so the same mistake can never sneak back in.

Lab I4.3 · Running evaluators expert

A dataset alone doesn't tell you anything — it's just questions and answers sitting in a drawer. The thing that turns it into a signal is an evaluator: a small function that looks at what your app produced for a given input and scores how good it was. Without evaluators, "we have 200 golden cases" is a filing cabinet; with them, it's a quality gauge you can read at a glance. This section is where the two halves of the platform finally connect — the traces you captured became a dataset, and now the dataset gets graded.

The cleanest way to think about an evaluator is that it's exactly like a unit test's assert, but returning a score instead of pass/fail — and crucially, one that can handle the fact that language has no single right answer. A normal test asserts add(2, 2) == 4 and that's the end of it. But "was this support answer helpful?" has no ==. So evaluators come in a spectrum of strictness, and matching the evaluator to the question is the real skill. Some questions do have an exact answer (did it return valid JSON? does the output contain the order number?) — for those you write a plain deterministic rule, fast and free and perfectly reliable. Other questions are irreducibly fuzzy (was the tone right? is the answer grounded in the retrieved context?) — for those you reach for an LLM-as-judge: a second model call that reads the output and grades it against a rubric, exactly the Chapter 5 technique.

The mistake nearly everyone makes is jumping straight to an LLM judge for everything because it feels the most "AI." It's the wrong default. A judge is slower, costs money on every example, and — this is the part that bites — is itself a fallible LLM that can be confidently wrong about whether your answer was good. If a cheap deterministic rule can answer the question ("does the JSON parse?"), use it: it's faster, free, and never hallucinates. Save the judge for the genuinely subjective calls it's the only tool for, and even then, spot-check its verdicts against your own the way Chapter 5 warned. The platform's contribution isn't deciding what to measure — that judgment stays yours — it's giving you a place to run whatever evaluators you write across the whole dataset and store every score over time.

The beautiful part of the interface you'll see below is that all three flavours — deterministic, judge, and anything custom you dream up — share one shape: a function that takes the run and the example and returns a {"key": ..., "score": ...} dict. Because a custom evaluator is just "any Python you can write," a latency budget, a cost cap, or a tool-call-correctness check all become first-class tracked metrics with no special API. Write the rule; the platform charts it.

With a dataset, you run evaluators over it — the same eval types from Chapter 5, executed and scored by the platform.

Has an exact answer? valid JSON? contains order #? → deterministic rule Irreducibly fuzzy? right tone? grounded? → LLM-as-judge fast · free · never hallucinates slower · costs · itself fallible {"key":..., "score":...}one shared shape Match the evaluator to the question. If a cheap rule can answer it, use the deterministic one — save the fallible, paid LLM judge for the genuinely subjective calls. Either way both return the same {"key", "score"} dict, so the platform charts them identically.
🗺️ How to read this diagram

Read the top two panels as a fork — you pick a lane based on the kind of question — then follow both arrows down to the one shape they share.

  • The left (green) panel is for questions with an exact answer (does the JSON parse? does the output contain the order number?): use a deterministic rule — fast, free, and it never hallucinates.
  • The right (amber) panel is for irreducibly fuzzy questions (is the tone right? is the answer grounded?): reach for an LLM-as-judge — but note the cost, the latency, and that the judge is itself a fallible model.
  • The two arrows converge on the bottom box: whichever flavour you write — deterministic, judge, or any custom Python — it returns the same {"key", "score"} dict, which is why the platform can run and chart them all the same way.

In short: pick the cheapest evaluator that can answer the question; the judge is a last resort, not the default.

Lab I4.3

Requires: pip install langsmith

evaluate.pyfrom langsmith import evaluate

def exact_match(run, example):        # deterministic evaluator
    return {"key":"correct",
            "score": run.outputs["output"] == example.outputs["expected"]}

def grounded_judge(run, example):     # LLM-as-judge evaluator (Ch 5)
    verdict = judge_model_says_grounded(run.outputs, example.inputs)
    return {"key":"grounded", "score": verdict}

evaluate(
    my_app,                            # the function/chain under test
    data="my-golden-set",               # the dataset
    evaluators=[exact_match, grounded_judge],
)   # scores stored & visualized per example
▶ How this works

Once you have a dataset (a saved list of example inputs plus their expected answers), an evaluator is a small function that scores how well your app did on each example. This block defines two evaluators and then runs them across the whole dataset with one call to evaluate().

  1. exact_match(run, example) is a deterministic evaluator — no AI, just a fixed rule. Every evaluator receives the run (what your app actually produced) and the example (the saved input + expected answer). It compares run.outputs["output"] to example.outputs["expected"] and returns a score of True/False.
  2. The returned dict — {"key": "correct", "score": ...} — is the required shape: key names the metric so LangSmith can chart it, and score is the value.
  3. grounded_judge(run, example) is an LLM-as-judge evaluator: instead of an exact rule it asks another model whether the answer was grounded in the input. That is the Chapter-5 idea — use a model to grade subjective quality — plugged into the same score-dict interface.
  4. evaluate(my_app, data="my-golden-set", evaluators=[...]) ties it together: run my_app on every example in the dataset named my-golden-set, then apply both evaluators to each result. LangSmith stores and visualises the scores.

What the output means: For each example you get two scores — a true/false correct and a grounded verdict — aggregated in the LangSmith UI so you can see, at a glance, what fraction of your golden set the current version passes.

Try this: Add a third evaluator that checks a latency budget — return {"key":"fast","score": run.latency < 2.0} — and pass it in the evaluators list. Any Python rule you can write becomes a tracked metric.

Evaluator typeWhat it checksFrom…
DeterministicExact match, regex, JSON-schema validity, contains-XCh 5 deterministic evals
LLM-as-judgeGroundedness, helpfulness, tone, rubric adherenceCh 5 LLM-judge
CustomAny Python: latency budget, cost cap, tool-call correctnessYour logic
HumanManual annotation queues for the subjective callsHuman review (O4)
The evaluator concepts are all from Chapter 5Deterministic checks, LLM-as-judge, and the caveat that a judge is itself fallible — none of that changed. LangSmith gives you a place to run them at scale, store the scores, and track them over time. The judgment about what to measure (and that a judge can be wrong) is still yours, from Chapter 5.

Comparing versions & the eval gate expert

Here's a scenario every LLM engineer lives through. You tweak a prompt — add one sentence, reword an instruction — run it against a couple of examples in your head, the outputs look better, and you ship it. A week later someone reports the assistant is now getting a category of question wrong that it used to nail. Your innocent one-sentence change fixed the case you were looking at and quietly broke three you weren't. This is the single most common way LLM apps regress, and it happens because prompt changes are invisible: there's no compiler error, no failing test, nothing to warn you that helping here hurt there.

The fix is to stop trusting your eyes and start trusting the dataset. Because you've stored evaluators and scores, you can run version A (today's app) and version B (your change) over the same golden set and compare them head to head. This is just A/B testing, the discipline that's been standard in web engineering for decades — you don't guess whether the new checkout button converts better, you measure it against the old one on the same traffic. Version comparison brings that rigor to prompts and models: change one thing, re-run the eval, read the result.

But the crucial word is per-example, and this is the part beginners miss. The lazy comparison looks at one number — "version A scored 0.82, version B scored 0.85, ship B." That average is a liar. A version can raise the mean while breaking a handful of specific cases, because the improvement on many easy examples mathematically drowns out a catastrophic regression on a few critical ones. Imagine B gets 5% better at FAQ questions but suddenly fails the one safety-critical case that must never fail — the average still goes up, and you'd have shipped a disaster. A per-example, side-by-side diff catches exactly this: you see the row that went from pass to fail, in red, no matter what the mean did. Always read the rows, not just the total.

Once you're comparing on data, the last step writes itself: turn the comparison into a gate. This is the O3 eval-gate discipline made concrete — the same idea as a CI test suite that blocks a merge when a test fails. Wire the evaluate() call into your pipeline so every prompt or model change re-runs the golden set, and fail the build if the score drops below threshold or any critical case regresses. Now no prompt change ships on vibes; it ships only after clearing the dataset. In production the same idea becomes a canary: roll B out to a slice of traffic and confirm it holds before going wide. The diagram traces that whole path — two versions, one dataset, a per-example compare, and a gate on the far end.

The payoff of storing evals: compare two versions of your app across the whole dataset. Change a prompt or swap a model, re-run the eval, and see per-example whether you improved or regressed — the safe-rollout discipline from O3, with data instead of vibes.

version A version B compareper example gate ship B only if it doesn't regress the dataset (O3) Data-driven rollout. Run both versions over the same dataset, compare scores example-by-example, and gate the deploy on the result — this is exactly O3's eval gate + canary discipline, backed by stored evaluations. In CI, a regression fails the build; in prod, a canary confirms it live.
🗺️ How to read this diagram

This diagram shows how you decide, with data, whether a change is safe to ship. You run two versions of your app over the same dataset and let the scores — not a gut feeling — decide.

  • On the left are two boxes, version A (today's app) and version B (your change — a new prompt or a swapped model). Both are run against the same golden set.
  • The arrow leads to compare ("per example"): LangSmith lines the two runs up example by example, not just as one average, so you can see exactly which cases got better and which got worse.
  • The final arrow reaches gate — the go/no-go decision. The caption states the rule: ship B only if it doesn't regress the dataset.
  • Why per-example matters: a version can raise the average score while quietly breaking a few important cases. The side-by-side comparison catches that; a single average would hide it.

In short: This is the O3 "eval gate": compare A vs B on the same data, then let the score gate the deploy. In CI, a regression fails the build automatically — no prompt change ships on vibes.

Wire it into CI (O3)The evaluate() call can run in your pipeline: on every prompt/model change, run the golden-set eval and fail the build if scores drop below threshold. That's the O3 eval gate made concrete — no prompt change ships without clearing the dataset. LangSmith stores the history so you can see quality trend over releases (the O4 continuous-eval idea).

Online monitoring expert

You can pass every offline eval and still be shipping a broken app. That sounds contradictory until you see why: your golden set only tests the cases you've collected, and production is a firehose of inputs you've never seen. A model provider silently updates the model behind the API, a new customer segment starts asking questions in a style your dataset doesn't cover, an upstream document source changes format — none of that shows up in an offline eval, because offline evals look backwards at a fixed dataset. The world moves on after your last eval run, and the only way to know is to watch the live system.

The clean way to hold the distinction: offline evaluation is the exam you study for; online monitoring is the job you do afterwards. The exam (your golden set) proves you're ready before you deploy — it catches regressions at the gate. Monitoring proves you're still doing well after, on real work you couldn't have rehearsed. You need both, and they're not interchangeable: passing the exam doesn't mean you'll never make a mistake on the job, and doing the job well doesn't retroactively prove your exam was comprehensive. Skip either and you have a blind spot — either you ship regressions, or you never notice drift.

Because LangSmith is already recording every production run as a trace, monitoring is almost free: it's the same trace data, aggregated into dashboards instead of inspected one run at a time. You watch the four signals from O4 — quality (run a sampled LLM judge over live traffic, since you can't judge every run), cost (tokens add up fast at scale), latency (the P95 your users actually feel, not the average), and errors (tool failures, timeouts, refusals) — and you set alerts so a spike pages you instead of waiting for a customer complaint. This is O4's monitoring pillar with a UI bolted on. And notice the through-line of the whole chapter: monitoring, offline evals, and datasets all run off the same captured traces. That's why Lab I4.1 was the foundation — no traces, no monitoring, no evals, nothing.

Beyond offline evals, LangSmith runs on live traffic too: dashboards over production traces (the four signals from O4 — quality via sampled judges, cost, latency, errors), and alerts. This is O4's monitoring pillar with a UI, closing the loop from "trace one run" to "watch the whole system."

Offline eval fixed golden set the exam · before deploy Online monitoring live production traffic the job · after deploy watch the four O4 signals → qual cost lat err a spike → alert pages you, not a customer both run off the SAME captured traces The exam vs the job. Offline evals prove you're ready before deploy; online monitoring watches the four O4 signals on real traffic after. You need both — and both run off the same captured traces, which is why Lab I4.1 is the foundation.
🗺️ How to read this diagram

Read the two top boxes as before-vs-after: the same question ("is it good?") asked at two different moments in a deploy's life.

  • The left (amber) box is offline eval — you score a fixed golden set before deploy. It's the exam you study for: it catches regressions at the gate.
  • The right (green) box is online monitoring — you watch live traffic after deploy. It's the job you do afterwards, on inputs you couldn't have rehearsed.
  • The arrow down from monitoring leads to the four O4 signal cells — qual (a sampled LLM judge), cost, lat (P95 latency), and err in red — and a spike fires an alert instead of waiting for a complaint.
  • The muted note is the through-line: both halves run off the same captured traces, so tracing (Lab I4.1) is the foundation everything else stacks on.

In short: offline = exam before deploy; online = watching the job after — both fed by the same traces.

Offline + online is the full pictureOffline evals (against a dataset) catch regressions before deploy; online monitoring (over live traces) catches drift and new failure modes after (O4). A platform like LangSmith does both from the same trace data — which is why tracing is the foundation everything else stacks on. No traces, no evals, no monitoring.

Not the only tool expert

It would be a shame to walk away from this chapter thinking you learned "LangSmith." You didn't — you learned observability for LLM apps, and LangSmith was the concrete tool we used to make it tangible, the way you might learn version control through Git without confusing the two. Tools churn: vendors get acquired, pricing changes, a better product appears, your company standardizes on something else. If your knowledge is "which button to click in LangSmith," it expires the day you switch tools. If your knowledge is "capture traces, build datasets from real runs, run deterministic and judge evaluators, compare versions, gate CI, monitor prod," it transfers to every platform, forever. Learn the jobs, not the UI.

And the jobs really are portable, because the whole space is converging on shared foundations. A growing number of these platforms trace on top of OpenTelemetry — the same open standard used for tracing ordinary microservices — which means your trace data isn't locked to one vendor's format. The landscape includes LangSmith, other hosted observability/eval products, and mature open-source options you can self-host; they differ in polish and integration, not in the fundamental jobs they do.

So how do you actually choose? Exactly the way you reasoned about stack layers in O2 — by your constraints, not by brand or hype. Three questions decide it. First, your stack: if you're deep in LangChain/LangGraph, LangSmith's zero-config auto-instrumentation is a real gravity well; if you're on the raw SDK or a mixed stack, an OpenTelemetry-based tool may fit more cleanly. Second, and often decisive, your data-residency needs: a hosted platform means your prompts and outputs leave your infrastructure, which for a regulated industry can be a non-starter — that's when self-hosted or enterprise deployments earn their cost. Third, budget: usage-based pricing on a high-traffic app adds up, and an open-source self-host trades engineering time for a lower bill. Weigh those three against how well each tool does the six jobs above, and the choice makes itself.

LangSmith is popular and tightly integrated with LangChain/LangGraph, but the capabilities — tracing, datasets, evaluators, version comparison, monitoring — are the transferable knowledge. Other observability/eval platforms and open-source options (including OpenTelemetry-based tracing) offer the same jobs. Pick by your stack, data-residency needs (self-host vs hosted), and budget, exactly as you chose stack layers in O2.

The portable jobs (learn these) trace datasets evaluators compare gate CI monitor pick a vendor decided by three constraints: stack fit data residency budget Learn the jobs, not the UI. The six capabilities are the same on every platform; the vendor is a swappable slot underneath them. Choose which tool fills it by three constraints — stack fit, data residency, and budget — exactly as you chose stack layers in O2.
🗺️ How to read this diagram

Read it top to bottom: the durable knowledge is on top, the replaceable choice is in the middle, and the tie-breakers are at the bottom.

  • The top grid of six boxes is the portable knowledge — trace, datasets, evaluators, compare, gate CI, monitor. These are the jobs; they transfer to every platform, so learning them (not "which button in LangSmith") is what lasts.
  • The arrow drops to "pick a vendor": the tool is just a swappable slot beneath the jobs — LangSmith, another hosted product, or a self-hosted OpenTelemetry-based option all fill it.
  • The three green boxes are the constraints that decide the slot: stack fit (LangChain gravity vs raw SDK), data residency (hosted means prompts leave your infra — often the decisive one), and budget (usage pricing vs self-host engineering time).

In short: the six jobs are permanent; the vendor is a choice you make on stack, residency, and budget.

The jobs transfer; the vendor is a choiceWhatever tool you land on, you'll want: capture traces, build datasets from real runs, run deterministic + judge + custom evaluators, compare versions, gate CI, and monitor prod. Evaluate a tracing/eval platform by how well it does those O4/Ch 5 jobs — not by brand.

Common pitfalls expert

The failure modes of observability are subtle, because a broken setup still looks like it's working. A dashboard is showing green numbers, evals are passing, traces are flowing — so it's tempting to assume you're covered. The pitfalls below are exactly the cases where that comfortable green hides a real gap, and every one of them maps back to a principle from this chapter or an earlier one. Read the table not as trivia but as the difference between observability that protects you and observability that gives you false confidence.

Two are worth calling out because they're the ones that quietly cause the most damage. The first is tracing with no data-handling plan — the moment you send traces to a hosted service, you're shipping full prompts and outputs off your infrastructure, and those may contain PII, secrets, or confidential content (O4, T1). Observability that leaks data isn't a feature, it's an incident waiting to happen; redact, control access, and check residency before you flip tracing on, not after. The second is building a golden set from imagination instead of promoting real traces — the whole flywheel of this chapter collapses if your test cases are guesses, because you end up perfectly passing evals for inputs no real user ever sends. The rest of the table is the same lesson applied to judges (they're fallible — spot-check them), to monitoring (offline isn't enough — watch live traffic for drift), to rollout (compare versions before shipping), and to trace depth (decorate every step, not just the top call, or your tree has holes exactly where the bug hides).

PitfallFix
Tracing with no data-handling planRedact PII/secrets; control access; check residency (O4, T1)
Building a golden set from imaginationPromote real traces (incl. failures) into the dataset
Trusting the LLM judge blindlyJudges are fallible (Ch 5); spot-check & calibrate
Evals only offlineAlso monitor live traffic for drift (O4)
Prompt changes with no version comparisonCompare across the dataset before shipping (O3)
Only tracing the top-level callDecorate steps/tools so you get the full tree

Exercises expert

Exercise I4.1 — Trace an agent

Context: The payoff of a tracing platform is seeing exactly where an agent run failed and what it cost — the O4 exercise, now inside a real tool.

Your task: Instrument an agent from an earlier lesson with @traceable (or LangChain auto-tracing) on the model call and each tool, then run a query that triggers a tool error.

Requirements:

  • Trace the model call and every tool as separate spans
  • Run a query that deliberately makes a tool fail
  • Confirm the trace tree pinpoints which span failed
  • Confirm the trace shows the cost/latency of the run

💡 Hint: Auto-tracing wraps each step for you; the goal is to read the resulting tree and find the failing span, not to hand-log.

Exercise I4.2 — Build a dataset from failures

Context: The evaluation flywheel starts the moment you promote a real failure into a golden case — after which it can never regress silently.

Your task: Run your app on 10 inputs, find the 2–3 that produce bad answers, and promote them into a dataset as golden cases with the expected output.

Requirements:

  • Exercise the app on ~10 real inputs
  • Identify 2–3 genuinely bad outputs
  • Add each as a dataset example with the correct expected output
  • Note that these cases now guard against silent regression

💡 Hint: Bad outputs are the highest-value eval cases — curate the failures rather than inventing synthetic inputs.

Exercise I4.3 — Version comparison

Context: This is Chapter 5's manual A/B test, now scaled and stored: two prompt versions, scored across a dataset, with the winner kept for a reason.

Your task: Write two prompt versions for a task, run evaluate() for each over your dataset with a deterministic and a judge evaluator, compare per-example, and keep the winner.

Requirements:

  • Two prompt versions, one dataset
  • Score each with both a deterministic evaluator and an LLM-judge
  • Compare per-example, not just on the aggregate
  • Keep the winner and write down why
  • Watch for a version that improves the mean but regresses specific cases

💡 Hint: The per-example diff is what catches a regression an average would hide — that's why you gate on the dataset, not a single example.

Show what to look for

Watch for a version that improves the average but regresses specific cases — the per-example diff catches what an aggregate score hides. That's exactly why you compare across the dataset, not just on one example, before gating a deploy.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · What a trace recordsBeginner

Context: Observability platforms like LangSmith start from one primitive: a trace that records what each run did. You'll model that primitive rather than read log lines.

Your task: Model one traced call offline as a decorator that captures inputs, output, latency, and any error — the job LangSmith's @traceable does.

Requirements:

  • Implement it as a decorator using only the standard library
  • Capture the function name, inputs, latency in ms, and error (None on success)
  • Record the run even when the wrapped function raises, then re-raise
  • Store captured runs somewhere inspectable and print one

💡 Hint: A try/finally around the call lets you time and record the run on both the success and the exception path; functools.wraps keeps the name.

Show solution

Tracing = recording what each run did. Model it in stdlib (the real thing is @traceable):

import time, functools

RUNS = []
def traced(fn):
    @functools.wraps(fn)
    def wrap(*a, **k):
        t0 = time.perf_counter()
        err = None
        try:
            out = fn(*a, **k)
            return out
        except Exception as e:
            err = repr(e); raise
        finally:
            RUNS.append({"name": fn.__name__, "inputs": {"args": a},
                         "latency_ms": round((time.perf_counter()-t0)*1000, 2),
                         "error": err})
    return wrap

@traced
def answer(q):
    return f"echo: {q}"

answer("what is prompt caching?")
print(RUNS[-1])   # captured inputs, latency, error=None

That is exactly the job LangSmith's decorator does — you just don't hand-roll or read log lines.

Exercise 2 · A trace is a tree of spansIntermediate

Context: A real trace is not a flat log — it's a tree of timed spans (retrieve → generate → parse). That structure is what lets a platform beat grep.

Your task: Model a span tree: a parent run with nested, individually-timed child spans, and print the tree with indentation.

Requirements:

  • A span has a name, a duration, and a list of child spans
  • Support arbitrary nesting (a child can have its own children)
  • Print the tree indented by depth, showing each span's time
  • Build a small example (e.g. answer → retrieve, generate → format)

💡 Hint: A tiny recursive node class plus a recursive print indented by depth is all you need; the visual, timed tree is the whole payoff.

Show solution

Traces are step-trees, not flat logs. Runnable:

class Span:
    def __init__(self, name):
        self.name, self.children, self.ms = name, [], 0
    def child(self, name, ms):
        s = Span(name); s.ms = ms; self.children.append(s); return s

def show(span, depth=0):
    print("  "*depth + f"- {span.name} ({span.ms} ms)")
    for c in span.children:
        show(c, depth+1)

root = Span("answer")
root.child("retrieve", 40)
gen = root.child("generate", 210)
gen.child("format", 5)
root.ms = 255
show(root)

The visual, timed tree is what makes a platform beat grep: you see where the time and the failure actually went.

Exercise 3 · Promote real traces into a datasetAdvanced

Context: LangSmith's flywheel turns real traffic into evaluation data: promote good runs into a golden dataset instead of inventing inputs by hand.

Your task: Given a list of captured runs, build a dataset of {input, reference_output} examples, dropping any run that errored or produced no output.

Requirements:

  • Input is a list of run dicts carrying input, output, and error
  • Keep only runs with no error and a non-empty output
  • Emit each survivor as an {input, reference} example
  • Report how many examples resulted (and that errored runs were dropped)

💡 Hint: A single list comprehension with the error/output filter is enough — the curation rule is the lesson, not the plumbing.

Show solution

A dataset is just curated examples pulled from real traffic. Runnable:

runs = [
    {"input": "reset password?", "output": "Go to Settings > Security", "error": None},
    {"input": "refund policy?",  "output": "30 days",                   "error": None},
    {"input": "crash case",      "output": None, "error": "Timeout"},
]
def to_dataset(runs):
    return [{"input": r["input"], "reference": r["output"]}
            for r in runs if r["error"] is None and r["output"]]

ds = to_dataset(runs)
for ex in ds:
    print(ex)
print(len(ds), "examples (errored run dropped)")

Real traffic is the best source of eval cases — you promote the good ones instead of inventing inputs by hand.

Exercise 4 · Run evaluators over a datasetExpert

Context: An evaluator scores an app's output against a dataset's reference. Real suites mix deterministic checks with an LLM-as-judge; both are just scoring functions.

Your task: Model two evaluators — exact-match and a keyword "LLM-judge" stand-in — and a harness that reports per-example and mean scores over a dataset.

Requirements:

  • Exact-match returns 1.0/0.0 on a normalized string comparison
  • The judge stand-in scores by whether the reference is contained in the output
  • The harness runs a given evaluator over the dataset and returns per-example scores plus the mean
  • Run both evaluators over the same dataset and show they can disagree

💡 Hint: Keep the evaluator as a parameter to the harness so swapping exact-match for the judge is a one-line change — exactly how LangSmith runs custom evaluators.

Show solution

Evaluation = score outputs against references. Model the harness offline:

dataset = [
    {"input": "capital of France?", "reference": "Paris"},
    {"input": "2+2?",               "reference": "4"},
]
def my_app(q):
    return {"capital of France?": "Paris", "2+2?": "four"}[q]

def exact_match(pred, ref):
    return 1.0 if pred.strip().lower() == ref.strip().lower() else 0.0

def judge_contains(pred, ref):          # stand-in for LLM-as-judge
    return 1.0 if ref.lower() in pred.lower() else 0.0

def run_eval(dataset, evaluator):
    scores = [evaluator(my_app(ex["input"]), ex["reference"]) for ex in dataset]
    return scores, sum(scores)/len(scores)

s, mean = run_eval(dataset, exact_match)
print("exact-match:", s, "mean", mean)   # [1.0, 0.0] 0.5
s, mean = run_eval(dataset, judge_contains)
print("judge:", s, "mean", mean)

LangSmith runs your evaluators (exact, custom, or LLM-as-judge) over the dataset and stores the scores; the logic is what you're modeling here.

Exercise 5 · Compare two versions on the same datasetProfessional

Context: The reason to store datasets and evaluators is version comparison: proving a prompt or model change actually helped, across the whole set, not on one lucky example.

Your task: Run two app versions over one dataset and report the per-example diff and the mean score delta.

Requirements:

  • Score v1 and v2 with the same evaluator over the same dataset
  • Show a per-example comparison flagging improvements and regressions
  • Compute the mean delta between the two versions
  • Make it obvious when the average improves but a specific case regressed

💡 Hint: Zip the two score lists with the dataset and label each row improved/regressed; the per-example view catches what the aggregate hides.

Show solution

Side-by-side scoring across the whole set beats eyeballing two outputs. Runnable:

dataset = [{"q": "a", "ref": "Paris"}, {"q": "b", "ref": "4"}, {"q": "c", "ref": "blue"}]
v1 = {"a": "Paris", "b": "four", "c": "blue"}
v2 = {"a": "Paris", "b": "4",    "c": "blue"}

def score(app):
    return [1.0 if app[ex["q"]].lower() == ex["ref"].lower() else 0.0
            for ex in dataset]

s1, s2 = score(v1), score(v2)
for ex, a, b in zip(dataset, s1, s2):
    flag = "" if a == b else ("  <- improved" if b > a else "  <- regressed")
    print(f"{ex['q']}: v1={a} v2={b}{flag}")
print("mean delta:", round(sum(s2)/len(s2) - sum(s1)/len(s1), 3))

This is how you prove a prompt/model change helped rather than hoping — the platform just does it over hundreds of cases.

Exercise 6 · An eval gate in CIIndustry scenario

Context: The LLMOps loop closes in CI: an eval gate blocks a deploy when quality drops. This is the O3 release-control pattern, wired to an exit code.

Your task: Model an eval gate that blocks the deploy if the mean score is below a threshold OR any critical example regressed versus the last release, returning a CI-style exit code.

Requirements:

  • Fail if mean score < a threshold
  • Fail if any named critical example scores worse than its previous score
  • Return whether it passed plus the human-readable reasons
  • Map the decision to an exit code (0 deploy / 1 block), as CI would
  • Show both a passing run and a blocked run (e.g. a safety regression)

💡 Hint: Accumulate reasons into a list; empty reasons means pass. The critical-example check needs both the current and the previous per-example scores.

Show solution

The gate turns eval into a release control (O3). Runnable, returns a CI-style exit code:

def eval_gate(mean_score, per_example_now, per_example_prev,
              threshold=0.9, critical=("safety-01",)):
    reasons = []
    if mean_score < threshold:
        reasons.append(f"mean {mean_score:.2f} < {threshold}")
    for k in critical:
        if per_example_now.get(k, 0) < per_example_prev.get(k, 0):
            reasons.append(f"critical '{k}' regressed")
    passed = not reasons
    return passed, reasons

now  = {"safety-01": 1.0, "faq-02": 1.0}
prev = {"safety-01": 1.0, "faq-02": 1.0}
ok, why = eval_gate(0.95, now, prev)
print("deploy" if ok else "BLOCK", why)          # deploy []

now2 = {"safety-01": 0.0, "faq-02": 1.0}          # a safety regression
ok, why = eval_gate(0.95, now2, prev)
exit_code = 0 if ok else 1
print("deploy" if ok else "BLOCK", why, "exit", exit_code)   # BLOCK ... exit 1

Closing the LLMOps loop: traces become datasets, datasets feed evals, and the eval gate stops a regression before it ships. LangSmith hosts each step; the real client wraps your app with @traceable and its evaluate() API (needs the SDK).

✓ Checkpoint — you can move on when you can…

  • Say what LangSmith adds over hand-rolled O4 tracing and Ch 5 evals.
  • Instrument an app (framework or raw SDK) to capture traces.
  • Promote real traces into a dataset and explain the flywheel.
  • Run deterministic, judge, and custom evaluators over a dataset.
  • Compare versions and wire an eval gate into CI (O3).
🏗️ Toward the capstoneThe AI DevOps Engineer's eval suite (Chapter 8d) and its production monitoring (O4) are exactly what a platform like LangSmith operationalizes: every incident it handles becomes a traced run you can promote into the golden set, its accuracy and hard-fail safety evals run in CI as the gate, and its live dashboard watches the four signals. Tracing is how you keep a safety-critical agent honest over time. See the evals build →

Knowledge check check yourself

✓ Knowledge check

What does LangSmith add over the hand-rolled span() tracing from O4, and why is tracing the foundation for everything else?

Show answer
It stores runs durably, makes them searchable and visual (a timed, costed trace tree), and connects tracing to evaluation — scaling past the thousands of runs, cross-version comparisons, and non-engineer inspection that hand-rolled logging can't handle. Tracing is foundational because datasets are promoted from real traces and both offline evals and online monitoring run off that same trace data — no traces, no evals, no monitoring.
✓ Knowledge check

Describe the trace→dataset→eval flywheel and why comparing versions per-example (not just on average) matters before gating a deploy.

Show answer
Real production traces are promoted into a golden-set dataset; evaluators score the dataset; failures found feed back as new cases so the same bug can't regress again. Per-example comparison matters because a new version can raise the average score while quietly breaking a few important cases — a side-by-side per-example diff catches that regression, whereas a single aggregate would hide it, which is why it gates the deploy (O3 eval gate).
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in