Repository navigation
Run react-to-me preprocessing in two rounds, and correct the latency record - #238
Merged
Merged
Conversation
…record **The change.** `ReactToMeGraphBuilder` overrides `preprocess` and ran all four calls back to back. The base class deliberately overlaps what it can, with a comment about the round trip it saves and a test pinning it -- and the profile both the chat UI and the answer endpoint actually use discarded that, with no test noticing. Only the dependencies force an order: language detection reads the raw input and needs nothing from the rephraser, while safety and intent both read the rephrased text and so follow it but not each other. Both already ran regardless of the safety verdict, so overlapping them changes no behaviour. Measured A/B in both orders, to rule out drift flattering whichever ran second. Preprocessing 5.1s -> 2.6s at the median; first answer token 13.1s -> 11.4s. Across the tracked question set, first token p50 12.0s -> 9.6s. **The tail is the honest caveat.** Sequential waits for a+b, where noise averages out; concurrent waits for max(a,b), where it does not. On the four-question repeated measure the p90 advantage was nil (14.57s against 14.60s) even though the median improved. On the fifteen-question set p90 did improve, 14.5s -> 12.2s. The median gain is consistent; the tail gain is not, and should not be claimed. **The record was wrong.** This contract told the website team the first token arrives "around 36 seconds". It came from one question and does not reproduce: collection routing narrowed retrieval, and preprocessing was never the ~16s recorded -- it was 5.1s before this change and is 2.6s after. The spec and the contract now carry the distribution, measured through the served HTTP path as well as the graph, and name the answer model's own 6.1s time-to-first-token as the largest block left. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adversarial review done before opening this, per request. It changed what I'm claiming twice — see "What the review caught".
The change
ReactToMeGraphBuilderoverridespreprocessand ran all four calls back to back. The base class deliberately overlaps what it can — with a comment about the round trip it saves, and a test pinning it. The profile that both the chat UI and the answer endpoint actually use discarded that, and no test noticed, because the existing test only coversBaseGraphBuilder.Only the dependencies force an order:
user_input, so it needs nothing from the rephraserrephrased_input, so they follow it — but not each otherBoth safety and intent already ran regardless of the safety verdict, so overlapping them changes no behaviour.
Measured, A/B in both orders
Sequential ran first in my initial A/B, which would flatter whatever ran second, so I repeated it reversed:
Across the fifteen tracked questions, two runs each: first token p50 12.0s → 9.6s, completion p50 13.2s → 10.4s. Through the served HTTP endpoint: first token p50 10.8s, and 0 anchors leaked.
What the review caught
I was about to claim "~20% faster" without qualification. Sequential waits for
a+b, where noise averages out; concurrent waits formax(a,b), where it does not. On the four-question repeated measure the p90 advantage was nil — 14.57s against 14.60s — even though the median improved. On the fifteen-question set p90 did improve (14.5s → 12.2s). The median gain is consistent; the tail gain is not, and I've stopped claiming it.An isolated measurement said the change made things slower (6.98s vs 4.45s preprocess). Those readings were 20 minutes apart and measured API weather, not code. Only same-window A/Bs are in this PR.
Static checks failed — 1 ruff error and 5 mypy errors in the new tests (
active_sourcesis a required key onReactToMeState; an annotation referenced a type imported only inside a function). Caught before the PR, not by CI.Behaviour under
gatherneeded pinning. Sequentially, a failed rephrase short-circuited everything after it. Undergatherthe sibling is already in flight and only the first exception propagates. What matters is that preprocess still raises rather than returning a half-built state — a silently emptyrephrased_inputwould retrieve against nothing and answer from it. Two tests pin that.The safety path was verified live through HTTP, since intent now runs even for unsafe questions: both unsafe questions return
nothing_foundin ~4.5s with zero tokens, and the control answers normally.The record was wrong
This contract told the website team the first token arrives "around 36 seconds". That came from one question and does not reproduce — collection routing (spec 009) narrowed retrieval, and preprocessing was never the ~16s recorded; it was 5.1s before this change. They are designing a blank-panel experience around a number 3× too pessimistic.
Spec and contract now carry the distribution, measured through the served path as well as the graph, and name the answer model's own 6.1s time-to-first-token as the largest block left — not retrieval, and not preprocessing.
Cost
No test in this repo reaches Tavily or OpenAI: the full suite passes with
TAVILY_API_KEY=andOPENAI_API_KEY=unset. Every measurement script here runs withenable_postprocess=False.CI-equivalent locally: ruff, format, mypy (128 files), full suite.
🤖 Generated with Claude Code