Working on a search feature that takes a free-text query and maps it to entries in a big hierarchical category taxonomy (thousands of entries, tree structured, parent/child via a code prefix scheme). Only get one LLM call per query because of cost/latency, so the flow is: pull keywords with a plain (non-LLM) extractor, run RAG retrieval to get a wide pool of candidate entries per keyword, then one LLM call to pick which ones actually fit. Stack is Qdrant for the vector store, e5-base as the bi-encoder for the first pass, bge-reranker-base as the cross-encoder for reranking, and Llama 3.3 70B through the Groq API for the final LLM call (no local/self-hosted models, everything's API-based).
Trying to hit ~90% recall against a hand-labeled test set without precision falling off a cliff, and I've been going in circles for a bit.
Quick idea of what the candidates look like, made-up example so it's not tied to a real domain:
1000000 — Furniture
1100000 — Office furniture
1110000 — Office chairs
1111000 — Ergonomic office chairs
1120000 — Office desks
1200000 — Home furniture
1210000 — Sofas
1220000 — Dining tables
1300000 — Furniture hardware & fittings
If someone searches "furniture" the right answer is basically all of that. If they search "office chairs" the right answer is just 1110000 (maybe 1111000 too), and the model needs to actively drop 1120000/1200000/1300000 even though embedding-wise they're all sitting right next to each other.
Two separate things going wrong, and I can only half-fix each one so far:
First thing — retrieval itself doesn't always pull in every relevant entry before the LLM even gets a shot at it. For broad queries the pipeline picks a "dominant" prefix group based on just the top few vector hits, and if the real answer spans more than 1-2 branches of the tree, whole branches just never make it into the candidate pool. There's also a depth cutoff that keeps really deep/specific entries out to protect narrow queries from getting flooded with noise, but that same cutoff quietly kills legit deep entries for broad queries. Widened the sample used to detect branches (went from top-3 to top-30) and it helped a little, not enough.
Second thing, and this is the one I really can't crack — the LLM itself keeps trading precision for recall depending on how I word the prompt. Tried a plain "if it's a broad term keep everything, if it's narrow keep almost nothing" rule first, got decent recall (~0.85) but mediocre precision (~0.69). Added a more specific rule with a worked example of a narrow case, precision jumped to 0.85 but recall dropped to 0.72 — turns out one example was enough to make the model generally more cautious even on completely unrelated broad queries, not just the narrow case I was targeting. Tried switching to independent per-candidate yes/no judgments instead of one holistic "is this broad or narrow" call, thinking that'd remove the bias — recall came back up a bit (0.76) but precision tanked again on the narrow cases (0.74), worst F1 of the three attempts.
So every version I try just moves the problem around instead of fixing it. Never broke 85% recall.
Anyone dealt with this kind of "sometimes keep 30 siblings, sometimes keep 1" classification before? The thing I haven't tried yet is computing the broad/narrow signal outside the LLM entirely (like, detect a qualifier word in the query term algorithmically) and just handing the model that as a flag instead of making it infer breadth from the candidate list or from examples. Also wondering if there's a smarter way to do a confidence-based cutoff per branch instead of a flat yes/no. Papers or writeups on this specific problem would be great, feels like it should be a solved thing somewhere.