钩子 — 路由与可靠性

模型选择、分诊、容错与成本。所有钩子的列表见 06-hooks-reference。


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 在下一条消息前将联系方式注入上下文

人工坐席(operator:)

如果设置了 operator 段,钩子会通过内核(EscalationService)开启真实升级, 而不是使用静态联系方式。当坐席进行对话时,智能体暂停:用户消息转发给坐席, 并回复 hold_message。如果无法开启升级(没有渠道/会话)—— 则回退到普通的 message。

operator:
  enabled: true
  on_off_topic: true
  on_empty_answer: true
  connecting_message: "Соединяю вас с оператором…"
  unavailable_message: "Оператор недоступен.\n{contact}"   # {contact} = message
  hold_message: "Передал ваше сообщение оператору."
  timeout_secs: 600
  timeout_message: "Оператор не отвечает, продолжаю помогать я."
  release_commands: ["/release", "/agent"]                 # команды возврата агенту
字段 描述
enabled 启用 handoff(默认 false)
on_off_topic 升级 off-topic 问题(默认 true)
on_empty_answer 升级「无用」回答(默认 true)
connecting_message 开启升级时向用户显示的内容
unavailable_message 如果 handoff 未开启时的回退联系方式({contact})
hold_message 坐席说话期间给用户的回复
timeout_secs 坐席空闲多久后将控制权交还智能体
timeout_message 因超时交还时给用户的消息
release_commands 坐席交还控制权给智能体的命令
context_messages 附加到坐席卡片的最近对话消息数量(0 = 不带历史,默认 12)

坐席渠道在 config/server.yaml → approval.operator_notify 中设置 (Yandex Messenger / Telegram / webhook;与审批共用列表,密钥 通过 token_env)。渠道配置和坐席命令(/accept、 /release)见 工具参考 和 审批。


6.15 loop_guard

在 before_tool_call 上打破工具调用循环:维护进程最近调用的 滑动窗口(工具名 + 规范化参数),如果同一调用在窗口内重复超过 max_identical 次,则拒绝该调用(deny)。思路与 tool_cache 相近, 但相反:缓存返回重复调用的结果,而此钩子停止循环 A,B,A,B,…, 其中参数形式上在变化,但智能体没有推进。确定性,无需 LLM;状态为进程内内存。

kind: loop_guard
name: loop_guard
max_identical: 3                 # сколько одинаковых вызовов допустимо (0 → 1)
window: 8                        # окно последних вызовов на процесс
tools: []                        # allowlist; пусто = все инструменты
exempt_tools: [read_overflow]    # не считать (напр. постраничное чтение)
deny_reason: "Повторяющийся вызов {{tool}} остановлен ({{count}}/{{max}})."
字段 描述
max_identical 重复阈值;超过时——deny 并附原因
window 滑动窗口大小(低于 max_identical 的值会提升到该值)
tools 工具 allowlist;空 = 全部
exempt_tools 完全不参与计数的工具
deny_reason 原因模板:{{tool}}、{{count}}、{{max}}

参数按规范化方式比较(对象键会排序),因此 {"a":1,"b":2} 和 {"b":2,"a":1} 是同一次调用。被拒绝的调用不会 进入窗口,因此只要智能体不改变方法,保护就会生效; 进程状态在 on_terminate 时删除。拒绝会作为 failed 工具结果返回给模型——模型可以重新表述调用。


6.17 circuit_breaker

钩子所绑定智能体的原生提供商 circuit breaker (agent_os_core::circuit_breaker)声明式配置。 复杂部分——统计提供商错误并在 failover 链中绕过故障提供商——位于 内核中;钩子只携带策略并将其应用于自己的 进程。补充(并覆盖)来自 server.yaml provider.circuit_breaker 的全局默认值。

kind: circuit_breaker
name: circuit_breaker
failure_threshold: 5      # подряд ошибок до размыкания (0 = выключить)
cooldown_secs: 30         # пауза до пробного запроса
half_open_max: 1          # успешных проб для замыкания
字段 描述
failure_threshold 连续错误达到 open;0 —— 为该智能体关闭 breaker
cooldown_secs 在 half-open 探测前保持 open 多久
half_open_max 连续成功探测多少次后回到 closed

在 before_inference 上幂等应用(因此较晚的 grant_hook 也有效);在 on_terminate 时重置。breaker 的键是 提供商 model-config 名称(default、strong、……)。状态可见于 GET /v1/circuit-breakers(+ 重置 POST /v1/circuit-breakers/:name/reset)和 /metrics(agentos_circuit_breaker_open、agentos_circuit_breaker_state)。


6.19 model_router

在 before_user_message 上的分类器模型路由:对消息进行分类,并在该轮 固定到 model_config(提供商/端点/定价)以及可选的 model/temperature/max_tokens。后端来自配置:onnx | llm | hybrid (ONNX 分类器 → 在低分/不可用时使用带严格 schema {"label": ...} 的 LLM 分类器)。 决策按消息哈希缓存,并且每轮只应用一次(在轮内是确定性的)。

kind: model_router
name: model_router
backend: hybrid              # onnx | llm | hybrid
onnx_model: topic-clf        # ONNX classifier id (onnx/hybrid)
min_confidence: 0.5          # ниже → LLM fallback (hybrid)
llm_provider: cheap          # провайдер для LLM-классификатора (опц.)
max_chars: 2000
cache_ttl_secs: 3600
labels:
  code: { model_config: strong }
  chat: { model_config: cheap, temperature: 0.7 }
字段 描述
backend onnx / llm / hybrid
onnx_model 本地分类器 id(用于 onnx/hybrid)
min_confidence ONNX 分数阈值;低于则用 LLM(在 hybrid 中)
llm_provider 用于 LLM 分类器的命名提供商
max_chars / cache_ttl_secs 消息截断 / 决策缓存 TTL
labels label → { model_config, model, temperature, max_tokens }

对于 config/server.yaml → security.onnx.models 中的 backend: onnx/hybrid, 已注册 langid(语言/脚本)和 topic-clf(多语言主题, 19 个类别);id 在 onnx_model 中指定。ONNX 分支的实时检查: AGENT_OS_ONNX_LIVE=1 cargo test -p agent_web_bot --features onnx --test model_router_onnx_live。 事件 model_routed(label、source、model_config、score)写入审计(events.db)。规则列表、手动测试器和统计信息位于管理页面 /model-router(以及 JSON GET /v1/model-router、POST /v1/model-router/test);统计信息从 events.db 读取(可跨重启保留),在没有存储时则从内核内存读取。


6.20 intent_router

在 before_user_message 上对传入消息进行分类器分流:对意图/主题进行分类(“需要人工”),并应用配置中的动作——代替常规回合或与之一起执行。后端与 model_router 相同:onnx | llm | hybrid,决策按消息哈希缓存。

kind: intent_router
name: intent_router
backend: hybrid              # onnx | llm | hybrid
onnx_model: topic-clf
min_confidence: 0.5
max_chars: 2000
cache_ttl_secs: 3600
labels:
  greeting: { reply: "Здравствуйте! Чем помочь?" }
  support:  { inject_system: "Это обращение в поддержку — уточни детали." }
  human:    { escalate: { reason: user_requested, message: "Соединяю с оператором…" } }
动作 效果
reply 短路:用现成回复代替 LLM
inject_system / inject_user 在用户消息之前注入消息
notice 向用户显示状态文本(LLM 看不到它)
escalate 打开转接给操作员(EscalationService)并回复 message

escalate.reason:off_topic | empty_answer | user_requested | other;可选 timeout_secs / timeout_message / context_messages。优先级:escalate → reply → inject。如果有 session id,则打开操作员。


6.22 llm_cache

原生推理缓存(crate::llm_cache)。缓存精确的请求 (provider, model, messages, max_tokens, temperature),并在重复时返回已保存的响应,而不调用提供商。与 faq_cache(用户问题 → 回答)不同,此缓存会捕获循环内部的重复:重试、rework、重复的相同提示、无工具子代理以及脚本化 llm(...)。

缓存存在于内核中,因为需要在三条没有共同 hook 点的路径上查看它:

Hook llm_cache 只承载策略并将缓存安装到进程上(类似 circuit_breaker);lookup/store 由内核完成。子进程继承父进程的缓存,因此 ask(...) 评判器会命中它。

kind: llm_cache
name: llm_cache
db_path: data/llm_cache.db
ttl_secs: 3600
scope: template            # template | global | agent | session | user
ignore_temperature: true   # не включать temperature в ключ
cache_with_tools: false    # по умолчанию кэшируются только requests без tools
max_entries: 10000
min_response_chars: 0
max_response_chars: 100000
字段 描述
db_path DuckDB 文件路径(默认 data/llm_cache.db)
ttl_secs 记录 TTL(秒);0 —— 不过期
scope 键隔离:template(默认)/ global / agent / session / user
ignore_temperature 不将 temperature 包含在键中
cache_with_tools 也缓存带 tools 的请求(仅保存不带 tool_calls 的响应);默认 false
min_response_chars / max_response_chars 不缓存过短/过长的响应(max 0 —— 无限制)
max_entries 超出限制时淘汰最旧的记录(0 —— 无限制)

默认策略——仅无工具。 带有非空 request.tools 的请求不会被缓存(带工具调用的响应是非确定性的,不应被重放)。对于脚本 llm(...),tools 始终为空,子评判器也是如此。

安装在 on_spawn 上是幂等的(以便 llm(...) 和第一回合的评判器已经能看到缓存),在 before_inference 上也是幂等的(以便在运行时绑定的 hook 立即生效);在 on_terminate 上会移除进程缓存。