A prompt is one call: input, output, easy to read. An agent is a graph — as I tell it in from a prompt to agentic: it gathers context, decides, calls one or more models, reviews itself, loops. When the output is bad, the real question is: which step went wrong?
1 call.
N nested calls.
Langfuse is built for this: it records the full tree of a run — every step, every LLM call, with tokens, cost and latency — and lets you attach quality scores to it.
Before instrumenting, four notions are enough to understand everything.
Install the Langfuse SDK and its LangChain integration, then set the keys — those of your instance.
Instrumentation is one object: the langfuse-langchain CallbackHandler passed to graph.invoke links every graph node — and every model call — to a single trace. No decorator to sprinkle everywhere.
Every node that calls a ChatOpenAI then shows up as a generation — prompt, response, tokens, cost — with nothing extra.
Result: in Langfuse you see the gather → clarify → draft → review → apply tree, with the latency and cost of each node. The review ↺ draft loop shows up as-is.
A trace tells you what happened. A score tells you whether it was good. Three sources, from least to most automatic.
Business rule and user feedback first — simple, reliable, free.
For subjective quality (faithfulness, tone, relevance), an LLM-as-a-judge scores another’s output. Trace it too — a judge that drifts is debugged like everything else.
An LLM judge is not ground truth: calibrate it against a few hand-scored examples before trusting it. And keep it small (gpt-4o-mini is enough) — otherwise scoring costs more than producing.
The real win comes when you replay a frozen set of cases on every change, via Langfuse datasets & experiments. Freeze ~30 representative briefs, and before each deploy re-run the agent on them, scored by the judge. A regression shows up before production, not after.
Keeping traces in-house solves the PII point in one move. That is exactly what my deploy Langfuse on AWS EC2 guide covers — self-hosted, your data stays yours.
An isolated trace answers “did this run work?”. But in production the real question is “did this run work *for this user*, in *this conversation*, on *this version* of the agent?”. Three attributes are enough to go from a heap of anonymous runs to something you can filter and aggregate.
With the CallbackHandler there is nothing new to install: you pass these attributes in the invocation `metadata`, and Langfuse lifts them to the trace level. Sessions accept any US-ASCII string under 200 characters as an identifier.
User tracking accepts an email, username or any stable identifier. Careful: a plaintext email ends up in Langfuse — same PII precaution as for prompts (section 06).
As long as your prompt lives in the code, changing an instruction means redeploying. Langfuse prompt management takes it out of the code: each edit creates an immutable version (1, 2, 3…), and a movable label (`production`, `staging`) points at the active version. You roll back by reassigning the `production` label to the older version, no code change. The SDK caches the prompt client-side, so it adds zero latency.
The payoff compounds with scoring: when a generation is linked to its prompt version, you compare the judge’s scores *per version*. “Prompt v4 of review dropped faithfulness by 8 points” becomes something the data tells you, not a hunch.
Three problems show up as traffic grows: fitting into an existing observability stack, not blowing up on volume, and getting alerted when things go sideways without watching the dashboard all day.
If you already run an OpenTelemetry collector, you do not have to choose. Langfuse is OTel-native: it exposes an OTLP endpoint at `/api/public/otel/v1/traces` (HTTP, JSON or protobuf — no gRPC yet). You add Langfuse as an *exporter* on your collector, and spans emitted by any OTel-instrumented library nest automatically into the right trace.
Classic gotcha: trace attributes (userId, sessionId, tags, version) must be propagated to *every* span for filtering and aggregation to work on the Langfuse side. One orphan span without userId, and it drops out of your segmentations.
At high volume, tracing everything costs storage and drowns the signal. Sampling is handled client-side: you set `LANGFUSE_SAMPLE_RATE` (or `sampleRate` in the constructor) between 0 and 1. The decision is made *at the trace level* — if a trace is kept, all its observations and scores are; otherwise nothing ships. No half-sent traces.
My rule: sample the high-volume, low-value traffic, but keep 100% of runs carrying negative feedback or a judge score below threshold — those are exactly the ones you want to debug. A global 0.2 rate on the firehose, a dedicated 1.0 handler on flagged cases.
Nobody watches a dashboard at 3 a.m. Monitors & alerts fire on a threshold — average cost, p95 latency, or a quality aggregate — and notify via Slack, webhook or a GitHub Actions trigger. “p95 latency > 8s” or “average cost per trace > $0.05” ping you before the user complains.
One caveat to know: monitors are currently Langfuse Cloud only. Self-hosted, you pull the same numbers via the Metrics API v2 (count, latency, totalCost, totalTokens) and wire your own alerting on top.
My first LangGraph agent, and why a good prompt stops being enough once the task gets real.
The full v3 stack in Docker Compose — Postgres, ClickHouse, Redis, MinIO — behind HTTPS. Copy-paste blocks.