For the past two months I've been building Biassemble, a retrieval engine that helps an LLM detect cognitive biases in personal stories. If you're curious, there's a live demo and the retrieval engine is open source — links are at the end.
The engine's only job is to answer one question:
Which cognitive biases should the downstream LLM investigate?
The stakes are simple: if the system can't recognize when someone is rationalizing, anchoring, or doubling down on sunk costs, everything built on top of it reasons about the wrong things.
The retrieval component alone went through three completely different architectures. Looking back, what changed the system wasn't discovering a better model — it was discovering that several of my engineering assumptions were simply wrong.
Assumption #1: A better knowledge base will improve retrieval
The first version was intentionally simple.
A user story was embedded, searched against a PostgreSQL + pgvector knowledge base of 38 cognitive bias descriptions, and the top candidates were passed to the assessment LLM. It worked. It was also fast — around 500 ms per query, pgvector round-trip included.
Unfortunately, it quickly hit a ceiling: on my labeled test set, the correct bias landed in the top-5 candidates for only two stories out of three — Recall@5 of 0.667, against a target of 0.85. That metric is the one this system lives or dies by: a bias missing from the shortlist is a bias the assessment LLM never gets to consider.
Instead of replacing the retrieval mechanism, I assumed the knowledge base wasn't good enough. So I tried to improve it:
rewrote bias indicators
split documents into smaller chunks
added domain-specific examples
generated synthetic story patterns
Each experiment sounded reasonable. None moved Recall@5.
That was the first important lesson. The bottleneck wasn't the knowledge base — it was the retrieval mechanism itself.
Vector search was excellent at finding similar topics. It was much weaker at recognizing how someone was reasoning.
Two stories about medicine can have nearly identical vocabulary. One may demonstrate overconfidence. The other may show careful uncertainty. The topic is identical. The reasoning pattern is not. Embeddings — at least the compact sentence embeddings we used — encode the first, not the second.
We weren't missing knowledge. We were using the wrong retrieval mechanism.
Result of Assumption #1: four content experiments, Recall@5 unchanged at 0.667. Knowledge-base work was closed as a dead end — the next move had to change the mechanism, not the content.
Engineering lesson: every gain in this project came from changing a mechanism — never from adding more content, more context, or more compute.
Assumption #2: Passing evaluation means you're ready for production
The next step was introducing a second retrieval stage using zero-shot Natural Language Inference.
Instead of asking "which bias document looks most similar?", the system asked "does this story entail the reasoning pattern behind this bias?" — one or more hypotheses per bias, evaluated against the story directly.
That change worked far better than expanding the knowledge base ever did. After several rounds of hypothesis engineering, local evaluation finally crossed the project's Recall@5 target of 0.85.
It felt like the problem was solved.
Then I deployed it.
The NLI model had to evaluate every bias hypothesis individually — 38 biases, several carrying multiple phrasings, 82 separate forward passes per story. On a free CPU tier, a 9-word story took 67 seconds. A realistic 234-word story timed out entirely with a 503.
The retrieval engine that looked excellent in evaluation never delivered a single real retrieval in production.
It was a useful reminder that evaluation and production measure different things. A model can satisfy every quality metric and still be unusable if the mechanism doesn't fit the hardware.
One architectural decision came from an AI conversation
Initially I treated retrieval as something that had to complete before the user could continue.
During architecture discussions, AI challenged that assumption. Users were already spending time reading and answering follow-up questions generated by the assessment LLM. Why not run retrieval during that time?
Instead of optimizing retrieval first, we optimized when retrieval happened.
The architecture became asynchronous. While the user answered questions, the retrieval engine continued working in the background. In many cases, both completed at roughly the same time. The user experienced almost no additional waiting. If a user answered unusually fast, the system fell back to LLM-only assessment.
Async solved the user experience — but it couldn't save NLI itself, which still timed out on longer stories regardless of who was waiting. The NLI stage was eventually replaced by a small generative LLM that shortlists biases directly: one call instead of 82, reading the whole catalog at once. Vector search stayed on as the second signal.
Sometimes architecture removes latency more effectively than optimization.
Assumption #3: Richer context produces better LLM output
One recurring suggestion during development was to give the retrieval model increasingly rich context — more taxonomy, more definitions, more explanation per bias.
I moved in the opposite direction.
The original prompt included the full text of all 38 bias descriptions. End-to-end latency: 48–60 seconds locally. I reduced the prompt to the smallest information required for the task — just a bare list of 38 bias IDs, no definitions. Latency dropped to around 8 seconds, results improved, and the approach scales trivially to a larger catalog.
Adding more context isn't always adding more information. Sometimes it's only adding more tokens.
Where the project is today
The current engine combines multiple retrieval signals. A bias is admitted if the generative LLM names it or if vector search scores it above a threshold. Every bias carries a source tag — ["llm"], ["vector"], or ["vector","llm"] — so the downstream assessment step knows where each candidate came from.
A recent evaluation across 80 stories — deliberately split between familiar domains (medical, legal, financial) and unfamiliar ones (mycology, bonsai, competitive sailing) — confirmed a domain-familiarity gap: positive recall was 0.450 on familiar domains and 0.300 on unfamiliar ones. Overall live positive Recall@5 sits at 0.562 against the 0.85 target — a separate measurement from the domain-split eval, and a gap that's documented, not hidden. The planned fix comes in two steps: first, fine-tuning the generative model on production stories already labeled by the orchestration layer; later, once enough labels accumulate, training a small dedicated selection model on them — cheap enough to run anywhere, calibrated on exactly this taxonomy.
The retrieval engine is intentionally limited. It doesn't decide whether someone actually exhibits a cognitive bias. It builds a shortlist of candidates for the downstream LLM to reason about. That separation keeps the system explainable, measurable, and replaceable.
Final thoughts
The biggest lesson wasn't that one model outperformed another.
It was that measurements repeatedly overturned my assumptions. A larger knowledge base didn't improve retrieval. Excellent evaluation metrics didn't guarantee a deployable system. Cutting context improved both speed and quality.
And some of the best ideas came from collaborating with AI — provided I treated its suggestions as hypotheses to test, not truths to accept.
There's a small irony I've come to appreciate: a system built to detect biased reasoning got better mainly because I kept catching my own — the conviction that more knowledge must help, that a passing eval means a working product, that richer context means better output. The engine audits stories. The metrics audited me.
Try Biassemble
Live demo: https://frontend-topaz-eight-10.vercel.app/
Retrieval engine (GitHub): https://github.com/lemind/biassemble-engine






Top comments (0)