钩子 — 路由与可靠性
模型选择、分诊、容错与成本。所有钩子的列表见 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 点的路径上查看它:
- 代理回合循环(令牌
before_inference); LlmService::llm—— 脚本化/用户llm(...)(这里有意不调用 hook,以避免递归);HookServices::ask—— 来自memory/faq_cache/condense/compact_results/language_guard的无工具子评判器(它们以hooks: []启动)。
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 上会移除进程缓存。