Советчик
Работающий агент видит свой тур изнутри — и оттуда не видно, что он час чинит не тот файл. Советчик — вторая, дешёвая модель со своей историей: раз в несколько итераций ей показывают дельту транскрипта (что Ева сказала, что позвала, что вернулось), она при желании читает своими инструментами и, если увидела промах, кладёт в тур одну заметку:
<advisory severity="concern" guidance="weigh, don't blindly obey">…</advisory>
Блок клеится спереди к результату инструмента только в промпт-копии — тем же каналом, что и правила. В базу и в стрим клиента уходит чистый выхлоп: это служебная вставка, а не речь.
Ева про советчика в промпте не знает вовсе. Тег со своим guidance — её
единственная подсказка, как к этому относиться: взвесь, не подчиняйся
слепо. Советчик не начальник, и заметка, в которой он неправ, обязана
проиграть тому, что Ева видит своими глазами.
Где он включается
| условие | зачем так |
|---|---|
задана llm.model.advisor | без роли советчика нет вовсе: он удваивает разговор с моделью, и это решение Господина, а не дефолт |
advisor.enabled | заткнуть, не убирая роль |
| рабочий тур | клиентские операции, рабочий режим набора (он же кодовый) или подагент — там легче всего увязнуть, и там ошибка дороже |
| тур Господина | в чужом туре он комментировал бы чужую переписку |
В болтовне советчика нет ни при каких настройках: вторая модель, читающая бытовые реплики, — это комментатор за живые деньги.
Цена
Цикл идёт каждой every_iterations-й итерации рабочего тура и стоит от одного
до max_steps вызовов дешёвой модели плюс его чтения. Платим ещё и
латентностью: цикл идёт внутри тура, между итерациями, и не уложившийся в
timeout просто выбрасывается — тур продолжается без него. Кулдаун после
заметки цикл не запускает вовсе, так что говорящий советчик обходится дешевле
молчаливого.
Инструменты
Только читающие: read_file, find_pattern, history_search. Мутирующих у
советчика не будет никогда, даже под флагом: за файловыми инструментами стоит
настоящая машина Господина. Список — детерминированный белый, а не просьба в
промпте: любое другое имя отбивается кодом.
Голос у советчика один — инструмент advisory_note (severity, text). В
реестре ядра его нет: Еве он не виден и не доступен.
Щит вокруг заметки
Без щита вторая модель превращает тур в поток «продолжай, ты молодец». Щит
живёт в коде (src/advisor.rs), а не в промпте советчика:
| правило | что делает |
|---|---|
| нормализация | NFKC, нижний регистр, всё неалфавитное — в пробел: перефразировка пунктуацией не становится новой мыслью |
| чёрный список | «lgtm», «продолжай», «замечаний нет», «готово», «стоп» и родня. Молчание — правильная форма «всё хорошо» |
| дедуп кольцом | та же заметка второй раз не проходит (32 последних на чат) |
| одна за цикл | вторая — подавляется; подавленные потолок max_notes не расходуют |
| кулдаун | после вклеенной заметки советчик молчит cooldown_cycles циклов: иначе он комментирует собственное вмешательство, не дав ему сработать |
| сброс | перезапись истории (компакция, пересборка резюме, chat_clear) стирает всю его память о чате — иначе он рецензирует то, чего уже нет |
Подавление невидимо советчику: на любую заметку инструмент отвечает «записано». Скажешь ему «подавлено» — он перефразирует ту же бесполезную мысль в обход дедупа, и заплатит за это Господин.
Ступени
severity — nit | concern | blocker. Ступень доезжает до Евы атрибутом
тега, но обрабатываются все три одинаково мягко: идущий тур советчик не
прерывает. Жёсткая половина (обрыв и ретрай) сядет вместе с жёсткой половиной
правил — до тех пор blocker отличается от nit только тем, как
Ева его прочтёт.
Телеметрия
Каждая заметка — вклеенная и подавленная — пишется в advisor_notes, а
вклеенная ещё и в лог. Свод — в GET /v1/stats, поле advisor:
"advisor": [{ "verdict": "spliced", "notes": 2 }, { "verdict": "hollow", "notes": 7 }]
Подавленные тут важнее вклеенных: по ним видно, во что вторая модель превращается без щита. Пустой список — советчик либо выключен, либо ни разу не попал в рабочий тур.
Настройка
llm:
model:
advisor: "qwen/qwen3.7-flash" # без этого советчика нет
advisor: # вся секция опциональна
enabled: true
every_iterations: 3 # цикл каждой третьей итерации рабочего тура
max_steps: 3 # его собственных шагов за цикл
max_notes: 2 # вклеенных заметок за тур
cooldown_cycles: 3 # молчания после заметки
delta_max_chars: 6000 # потолок дельты транскрипта
timeout: 60s # не уложился — тур идёт дальше без него