Skip to content

Spec: retriever rewrite - #179

Merged
adamjohnwright merged 2 commits into
mainfrom
spec/retriever-rewrite
Sep 8, 2026
Merged

Spec: retriever rewrite#179
adamjohnwright merged 2 commits into
mainfrom
spec/retriever-rewrite

Conversation

@adamjohnwright

Copy link
Copy Markdown
Contributor

First use of Spec Kit, on the decision three open PRs are queued behind (#116, #133, and the config work in #112/#151).

This is a specification, not an implementation. It exists to make four decisions explicit and cheap to make.

What it records

What the retriever does today, including the five places it reaches into LangChain internals — with line numbers, so the table can be re-checked rather than trusted:

location what it does
csv_chroma.py:143 subclasses MultiQueryRetriever
csv_chroma.py:59,144 overrides a required field to None via SkipJsonSchema
csv_chroma.py:192-194 builds a throwaway MultiQueryRetriever to steal its .llm_chain
csv_chroma.py:200 assigns _retrievers outside pydantic
csv_chroma.py:206 instantiates EnsembleRetriever(retrievers=[]) to borrow one method

Those are why the LangChain upgrade is blocked.

The four open decisions

Notes

Every factual claim was checked against the code rather than written from memory, which caught two errors: the mypy baseline holds four retrievers.*.rag entries, not three — userguide arrived with feature/userguide-qa — and the reach table needed line numbers.

Success criteria are observable rather than aspirational: no LangChain internals remain, sync and async agree on identical inputs, two callers get different budgets in the same process, and the four mypy baseline entries are deleted rather than carried forward.

Checked against .specify/memory/constitution.md; all six articles are addressed.

Nothing is implemented here. The next step is D1–D4, then /speckit-plan.

🤖 Generated with Claude Code

adamjohnwright and others added 2 commits September 8, 2026 19:30
First use of Spec Kit, on the decision three open PRs are queued behind.

The specification records what the retriever does today -- including the five
places it reaches into LangChain internals, with line numbers -- and separates
the parts that are settled from the four that are not:

  D1  is SelfQueryRetriever replaced by plain semantic search? Helia's proposal.
      Removes 20 LLM calls per message and the component most likely to break on
      LangChain 1.x. But the measurement in #171 shows SelfQuery is stable run to
      run and agrees with plain vector on only 0.48 of documents and 0.19 of
      positions, so this is a real behaviour change, not the removal of noise.
  D2  per-collection budget or global?
  D3  documents or tokens? (1.6x spread measured; not currently binding)
  D4  does multi-query expansion stay? Its value has never been measured.

Every factual claim was checked against the code rather than written from
memory, which caught two: the mypy baseline holds four `retrievers.*.rag`
entries rather than three, since userguide arrived with feature/userguide-qa,
and the reach table now carries line numbers so it can be re-checked.

Success criteria are things that can be observed -- no LangChain internals
remain, sync and async agree, two callers get different budgets in one process,
and the four baseline entries are deleted rather than carried forward.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
D1 is Helia's call: SelfQueryRetriever is replaced by plain semantic search, BM25
stays. Recorded with its consequences rather than as a simplification, because it
is not one -- retrieval results change materially (#171 measured SelfQuery as
stable run to run, agreeing with plain vector on 0.48 of documents and 0.19 of
positions), metadata filtering is lost as a capability rather than just as code,
and 339 lines across the metadata_info files fall out of the retrieval path while
remaining in use by the evaluator and the baseline harness.

What it buys: LLM calls in retrieval drop from 21 per message to 1, and the
component most likely to break on LangChain 1.x goes away.

D2, D3 and D4 are mine, each with the reasoning that decided it:

  D2  budget stays per collection. The decisive argument is technical: each
      collection is fused independently, so RRF scores from different collections
      are not comparable quantities and a global top-N would sort by a
      meaningless comparison. Coverage is the secondary reason.
  D3  budget counted in documents. Tokens are the better unit in principle and
      #139 was right, but 40 documents span 5,796-9,531 tokens at roughly 7% of
      the window -- a ceiling there would tighten a bound that is not binding.
      Kept as one named parameter so a token bound can be added later.
  D4  multi-query expansion stays for now, and is measured separately afterwards.
      Not because it is known to be valuable -- that has never been measured --
      but because D1 already changes retrieval materially, and removing both at
      once would make any quality difference impossible to attribute.

FR-003, FR-005, FR-009 and SC-005 updated so the requirements match the
decisions rather than drifting from them.

The #171 figures predate the #169 and #170 fixes, so they describe a retriever
that no longer exists. Recapturing them is recorded as the first verification
step before implementing D1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@adamjohnwright
adamjohnwright merged commit d534e27 into main Sep 8, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant