October 2026 upgrade
On 42 numeric targets from a frozen set of 43 questions, a coach that calls my pace and heat models as tools got 42 right. The same model with retrieval got 21, and on its own 14. A question costs a median 1.7 cents at API prices and takes a median 8.4 seconds.
The July version of this coach retrieved passages from 14 knowledge cards and answered with
[n] citations. It could quote "about a minute per °F" but could not tell a 3:30
runner what 75°F costs, or what a 1:42 half means for the finish. The October version
can call tools: a finish-time and wall-risk model fitted on Boston splits, a heat model fitted
on Boston results against race-day weather, and a pace table. I froze 43 test questions
before running anything, then put the same questions to three systems: the model on its own,
the July RAG coach, and the tool coach.
numeric targets within tolerance for the tool coach, against 21 for the RAG coach and 14 for the bare model, all on claude-sonnet-5-5.
eb413bf before any system ran; the eval refuses to run if the file's hash changes.Share of 42 numeric targets the tool coach got within tolerance, against 50% for the RAG coach and 33% for the bare model.
Share of tool-coach answers with at least one number that has no source, against 65% for RAG. A check in code, not the judge.
Judge's faithfulness for the tool coach, against 0.88 for RAG and 0.33 for the bare model.
Median time per tool-coach answer (2.3 model calls on average), against 7.4 s for one RAG call.
The comparison
Of the 43 questions, 26 ask for numbers (42 scored values in all) and 17 are strategy questions,
including the seven from July. Each numeric question stores the tool calls that answer it, and
src/gold.py computes the expected value and its tolerance from the models' own
output, so nothing is typed by hand. A finish time counts as correct within a quarter of the
model's 80% interval (at least a minute); a heat slowdown within a quarter of its range (at
least 30 seconds); a wall-risk percentage within 5 points; a pace within 2 seconds. The set was
committed at eb413bf before any system saw it, and the eval refuses to run if the
file's hash changes. Every system got the same question text and the same model,
claude-sonnet-5-5, through the Claude CLI.
Share of scored values each system got right, by the kind of question. Focus or hover a group for counts.
From eval/web.json, written by scripts/export_web.py from the eval run of 28 October 2026 (eval/runs/run_2026-10.jsonl).
| Metric | Bare | RAG | Tool coach |
|---|---|---|---|
Faithfulness, relevance and context precision come from an LLM judge (the same model). The bare answer is judged against the passages the RAG coach retrieved for that question, as in July. Citation validity and the unsourced-number check are computed in code.
The tool coach got all 42 values. That figure is closer to circular than it looks: the expected values come from the same models the coach calls, so the numeric score measures whether a system reproduces the models, and a perfect score means the loop picked the right tool, passed the right arguments and copied the result back on all 26 questions. It did, with no JSON repairs and no step-cap stops. Whether the models themselves are right is a separate question, covered under the limitations.
Without tools, the RAG coach got 21 of 42 and the bare model 14. Retrieval settles the card-arithmetic questions (RAG 8 of 8, bare 3 of 8) and does little for the rest. On heat the RAG coach applied the July card's minute-per-degree rule, which for a 3:30 runner at 75°F gives about 25 minutes against the model's 16.9. On finish times both baselines landed inside the tolerance half the time, since doubling the half split is a fair guess, but they missed every wall-risk figure: the bare model guessed between 25% and 65% where the model said 0% to 19%.
The number check separates the systems more clearly than the judge does. In 28 of 43 RAG answers at least one number had no source, mostly the model's own arithmetic ("a 1:42 half is about 4:50/km", "about 204 minutes"). The tool coach did this 3 times, and failed one strategy question outright; both are in the error analysis below. On faithfulness the two coaches are close (0.90 against 0.88).
The tool coach made 2.3 model calls per answer and needed no more time than RAG (median 7.2 s against 7.4 s; means 8.0 s and 11.8 s). Each of its calls carried a short prompt and returned a short reply, a JSON action, where the single RAG call carries five passages and writes the whole answer. Latencies are wall-clock with four answers running at once.
Tool-call trace
A recorded run of question n01 from 4 November 2026, saved as eval/trace_n01.json by
scripts/measure_cost.py. Each model call returns one JSON action; the loop runs the tool and feeds
the result back as [T1]. Times and costs are per call, as claude -p reported them.
“I went through halfway in 1:42 and 10 km in 47:30. What finish should I expect, and how likely am I to hit the wall?”
Model reply (46 tokens out)
{"action": "tool", "tool": "predict_finish", "args": {"splits": {"10": 47.5, "21": 102}}}
Loop runs predict_finish as [T1] in 53 ms, returns
{
"median_min": 213.54,
"lo80_min": 207.48,
"hi80_min": 226.75,
"lo95_min": 204.1,
"hi95_min": 239.48,
"wall_risk": 0.1645,
"pace_type": "Steady fade",
"n_train": 176059,
"model_version": "2026-10-28.1",
"caveat": "Fitted on Boston splits; has no weather input. On the 2024 holdout its 80% interval at 35 km covered 74% of runners, so late-race intervals run narrow."
}Model reply (693 tokens out)
{"action": "final", "answer": "Expect to finish in about 213.5 minutes (median), with a 80% range of 207.5 to 226.8 minutes and a 95% range of 204.1 to 239.5 minutes [T1]. The model ..."}Final answer, as the runner sees it
Expected values for n01 from src/gold.py: finish 213.54 min (tolerance 4.8), wall risk 16.45% (tolerance 5). The answer matches both. The second call cost more because it writes the whole answer; the first is mostly a cached system prompt.
Three of the coach's answers from the 28 October evaluation run, exactly as recorded: each tool it called, with the arguments it chose, what came back, and the final answer. Long passages are cut short here; the full records are in the repo.
Cost and latency
The October run timed every answer but did not record cost, so on 4 November scripts/measure_cost.py
reran a fixed sample, every fourth question of the gold set (11 questions, 7 numeric and 4 strategy), through all three
systems one answer at a time, without the judge. Cost is the total_cost_usd that
claude -p --output-format json reports: the API-equivalent price, since the runs used a Claude
subscription. It includes the CLI's automatic prompt caching, which is why the coach's repeated system prompt is cheap.
Retrieval runs locally on an Apple M4.
| System | Calls | Cost med. | Cost p95 | Seq. med. | Seq. p95 | Run med. | Run p95 |
|---|---|---|---|---|---|---|---|
| Bare model | 1.0 | $0.0071 | $0.0114 | 7.2 s | 17.1 s | 6.6 s | 10.1 s |
| RAG coach | 1.0 | $0.0146 | $0.0160 | 7.2 s | 15.4 s | 7.4 s | 18.7 s |
| Tool coach | 2.4 | $0.0165 | $0.0269 | 8.4 s | 14.6 s | 7.2 s | 12.5 s |
Calls are model calls per answer. Seq. is one answer at a time; Run is the October eval run. Cost and Seq. columns: n = 11 per system, eval/cost_latency.json, per call in eval/cost_latency_calls.csv. Eval-run columns: n = 43 per system with four answers running at once, 28 October, from eval/runs/run_2026-10.jsonl via eval/error_analysis.json. Judge calls are excluded throughout.
The coach makes more calls but each is short: a JSON action, then an answer written from compact tool results, where the RAG call reads five passages. So it costs about what RAG costs and finishes in about the same time. Its p95 is below RAG's in both runs. The RAG coach's slowest answer in the eval run took 128 s, which sets its p95 there.
Error analysis
The tool coach got every scored value, so the failures worth reading are the other two systems'. Grouped by
the kind of question, with one recorded answer from each group. Counts are from
eval/error_analysis.json, which scripts/error_analysis.py builds from the saved run
without calling a model.
| Question group | Values | Bare misses | RAG misses |
|---|---|---|---|
| Heat slowdown (h01 to h06) | 9 | 9 | 7 |
| Wall risk (n01, n04, n05, n07) | 4 | 4 | 4 |
| Heat plus pace (c01, c02) | 4 | 4 | 4 |
| Finish time from splits (n01 to n08) | 10 | 3 | 3 |
| Pace and split targets (p01 to p05) | 7 | 3 | 3 |
| Arithmetic on a card (k01 to k05) | 8 | 5 | 0 |
| All scored values | 42 | 28 | 21 |
Combined temp + dew point: 80 + 65 = 145 ... about a 3% slowdown.The RAG coach applied the July card's minute per degree from a 50°F base and mostly came in high: 31 minutes on the same question.
In the Boston data, 27% of finishers slowed by more than 25% in the closing kilometers [1]. That is my baseline.The bare model's guesses ran from 25% to 65%.
Slowing 15% from there gives about 6:05/km, which puts you at about 229 min.
Outside the 42 values, three things went wrong. Asked "how much slower will race-day heat around 80°F make me?" (s02), it called no tool and asked for the runner's finish time instead. The judge scored that 0.30 for relevance and 1.00 for faithfulness, since an answer with no claims has nothing unsupported in it. On c01 it gave an adjusted finish range of "205 to 219 minutes" that no tool returned; it added the heat model's slowdown range (4.53 to 19.33) to 200 in its head, and the number check's 2.5% rounding allowance let it through. And 3 of 43 answers carried a number with no source: once it turned 17.7% into "a 1-in-6 chance", and twice it gave "30 km" advice without citing the card that says so.
How the loop works
The Claude CLI takes text in and gives text out, with no function-calling interface, so
src/agent.py runs the loop itself.
predict_finish and heat_effect are the models from the pacing and heat projects, vendored with their source commits. pace_table does even or fade splits, with fade sizes read from the negative-split card. retrieve_cards searches the cards. A calculator covers sums so the model does not do arithmetic in its head.[1], [2] and tool results [T1], [T2], so the answer can cite both.The model results carry their own caveats, copied from the holdout checks in the source projects, and the coach is told to pass them on. The pace model has no weather input, and on its 2024 holdout its 80% interval at 35 km covered 74% of runners. The heat model did not beat a flat no-weather guess on its 2015–2019 holdout (2.26% error against 2.15%), and it ignores dew point, which did not beat dry-bulb temperature in cross-validation.
Retrieval
The July page guessed that BM25 plus a cross-encoder would help, as it did for the wine sommelier RAG. On the frozen questions, scored by which cards each question needs, it did not. Dense search already finds a relevant card in its top three for nearly every question, and no mode's mean reciprocal rank differs from dense by more than its bootstrap noise. The coach keeps dense search, which is also the fastest of the embedding-based modes.
| Mode | Hit@1 | Recall@3 | MRR | MRR vs dense, 95% CI | ms per query |
|---|---|---|---|---|---|
From eval/retrieval_compare.json (src/retrieval_eval.py): 43 questions, 46 chunks, card-level relevance labels written with the gold set. Reranking uses cross-encoder/ms-marco-MiniLM-L-6-v2 on a fused shortlist of 20.
MCP server
src/mcp_server.py exposes retrieve_cards, predict_finish,
heat_effect and pace_table over stdio with the official
mcp Python SDK. In a recorded round trip (eval/mcp_roundtrip.json), Claude
Code was asked for a 3:40 even pace, called pace_table and quoted 8:23 per mile and a
1:50:00 half from the result.
claude mcp add marathon-coach -- \ ~/venvs/marathon-strategy-rag/bin/python \ /path/to/marathon-strategy-rag/src/mcp_server.py
Claude Code. Use the Python that has the repo's requirements installed.
{
"mcpServers": {
"marathon-coach": {
"command": "/Users/you/venvs/marathon-strategy-rag/bin/python",
"args": ["/path/to/marathon-strategy-rag/src/mcp_server.py"]
}
}
}
Claude Desktop, in claude_desktop_config.json.
The knowledge base
The corpus ships in the repo as markdown, one card per topic, each with a
> Source: line. Eight cover established training
principles: carbohydrate fueling, carb-loading, tapering, hydration and
sodium, even pacing, the long run, race week, and hills. Six are distilled
from analyses I ran and published:
Limitations
The numeric score is only as good as the models behind it. The heat model did not beat a flat no-weather guess on its own 2015–2019 holdout, and the pace model's late-race intervals ran narrow on 2024. A system that matches them perfectly is consistent, and that does not make it correct. In the recorded runs the coach passed both caveats on.
The judge is the same model as the three systems. The bare model's faithfulness is judged against the passages the RAG coach retrieved, so a correct claim the cards don't contain counts against it. That figure fell from 0.76 in July to 0.33 here: the judge prompt now scores one answer at a time and counts numbers as claims, so the two runs are not comparable.
I wrote all 43 questions, and the numeric ones hand the system every input it needs. Real runners leave things out, which is how the one failed answer happened. Each system answered each question once, so run-to-run variation is not measured.
The number check is rule-based. Its rounding allowance (2.5% of the source value) lets through some
arithmetic done in the model's head, as with the "205 to 219 minutes" range above. It also flags correct
paraphrases such as "1 in 6". I changed its rules twice after the run began, to count the tool
descriptions and digits in result field names (the "95" in lo95_min) as sources, and
to skip bold list numbers. Every answer was rescored with the final rules; under the first version
it fired on 11 coach answers and 29 RAG answers.
The retrieval comparison uses 43 questions and relevance labels I wrote. The course card's figures are simulated, and the card says so next to each one.
July 2026, superseded
In July the coach was scored on seven strategy questions: faithfulness 0.98 grounded against 0.76 bare, answer relevance 0.95, context precision 0.78, citation validity 1.00. Those seven questions are s01 to s07 in the new set.
2026-10-28: superseded by the three-system run above, which uses a different judge prompt (one answer
at a time, judged against that system's own context) and a pinned model, so the two sets of scores
are not directly comparable. The July figures stay in eval/results.csv. Also 2026-10-28:
the simulated-data label was added to the course card after the July evaluation; the October run
used the labeled card.
Stack
all-MiniLM-L6-v2, Chroma (cosine); BM25, RRF and a cross-encoder available by flagmcp Python SDK, stdio transport--engine lc)