🤖 AI-агенты для бизнеса

6. Справочник хуков

🔵 Хуки перехватывают жизненный цикл агента. Это YAML-файлы в config/agents/<agent>/hooks/, подключаются через hook_files: (или по имени в hooks:). Поле kind: выбирает хук.


6.1 Точки жизненного цикла

Точка Сигнатура Что делает
before_user_message (ctx, msg) впрыснуть, переписать, отбросить сообщение или ответить самому
after_turn (ctx, question, answer) после финального ответа; может выдать пост-ходовое уведомление
after_loop (ctx, reason) при остановке цикла работы (job_done, terminated, blocked, budget_exhausted, max_turns, …)

Исход before_user_message

Хук может вернуть карту с любым из полей (все опциональны):

Поле Эффект
notice текст, стримящийся в чат-UI; не попадает в контекст LLM
reply полный ответ — ход LLM пропускается
suppress полностью отбросить сообщение пользователя
inject дополнительные сообщения перед сообщением пользователя
replace_message заменить сообщение пользователя

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


6.2 Виды хуков

kind Назначение
faq_cache кэш (вопрос → ответ) в DuckDB; отдавать повторы, пропуская LLM
inject декларативно впрыскивать системные/пользовательские сообщения
escalate эскалация на человека, когда агент не может помочь
memory долговременная память на пользователя (факты + сводка)
script_hook скриптовый хук на Rhai/JavaScript (см. 07-scripts)

6.3 faq_cache

Запоминает пары (вопрос, ответ) и отдаёт кэшированный ответ на повторные или похожие вопросы:

  1. На сообщении пользователя ищет в хранилище уже отвеченные вопросы (префильтр по пересечению токенов), затем спрашивает судью (дочерний агент-процесс — свой PID, провайдер ядра, бюджет токенов), похож ли кандидат.
  2. При уверенном совпадении кэшированный ответ впрыскивается как подсказка (short_circuit: false) или возвращается напрямую, пропуская LLM (short_circuit: true).
  3. После каждого хода новая пара записывается.
kind: faq_cache
name: faq_cache
db_path: data/faq_cache_ngu.db    # запасной путь (в проде — messages.db)
max_candidates: 5
min_confidence: 0.7
short_circuit: true
hit_notice: "⚡ Отвечено из кэша FAQ"
hint_template: ""                 # подсказка при short_circuit: false
fallback_score: 0.6               # пересечение токенов, если судья недоступен
judge_instructions: |             # опциональный промпт судьи (JSON-ответ)
  You are a question similarity judge...
store:
  enabled: true
  min_question_chars: 8
  min_answer_chars: 8
Поле Описание
db_path файл DuckDB (для standalone/тестов); в проде — общий message store в скоупе faq:<template>
max_candidates сколько кандидатов отдаётся судье
min_confidence мин. уверенность судьи (0..1) для принятия совпадения
short_circuit true = отвечать напрямую (пропустить LLM)
hit_notice уведомление при попадании в кэш
judge_instructions кастомный промпт судьи
fallback_score порог пересечения токенов без судьи
store.* что записывать после хода

6.4 inject

Чистое YAML-впрыскивание контекста на сообщениях пользователя:

kind: inject
name: city_hint
first_n: 1                  # сработать только на первые N сообщений (на процесс)
notice: "Reminder: city context active"
messages:
  - role: system
    content: "Important: the user's city is Moscow."
  - role: user
    content: "Context reminder: {{question}}"
Поле Описание
first_n сработать только на первые N сообщений (опущено = на каждое)
notice опциональное уведомление в чат-UI
messages список { role, content } (роль по умолчанию system)

{{question}} заменяется входящим сообщением. Роли: system | user | assistant.


6.5 escalate

Детерминированная эскалация на контакт человека, когда агент не может помочь:

kind: escalate
name: escalate
message: |
  Если это не то, что вы искали — напишите: 📞 +7 ... ✉️ a@b.c
off_topic_keywords:
  - iphone
  - доставка
suppress_on_off_topic: true
empty_answer_patterns:
  - "не нашёл"
  - "нет информации"
min_answer_chars: 30
remind_next_turn: true
Поле Описание
message контакт, показываемый пользователю (поддерживается {{question}})
off_topic_keywords вопросы с любым из слов считаются вне домена
suppress_on_off_topic true = отказаться сразу (пропустить LLM); false = впрыснуть контакт подсказкой
empty_answer_patterns финальные ответы с этими подстроками — «бесполезные»
min_answer_chars ответы короче — «бесполезные»
remind_next_turn впрыснуть контакт в контекст перед следующим сообщением

6.6 memory

Долговременная память на пользователя, переживающая сессии:

  1. Перед сообщением вспоминает релевантные факты (префильтр по стеммингованному пересечению токенов — RU/EN словоформы совпадают) и просит судью отобрать полезные. Кандидаты несут importance и давность, предранжируются по релевантности → важности → давности. Отобранные факты + скользящая сводка впрыскиваются системным сообщением.
  2. После хода запоминатель извлекает устойчивые факты и обновляет скользящую сводку на пользователя (не на сессию). Факты дедуплицируются, ограничиваются, могут помечаться stale. Новый факт, противоречащий старому, перечисляет старый в supersedes — тот автоматически выводится.
  3. Запоминатель видит существующие факты каждый ход и может их исправлять; обратная связь пользователя (like/dislike) поднимает/опускает importance вспомненных фактов.
kind: memory
name: user_memory
db_path: data/memory_ngu.db
max_candidates: 8
min_confidence: 0.6
notice: ""                    # опциональное уведомление о воспоминании ({{count}})
inject_template: ""           # шаблон: {{facts}}, {{summaries}}, {{memories}}
judge_instructions: ""        # промпт судьи релевантности
judge_model: ""               # более дешёвая модель для судьи
backfill: true                # false → пропускать кандидатов при слабом пересечении
max_memories_per_scope: 200
dedup_threshold: 0.8
store:
  enabled: true
  min_question_chars: 4
  min_answer_chars: 20
  extractor_instructions: ""  # промпт запоминателя
  memorizer_model: ""         # более дешёвая модель для запоминателя
  memorize_every: 1           # запускать запоминатель каждые N ходов
  max_episodes_per_scope: 50
  summary_max_chars: 2000

Память скоупится на пользователя (user_id с фронтенда), с откатом к сессии → шаблону → имени процесса, если user id нет.

Поле Описание
judge_model / store.memorizer_model запускать вспомогательный процесс на другой (дешевле) модели
backfill true (по умолч.) добирает недавние несовпавшие факты, чтобы судья видел факты при языковых расхождениях
store.memorize_every запускать запоминатель раз в N подходящих ходов на пользователя
dedup_threshold порог стеммингованного пересечения, при котором новый факт сливается с существующим

inject_template поддерживает три плейсхолдера: {{facts}} (факты списком), {{summaries}} (сводки списком), {{memories}} (всё вместе, для обратной совместимости).

Посмотреть, что агент помнит — админ-эндпоинт GET /v1/memory/:scope (факты с флагами importance/stale, скользящая сводка, эпизоды).


6.7 script_hook

См. 07-scripts (Rhai или JavaScript).


6.8 Подключение хуков к агенту

# config/agents/<agent>/agent.yaml
hook_files:
  - hooks/faq_cache.yaml
  - hooks/memory.yaml

6.9 Тестирование хуков локально

hook_tool <hook.yaml> info                    # показать распарсенный конфиг
hook_tool <hook.yaml> user "Привет, мир"      # запустить before_user_message
hook_tool <hook.yaml> turn "question" "answer" # запустить after_turn
hook_tool <hook.yaml> loop "job_done"          # запустить after_loop

# флаги
--db <path>       # перекрыть db_path (для тестов — временный файл)
--live            # судья/запоминатель — реальный дочерний процесс (нужен ключ API)
--agent <name>    # имя процесса в контексте хука
--template <name> # имя шаблона
--user <id>       # user_id
--session <id>    # session_id