LangSmith: Datasets, Experiments & Regression Testing

Lesson 6: LangSmith โ€” Datasets, Experiments & Regression Testing

LangSmith is the closest thing to "CI for LLM apps" that exists today: it traces every run of your application, stores datasets, runs experiments (your app against a dataset, scored by evaluators), lets you compare versions side-by-side, and extends into production with online evals and monitoring. This lesson walks the full implementation: from dataset to gated CI job. Everything here works for plain LLM chains, LangChain, and LangGraph apps (Lesson 7) alike.

The Pieces and How They Fit

Piece What it is Your job
Tracing Every invocation is recorded: inputs, outputs, latency, tokens, tool calls, model Turn it on (env vars). It's the raw material for everything else.
Datasets Versioned collections of examples (inputs ยฑ reference outputs) from Lesson 3 Create them; keep them versioned and PII-free
Experiments Running a target (your app/prompt/model) over a dataset with evaluators attached Run one per change; name them so history is readable
Evaluators Scoring functions from Lesson 4's toolbox Pick cheap-first; write custom ones for domain checks
Comparison view Side-by-side diff of two experiments on the same dataset The regression test โ€” never skip the baseline
Online evals / monitoring Evaluators + thresholds attached to production traces Production safety net (Lesson 10)
Annotation queues Curated queues of traced runs for human review Human loop: turn bad runs into new dataset examples

Step 1 โ€” Create the Dataset (SDK or UI)

from langsmith import Client

client = Client()  # uses LANGCHAIN_API_KEY env var

ds = client.create_dataset(
    dataset_name="support-golden-v1",
    description="Support copilot: 40 happy/edge/failure + 10 adversarial"
)
client.create_examples(
    dataset_id=ds.id,
    inputs=[{"question": "How do I reset my password?"}],
    outputs=[{"answer": "Go to Settings > Security > Reset password."}],
    # ... one dict per example; adversarial examples can omit outputs
)

You can equally create datasets and paste examples in the UI โ€” the SDK version is what makes it scriptable and CI-friendly.

Step 2 โ€” Define Evaluators

from langsmith.evaluation import evaluate
from langsmith.schemas import Example, Run

# (a) Deterministic: exact match on a field
def exact_match(run: Run, example: Example) -> dict:
    pred = run.outputs.get("answer", "")
    return {"key": "exact_match", "score": int(pred == example.outputs.get("answer", ""))}

# (b) LLM-as-judge: built-in criteria scorer ("correctness")
from langsmith.evaluation.evaluators import criteria
correctness = criteria("correctness")

# (c) Custom judge with your own rubric prompt
def rubric_judge(run: Run, example: Example) -> dict:
    # call your judge model with a detailed rubric; return {"key","score","comment"}
    ...

# (d) Programmatic pipeline check: did the right tool get called?
def tool_check(run: Run, example: Example) -> dict:
    called = [c.get("name") for c in run.outputs.get("calls", [])]
    return {"key": "tool_check", "score": int(example.outputs.get("expected_tool") in called)}

Step 3 โ€” Run the Experiment

results = evaluate(
    target=my_app,               # any callable: chain.invoke, graph.invoke, plain function
    data="support-golden-v1",    # dataset name
    evaluators=[exact_match, correctness, rubric_judge, tool_check],
    experiment_prefix="prompt-v2",
)
summary = results._summary  # aggregate scores

Each example runs once; every evaluator scores every output; results land in the UI with per-example breakdowns, failure traces, and token/latency stats. Name experiments by change (prompt-v2, model-gpt5-mini) so the history reads like a changelog.

Step 4 โ€” Regression Testing: Diff Against Baseline

In the UI, select two experiments on the same dataset and LangSmith shows a per-example score diff โ€” green/red per row, with the failing traces one click away. Programmatically, fetch both experiments and compute the deltas yourself for CI:

from langsmith import Client
client = Client()

def pass_rate(experiment_id: str) -> float:
    ex = client.read_experiment(experiment_id)
    scores = [f.get("score") for f in ex.feedback_stats.get("correctness", [])]
    return sum(1 for s in scores if (s or 0) >= 0.8) / len(scores)

new_rate = pass_rate(new_exp_id)
base_rate = pass_rate(baseline_exp_id)
print(f"new={new_rate:.2f} baseline={base_rate:.2f}")
# CI gate: fail on regression or sub-threshold
The regression rule: a change that drops any gated metric below its threshold, or regresses more than a small tolerance vs baseline, is rejected โ€” even if it "feels better" in the playground. This is your hard blocker (full treatment in Lesson 9).

Step 5 โ€” Wire It Into CI

# ci_eval.yml (GitHub Actions sketch)
# 1. checkout, pip install langsmith langchain
# 2. LANGCHAIN_API_KEY=${{ secrets.LANGCHAIN_API_KEY }}
# 3. python run_evals.py --experiment-prefix "ci-${{ github.sha }}"
#    (runs steps 2-4, then exits non-zero if gate fails)

The eval job becomes a required check on every PR touching prompts, models, retrieval config, or guardrails. Slow? Yes โ€” and that's the point: the eval job is your test suite; a 10-minute eval job on a 50-example dataset is cheap insurance against shipping a regression.

Production: Online Evals, Annotation, Playground

  • Online evals โ€” attach evaluators to production traces so every live conversation gets scored (e.g. guardrail-pass, topic-relevance). Use LangSmith monitoring dashboards and thresholds to alert when the live pass rate dips.
  • Annotation queues โ€” human reviewers mark real conversations; the flagged ones become your next dataset version. This closes the loop: production failures become regression tests.
  • Playground + prompt versioning โ€” iterate on prompts in the Playground against dataset examples, then promote a winning version and validate it with a full experiment before shipping. The prompt, not just the code, is a versioned artifact.
Pitfall: running evals without a locked dataset version poisons your regression history โ€” a score change becomes uninterpretable (did the app improve, or did the dataset?). Version datasets when you edit them, and record which version each experiment used.

๐Ÿง  Knowledge Check

1. What is the LangSmith object that plays the role of "running the app over the test suite"?

2. How do you turn LangSmith into a hard blocker in CI?

3. Which pattern turns production failures into regression tests?

Further Reading