Перейти к основному содержимому

Измерение эвристики языка

AT-160 (EP-028, HS-053). Измеряемая эвристика — та, что предложила спецификация AT-080 («A — собственная эвристика, без зависимостей»). Начиная с AT-163 (RN-624) продукт её выполняет: api определяет с её помощью язык автора и СПРАШИВАЕТ, прежде чем что-либо изменится. Пороги продукта предварительные — см. Предварительные пороги продукта.

Что существует​

Инструмент живёт в scripts/idioma/ и запускается Node репозитория (type stripping, без шага сборки). Никаких новых зависимостей: эвристика — это список маркеров и регулярные выражения, а извлечение общается с Postgres через psql внутри контейнера базы.

Сама эвристика живёт в api — apps/api/src/domain/iam/heuristica-de-idioma.ts, — а scripts/idioma/heuristica.ts лишь реэкспортирует её, поэтому инструмент измеряет ровно тот код, который выполняет продукт. Это направление вынужденное: api не достаёт ни до scripts/, ни до какого-либо runtime-пакета без шага сборки, а packages/shared содержит только типы. В файле нет импортов, поэтому Node запускает его из scripts/ через type stripping (команды idioma:* глушат сопутствующее предупреждение о "typeless package"), а у scripts/idioma/ есть собственный tsconfig.json — единственное отличие в отключении verbatimModuleSyntax, который отвергает любой export модуля CommonJS, как в api. Сначала пробовали файл .mts: он прошёл обе проверки типов и сборку, но сломал все скрипты ts-node в api. Правило о том, что является доказательством (evidenciasDoAutor), переехало вместе с эвристикой, и извлечение реального корпуса использует то же правило.

файлроль
heuristica.tsреэкспортирует эвристику api: очистка, контрастная оценка, кандидат и предварительные параметры, выборка из 10 сообщений, гистерезис и правило доказательства
corpus-sintetico.jsonlверсионируемый синтетический корпус — каждая строка написана вручную для инструмента, origem: "sintetico"
sequencias-sinteticas.jsonсинтетические диалоги для пути выборки и гистерезиса
medicao.ts / medir.tsинструмент и его CLI
extrair.tsизвлекает реальный корпус из локального event log в файл вне git
rotular.tsразметка реального корпуса с подсказками, в терминале

Два корпуса и где живёт каждый​

Решение сопровождающего (AT-168, ответ 5) — реальный + синтетический. У них противоположные правила хранения:

  • Синтетический — версионируется, ревьюится как код. Покрывает девять обязательных случаев AT-080 (опечатки, настоящий испанский, английский, смешанный текст, "ok", код, логи, цитаты на другом языке) плюс португальский, насыщенный английским жаргоном, встроенные идентификаторы и языки вне трёх (французский, итальянский, немецкий, галисийский, каталанский, нидерландский) — список языков открыт (AT-168, ответ 2).
  • Реальный — сообщения самого пользователя в локальном event log. Никогда не попадает в git, в fixture, в лог CI или в тело PR. extrair.ts и corpus.ts отвергают любое место назначения внутри checkout, а файл записывается с правами 600 в $XDG_CACHE_HOME/brabo/corpus-idioma/ (иначе ~/.cache/…). Отчёт никогда не печатает текст элемента — только id, метки и счётчики, — поэтому его вывод можно вставить в PR.

То, что извлечение считает доказательством, следует очистке AT-080: только события, актор которых — пользователь (ответ агента никогда не является доказательством, поэтому испанский ответ модели не может подпитывать определение), и в структурированной форме — только ответы: склеенный chat.message, который api записывает сразу после, и чьи метки — текст агента, пропускается.

Помощник разметки не подставляет заранее догадку эвристики: её принятие нажатием ENTER привязало бы метку именно к тому, что метка призвана измерять. ENTER принимает язык, который владелец объявляет через --padrao.

Метки: код BCP-47 (pt-BR, es, fr…), und (решать нечего: "ok", только код, только лог) или mul (смешанный, без доминирующего языка). Оценка: pt/es/en должны получить свой язык; und, mul и любой другой язык должны получить indeterminado — единственный правильный ответ, который может дать им эвристика трёх языков. indeterminado на решаемом элементе — это упущенное покрытие (известное предпочтение сохраняется); неверный язык — это ошибка, а falso es считается отдельно, потому что это тот дефект, ради устранения которого существует эпик.

Как запускать​

# synthetic corpus (the default)
pnpm --filter @brabo/scripts idioma:medir

# the owner's real corpus, on the owner's machine
pnpm --filter @brabo/scripts idioma:extrair --container brabo-dev-postgres-1
pnpm --filter @brabo/scripts idioma:rotular --padrao pt-BR
pnpm --filter @brabo/scripts idioma:medir --real

--container запускает psql внутри этого контейнера с единственным SELECT только на чтение в session_events (--usuario/--banco по умолчанию brabo/brabo из compose); --database-url <url> использует вместо этого psql хоста. Повторный запуск извлечения сохраняет существующие метки и добавляет новые сообщения без метки.

Числа на синтетическом корпусе (2026-09-28)​

123 размеченных элемента, 7 последовательностей, параметры-кандидаты AT-080 (минимум 20 слов, порог 0,8, отрыв 0,3, 10 сообщений / 2 000 символов, гистерезис 2). Два списка маркеров: at080 — список из спецификации, дословно; ampliada — больший список, написанный этим инструментом до запуска на корпусе (частые служебные слова, отсутствующие в двух других языках). Корпус тот же, поэтому ampliada этими числами не подтверждается — сказать может реальный корпус.

По сообщениям, параметры-кандидаты:

меткаnat080 верноat080 не определеноampliada верноampliada не определено
pt5626 (46,4%)3030 (53,6%)26
es206 (30,0%)147 (35,0%)13
en146 (42,9%)86 (42,9%)8
und1515 (100%)015 (100%)0
mul55 (100%)05 (100%)0
другие языки1310090

Вердикты с неверным языком: ни одного на элементе pt/es/en ни в одном из списков. Все они на языках вне трёх — at080: французский → pt (ê в arrêté), галисийский → es (единственный falso es), нидерландский → en; ampliada: французский → es, немецкий → pt и → en, нидерландский → en.

Очистка удалила больше половины слов у нуля решаемых элементов.

Семь синтетических последовательностей заканчиваются там, где ожидается, с не более чем допустимым числом переключений, в обоих списках — включая португальский с длинной цитатой на испанском, целое сообщение на испанском посреди португальского и португальский, плотно насыщенный английским жаргоном.

CPU на этой машине: at080 11,7 мкс на сообщение и 49 мкс на оценку выборки из 10 сообщений; ampliada 16,8 мкс и 60 мкс. Зависит от машины; важен порядок величины.

Что говорят числа​

  • Со списком at080 порог и отрыв никогда не срабатывают. 66 из 67 элементов, которые хоть что-то набирают, набирают для одного языка (уверенность = 1,0), поэтому перебор порога от 0,5 до 1,0 ничего не меняет. Минимум слов — единственная действующая ручка: по сообщениям решаемые элементы приходят к своему языку в 42,2% случаев при 20 словах, 58,9% при 10 и 66,7% без минимума — при 20 он выбрасывает каждое короткое сообщение с опечатками и большую часть технических.
  • Снятие минимума позволяет решать смешанному тексту. Без минимума at080 превращает три из пяти смешанных сообщений в en или pt, а ampliada также решает два элемента und как en — код без ограждения (const total = itens.reduce(…)) и вывод pnpm (ELIFECYCLE Test failed. See above for more details.) проходят через очистку, которая знает только огороженный код и форматы логов, перечисленные в AT-080.
  • Отрыв 0,3 избыточен при пороге 0,8 в любом списке: при трёх языках победитель с ≥ 0,8 оказывается минимум на 0,6 выше второго.
  • Значимая единица — выборка, а не сообщение: в последовательностях окно из 10 сообщений приходит к языку с сообщениями, которые по отдельности были бы indeterminado. Единственное испанское сообщение в португальском окне делает выборку indeterminado, пока оно находится в окне (последовательность q03): ни переключения, но и ни подтверждения.
  • Ошибки — в открытом списке языков. При трёх языках пользователь, пишущий по-галисийски или по-французски, может услышать, что пишет по-испански или по-португальски. По правилу AT-168 определение лишь спрашивает, а пользователь подтверждает, поэтому цена — неверный вопрос, но это всё равно единственный режим отказа эвристики здесь.

Это синтетические числа: корпус написан той же рукой, что и эвристика. Пороги AT-163 должны были бы прийти из реального корпуса.

Предварительные пороги продукта​

AT-163 должна была выйти с каким-то числом до появления реального корпуса, поэтому api читает PARAMETROS_PROVISORIOS — предварительные, не откалиброванные и так и объявленные в RN-624:

параметрпредварительныйкандидат AT-080почему
список маркеровat080—ampliada выигрывает 4 пункта в pt и дважды ошибается на немецком; ни один из двух не подтверждён реальными данными
минимум слов (по выборке)1020покрытие решаемых элементов 42,2% → 58,9% при тех же трёх неверных вердиктах (французский, галисийский, нидерландский — языки вне трёх); ниже 10 начинает решать смешанный текст
порог / отрыв0,8 / 0,30,8 / 0,3на синтетическом корпусе никогда не срабатывают, поэтому выбрать лучшее число пока не из чего
выборка10 сообщений / 2 000 символовто же—
гистерезис22—

Гистерезис пересчитывается, но никогда не хранится: api читает последние 11 доказательств и проверяет, согласны ли две последние оценки выборки. CPU на этой машине, синтетический корпус: 11,6 мкс на очистку и классификацию сообщения, 110 мкс на определение (11 сообщений, две оценки). Чтение обслуживается частичным индексом в session_events: 0,056 мс против 13,5 мс последовательного сканирования, измерено на 200 000 событий.

Для перекалибровки: запустите реальный корпус (idioma:medir --real), а затем измените PARAMETROS_PROVISORIOS в api — это единственное место, откуда продукт их читает, и инструмент импортирует тот же файл (его отчёт продолжает перебирать кандидатов AT-080).

Протокол — из AT-082, с решениями AT-169: бюджет US$ 5, 5 прогонов на случай и ветвь, порог приёмки «в обработанной ветви ноль испанских ответов там, где ожидается pt-BR, и правильный язык ≥ 95% в C01–C04 и C06–C15», тестируемая модель deepseek/deepseek-v4.1-flash, модель сравнения anthropic/claude-haiku-4.5, потолок дополнительной стоимости 50 входных токенов на вызов. Код в d48f99f8e (dev от 2026-09-29).

Где запускается и что является (и что не является) продуктом​

Скрипт вне продукта, scripts/idioma/validar.ts (чистая часть — матрица, оценка, отчёт — в validacao.ts, протестированная validacao.spec.ts). Он вызывает OpenRouter напрямую и ничего не пишет в базу. То, что продукт отправляет модели, читается из файлов продукта при каждом запуске, никогда не копируется — чтение выбрасывает исключение, называя то, чего не нашло, если файл изменит форму:

  • ориентация (RN-622/623): @textos, общая фраза, оговорка об артефакте, @forma_curta и @ferramentas_de_artefato из apps/engine/lib/engine/harness/idioma_da_resposta.ex, отправляемая как ПОСЛЕДНЕЕ сообщение, role: "system", в каждом вызове обработанного хода (включая итерации инструментов) и никогда не сохраняемая в истории;
  • промпт резюме и его фраза о языке (RN-621) из apps/engine/lib/engine/harness/context_manager.ex;
  • персона Criativo (CRIATIVO_INSTRUCTIONS), два его инструмента с настоящими описаниями и результаты инструментов, которые возвращает продукт.

Заявленные отличия от продукта: system prompt — только персона (без слоёв ContextBuilder); сжатие принудительное перед пятым ходом C04 (с сохранением двух последних сообщений, keep_recent продукта), с тестируемой моделью в роли резюмирующей; возобновление (C15) — история, восстановленная как текст, в форме Engine.Agents.Reidratacao; не более 4 вызовов модели за ход (Criativo допускает 12); и max_tokens ограничен 4000 (продукт его не передаёт). Upstream-провайдер не был зафиксирован — записан по каждому вызову как смешивающий фактор. Baseline = код до RN-621/622: без ориентации и со старым промптом резюме (AT-169, ответ 4: проверка измеряет совокупность, отдельного baseline для старого резюме нет).

Разрешённый язык автора каждого сообщения — часть случая: pt-BR для Ana (C01–C05, C07–C11, C13–C15), en для Bob (C06 как определённый и подтверждённый en, C12 как явное предпочтение en при письме по-португальски); язык проекта pt-BR. C05 ожидает pt-BR: по ответу 4 AT-168 определение лишь спрашивает, поэтому разрешённый язык автора остаётся pt-BR до подтверждения.

Первый запуск нулевого шага с max_tokens 1000 был отброшен: рассуждения DeepSeek израсходовали весь потолок в 7 вызовах, и ответ вернулся пустым (два резюме превратились в запасной текст продукта). Его записи лежат отдельно в папке вывода; пустой ответ теперь считается falha, а не indeterminado.

Нулевой шаг: не воспроизведено​

Только baseline, deepseek/deepseek-v4.1-flash, C01–C04, 5 прогонов: 0 испанских ответов из 35 оценённых (35 pt, 0 неопределённых). Сообщение от 13.09 (испанский в ответ на португальский ввод) не воспроизведено, поэтому никакое улучшение именно этого дефекта нельзя приписать обработке. Запуск продолжился, потому что порог AT-169 абсолютен в обработанной ветви, а остальные случаи (C05, C06, C12, C16, C17) действительно показывают, что baseline отвечает на неверном языке.

Результаты по случаю × ветви × модели​

Первый судья — локальный классификатор (список ampliada, параметры-кандидаты) по прозе. "ok" = ожидаемый язык; "errado (es)" = другой язык (из них испанский); "ind." = не определено. n — число оценённых ответов (5 прогонов × оценённые ходы).

случайDeepSeek baselineDeepSeek обработкаHaiku baselineHaiku обработка
C01 (n=5)5 ok5 ok5 ok5 ok
C02 (n=5)5 ok5 ok5 ok5 ok
C03 (n=15)15 ok15 ok15 ok15 ok
C04 (n=10)10 ok10 ok10 ok10 ok
C05 (n=5, вне списка 95%)1 ok, 4 errados (4 es)5 ok0 ok, 5 errados (5 es)2 ok, 3 errados (3 es)
C06 (n=5)2 ok, 2 errados, 1 ind.3 ok, 2 ind.4 ok, 1 errado5 ok
C07–C10 (n=5 каждый)все okвсе okвсе okвсе ok
C11 (n=10)6 ok, 4 errados5 ok, 5 errados5 ok, 1 errado, 4 ind.6 ok, 4 errados
C12 (n=10)0 ok, 10 errados4 ok, 6 ind.0 ok, 10 errados0 ok, 10 errados
C13 (n=5)5 ok5 ok5 ok5 ok
C14, C15 (n=5 каждый)все okвсе okвсе okвсе ok
C16 (n=20, вне списка)10 ok, 10 errados19 ok, 1 ind.10 ok, 10 errados18 ok, 2 errados
C17 (n=20, вне списка)12 ok, 8 errados20 ok16 ok, 4 errados20 ok

C13 вызвал emit_artifact в 20 из 20 диалогов. Ни один вызов не упал.

Против порога​

модельиспанский там, где ожидается pt-BR (обработка)верно в C01–C04, C06–C15 (строго, классификатор)вердикт по классификатору
deepseek/deepseek-v4.1-flash082/95 (86,3%), 8 неопределённыхне проходит
anthropic/claude-haiku-4.53 (все в C05)81/95 (85,3%), 0 неопределённыхне проходит

13 отказов DeepSeek образуют две группы. Прочтение ниже — прочтение агента, сделанное ДО ревью; ревью (следующий раздел) его исправило — 8 неопределённых не все являются прозой на английском:

  • 8 неопределённых (C06 ×2, C12 ×6) выглядят как проза на английском — список ampliada плохо оценивает английский ("Great starting point — …");
  • 5 отказов C11 отвечают на просьбу о переводе переводом на английский внутри португальской рамки, что ориентация допускает («если только пользователь явно не попросит … в этом сообщении» покрывает перевод, но не рамку). «Перевод на en» в протоколе не говорит, какой из двух вариантов правильный.

Если ревью примет обе группы, DeepSeek доходит до 95/95 и проходит; если отвергнет C11 — 90/95 (94,7%) и не проходит на один ответ. Haiku не проходит в любом случае: при явном предпочтении en и сообщениях по-португальски (C12) он ответил по-португальски в 10 из 10 обработанных ответов и ответил на 3 из 5 испанских сообщений по-испански (C05). Гипотеза, не измеренная: для моделей Anthropic OpenRouter поднимает сообщение system из конца в верхний system prompt, где оно теряет новизну, которую имеет в конце списка. (Измерено позже и не подтверждено: см. диагноз ниже.)

Ответы для ревью (все неопределённые или расходящиеся, все переводы C11 и детерминированная выборка 1 из 10 согласующихся) — 146 из 560, перечислены в revisao.md в папке вывода.

Дополнительная стоимость ориентации​

Парные prompt_tokens первого вызова каждого диалога (вход идентичен в обеих ветвях, кроме ориентации), взятые из usage OpenRouter, никогда не оценённые:

модельориентацияпарΔ входных токенов
DeepSeekpt-BR (108 символов)7532 (каждая пара)
DeepSeeken + оговорка об артефакте (128 символов)1527
Haikupt-BR7530
Haikuen + оговорка об артефакте1527

Всё ниже потолка в 50 токенов. По входной цене каталога это около US$ 0,0000096 за вызов у DeepSeek и US$ 0,00003 у Haiku — верхняя граница, поскольку DeepSeek обслужил большую часть входа из кеша (кешированные токены возвращаются; стоимость вызова с кешем находится внутри usage.cost, не разделённая по сообщениям). Ориентация идёт в каждом вызове хода, поэтому ход с итерациями инструментов оплачивает её по разу на итерацию.

Расходы​

Стоимость проверки — сумма usage.cost каждого ответа, сохранённая по вызовам в chamadas.jsonl: US$ 2,3071 всего — US$ 2,2629 на учтённых запусках (DeepSeek 0,1526 baseline + 0,2133 обработка; Haiku 0,9526 + 0,9443; 865 вызовов), US$ 0,0281 на отброшенном нулевом шаге и US$ 0,0162 на smoke одного случая. Счётчик GET /api/v1/key вырос с 1,8301 до 4,1578 (Δ 2,3277) за тот же период, но ключ общий с другими платными lanes, работавшими одновременно, и счётчик запаздывает, поэтому эта дельта — контекст, а не расход этой проверки. Id генераций не записывались, поэтому стоимость каждого вызова не сверялась с GET /api/v1/generation. Собственный metering продукта (token_usage) не участвовал: скрипт идёт в обход api.

Как воспроизвести​

# the key file holds OPENROUTER_TEST_KEY=…; the key is never printed
pnpm --filter @brabo/scripts idioma:validar --arquivo-de-chave ~/.config/brabo/openrouter-test.env \
--modelos deepseek/deepseek-v4.1-flash --bracos baseline --casos C01,C02,C03,C04 # step zero
pnpm --filter @brabo/scripts idioma:validar --arquivo-de-chave ~/.config/brabo/openrouter-test.env \
--pular-existentes --teto-usd 2.5 # everything else
pnpm --filter @brabo/scripts idioma:validar --relatorio # report only

Вывод идёт в $XDG_CACHE_HOME/brabo/validacao-idioma/<date>/ (иначе ~/.cache/…), права 600, и скрипт отвергает папку внутри checkout. --teto-usd останавливает перед вызовом, который превысил бы его, по текущей сумме usage.cost.

Не измерено: сама экономия от кеша, полный system prompt продукта, реальное сжатие (запускаемое порогом), перезапуск настоящего engine, подсчёт токенизатора для оговорки об артефакте на португальском и любой фиксированный upstream.

146 ответов были проверены вручную; результат и единственное решение, которое по-прежнему за владельцем, — в следующем разделе.

Ревью человеком и диагноз Haiku (продолжение AT-167)​

Два офлайн- или дешёвых продолжения платной проверки, оба по одному и тому же запуску (respostas.jsonl, 560 оценённых ответов): ревью человеком 146 ответов, отмеченных классификатором (Часть A, без расходов), и проверка гипотезы «OpenRouter поднимает сообщение system из конца» для Haiku (Часть B, US$ 0,1485). Ни то, ни другое не меняет продукт.

Часть A — ревью 146​

Каждый из 146 ответов в revisao.md прочитан, один за другим, агентом, написавшим этот раздел (а не вторым человеком; файл меток сохранён для тех, кто захочет ему возразить). Для каждого: реальный язык текста, который читает пользователь, верен ли он для случая, и причина в одну строку. Файлы живут вне git, рядом с ответами — ~/.cache/brabo/validacao-idioma/2026-09-29/revisao-feita.md (146) и amostra-lida.md (выборка ниже). pnpm --filter @brabo/scripts idioma:revisar читает их и печатает все таблицы этого раздела; метки — данные, арифметика — код (scripts/idioma/revisao.ts, протестирован).

Меток четыре, потому что чтение обнаружило факт, которого классификатор не видит:

  • sim / nao: ответ на ожидаемом / не на ожидаемом языке;
  • formulario: проза на ожидаемом языке, но вопросы структурированной формы (ask_structured_questions, суть первого ответа) вышли по-португальски для автора, чей язык — en;
  • c11: перевод на английский внутри португальской рамки — решение владельца, см. ниже.
из 146верныеневерныепроза верна, форма на португальскомC11 (решение владельца)
все4281815
ветвь baseline (89)186416
обработанная ветвь (57)241779

14 ответов, которые классификатор назвал неопределёнными, после прочтения оказались: 3 ответа на английском (простые переводы C11 от Haiku), 2 неверных (DeepSeek C12, португальские и английские фразы вперемешку в прозе), 8 — проза на английском с формой на португальском, и 1 C11. Значит, прежняя фраза агента — «8 неопределённых выглядят как проза на английском» — была наполовину верна: проза была английской, а форма не была в 6 из 8.

Ошибка классификатора, измеренная. В 118 окончательных вердиктах (pt/es/en) среди 146 прочитанных ответов чтение противоречило 0. Классификатор ни разу не назвал верным неверный ответ и неверным — верный; его единственные сбои — 3 «неопределённых», оказавшихся простым английским. Для 414 ответов, которые не были отмечены (все согласны с ожиданием), была прочитана выборка: 60 отобранных через random.Random(20260929).sample(range(414), 60) по ответам в порядке respostas.jsonl, плюс 14 неотмеченных ответов C06/C12 с ожидаемым en, которые не попали в отбор (они решают порог, поэтому прочитаны целиком), — 74 прочитано, 0 расхождений (верхняя граница Уилсона при 95%: 4,9%). Вместе с 39 согласующимися ответами, которые уже отметил хеш 1 из 10 и которые тоже были прочитаны, случайная часть даёт 0 из 99 (верхняя граница 3,7%): из 414 неверными могли бы быть не более ~15, а выборка указывает на ноль. Две оплошности, замеченные в выборке, — вставки одного слова ("What falta for us…", "reminders automáticos"), не меняющие языка ответа.

Чего классификатор не может увидеть — это форму: clf=en на ответе на английском не говорил, что его вопросы были на португальском (он отмечал такие ответы как неопределённые, только когда смесь была сильной). Поэтому выборка прочитала ответы C06/C12 целиком, и поэтому существуют два прочтения ниже.

Порог, пересчитанный по проверенным меткам​

Обработанная ветвь, C01–C04 и C06–C15 (n = 95), испанский считается по реальному языку каждого обработанного ответа там, где ожидается pt-BR (включая C05). Интервал Уилсона при 95%. Два независимых прочтения: C11 (за владельцем, открыто) и форма (проза верна, форма на португальском — агент разрешил её как факт о том, что читает пользователь, и показывает другую сторону, чтобы ей можно было возразить).

модельC11 засчитываетсяформа засчитываетсяпопаданийдоля95% интервалиспанский (ожидается pt)вердикт
DeepSeekдада93/9597,9%92,6–99,4%0проходит
DeepSeekданет87/9591,6%84,3–95,7%0не проходит
DeepSeekнетда88/9592,6%85,6–96,4%0не проходит
DeepSeekнетнет82/9586,3%78,0–91,8%0не проходит
Haikuда(любое)85/9589,5%81,7–94,2%3 (C05)не проходит
Haikuнет(любое)81/9585,3%76,8–91,0%3 (C05)не проходит

Порог прохождения — 91 попадание из 95 (94,7% не проходит). Читая таблицу:

  • Haiku не проходит ни при каком прочтении: 3 испанских ответа в C05 (одного условия нуля испанского уже достаточно) и, при принятии всего, 85/95.
  • DeepSeek проходит ровно в одной ячейке: C11 принят и проза на английском с формой на португальском принята. Отвергните любое из двух — и он не проходит: на 4 ответа, если отвергнута форма (87 из нужных 91), на 3, если C11 (88). Отвергните оба — и получится число классификатора, 82.
  • Поэтому решение по C11 важно только при принятой форме. Если форма засчитана как сбой, DeepSeek не проходит при любом C11, и находка, на которую нужно реагировать, — не C11, а форма: в обработанной ветви 8 из 15 ответов C06+C12 у DeepSeek несут португальский текст внутри английского ответа (6 только в форме, 2 вперемешку), а 7 — чистый английский, включая формы. Модель способна написать форму по-английски — делает это примерно в половине случаев. Откуда это взялось (AT-282, RN-667): само описание инструмента говорило модели, что label — это «текст вопроса, em pt-BR», — вторая языковая инструкция, фиксированная, вне фасада, конкурирующая с ориентацией в конце в каждом вызове. Теперь оно гласит «no idioma da sua resposta» и не называет язык; сама ориентация не изменилась. Измерение «после» не запускалось (не было ключа OpenRouter на машине, где делалось изменение) и ожидает владельца — C06 и C12, обработанная ветвь, 5 прогонов, с потолком бюджета из RN.

Что такое C11 в данных. 5 обработанных отказов DeepSeek: у 3 перевод на английский процитирован в португальской рамке ("Registrei a regra… A tradução que você pediu: > …"), а у 2 нет перевода в записанном тексте — запуск хранит только последнее сообщение хода, а эти ходы сначала вызвали emit_artifact, так что перевод был в предыдущем сообщении того же хода (10 вызовов на 5 диалогов, по два на диалог), то есть перевод, предположительно, был в предыдущем сообщении того же хода и не сохранился. Оба считаются только рамкой, то есть попаданиями в «C11 засчитывается». 4 обработанных отказа Haiku — перевод на английский с примечанием на португальском или португальскими альтернативами вокруг.

TODO(humano): Засчитывается ли в C11 как верный перевод на английский внутри португальской рамки? Это решает ось C11 таблицы выше: принят → DeepSeek 93/95 и проходит (если проза на английском с формой на португальском тоже принята; 87/95, если нет); отвергнут → DeepSeek 88/95 (или 82/95) и не проходит. Haiku не проходит в любом случае. Рекомендация, одной фразой: принять — исключение ориентации покрывает только запрошенный перевод, а рамка остаётся на языке автора, поэтому отказ наказал бы именно то поведение, ради которого ориентация существует. (Второй вопрос, форма на португальском, относится к рамкам продукта и сформулирован как находка выше; владельцу он не передавался, потому что агент разрешил его как факт — то, что читает пользователь.)

Часть B — дело в позиции ориентации? (AT-279)​

Гипотеза: для моделей Anthropic OpenRouter поднимает сообщение system из конца в верхний system prompt, теряя его новизну. Проверка: C12 (предпочтение автора en, сообщения на pt-BR), anthropic/claude-haiku-4.5, первый ход, 5 прогонов на позицию, текст ориентации прочитан из продукта (он был Respond in English (en), unless the user explicitly asks for another language in this message. Write project artifacts in pt-BR.), меняется только позиция:

  1. ultima-system — как сегодня (idioma_da_resposta.ex:231, role: "system" в конце);
  2. system-fundido — дописана к первому system (персона + ориентация);
  3. sufixo-user — суффикс последнего сообщения user, без добавленного system;
  4. ultima-system-e-sufixo-user — system в конце плюс та же фраза как суффикс.
позицияпопаданий классификаторапрочтение
1 ultima-system (как сегодня)0/55 на португальском, включая формы
2 system-fundido0/55 на португальском
3 sufixo-user0/55 на португальском
4 ultima-system-e-sufixo-user0/55 на португальском
контроль: 1, без оговорки об артефакте0/55 на португальском
контроль: 3, без оговорки об артефакте0/55 на португальском

Четыре позиции прочитаны целиком, два контроля — частично, и все 30 ответов также проверены на служебные слова английского (ни одного ни в одном): все 30 — на португальском, проза и форма. Дефект позиции 1 воспроизведён (0/5, как 0/10 платного запуска), и ни одна позиция его не исправляет — даже та, где ориентация находится внутри самого последнего сообщения пользователя и system в конце нет. Гипотеза не подтверждается. Подсчёты токенов промпта показывают, что текст дошёл до модели в каждой позиции (2 426, 2 427, 2 427 и 2 454 входных токена: +1 для двух простых размещений и +28 для двойного), так что сообщение не отбрасывается. Контроль исключает и вторую подозрительную фразу: оговорка об артефакте ("Write project artifacts in pt-BR.", RN-623) тоже не причина (0/10 без неё).

Чего это не исключает и что не измерялось: второй ход (запускался только первый, из-за бюджета — в платном запуске второй ход тоже был неверен 5/5); более сильная формулировка (фраза гласит "unless the user explicitly asks", и португальский ввод можно так прочитать); язык самой персоны (CRIATIVO_INSTRUCTIONS — на португальском); и upstream (все 30 вызовов обслужил Amazon Bedrock, поэтому эффект upstream отделить нельзя). DeepSeek следует той же ориентации в том же случае — его обработанные ответы C12 были прозой на английском в 8 из 10, — что указывает на модель, а не на размещение.

Предлагаемое изменение: никакого по позиции. Единственное место, где позиция была бы изменена, если бы это помогло, — Engine.Harness.IdiomaDaResposta.anexar/4 (apps/engine/lib/engine/harness/idioma_da_resposta.ex:231, messages ++ [%{"role" => "system", …}]), вызываемое обоими путями llm_turn в apps/engine/lib/engine/sessions/engine_api_client.ex:609 и :661. Оно остаётся как есть. Следующая работа, если владелец хочет, чтобы Haiku подчинялся en при португальском вводе, — проверить формулировку и язык персоны (список выше) этим же скриптом.

Расходы (сумма usage.cost каждого ответа, никогда не счётчик /key): US$ 0,0980 на четыре позиции (20 вызовов) и US$ 0,0505 на два контроля (10 вызовов) — US$ 0,1485 из потолка US$ 0,20, 30 вызовов, ни один не упал.

Чтобы воспроизвести Часть B (файл ключа хранит OPENROUTER_TEST_KEY=…; он никогда не печатается; вывод идёт за пределы checkout):

pnpm --filter @brabo/scripts idioma:diagnosticar \
--arquivo-de-chave ~/.config/brabo/openrouter-test.env \
--turnos 1 --rodadas 5 --teto-usd 0.13 --concorrencia 4 # the four positions
pnpm --filter @brabo/scripts idioma:diagnosticar \
--arquivo-de-chave ~/.config/brabo/openrouter-test.env \
--turnos 1 --rodadas 5 --posicoes ultima-system,sufixo-user --sem-clausula \
--saida ~/.cache/brabo/validacao-idioma/<date>-diagnostico-haiku-sem-clausula \
--teto-usd 0.06 --concorrencia 4 # the controls
pnpm --filter @brabo/scripts idioma:revisar # Part A tables

--turnos N и --sem-clausula существуют только для этой диагностики; продукт никогда не помещает ориентацию никуда, кроме конца, и posicionar в validacao.ts им не используется.