Tool calls, retrieval, MCP, a frozen gold set

October 2026 upgrade

A marathon coach that runs the numbers before it answers

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.

At a glanceEval run 2026-10-28 · cost run 2026-11-04
42/42

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.

Task
Answer a runner's pacing, heat and race-plan questions, with every number traced to a tool result or a cited card.
Eval set
43 questions (26 numeric with 42 scored values, 17 strategy), committed at eb413bf before any system ran; the eval refuses to run if the file's hash changes.
Baselines
The bare model, and the July RAG coach (dense top-5 passages, cited answers). Same model, same question text.
Cost per question
Median $0.017 tool coach, $0.015 RAG, $0.007 bare, API-equivalent; n = 11 per system.
Latency
Tool coach median 8.4 s, p95 14.6 s, one answer at a time (n = 11). In the full run, four at a time: median 7.2 s, p95 12.5 s (n = 43).
Main failure
Given a question with an input missing, it asked for the input instead of answering (1 of 17 strategy questions). The 42/42 is close to circular: expected values come from the models it calls.
100%

Share of 42 numeric targets the tool coach got within tolerance, against 50% for the RAG coach and 33% for the bare model.

7%

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.

0.90

Judge's faithfulness for the tool coach, against 0.88 for RAG and 0.33 for the bare model.

7.2 s

Median time per tool-coach answer (2.3 model calls on average), against 7.4 s for one RAG call.

The comparison

Three systems, one frozen question set

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.

Numeric answers within tolerance

Share of scored values each system got right, by the kind of question. Focus or hover a group for counts.

Bare modelRAG coachTool coach

From eval/web.json, written by scripts/export_web.py from the eval run of 28 October 2026 (eval/runs/run_2026-10.jsonl).

MetricBareRAGTool 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

One question, call by call

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?”

  1. Call 12.9 s
    $0.0007

    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."
    }
  2. Call 27.0 s
    $0.0072

    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 ..."}
  3. Answer10.0 s total

    Final answer, as the runner sees it

    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 reads your pacing as a "Steady fade" [T1]. Your chance of hitting the wall, meaning a 35-40 km pace more than 25% slower than your 10-20 km pace, is about 16% (0.1645) [T1].

    How far to trust this: the model was fitted on Boston splits and has no weather input. On the 2024 holdout, its 80% interval at 35 km covered only 74% of runners, so the late-race ranges are narrower than they should be [T1]. Treat the upper end (226.8 minutes and beyond) as quite possible, especially if the day is warm or windy.

    What to do: (1) Hold your current effort through 30 km and don't chase time in the middle miles, since that is what drives wall risk. (2) If it is warm or windy, check the weather effect and slow your target pace to match, because this prediction ignores it.

    FINAL: finish_min=213.54; wall_risk_pct=16.45

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.

More recorded runs from the eval

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

Under two cents a question

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.

SystemCallsCost med.Cost p95Seq. med.Seq. p95Run med.Run p95
Bare model1.0$0.0071$0.01147.2 s17.1 s6.6 s10.1 s
RAG coach1.0$0.0146$0.01607.2 s15.4 s7.4 s18.7 s
Tool coach2.4$0.0165$0.02698.4 s14.6 s7.2 s12.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

Where the baselines miss

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 groupValuesBare missesRAG misses
Heat slowdown (h01 to h06)997
Wall risk (n01, n04, n05, n07)444
Heat plus pace (c01, c02)444
Finish time from splits (n01 to n08)1033
Pace and split targets (p01 to p05)733
Arithmetic on a card (k01 to k05)850
All scored values422821
  1. Heat: the wrong rule of thumb. The bare model came in low on all nine heat values. For a 2:55 runner at 80°F it used a temperature-plus-dew-point table and said 5.3 minutes against the model's 19.2: 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.
  2. Wall risk: a population rate, nudged. Neither baseline can compute a per-runner probability, so both started from a card figure or a hunch. At 35 km in 3:05 the runner had been speeding up and the model put wall risk at 0%; the RAG coach said 30%: 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%.
  3. Heat plus pace: the first error carries through. Both c-questions chain a heat or finish estimate into a pace, so a miss on the first value costs the second. For 3:20 at 76°F the RAG coach reset the goal to 226 minutes by the minute-per-degree rule; the model gives 216.1, and the pace follows.
  4. Finish times: mostly close. Doubling the half split is a fair guess, so both baselines landed seven of ten. The RAG coach missed n04 by 17 minutes: the runner had been speeding up, and it applied the card's median 15% late slowdown anyway: Slowing 15% from there gives about 6:05/km, which puts you at about 229 min.
  5. Pace targets: the fade left out. Asked for a first-half pace for 4:30 with the typical fade, the bare model assumed a 3% fade and said 6:18/km; the RAG coach advised against planning a fade at all and gave 6:24/km. The pace table, using the fade band from the negative-split card, says 5:46/km.
  6. Card arithmetic: retrieval fixes it. The RAG coach got all eight. The bare model used numbers from memory instead of the cards: for carbs over four hours it used 60 to 90 g an hour and said 240 to 360 g, where the card's 30 to 60 g gives 120 to 240.

The tool coach's own failures

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

Plan, call, read, repeat

The Claude CLI takes text in and gives text out, with no function-calling interface, so src/agent.py runs the loop itself.

  1. Ask for one action. The model sees the question, the tool list and every earlier step, and must reply with one JSON object: a tool call or a final answer.
  2. Run the tool. 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.
  3. Feed the result back. Passages are numbered [1], [2] and tool results [T1], [T2], so the answer can cite both.
  4. Repair or stop. A reply that is not valid JSON gets one retry with the parse error quoted back. After six tool calls the model must answer with what it has.
  5. Check the numbers. A deterministic post-check pulls every number out of the answer and looks for it in the question, the tool results and the passages the answer cites, allowing for rounding and for clock times written as minutes or seconds. Anything unmatched is flagged.

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

Hybrid search and reranking, measured: no gain on 14 cards

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.

ModeHit@1Recall@3MRRMRR vs dense, 95% CIms 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

The same tools, from Claude Desktop or Claude Code

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

Fourteen cards, six from my own studies

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:

Negative splits
2.5% of 31,912 Boston 2014 finishers ran a negative split; about 94% slowed, median +14%.
The wall
Pace inflects at about 30 km; 27% of finishers slowed by more than 25% late in the race.
Heat
About one minute slower per °F of race-day heat at Boston (the July card; the heat tool is the newer model).
Courses
Boston +101 s and New York +78 s against Berlin, from a simulated field with planted penalties. The card labels them as simulated; they are not measurements of the courses.
Super-shoes
About 67 seconds of the modern elite gain is footwear.
BQ standards
Across 22 age and sex brackets there is no average gender gap, but women's brackets vary three to four times more in difficulty.

Limitations

What the numbers don't show

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

The first evaluation: grounded against bare

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

What it runs on

Retrieval
sentence-transformers all-MiniLM-L6-v2, Chroma (cosine); BM25, RRF and a cross-encoder available by flag
Tools
Boston finish-time and wall-risk model, Boston heat model, pace table, card search, calculator
Agent
Hand-written JSON action loop over the Claude CLI; optional native tool use through the Anthropic API
MCP
Official mcp Python SDK, stdio transport
Evaluation
Frozen 43-question gold set; numeric scoring in code, LLM judge, citation and unsourced-number checks
Also
The July RAG chain by hand and in LangChain LCEL (--engine lc)

Advice you can trace back to a number