When a support answer is wrong, the first question is rarely “which model?” It is “what did retrieval actually return?” For a customer-support knowledge base, I treat observability as part of the retrieval architecture: capture the query, candidate set, filters, scores, and final citations as one trace. That gives the team a way to separate stale content from a bad query, a broken filter, and a slow reranker.

Short answer: instrument each retrieval stage with trace-level evidence, then tune quality and latency from the same evaluation set. A single latency number or an average relevance score cannot explain a missing article.

What should a retrieval trace contain?

Start with a stable trace_id that follows one support request from query rewriting through answer generation. Every span should carry a redacted query hash, tenant or locale (when applicable), index version, and a policy decision. Never put a customer's email address, phone number, or full ticket text into ordinary logs. Hashing is useful for joining events, but it is not anonymization if the original value can be guessed, so apply retention and access controls as if it were sensitive data.

The retrieval trace needs four kinds of evidence:

  1. Input: normalized query, language, channel, and any extracted product or plan filter.
  2. Candidates: document IDs, source, chunk IDs, retrieval method, rank, and score. Store a compact sample or encrypted payload rather than unlimited text.
  3. Decisions: filter counts, deduplication count, reranker model version, score thresholds, and why a fallback ran.
  4. Outcome: cited IDs, answer refusal or escalation flag, end-to-end latency, and user feedback event.

I also record cardinalities at each boundary. If 200 chunks enter a filter and zero leave, that is a different incident from a semantic search that returns 200 weak chunks. Both may produce the same “I don't know” answer.

A useful event shape is boring on purpose:

from dataclasses import dataclass from typing import Optional @dataclass class RetrievalEvent: trace_id: str stage: str index_version: str candidate_count: int latency_ms: int top_score: Optional[float] filter_name: Optional[str] fallback: bool def emit(event: RetrievalEvent) -> None: # Send to a structured sink with redaction and a bounded retention period. print(event.__dict__) 

The exact sink is an implementation choice. OpenTelemetry gives you a common vocabulary for traces, metrics, and logs, while your search engine remains free to expose its own score details.

How should a customer-support knowledge base retrieval architecture expose observability?

Use two clocks. The first is user-facing latency: query-to-first-token and query-to-final-answer. The second is stage latency: rewrite, sparse retrieval, dense retrieval, merge, rerank, and context assembly. A p95 budget of 800 ms might be reasonable for one support channel and unacceptable for another; set it from the interaction, then allocate it across stages.

Quality needs a paired view. Maintain a small, versioned evaluation set with the question, acceptable source IDs, required filters, and an escalation label. Measure recall@k for finding an acceptable source, citation precision for the sources actually shown, and refusal accuracy for questions with no approved answer. Plot those against p50 and p95 latency after every index or reranker change. Your mileage may vary with language mix and corpus size, so publish the slice sizes beside every number.

The most actionable dashboard has four panels:

  • candidate recall and citation precision by product area;
  • p50/p95 latency by stage and cache status;
  • empty-result, low-score, and policy-filter rates;
  • feedback, re-opened tickets, and escalation rates linked by trace_id.

Do not average away the tails. A reranker that improves mean precision while adding 400 ms to the slowest locale can still hurt customers. Conversely, a fast index that silently drops policy-filtered documents is a quality regression, even if latency is green.

Failure patterns that metrics alone miss

An empty result is not one failure mode. Classify it at the boundary where it appears: no lexical hits, no vector hits, all candidates removed by metadata, or all candidates rejected by a minimum score. This classification makes alerts specific and keeps operators from lowering thresholds blindly.

Staleness has a similar shape. Include source_updated_at and indexed_at in each candidate event, then watch the age distribution for documents that answer high-volume questions. A healthy index can still serve an obsolete refund policy if the crawler accepted a 200 response containing an error page. Content validation belongs in ingestion, with checks for title, body length, canonical URL, and content type.

Deduplication deserves its own counter. Syndicated help-center pages often differ only in tracking parameters or a locale slug. If five copies occupy the top five ranks, the apparent recall is high but the answer has no diversity. Keep a source fingerprint and report unique-document recall separately from chunk recall.

I once assumed a low score meant a weak embedding. The more common culprit was a filter extracted from a plan name with a different spelling. That is why traces should retain the normalized filter value and the number of documents it removed. A three-word log line can save an afternoon.

Privacy failures are retrieval failures too. Redact secrets before the query is persisted, enforce tenant checks before ranking, and test that a trace viewer cannot pivot from one tenant's ID to another's document text. A 403 should be an explicit policy outcome in telemetry, not a generic “zero hits” bucket.

Choosing instrumentation depth without freezing delivery

There is a real trade-off between evidence and overhead. Full candidate payloads help offline debugging but increase storage, privacy exposure, and serialization time. Sampled traces are cheaper but can hide rare policy or locale failures. The table below is a practical starting point, not a law.

Mode Capture Best use Catch
Request counts, stage timings, IDs always-on production latency shifts, empty-result spikes
Sampled debug top 20 scores and filter reasons 1–5% of traffic ranking and threshold errors
Replay full redacted candidates evaluation and incident review regressions that dashboards compress

Keep schemas backward-compatible. Add fields rather than renaming candidate_count; dashboards and alert rules should survive a rolling deployment. Emit the index and prompt configuration versions so a later replay uses the same ingredients. If a field cannot be retained for compliance reasons, record that it was intentionally omitted and why.

Roll out in small steps: first stage timings, then candidate IDs and counts, then sampled score details. Gate each step on storage cost, redaction tests, and a replay of known support questions. The catch is that deep traces are not suitable for unrestricted, long-term retention; stick with aggregate metrics when a team cannot enforce access controls, or use a self-hosted telemetry store with a shorter retention window.

Observability also has a failure path. Your answer service should continue with a bounded timeout when the telemetry sink is unavailable, and it should increment a local drop counter. Never block retrieval on logging. A missing trace is easier to repair than a support queue that stops responding.

The design decision is therefore straightforward: make every retrieval decision explainable, budget each stage, and evaluate quality on the same traces that operators inspect. Start with four signals—candidate recall, citation precision, stage latency, and policy-filter rate—then add slices for locale, source age, and deduplication as the system grows. That architecture keeps a search-RAG service diagnosable without tying it to a vendor or pretending that one score describes the whole customer experience.

References