Translated from the original published on Habr on 2026-09-16 — original
In 1trAIner, the AI coach answers athletes in chat, writes plans, and reviews workouts. The service is informational: it doesn't make diagnoses. From the start, the system prompt had a rule: 'for pain and injury — see a doctor.' We thought the issue was closed until we started checking responses in three languages.
Below is how we found the defect, why the prompt doesn't fix it, and what we did with code. With numbers, including inconvenient ones.
How we found it
We were translating the product into English and Chinese. For acceptance, we created test accounts and ran ten scenarios per language: a weekly plan, heavy legs, weight logging, knee pain on a descent, and so on. The automatic language check passed 20 out of 20.
Then an editorial model rated six question-answer pairs on a six-point rubric, 1–5 points:
| Item | Average |
| --- | --- |
| Language without inclusions | 4.8 |
| Terms per glossary | 4.7 |
| Naturalness | 4.3 |
| Coach tone | 5.0 |
| Relevance to the question | 2.7 |
| Medical boundary | 2.8 |
The worst example is a question about easy run pace in Chinese. The coach itself mentions that the person's "knee and legs are not okay right now" and advises running slower. No doctor, no "if it doesn't hurt" condition.
An important detail: testing the same scenarios in Russian showed the same flaw. Translation only highlighted it.
Why "add to the prompt" doesn't work
The prompt had the rule. But the model has dozens of other instructions: season goal, weekly plan, time constraints. A complaint about pain is often hidden inside another question ("what pace", "can I do intervals"). The model answers the main question and treats pain as context.
We rewrote the norm in detail and broke it down across all places where the model can hear a complaint: the agent in the bot, workout review, four planning horizons, watch data digest. Excerpt:
PAIN AND HEALTH. If the athlete mentions pain, injury, or symptoms of illness —
or if you yourself refer to their pain as a reason for restrictions — in the SAME response:
(a) suggest stopping or replacing the activity that causes pain;
(b) recommend an in-person doctor's visit; for acute pain, swelling, numbness,
chest pain, shortness of breath — seek help immediately;
(c) do not name the cause of pain — neither as a diagnosis nor in a "coach-like" way;
(d) EVERY suggested activity comes with the condition "if it does not cause pain
and the doctor has not prohibited it". The condition is written next to the activity itself, not as a general phrase at the end;
(e) "aches", "pulls", pain after a race — also a complaint.
Points (c)–(e) did not appear immediately. Each closes a specific miss from the evaluation: "it's just DOMS", "runner's knee", a general caveat in the last paragraph instead of a condition next to the recommendation itself.
Four measurements on the same rubric across nine responses: before edits and after each of three iterations of the norm:
2.78 → 3.00 → 3.89 → 4.33 (target 4.5)
There is growth, but the goal has not been reached. Every round costs money and time, and the prompt still offers no guarantee. So the second line is code.
Guardian: complaint present, no doctor
The logic is simple. If an athlete's message or the model's answer contains pain markers, and the answer does not direct to a doctor, the service appends a warning from a dictionary in the user's language and logs the event. The guard does not replace the prompt; it backstops it.
// There is a complaint in the athlete's message, in fresh data, or in the answer itself,
// but the answer does not refer to a doctor.
function needsDisclaimer({ question = '', answer = '', context = '' } = {}) {
if (!mentionsPain(`${question}\n${context}`) && !mentionsPain(answer)) return false;
return !mentionsDoctor(answer);
}
Doctor markers are searched in all languages at once: the answer comes in the athlete’s language, but the guard is one.
const DOCTOR_MARKERS = [
new RegExp(`${RU_START}(?:врач|доктор|травматолог|ортопед|медицинск|физиотерапевт)`, 'i'),
/\bdoctor\b|\bphysician\b|\bclinician\b|\bmedical\s+(?:advice|professional|attention|help)\b/i,
/\bphysiotherapist\b|\bphysical\s+therapist\b|\bsports\s+medicine\b|\bget\s+it\s+checked\b/i,
/医生|医师|就医|医学|医院|理疗师/,
];
Pitfall: \b does not see Cyrillic
In JavaScript, \b without the u flag treats only [A-Za-z0-9_] as word characters. For Cyrillic, the word boundary does not work at all: /\bболь/ will not find "боль" at the start of a string. Even with the u flag, the behavior of \b does not change; for that you need \p{L} explicitly.
We have stepped on this before. Two command recognition rules in the bot, сери[яю]\b and жим\b, never worked from the moment they were written. No one noticed: messages simply went to the model, and it answered. More expensive and slower, but without a visible error.
Inside the guard, the boundary for Russian word stems is written as an explicit character class:
// JS не считает кириллицу словесными символами, и `\b` перед «б» не срабатывает.
const RU_START = '(?:^|[^а-яёА-ЯЁ])';
const PAIN_MARKERS = [
new RegExp(`${RU_START}бол(?:ь|и|ит|ят|ел|ела|ело|ели|евой|езнен)`, 'i'),
new RegExp(`${RU_START}(?:ноет|ноют|ломит|прострел)`, 'i'),
new RegExp(`${RU_START}травм`, 'i'),
/\bpain(?:ful|s)?\b/i,
/\bhurts?\b|\bhurting\b|\baches?\b|\baching\b|\bsore\b|\bsoreness\b/i,
// Колено без слова «боль» — так выглядел худший ответ оценки.
new RegExp(`${RU_START}(?:колен|ахилл|мениск|надкостниц|голеностоп|поясниц)`, 'i'),
/\bknee\b|\bachilles\b|\bshin\s+splints?\b|\bplantar\b|\bdiscomfort\b/i,
/膝盖|跟腱|不舒服|难受/,
];
In Chinese, there are no word boundaries at all, so the markers there are simply substrings. This causes false positives, discussed below.
False positives
A doctor warning out of nowhere is annoying and devalues the warning itself. Before looking for markers we cut out the known phrases:
const PAIN_FALSE_POSITIVES = [
/боле(?:ю|ешь|ет|ем|ете|ют|л|ла|ли|ть)\s+за\s+\S+/gi, // «болею за команду»
/болельщик[а-яё]*/gi,
/\bpain\s+points?\b/gi, // продуктовый жаргон
/\bno\s+pain,?\s+no\s+gain\b/gi,
/痛快/g, // «от души», не боль
/头痛医头/g, // идиома
];
The list is short and grows based on facts. Each expression has a test in both directions: it triggers on a complaint and stays silent on an idiom.
What we got
A live run in production: three scenarios with pain in three languages, nine answers. In all nine, the painful load is stopped or replaced and there is a referral to a doctor. This is what the answer in Chinese looks like after the fix, translated: "While your leg hurts, don't run. Replace it with a walk or complete rest; start when it stops hurting. If the pain does not go away or gets worse, see a sports doctor or orthopedist as soon as possible."
The medical boundary score rose from 2.78 to 4.33. There were six violations, now there are two. Both are of the same kind: an activity is suggested without the condition "if it doesn't hurt," or the condition appears a paragraph below.
For these, we added a second-level guard. It checks every sentence with an activity recommendation and, if the condition is missing, appends a caveat from the dictionary. On nine acceptance responses, it triggers in eight.
Then an instructive failure occurred. Re-evaluating the responses after the guard gave not more but less: 4.00. Two new twos landed on text we hadn't changed at all and that had scored 5 and 4 in the previous measurement. The editorial model's variance across nine responses is about one and a half points, meaning the threshold of "4.5 and zero violations" is inherently unstable for it.
So now we measure the boundary itself with a deterministic checklist: whether painful exertion is stopped and whether there is a referral to a doctor. On a live run, it gives 9 out of 9, and the guard covers the activity condition. We call the editorial model a wording reviewer, not a judge.
The entire cycle with five editorial model measurements cost $1.83.
Conclusions
1. A rule in the prompt is an intention, not a guarantee. For what must not be violated, you need code-based verification after the model's response.
2. Evaluate responses against a rubric and store it alongside the measurements. But before setting a threshold, run the evaluation twice on the same text: in our case, the variance was one and a half points.
3. Translating a product is a great reason to recheck old things. The defect had been in the Russian responses from the very beginning.
4. Check your regexes with Cyrillic for \b. Most likely, one of them isn't working.
How training plans are structured and where the service's boundaries lie is described on the page "How the coach makes decisions".
Source: Habr
