Правила поверх генерации
Правило смотрит на то, что Ева сделала, и вмешивается на месте. Строка в системном промпте платится каждым запросом и всё равно теряется к пятидесятому ходу; правило молчит, пока промаха нет, и говорит ровно там, где промах случился.
Правила — данные, а не код: таблица stream_rules, мастерский CRUD
(stream_rule_list / _create / _edit / _delete) и первичный посев один
раз за жизнь базы, как у наборов.
Что это НЕ заменяет
Правило — для того, что модель знает, но забывает. Если промах системный (модель не знает, что так можно), это строка в правилах промпта, а не правило: напоминание после факта не научит тому, чего в промпте не было.
Две половины
| половина | scope | mode | что делает |
|---|---|---|---|
| мягкая | tool:<имя> | remind | напоминание в результат вызова, тур идёт дальше |
| жёсткая | text | interrupt | готовый ответ выбрасывается, ход переписывается |
Половина у каждого scope своя, и пара проверяется на входе: исполненный
вызов уже не выбросить, а сказанному ответу некуда дописать напоминание.
mode поэтому обычно не называют — он следует из scope.
Мягкая
Смотрит на имя и аргументы вызова и, если совпало, клеит спереди к результату этого вызова блок:
<system-reminder rule="local_shell_for_his_machine">…текст правила…</system-reminder>
Вклейка идёт только в промпт-копию результата. В базу и в стрим клиента уходит чистый выхлоп инструмента: напоминание — служебная вставка, а не речь и не история. Стрим при этом не рвётся, префикс кэша промпта не страдает, лишнего похода к провайдеру нет.
Порядок в точке вклейки важен: сначала снимается служебная метка «пусто»,
потом выхлоп режется капом context.tool_result_max_chars, и только потом
сверху ложится напоминание — кап считается по выхлопу инструмента, а не по
правилу.
Жёсткая
Смотрит на готовый ответ перед его фиксацией — не по дельте: тур уже оплачен, зато ретрай даёт исправленный ответ вместо неверного. Промах — ответ выбрасывается целиком (в базу не попадает, клиенту не уходит, объявленные в нём вызовы не исполняются), а в промпт-копию тура ложится реплика:
<system-interrupt reason="rule_violation" rule="telegram_reply_tail">…текст правила…
That reply was discarded and NOBODY saw it. …</system-interrupt>
Дальше ход играется заново с той же историей. В базу и клиенту уезжает
только исправленный ответ; сам <system-interrupt> живёт лишь в промпте
этого тура и никуда не сохраняется.
Пока текстовое правило заряжено, речь придерживается. Отданную клиенту реплику назад не забрать, поэтому текст и объявленные вызовы копятся и уходят одним куском ровно тогда, когда ответ дописан. Ожидания это не добавляет — теряется только эффект печатной машинки, и только в туре, где такому правилу есть чем сработать: без заряженного правила стрим идёт как раньше, буква за буквой.
Потолок ретраев — один на ход. Второй промах в том же ходу ответ уже не выбрасывает: он остаётся как есть, а правило говорит вдогонку мягко — напоминание едет на ближайшем результате инструмента. Ход, который кончился этим ответом, такого напоминания не увидит: след останется в телеметрии и в логе.
Зациклить тур правилу нечем и без потолка: политика повтора считается по ходу чата, а он внутри тура не растёт, — сработав, правило до конца хода молчит. Потолок закрывает другой случай: несколько разных правил, каждое из которых требует своего ретрая.
Форма правила
| поле | что значит |
|---|---|
name | ключ дедупа и повтора; он же едет модели в атрибуте тега |
scope | tool:<имя> — вызов инструмента; text — готовый ответ. Мыслей у нас в истории нет, thinking не поддерживается |
condition | regex по цели: аргументы вызова компактным JSON либо текст ответа. Пусто — совпадает любая цель этого scope |
negate | сработать, когда regex не нашёлся: «не хватает хвоста» иначе не выразить — в rust-regex нет lookaround |
when | условия окружения: telegram, client_ops, master, trusted, picture. Все обязаны выполниться; пусто — любой тур |
text | что подставится модели (до 400 символов) |
mode | remind | interrupt; следует из scope, называть не обязательно |
repeat | once — раз на чат навсегда; after:<N> — не чаще раза в N ходов чата |
enabled | выключенное правило не поднимается, но не теряется |
Без when правило про телеграм палило бы в тихом чате: окружение берётся у
самого тура, а не угадывается по тексту. picture — картинка в контексте
тура, которую модель действительно видит: слепой она приезжает текстовой
пометкой, и «не вижу» из её уст правда, а не промах.
Без repeat правило говорит одно и то же каждым вызовом, и напоминание
перестают замечать. Ход считается ответом модели; правило, сработавшее в
туре, в этом же туре не повторится.
Невалидная regex в базе — правило не регистрируется, warning в лог, тур идёт дальше: ядро не падает из-за кривых данных. На входе (создание, правка) такая regex просто не принимается.
Что посеяно
Шесть хронических промахов: два мягких, четыре жёстких.
| правило | половина | ловит |
|---|---|---|
write_diff_over_write_file | мягкая | write_file — напомнить, что правка существующего файла идёт через write_diff |
local_shell_for_his_machine | мягкая | shell в туре с клиентом — это машина ядра, а не машина Господина |
telegram_markdown_image | жёсткая |  в телеграмном ответе — там markdown не рендерится |
telegram_reply_tail | жёсткая | ответ в телеграм без хвоста ↩ #id (правило с negate) |
look_before_refusing | жёсткая | «не могу посмотреть картинку» там, где картинка в контексте и видна |
telegram_react_not_emoji | жёсткая | голый эмодзи вместо вызова telegram_react |
Посев — один раз за жизнь базы. Правку правила Господином ядро отличает по
updated_at: волна калибровки (когда меняются сами тексты посева)
переписывает только то, к чему он не прикасался, и переписанное им не
трогает.
Телеметрия
Сработавшее правило никогда не молчит: строка stream rule fired (у
текстового — stream rule fired on the answer, с полем retry: переписан ли
ход) в лог и запись в stream_rule_stat. Иначе через месяц никто не поймёт,
откуда в контексте посторонний текст и куда делся ответ. Свод — в
GET /v1/stats, поле rules:
"rules": [{ "rule": "local_shell_for_his_machine", "fired": 3 }]
Ноль срабатываний у правила — тоже ответ: либо промах вылечен, либо условие мимо.
Записи переживают своё правило: удалить правило — не то же самое, что переписать историю.