Tracing LLM and agent apps

MLflow · Advanced · 12 min

LLM apps fail in non-obvious ways; traces show exactly which prompt, retrieval or tool call went wrong.

A trace records one request through your application as a tree of spans. Each span has a name, type (LLM, RETRIEVER, TOOL, CHAIN...), inputs, outputs, timing, attributes and status. MLflow's tracing is OpenTelemetry-compatible.

Three ways to create traces:

  • Autolog: mlflow.openai.autolog(), mlflow.langchain.autolog(), mlflow.anthropic.autolog() and many more capture LLM calls, token usage and tool calls with no code changes.
  • Decorator: @mlflow.trace on your own functions records their arguments and return values; nested decorated calls become child spans.
  • Context manager: with mlflow.start_span("rerank") as span: with span.set_inputs(...), span.set_outputs(...), span.set_attribute(...) for fine-grained blocks.

Traces appear in the experiment's Traces tab, where you can open the span tree, see latency per step and inspect prompts. Programmatically, mlflow.search_traces() returns a DataFrame for analysis, and mlflow.get_last_active_trace_id() gives the most recent one.

You can attach tags (user, session, version) with mlflow.update_current_trace(tags=...) and feedback with mlflow.log_feedback(...), which later feeds evaluation.

In production, use the lightweight mlflow-tracing package and asynchronous logging so tracing adds minimal latency.

Example

import mlflow

mlflow.set_experiment("rag-app")

@mlflow.trace(span_type="RETRIEVER")
def retrieve(question: str) -> list[str]:
    return ["MLflow traces are trees of spans."]

@mlflow.trace(span_type="LLM")
def fake_llm(prompt: str) -> str:
    return "A trace is a tree of spans."   # stand-in for a real model call

@mlflow.trace
def answer(question: str) -> str:
    docs = retrieve(question)
    with mlflow.start_span("build_prompt") as span:
        prompt = f"Context: {docs}\nQ: {question}"
        span.set_attribute("n_docs", len(docs))
    return fake_llm(prompt)

print(answer("What is a trace?"))
trace = mlflow.get_trace(mlflow.get_last_active_trace_id(), flush=True)
print([s.name for s in trace.data.spans])  # ['answer', 'retrieve', 'build_prompt', 'fake_llm']

Remember

  • A trace is a tree of typed spans with inputs, outputs and timing
  • Use autolog for supported libraries and @mlflow.trace/start_span for your code
  • Search, tag and attach feedback to traces for debugging and evaluation

Watch out for

  • Tracing only the final LLM call, which hides retrieval or tool failures that cause most bad answers.

Check yourself

  1. You decorate answer() and retrieve() with @mlflow.trace, and answer() calls retrieve(). How do they appear?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: mlflow.org/docs/latest/genai/tracing/