The short version: Langfuse is the open-source, self-hostable way to see what your agent actually did — every prompt, tool call, token, and latency — and then score it. But the current v4 Python SDK is a full OpenTelemetry rewrite, and it renamed several of the calls every older tutorial still uses. This walkthrough is the current path: from pip install to a scored, inspectable trace, in the API that exists today. Two lines get you a traced LLM call; a context manager gets you a full agent tree; run_experiment gets you an eval suite. And there is no langfuse.anthropic wrapper — if a code assistant handed you one, delete it.

1. Install and point it somewhere#

pip install langfuse

Langfuse needs two keys and a host. Create a project on Langfuse Cloud or your own self-hosted instance, grab the key pair, and set three environment variables:

import os
from langfuse import get_client

os.environ["LANGFUSE_PUBLIC_KEY"] = "pk-lf-..."
os.environ["LANGFUSE_SECRET_KEY"] = "sk-lf-..."
# Cloud EU (default): https://cloud.langfuse.com
# Cloud US:           https://us.cloud.langfuse.com
# Self-host:          http://localhost:3000
os.environ["LANGFUSE_HOST"] = "https://cloud.langfuse.com"  # v4 also reads LANGFUSE_BASE_URL

langfuse = get_client()          # reads the env vars; lazy singleton
assert langfuse.auth_check()     # optional: fail fast if the keys are wrong

get_client() is the v4 way to obtain the client — it reads the environment and hands you a shared instance. Because self-hosting the core is free under MIT, the only thing that changes between "trying it on cloud" and "running it on your own box" is that LANGFUSE_HOST value. Everything below is identical either way.

2. The two-line win: trace an OpenAI call#

The single highest-leverage change is one import. Swap from openai import OpenAI for Langfuse's drop-in and every completion is captured automatically — model, messages, output, token counts, latency — with no other edits:

from langfuse import observe
from langfuse.openai import OpenAI   # native drop-in; each call becomes a "generation"

client = OpenAI()

@observe
def answer(question: str) -> str:
    resp = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": question}],
    )
    return resp.choices[0].message.content

answer("What is the capital of France?")

The @observe decorator (imported from langfuse import observe) wraps the function in a trace and records its input and output as an observation; the langfuse.openai import nests the model call inside that trace as a generation. Open the trace in the UI and you'll see the function call as the root and the LLM call as a child, with usage and cost already filled in.

One import and one decorator is the whole starter kit. Everything else in Langfuse is you choosing to capture more structure than that default gives you.

If you're on Claude, not GPT

There is no native Anthropic drop-in — this is the most common mistake in AI-generated Langfuse code. from langfuse.anthropic import ... does not exist. For Claude you have two honest options: keep the @observe decorator (Langfuse records the function's input/output, which is often enough), or add an OpenTelemetry instrumentor for Anthropic and let Langfuse ingest its spans. Because v4 is OpenTelemetry underneath, any OTEL-emitting library lands in your traces for free.

3. Trace a real agent: build the span tree by hand#

Decorators are enough for a single call. A multi-step agent — retrieve, reason, call a tool, answer — deserves an explicit tree so you can see where the latency and the mistakes live. In v4 you do that with start_as_current_observation, passing as_type to say whether a node is a plain span or a model generation:

from langfuse import get_client
langfuse = get_client()

with langfuse.start_as_current_observation(
    as_type="span", name="research-agent",
    input={"query": "What changed in the MCP spec?"},
) as root:

    with langfuse.start_as_current_observation(as_type="span", name="retrieve") as retrieval:
        docs = my_vector_search("MCP spec changes")     # your code
        retrieval.update(output={"n_docs": len(docs)})

    with langfuse.start_as_current_observation(
        as_type="generation", name="synthesize", model="gpt-4o",
        input={"docs": docs},
    ) as gen:
        answer = my_llm_call(docs)                       # your code
        gen.update(output=answer)

    root.update(output={"answer": answer})

langfuse.flush()   # REQUIRED before a short script exits, or the trace never ships

Two things to internalize here. First, start_as_current_observation(as_type=...) is the v4 rename of what v3 called start_span and start_generation — if you copied a 2025 tutorial and got AttributeError: 'Langfuse' object has no attribute 'start_span', this is why. Second, langfuse.flush() matters: the SDK batches and ships traces in the background, so a script that exits immediately can drop the last batch. Long-running servers flush on their own cadence; short scripts must call it.

4. Score the trace — evaluation is the second half#

A trace you can't grade is just a log. Langfuse treats scores as first-class, so you can attach one to any trace — a human review, a cheap heuristic, or the verdict of a judge model — without ever leaving the SDK you already imported. The low-level call is explicit and the in-context call is convenient, and both write to the very same place. Here are the two you will actually reach for:

# inside an active trace:
langfuse.score_current_trace(name="relevance", value=1, data_type="NUMERIC")

# or explicitly, by id (idempotent if you pass a stable score_id):
langfuse.create_score(
    name="correctness",
    value="pass",
    data_type="CATEGORICAL",
    trace_id=langfuse.get_current_trace_id(),
)

Scores come in three data types — NUMERIC, CATEGORICAL, and BOOLEAN — which is enough to encode a 1–5 quality rating, a pass/fail gate, or a labeled category. This is the same scoring surface that feeds eval-driven development: once every trace carries a score, "is the agent getting better?" becomes a dashboard, not a vibe.

5. Run an offline experiment with an LLM-as-a-judge#

For a real eval suite you don't want to score production traffic by hand — you want a dataset and a graded run. run_experiment takes your data, a task that produces an output per item, and a list of evaluators:

from langfuse import get_client, Evaluation
from langfuse.openai import OpenAI

langfuse = get_client()

def task(*, item, **kwargs):
    resp = OpenAI().chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": item["input"]}],
    )
    return resp.choices[0].message.content

def contains_expected(*, input, output, expected_output, **kwargs):
    return Evaluation(
        name="contains_expected",
        value=int(expected_output.lower() in output.lower()),
    )

data = [
    {"input": "Capital of France?", "expected_output": "Paris"},
    {"input": "Capital of Germany?", "expected_output": "Berlin"},
]

result = langfuse.run_experiment(
    name="Geography quiz",
    data=data,
    task=task,
    evaluators=[contains_expected],
)
print(result.format())

Each evaluator returns an Evaluation that Langfuse turns into a score on the run — so an LLM-as-a-judge is just an evaluator that calls a model to grade the output against a rubric instead of doing a string match. (If you want the judge to write its own rubric, that pairs well with auto-generated eval rubrics.) One nuance worth knowing before you wire this into a pipeline: evaluators inside run_experiment run synchronously, inline, while Langfuse's managed LLM-as-a-judge — the kind you configure in the dashboard — runs asynchronously against traces after they're ingested. Inline for a gated CI run; managed-async for continuous scoring of live traffic. That maps cleanly onto the online-vs-offline evals split.

The v3 → v4 gotchas, in one place#

If you're copying older code or an assistant's suggestion, these are the renames that will throw:

What it costs and where it runs#

Langfuse is MIT-licensed and self-hostable, and self-hosting the core is free with no event limit — the reason it's a safe dependency for a solo builder who doesn't want a per-trace meter running. Langfuse Cloud adds a managed tier on top: a free Hobby plan for trying it, paid plans as your volume grows, and enterprise options — check the current pricing before you budget, since the tiers move. The ClickHouse acquisition announced in January 2026 didn't change any of that: same license, same self-host path, same SDK.

If you're still choosing an observability tool rather than instrumenting one, the Langfuse vs LangSmith vs Phoenix comparison is the decision piece to read first. But if you've already picked Langfuse, the code above is the whole on-ramp: one import to see your calls, a context manager to see your agent, and run_experiment to know whether any of it is getting better.