Skip to content

Draft: feat(chatqna): multi-turn vector-space blending (issue #833)

Jérôme Revillard requested to merge feat/multi-turn-vector-blend-833 into main

Context

Resolves #833.

ChatQnA retrieval is stateless: only the last user message is embedded and searched, so pronoun-heavy follow-ups ("can you elaborate on this?") retrieve garbage. This MR adds feature-flagged multi-turn vector-space blending — blends the query embedding with the previous N turns (V = α·EQ + (1-α)·EH) so follow-ups retrieve the prior turn's subject.

MULTI_TURN_BLEND_ENABLED defaults to false (no-op until explicitly enabled).

Design (E2 — batch via existing embedding node)

Zero duplicated embedding logic, single batched TEI call:

  • handle_request extracts history_text → initial_inputs={text: query, _blend_history_text, _blend_alpha}
  • align_inputs EMBEDDING: inputs['input'] = [query, history] (batch) when history present, single-string otherwise (baseline unchanged)
  • align_outputs EMBEDDING: blends batch response data[0]+data[1]; sets text to the isolated query string so the retriever's text:str field and BM25 leg (when hybrid is explicitly enabled) are not broken
  • Retriever consumes the blended embedding preferentially (skips its internal re-embed)

Rejected alternatives documented in commit + code comments: side-channel httpx embed (duplicates logic), Redis cache (hit rate ≈0, no session_id in payload, silent failure modes), incremental combine (non-validated approximation), LLM query-rewriting (benchmark favored blending).

Validation

  • TEI batch validated live on 10.0.0.102 (El Salvador stack): POST embedding:6000/v1/embeddings {"input":[t1,t2]} → 2 distinct 768-dim vectors (cos=0.7476). OPEA wrapper passes batch through to TEI.
  • Benchmark (Roman, external): blending Recall@1=91.7% vs baseline 66.7%; beat LLM query-rewriting head-to-head. Reproduction into tests/rag-benchmarks/ is a follow-up (multi-turn gold dataset + Recall@k metric don't exist yet).
  • Tests: 162 pass (29 new). Code review caught + fixed a Critical (align_outputs sent batch list as text → would 422 retriever / BM25 garbage).
  • CI: pipeline #5187 success (11 jobs). Rebased on main (2026-07-24).

Limitations (documented, accepted)

Under the default production configuration (RETRIEVER_HYBRID_RETRIEVAL_ENABLED=false), the retriever runs dense-only — so the blended vector controls all retrieval. This has no practical impact unless you explicitly enable hybrid retrieval (opt-in), in which case only the dense leg is blended and the BM25 lexical leg still uses the isolated query text.

The reranker scores against the isolated query text (separate concern).

Files (10)

genieai_chatqna.py (config + 2 pure helpers + handle_request + align_inputs/outputs), test_chatqna.py (29 tests), env, docker-compose.yaml, chatqna/README.md, new site/docs/rag/multi-turn-retrieval.md, architecture.md, install-guide.md, ansible/env.j2, ansible/README.md.

Also documents the previously-undocumented RETRIEVER_HYBRID_RETRIEVAL_ENABLED flag in retrieval.md and install-guide.md (was missing from user-facing docs — discovered during multi-turn blend review).

Follow-ups (separate issues)

  1. Build multi-turn gold dataset + Recall@k metric in tests/rag-benchmarks/ to reproduce the benchmark in-repo.
  2. Confirm with Roman whether the external benchmark ran with hybrid ON or OFF.
  3. Incremental embedding + optional cache IF a measured latency problem justifies it (currently no evidence).
Edited by Jérôme Revillard

Merge request reports

Loading