As the test suite grew, an operational issue became impossible to ignore: LLM usage costs scale quickly when running comprehensive tests. To manage this, we integrated Langfuse for granular cost tracking. Over time, it became just as valuable for end-to-end tracing and debugging, allowing us to connect failures and regressions back to specific prompts, model calls, and execution paths.
How to make tests stable in CI/CD pipelines?
LLMs are probabilistic by design. The same prompt can produce different responses across runs. This creates a direct tension with CI/CD pipelines, which assume tests fail only when something is broken. If left unaddressed, randomness alone can destabilize builds. Stability requires explicit engineering choices. Over time, we converged on a small set of techniques that reduce noise without masking real regressions. Our stability toolkit includes:
- Retry logic with flaky library: For most tests, we require 2 out of 3 test runs to pass. This absorbs random variation without masking real regressions. Legitimate inconsistencies no longer fail builds, while persistent failures still surface.
- Low LLM temperature: Lower temperature settings reduce randomness in LLM responses, making tests more consistent. This might not work for every scenario, but in our case, having the lowest temperature was acceptable.
- Structured outputs: Wherever possible, LLM responses are constrained to JSON with predefined schemas. This sharply reduces ambiguity and parsing failures. While now considered standard practice, it was a major stability improvement for us.
- Smoke test suites: A small, critical set of tests runs on every pull request, while full suites run nightly. Fewer tests mean lower cost and higher reliability. Running 10 tests with occasional variance is far more stable than running 100 with the same failure rate.
- Handcrafted test cases: Auto-generated tests are useful, but they require manual curation. Carefully designed test cases produce more predictable behavior and reduce noise.
- Soft assertions for end-to-end tests: Individual failures do not immediately fail the suite. Each test contributes to an aggregated score, and the build fails only when the aggregate drops below a threshold. It allows you to identify problematic areas without letting occasional fluctuations derail your CI/CD pipeline.
The human element in GenAI testing
Fully automating GenAI testing is far harder than most teams expect. Traditional testing already requires judgment in edge cases. With LLMs, non-determinism makes that judgment unavoidable.
We automate what we can: F1 scores are computed automatically, builds pass or fail based on defined thresholds. But automation alone is not enough. Someone still needs to review results, especially failures and borderline cases.
A failing test can mean very different things:
- The ground truth needs adjustment. The response may be acceptable, but the test case is too rigid and does not reflect valid variation.
- The response is genuinely wrong. This is a real defect that needs to be fixed in prompts, retrieval, or logic.
- The metric is misaligned. The chosen metric may not capture what actually matters, which is common early in metric design.
- The threshold is too strict. The system meets user expectations, but the pass threshold does not reflect a realistic definition of “good enough.”
To resolve this, QA engineers, product owners, and domain experts review results together. They examine low-scoring responses, discuss expected behavior, and refine both tests and implementation. The process is slow, but it is necessary.
Subject matter experts are non-negotiable. Without domain experts who understand the nuances of what constitutes a good response in your specific context, you simply cannot achieve the quality your users deserve. Their expertise guides everything from writing test cases to interpreting results to setting appropriate thresholds.
The tooling ecosystem is still evolving, so human judgment remains a core part of GenAI testing.
The bottom line: Building a new testing model for AI apps
Testing GenAI systems forces you to unlearn long-held assumptions about quality assurance. The traditional testing pyramid collapses once core business logic stops running inside your codebase and starts living inside a model you can only steer through prompts, not control outright.
Our approach accepts that reality. We center testing on real integrations, anchor evaluation in metrics, automate with intent, and keep human review in the loop. This demands more upfront engineering effort and ongoing expert involvement, but it aligns testing with how GenAI systems actually fail.
The payoff shows up in production: Our GenAI app quality improved drastically. Most issues never reach our QA team, let alone our users, and we catch problems before they impact production.
GenAI is changing the rules for how we build software. It's time we adapt the rules for how we test it.