Feedback Loops and Dataset Curation
9 min read
Close the production feedback loop by capturing user signals, curating datasets from real traffic, and continuously improving your system.
Shipping an LLM application is not the end of the work — it is the beginning of a feedback cycle. Production traffic surfaces the real distribution of user inputs, edge cases your evals never covered, and quality failures that only emerge at scale. Capturing and acting on that signal is what separates improving systems from stagnating ones.
Collecting user feedback signals
The most direct signal is explicit user feedback: thumbs up/down ratings, correction submissions, or flagging harmful content. These are sparse — most users do not provide feedback — but they are high-signal when they appear. Implicit signals are denser: request retries (the user rephrased because the first response was unsatisfactory), session abandonment, and follow-up messages that indicate confusion.
- Thumbs up/down: simple, low-friction, but sparse — expect under 5% response rate
- Corrections: users editing or overriding the model output — high signal about specific failure modes
- Retries: user rephrasing the same question within a session — indicates response quality failure
- Session length: very short sessions may indicate the user gave up; very long ones may indicate confusion
- Escalations: cases where users asked to speak to a human override the AI agent
Curating a golden dataset from production
Production traces are the richest source of eval data. Build a pipeline that samples interesting cases from production traffic: negatively-rated responses, high-latency requests, inputs that triggered guardrails, and a random baseline sample. Review sampled cases, add ground-truth labels, and add them to your eval suite. Your eval dataset grows organically with your product's actual usage.
def sample_for_review(traces: list[dict], n: int = 50) -> list[dict]:
"""Sample a stratified set of production traces for human review."""
negatives = [t for t in traces if t.get("user_rating") == -1]
retries = [t for t in traces if t.get("was_retry")]
guardrail_hits = [t for t in traces if t.get("guardrail_triggered")]
random_sample = random.sample(traces, min(20, len(traces)))
# Combine and deduplicate
candidates = {t["id"]: t for t in negatives + retries + guardrail_hits + random_sample}
return list(candidates.values())[:n]The improvement cycle
Dataset curation feeds a continuous improvement cycle: sample production failures, root-cause them (wrong tool chosen, missing context, unclear prompt instruction), fix the underlying issue (update the prompt, improve retrieval, add a guardrail), add the failure cases to the eval suite so the fix is regression-tested, then deploy and monitor. Each cycle tightens the feedback loop and raises the quality floor.
Fine-tuning vs. prompt improvement
Most quality issues are better addressed by fixing the prompt or retrieval than by fine-tuning. Fine-tuning is expensive, slow to iterate, and can introduce regressions in areas you did not target. Reserve it for cases where the task is well-defined, you have hundreds of high-quality examples, and prompt engineering has provably hit a ceiling. The feedback loop should exhaust prompt and context improvements before reaching for fine-tuning.
A dataset of 100 well-curated production failure cases is worth more than 10,000 synthetically generated examples. Real failures reveal the actual distribution of edge cases that matter for your specific application.
Assign a rotating dataset curation rotation to your team: each engineer spends a few hours per sprint reviewing and labeling production samples. This keeps the team calibrated on real failure modes and prevents the eval suite from drifting away from what actually matters in production.