- Python orchestration with LangGraph nodes.
- Chroma for vector storage, about 36K chunks across 11 sources.
- A reranker stage (Cohere or a cross-encoder) to reorder candidates.
- GPT-4o-mini for generation.
- An evaluation harness with 100+ annotated traces.
The four failure modes, in one line each
| Failure | What happened | Example |
|---|---|---|
| Poisoning | Wrong or outdated info entered context and was treated as true. | Retriever returns refund policy v1 (15 days) when v2 (45 days) is live. |
| Clash | Conflicting info entered with no rule for which wins. | v1 and v2 both retrieved, neither carries a version or effective date. |
| Confusion | Ambiguous instructions or overlapping tools led to the wrong action. | "Cancel my order" meant a refund; agent called cancel_order. |
| Distraction | Irrelevant material crowded out what the task needs. | Raw web-search HTML dumped into a refund conversation. |
Plus context rot: as a long-running agent accumulates turns and tool outputs, the model gets worse at using what is in the window. This happens before the token limit, so having room left does not prevent it.
Mapping each failure to a stage
1 · Poisoning: check ingestion
Risk: Chroma holds outdated, duplicate or badly extracted chunks, and nothing stops them from being retrieved.
- Version metadata on every chunk (
version,effective_date,status). - Freshness validation at ingest and active/inactive status per source.
- Provenance on every retrieved chunk (source, page, ingest run).
- Filter on metadata at query time, not just similarity:
filter={"status": "active"}. Syntax varies by vector DB.
2 · Clash: check retrieval and reranking
Risk: the reranker puts two highly relevant chunks on top that contradict each other.
- Carry version and source through reranking. Don't strip metadata.
- Define source authority (registry > current policy > FAQ > archived).
- Detect conflicting facts and resolve them before generation. Never leave version management to the LLM.
3 · Confusion: check LangGraph orchestration
Risk: nodes or tools with overlapping responsibilities.
- One clear responsibility per node.
- Tool docstrings that state side effects and when not to use the tool ("this changes order status; it does NOT issue a refund").
- Explicit conditional routing: intent → validate → route (refund / cancel / clarify) → human approval for money-moving actions.
- Structured outputs between nodes, not free text.
4 · Distraction: check context assembly
Risk: too many chunks, verbose tool outputs and stale history all reach the prompt.
- Retrieval limits and a relevance threshold after reranking.
- Trim tool output to what the next decision needs (title, snippet, url; top 3).
- Route unrelated requests to a separate workflow instead of mixing them into the active task.
- Compact task state in the prompt; full raw records kept separately for audit.
The pipeline I would build
| Node | Responsibility | Prevents |
|---|---|---|
query_analysis | Understand intent | Confusion |
query_rewrite | Improve the retrieval query | Poor recall |
retrieve | Fetch candidate chunks, metadata-filtered | Poisoning, distraction |
validate_sources | Check freshness and authority | Poisoning |
rerank | Order by relevance | Distraction |
detect_conflicts | Find contradictory evidence and resolve by authority | Clash |
context_builder | Assemble a small, deduplicated, cited context | Distraction, rot |
generate | Grounded answer with citations | All four |
evaluate | Check quality and grounding | All four |
Reranking improves relevance. Metadata filtering enforces constraints. Conflict detection finds contradictions. The context builder controls what reaches the LLM. A high rerank score does not make a document current or authoritative.
The architectural change
Before: retrieval output goes straight into the prompt.
context = "\n\n".join(doc.page_content for doc in retrieved_docs)
response = llm.invoke(prompt.format(context=context))
After: a validation and assembly stage sits between retrieval and generation.
def build_context(documents, k=5):
selected = []
for doc in documents:
m = doc.metadata
if m.get("status") != "active":
continue
selected.append({"source": m.get("source"),
"version": m.get("version"),
"content": doc.page_content})
return selected[:k]
Production version adds date applicability, source authority, duplicate handling and conflict detection. Retrieved text is not trusted context until it passes this stage.
Audit checklist for my pipeline
- Inspect Chroma metadata for all 11 sources.
- Check whether versions and timestamps are recorded.
- Find whether outdated or duplicate chunks can reach results.
- Compare top 5 chunks before and after reranking.
- Check whether contradictory chunks are detected.
- Inspect the actual prompt sent to GPT-4o-mini.
- Measure tokens entering the generation step.
- Inspect LangGraph state for stale tool outputs.
- Write four test cases: poisoning, clash, confusion, distraction.
- Run the eval harness before and after each fix.
Retrieval, reranking and prompt assembly are three different stages, and each one can introduce a failure. Look at what the model actually receives, not just what Chroma returns.
Interview answers
Q: Your retriever returns an outdated doc, the reranker scores it highest, and the model answers wrong. What failed?
Context poisoning. The model reasoned correctly from bad input. The fix goes upstream: version metadata, active-status filtering and source validation. Relevance is not authority.
Q: Two nodes return different answers about the same policy, with no version info. What is it?
Context clash. I resolve it at the data layer with versioning and an authority order, not by hoping the model guesses the newer one.
Q: The agent passes 30 raw tool responses and 20 irrelevant chunks to the LLM. What do you do?
Context filtering and compression: relevance threshold, trimmed tool outputs, a compact state object, and raw history moved to audit logs. Raising temperature or adding more system instructions does not fix this.
- Garbage in, garbage out: a strong model can't fix bad input.
- Relevance is not authority.
- More tokens are not better. Quality beats quantity.
- Tools need precise contracts: inputs, outputs, side effects.
- Context is dynamic: what the agent needs changes as the task moves.