Blog

Tuning an AI running coach

Posted 2026-07-17

AudioRun has a live AI coach feature that aims to provide helpful information throughout the run. It takes in information such as pace, heart rate, elevation change and user defined goals and pace targets. This information is fed into an LLM which generates coaching advice and outputs it as speech according to predefined triggers. Doesn’t sound too hard, but the specific constraints of a running app made this a challenge:

Due to the huge number of possible running scenarios, combined with the various user-defined parameters, it is not realistic to do manual e2e testing that has any hope of being exhaustive. Even a short 45-minute run typically yields a dozen coaching events.

The solution I use to evaluate the AI coaching feature is to simulate a variety of different runs using spoofed GPS data (using the Lockito app) and health metrics such as heart rate. The harness replays scripted scenarios, including segments with varying pace, heart rate, and elevation and feeds this into the actual production code: the same trigger evaluator, prompt builder, request/parse/timeout logic as the production app. This harness was created using Claude Code with Fable 5 (xhigh). Altogether we simulate nine different scenarios, including a 10k and an indoor treadmill run.

We measure how timely the responses come in, check for parse failures, word counts, digit and pace-notation leaks, repeated advice types and latency percentiles. Due to the sheer quantity of information provided by the simulations we hand off the first round of quality evaluation to Claude Opus 4.8. Claude reads the coach’s system prompt, and, for every delivered reply, the exact context prompts the coach saw. Claude can then verify coaching claims against the real numbers. Each reply gets a score based on the following parameters: was the message triggered correctly, was it factually grounded and did the user specified AI-persona match. An evaluation of the overall results then ranks the models.

Evaluation transcript showing coaching replies for heartbeat, final-stretch and debrief events, each tagged with latency, word count and a judge score, ending in a session verdict from Claude Opus 4.8.
Screenshot from a round of evaluating Claude Haiku — the model’s responses to the data, and then the verdict as delivered by Claude Opus 4.8.

I used this harness to evaluate several cloud models (using OpenRouter) such as Claude Haiku as well as self-hosted models like Qwen and Gemma. I was particularly interested in finding out if my local hardware was fast enough to deliver responses within the 15 second cutoff, and at what level of quality.

Hardware and software

The local self-hosted models ran on a computer with a Ryzen 5700X3D, 32 GB RAM and an RTX 5080 GPU. I used llama.cpp as the inference engine and used Tailscale to make it reachable from the app.

Local, self-hosted models Cloud models
gemma-4-12b-it-Q4_0deepseek-v4-flash
Qwen3.5-9B-Q8_0deepseek-v4-pro
Qwen3-4B-Instruct-2507-Q4_K_Mglm-5.2
Ternary-Bonsai-27B-dspark-Q4_1gemini-3-flash-preview
claude-haiku-4.5
Comparison table from the 'Rolling hills, no goal' scenario: five models' coaching replies to the same heartbeat, heart-rate and fade events, each tagged with advice type, latency and word count.
5 different models responding to the same information.

What I found

The first evaluation of the local self-hosted models (Gemma 4 12B, Qwen3.5 9B) was a disappointment. While they beat the cloud models on latency (p50 0.6–0.9 s versus ~1.8 s) and perfect 88/88 responses delivered, judge Opus disqualified both anyway. One spent ten-plus minutes watching the runner operate at critical heart rate while offering nothing but reassurance, instead of telling the runner to ease back. Both fabricated goals and training history in post-run debriefs. So, while the speed and latency were more than good enough, the quality was simply not there. But I did suspect that the system prompt and the format of the provided information was to blame. Sure enough, Opus identified reproducible failures in the prompts.

The tuning loop

Five edit→validate iterations plus a confirmation run. Each cycle: mine the newest report’s judge notes and exact context prompts, edit the prompt builders, run the offline guardrail tests (~20 s), fire a full judged run (~5 min), diff the flagged scenarios against both baselines.

What we learned:

Fabrication usually means missing data, not weak instructions. The overshoot problem — coach praising a precariously fast run — survived every instruction rewrite until it turned out the debrief prompt never stated the target pace. The model couldn’t see the 5:40-vs-6:00 gap without doing arithmetic it’s forbidden from doing. One precomputed “pacing vs plan” line fixed what three rules couldn’t.

Point-of-use context beats distant rules. System-prompt rules got ~80% compliance under repeated trigger fire. Every fix that stuck was data or a short note injected next to the stats it governs.

Ground the entailed fact instead of banning the phrasing. Banning the word “cushion” (time banked by running fast) just made the judge lint for the word while the model kept producing the frame. Stating the entailed truth in the context — the pace needed to finish exactly on target — made the claim checkable and the phrasing harmless.

Prompt fixes regress neighbors. An innocuous “splits a few seconds apart is even pacing” clause handed the model a praise label that broke the redline scenario’s debrief — “real pacing discipline” for a run spent at maximum heart rate — two runs straight, until reworded to be purely descriptive. Only the full scenario matrix caught it.

Results and open costs

Fabricated figures: gone in all six runs. PR moments: scored 5/5 in all six. Indoor pace leaks: gone. Overshoot debriefs: honest five runs straight. Ungrounded-reply count: 4 versus baselines of 5 and 9.

Takeaways