The Support Agent
RAG-based support that refuses to guess
Role
Agent architecture, RAG pipeline, evaluation
Timeline
2024 - now
Status
Running in production
Stack
Stack
- 3
- deterministic escape hatches
- 10
- tools bound to the agent
- 4
- chunks survive into the prompt
The support agent handles orders, delivery, returns and payments for Harrir's customers, in English, Modern Standard Arabic, Iraqi dialect and Franco-Arabic. The design constraint that shaped everything: a support bot that invents a return policy is worse than no bot at all.
Retrieval that earns trust
Answers are grounded in a Qdrant collection with three independently queryable named vectors: dense embeddings plus two BM25 legs, one English and one Arabic with proper normalization for tatweel, diacritics and alif variants. A character-ratio detector routes each query, so a pure-English question skips the Arabic leg entirely.
Surviving legs run concurrently under a 2-second timeout, fuse with Reciprocal Rank Fusion, and a cross-encoder trims the result to the four chunks that actually enter the prompt.
Three ways to fail safely
- Retrieval unavailable: every leg failed or fusion returned nothing.
- Below the score floor: retrieval found something, but nothing scored well enough to ground a reply.
- Citation validation failed: a post-hoc check parses the [chunk_id] markers in the draft and rejects any factual sentence without a valid one.
All three converge on a node that bypasses the model completely, writes a fixed abstention reply, and pauses the run on a native LangGraph interrupt while a Slack thread opens for a human. The graph resumes from the same checkpoint once the admin replies.
An async checkpointer, written from scratch
LangGraph's official MongoDB integration is synchronous, which would have forced every checkpoint read and write in this FastAPI app through a thread-pool detour. I wrote a fully async replacement on pymongo's native async client, schema-compatible with the official saver so checkpoints stay readable by either.