DEV Community

Cover image for LLM Observability is Broken: Why MLflow 3 is the Only Way Out
Aniket Abhishek Soni
Aniket Abhishek Soni

Posted on

LLM Observability is Broken: Why MLflow 3 is the Only Way Out

Six months ago, debugging our RAG pipeline meant staring at a wall of unstructured CloudWatch logs, trying to figure out which chunk of a 50-page PDF caused the hallucination. It was a digital scavenger hunt where the clues disappeared as soon as the request finished. Today, I look at the MLflow UI, filter by a specific trace_id, and see the exact RAG retrieval context, the system prompt, and the vector similarity score side-by-side.

If you’re still using print() statements and manual CSV tracking for your GenAI prompts, you aren’t building production software; you’re building a ticking time bomb.

Why the common approach falls short

Most of my peers in financial services treat LLMs like traditional Scikit-Learn models. They log a few parameters, save a pickle file, and call it a day. That works for a Random Forest where the input is a static feature vector. It fails catastrophically for LLMs.

In a GenAI system, the "code" is the prompt, the "data" is the retrieved context, and the "model" is a black box that changes its output based on a slight shift in temperature or a system message tweak. When your customer support bot starts telling users it’s okay to bypass compliance, you don’t need a model weight update; you need a prompt versioning history and a way to re-run that specific inference against a regression test suite.

The "standard" way—logging inputs and outputs to a SQL database—is insufficient because it ignores the call stack. You need to know the latency of the embedding call versus the generation call. You need to see the chain of thought. If you aren't capturing the full trace, you're debugging blind.

Photo by Tobias Fischer on Unsplash
Photo by Tobias Fischer on Unsplash

Versioning prompts, not just weights

I’ve seen senior engineers hardcode prompts into their Python logic. prompt = "You are a helpful assistant...". This is amateur hour. When the product team wants to tweak the tone of the bot, they have to wait for a full CI/CD cycle, re-testing, and redeployment.

With MLflow 3, we treat prompts as first-class artifacts. We store them in the MLflow Model Registry, versioned like code.

import mlflow

# In production, we fetch the prompt by alias
prompt_template = mlflow.models.get_model_uri(
    model_uri="models:/compliance-bot-prompt/production"
)

# Using mlflow.log_input to track the exact prompt version
with mlflow.start_run():
    mlflow.log_param("prompt_version", "v1.4.2")
    mlflow.langchain.log_model(lc_model, "model")
Enter fullscreen mode Exit fullscreen mode

When I get a ticket saying the bot is acting weird, I check the mlflow.run metadata. I see that v1.4.1 was deployed at 10:00 AM, and the latency spikes began at 10:05 AM. I don't guess; I roll back the registry alias to v1.4.0 with one API call. It’s boring, reliable engineering.

Tracing is the new unit testing

Evaluation in GenAI is messy because there is no ground truth. Is the answer "correct"? Who knows. But we can measure the components of the answer.

We use MLflow Tracing to capture the entire request lifecycle. When we run an evaluation suite, we don't just check the final string; we check the RAG context. Did the retrieval engine pull the right document? If the vector database returned a 0.7 cosine similarity score, that’s a failure mode we can track.

I’ve set up a custom evaluator that runs automatically on every PR. It pulls a subset of "golden questions," runs them through the trace, and uses an LLM-as-a-judge (usually GPT-4o) to compare the current trace against a known good trace.

# A snippet of our custom evaluation logic
eval_results = mlflow.evaluate(
    model_uri="runs:/<run_id>/model",
    data=eval_df,
    targets="expected_output",
    evaluators="default",
    evaluator_config={
        "col_mapping": {"inputs": "prompt", "outputs": "response"}
    }
)
Enter fullscreen mode Exit fullscreen mode

The key here is that mlflow.evaluate isn't just checking accuracy; it’s checking the integrity of the trace. If the trace shows the model didn't use the provided context, the PR fails. No human needs to review the output manually.

Photo by Nicholas Cappello on Unsplash
Photo by Nicholas Cappello on Unsplash

The objections (and my answers)

The pushback I usually get is: "MLflow adds too much overhead. Why not just use a dedicated LLM observability tool like LangSmith or Arize?"

My answer is simple: Integration and ownership.

In a healthcare or fintech environment, sending proprietary PII-heavy traces to a third-party SaaS vendor is a compliance nightmare. You have to go through months of security reviews for every new vendor. MLflow is open-source. We host it on our own Kubernetes cluster. We own the data, the security posture, and the uptime.

Another objection: "MLflow 3 is heavy. We just need a simple logging dashboard."

That’s what I said three years ago. Then we had an incident where we needed to audit every single prompt-response pair sent to a customer over a 48-hour period to comply with a regulatory request. If we were using a "simple" tool, we would have been screwed. MLflow’s backend is extensible. We use S3 for artifacts and a Postgres database for metadata. It scales because it relies on infrastructure we already manage.

Finally, the "it's too complex" argument. Yes, learning how to structure your code for tracing takes a day of reading documentation. But you spend that day once. You spend the rest of your career fixing bugs caused by "simple" logging that didn't tell you the whole story.

Conclusion

Stop building "GenAI apps" that are just glorified CLI scripts wrapped in a web framework. You are shipping production software, and that requires production-grade tooling.

MLflow 3 gives you the observability, versioning, and evaluation framework that turns "it feels like it works" into "I can prove it works." If you aren't tracing your prompts, you don't know what your model is doing. And in the world of financial services and healthcare, "not knowing" is a liability you cannot afford.

The tooling exists. Stop making excuses, stop printing logs to stdout, and start versioning your prompts like the rest of your production code. Your future self, debugging a P0 incident at 2:00 AM, will thank you.

Cover photo by Wood Hong on Unsplash.

Top comments (0)