构建智能体
🔵 创建智能体的完整路径——只需 YAML,以及可选的脚本。先讲哲学,再讲机制。
4.1 哲学
在编写 agent.yaml 之前,值得先理解你究竟在创建什么。
智能体是描述,而非代码
在 Agent OS 中,智能体是由 YAML 组装而成的:身份、工具、钩子、预算。 代码(Rhai/JS)只有在需要自定义工具或钩子时才需要。 这是一种声明式组装:你在“职位说明书”中描述一名员工, 而不是编写他的“大脑”。所有繁重的部分——回合循环、工具调用处理、 token 核算、隔离、日志记录、规划——都由内核完成。
定义智能体的五个问题
任何智能体都是对五个问题的回答。每一个都是独立的一面,在配置中都有独立的位置:
| 问题 | 字段 | 这是什么 |
|---|---|---|
| 他是谁? | instructions |
角色、身份、行为规则和回复格式 |
| 他会什么? | tools / tool_files |
内置工具和 YAML/脚本工具 |
| 他在生命周期中如何表现? | hooks / hook_files |
记忆、缓存、升级、上下文注入 |
| 他有多少额度? | budget / budget_group |
token 限额(这就是钱) |
| 请求从哪来? | telegram、email、widget、API |
输入通道 |
职责分离
每一面都存在于自己的文件中,因此可以单独修改它们:
替换工具而不动提示词;重写提示词而不动钩子;
把限额移到 budgets.yaml 中,完全不碰智能体。这正是结构的核心
意义:
config/agents/<name>/
├── agent.yaml # 身份 + 绑定
├── tools/*.yaml # 能力
├── hooks/*.yaml # 生命周期
├── bench/scenarios.yaml # 质量控制
└── tests/*.yaml # 工具测试
智能体作为员工
一个有用的比喻:instructions —— 职位说明书(做什么以及如何
回复),tools —— 工作岗位上的技能和工具,hooks —— 反射
和规则(记忆、常用缓存、转交人工),budget —— 权限
限额,通道 —— 通信线路。
优秀智能体的原则
- 脚踏实地。 要求来源和链接,禁止编造,用
工具验证。示例
ngu-agent强制 引用来源;trace-agent—— “永远不要猜测,始终通过调用验证”。 - 清晰的角色和格式。 直接在指令中固定语言和回复结构。
- 节约。 预算就是钱:缓存(
faq_cache)和上下文 限制能削减开销。 - 可观测性。 用基准测试(
bench/)覆盖智能体——否则回归 不会被察觉。
模板 → 进程 → 会话
你描述的是模板(配方)。内核根据第一个请求创建进程 (lazy spawn),而用户的对话就是会话。修改模板——新的 会话会获得新行为;已经启动的进程会保留旧的 系统提示词。
4.2 目录结构
服务器会自行查找智能体:任何 config/agents/ 的子文件夹,只要包含
agent.yaml(或 agent.yml),就会成为模板。tool_files 和
hook_files 相对于智能体文件夹解析。
4.3 agent.yaml —— 参考手册
name: my-agent
welcome: |
👋 Привет! Чем помочь?
instructions: |
You are a helpful assistant.
Use my_tool to answer questions about X.
Answer in Russian.
tools: [http_fetch]
tool_files:
- tools/my_tool.yaml
hooks: [] # имена хуков, зарегистрированных при старте
hook_files:
- hooks/faq_cache.yaml
route_files: # HTTP-маршруты, которые добавляет агент (http_route / static_route)
- routes/api.yaml
max_context_tokens: 32000
max_output_tokens: 8192 # потолок ответа за inference (перекрывает kernel.max_output_tokens)
model: "" # именованная модель из server.yaml → models:
output_schema: null # JSON Schema структурированного завершения
budget: # опциональное перекрытие бюджета
refill_per_sec: 30
max_bucket: 10000
max_total: 200000
budget_group: my-shared
budget_messages:
session_exhausted: "Лимит диалога исчерпан. Начните новый."
user_exhausted: "Лимит на сегодня исчерпан."
group_exhausted: "Общий лимит исчерпан. Попробуйте позже."
rate_limited: "Слишком много запросов. Подождите."
inherit_budget: false
durable: false # true = сессия переживает рестарт (opt-in)
supervision: null # политика авто-рестарта (opt-in), см. ниже
parent: null
attachments: # что виджет принимает от пользователя (файлы/скриншоты/камера)
enabled: true # false = виджет скрывает кнопку вложений
file: true # загрузка файлов (по умолч. true)
screenshot: true # снимок экрана (по умолч. true)
camera: true # фото с камеры (по умолч. true)
accept: "image/*,.pdf" # маска accept для файлового инпута (опционально)
max_size: 10485760 # макс. размер файла в байтах (опционально)
login: # вход в виджете (см. §19.1)
enabled: false # показывать кнопку «Войти»
required: false # требовать вход (анонимные отклоняются, 401)
providers: [yandex] # OIDC-провайдеры (пусто = все)
allow_users: false # разрешить вход логином/паролем из auth.users
prompt: "Войти через Яндекс"
widget: # оформление виджета (см. §19.1)
show_close: true # кнопка «закрыть» (×); false — для инлайн-встраивания
starters: [] # вопросы-кнопки под приветствием нового диалога
persist: true # сохранять диалог при закрытии/перезагрузке (по умолч. true)
history_limit: 40 # сколько завершённых обменов хранить в истории
tool_visuals: final # визуалы тулов: final|always|never (см. §19)
cron: [] # задания по расписанию этого агента
telegram:
token_env: MY_TG_BOT_TOKEN
allowed_user_ids: []
show_typing: true
show_tool_calls: true
email:
imap_host: imap.example.com
imap_port: 993
imap_username: support@example.com
imap_password_env: EMAIL_PASSWORD
allowed_senders: []
smtp:
host: smtp.example.com
username: support@example.com
password_env: SMTP_PASSWORD
from: support@example.com
notify: []
poll_secs: 60
vk:
token_env: VK_GROUP_TOKEN
group_id: 123456789
allowed_user_ids: []
show_typing: true
show_tool_calls: true
poll_wait: 25
字段参考
| 字段 | 必填 | 默认值 | 描述 |
|---|---|---|---|
name |
是 | — | 模板的唯一名称 |
instructions |
是 | — | 系统提示词 |
welcome |
否 | — | widget 的欢迎语 |
tools |
否 | [] |
内置工具(http_fetch、fs_read、job_done) |
tool_files |
否 | [] |
YAML 工具的路径(相对于智能体文件夹) |
hooks |
否 | [] |
钩子名称(在启动时注册) |
hook_files |
否 | [] |
YAML 钩子的路径(相对于智能体文件夹) |
route_files |
否 | [] |
YAML HTTP 路由 http_route/static_route 的路径(相对于智能体文件夹;见 §21) |
max_context_tokens |
否 | 32000 |
上下文窗口限制 |
max_output_tokens |
否 | 全局 kernel.max_output_tokens(4096) |
单次 inference 的回复长度上限。覆盖全局值,并在需要时提升进程的 per-turn 预算 |
model |
否 | 空 | 来自 server.yaml → models: 的命名模型(空 = 默认 provider) |
output_schema |
否 | — | 完成项的 JSON Schema:替换 job_done 参数 |
budget |
否 | — | {refill_per_sec, max_bucket, max_total} |
budget_group |
否 | — | 共享预算组的名称 |
budget_messages |
否 | — | 耗尽消息(4 个级别) |
inherit_budget |
否 | false |
子级继承父级的组 |
durable |
否 | false |
会话在核心重启后仍然存活:进程、预算组和会话在回合边界保存,并在 boot 时恢复(需要 logging.durable_db_path;见 §27.2) |
supervision |
否 | null |
崩溃时的自动重启策略(见下文) |
sandbox |
否 | null |
per-agent 沙箱策略(限额/文件系统根),覆盖全局 server.yaml → sandbox(见 §3.2) |
parent |
否 | — | 父级名称(进程树) |
attachments |
否 | — | 来自用户的附件:{enabled, file, screenshot, camera, accept, max_size} —— 在 widget 中启用附件按钮,并接收来自通道的附件(email/telegram/vk,§19) |
login |
否 | — | widget 中的登录:{enabled, required, providers, allow_users, prompt} —— 强制 OIDC 授权和/或 auth.users(§19.1) |
admin_only |
否 | false |
仅限管理员:对 agent 的聊天请求必须携带有效的 admin 凭据(bearer token 或 admin login-cookie),否则返回 403。关闭认证时该门禁开放。适用于高权限 agent(例如 coder,它会编辑文件并执行命令) |
cron |
否 | [] |
按计划触发该 agent 的定时任务 |
telegram |
否 | — | {token_env, allowed_user_ids, show_typing, show_tool_calls} |
email |
否 | — | 入站 email 通道(见下文) |
vk |
否 | — | 入站 VK 社区机器人(见下文) |
yandex_messenger |
否 | — | 入站 Yandex Messenger 机器人(Bot API,轮询) |
监督(自动重启)
可选:agent 在崩溃时重启(非瞬态推理错误或意外退出且 exit≠0)。
supervision:
restart: on_failure # never | on_failure | always
max_restarts: 3 # бюджет перезапусков в окне
window_secs: 300
backoff_secs: 5 # задержка перед рестартом
resume: from_template # from_template | from_checkpoint
replay_input: false # opt-in: переиграть вход, на котором упало
on_exhausted: terminate # terminate | quarantine
poison_threshold: 3 # одинаковых ошибок до карантина (0 = выкл)
- 重启会重新创建进程,并将会话重新绑定到新的 pid; 预算按会话计算(可跨重启保留)。
- 默认情况下,新 agent 等待下一条入站消息;在
replay_input: true时重放导致崩溃的那次输入(对于from_checkpoint是「从边界开始」,对于from_template是重新注入 + 一轮)。 - 预算耗尽 →
on_exhausted(terminate或quarantine—— 进程 被挂起,不再重启)。 - 事件:
process_restarted、supervisor_gave_up。
输入/输出通道
模板可以绑定通道——将消息路由到该 agent 的入站来源:
widget、Telegram 机器人、VK 机器人、Yandex Messenger 机器人、
email、HTTP API、webhook。在 agent.yaml 中,这些是字段 telegram、vk、
yandex_messenger 和 email。所有通道的完整解析(widget 嵌入、
Telegram/VK/Yandex Messenger/email 配置、sink)——见 §19。
结构化输出(output_schema)
设置 output_schema,让 agent 返回固定形式而非
自由文本。内核会用该 schema 替换参数 job_done(如果 tools: 中没有
job_done,则添加它),并且模型必须以调用
job_done(...) 并传入合适的值来结束:
output_schema:
type: object
properties:
needs_action: { type: boolean }
summary: { type: string }
required: [needs_action, summary]
job_done 的参数在结束前会被校验;无效调用
会被拒绝(模型看到错误,并可在同一循环中修正)。
校验后的值可供调用方使用(例如 subagent_tool 会将其放入
结果的 data 中)。
4.4 分步操作
第 1 步 —— 骨架
mkdir -p config/agents/mybot/tools config/agents/mybot/hooks config/agents/mybot/bench config/agents/mybot/tests
编写 config/agents/mybot/agent.yaml(见 §4.3)。
第 2 步 —— 工具
创建 config/agents/mybot/tools/my_tool.yaml(见
工具参考),并在 tool_files: 中列出它。
无需服务器即可验证:
agent-os tool config/agents/mybot/tools/my_tool.yaml '{"query":"..."}'
第 3 步 —— 钩子(可选)
创建 config/agents/mybot/hooks/<hook>.yaml(见 06-hooks-reference),
并在 hook_files: 中列出它。无需服务器即可验证:
agent-os hook config/agents/mybot/hooks/<hook>.yaml user "Привет"
第 4 步 —— 预算组
将组添加到 config/budgets.yaml 并绑定模板:
budget_groups:
- name: mybot-shared
refill_per_sec: 30
max_bucket: 30000
max_total: 3000000
agents:
- template: my-agent
group: mybot-shared
第 5 步 —— 基准测试场景
创建 config/agents/mybot/bench/scenarios.yaml:
name: "MyBot"
model: "my-agent"
scenarios:
- name: "Простой вопрос"
turns:
- user: "Как мне ...?"
assert:
contains: [ожидаемое, слово]
min_links: 1
valid_urls: true
检查类型:contains(子串,不区分大小写)、min_links /
min_urls(markdown 链接数量)、valid_urls(所有链接均为 http/https)。
运行(见 09-operations):
agent-os bench --port=3001 --agent=my-agent --max=3
第 6 步 —— 工具测试(可选)
config/agents/mybot/tests/my_tool_tests.yaml 中的 fixture 测试(无网络):
kind: site_tool_tests
tool: tools/my_tool.yaml
tests:
- name: "извлекает заголовок"
input: { url: "https://example.com/p/1" }
fixture: { productData: { title: "Widget", price: 99 } }
assert:
- { path: /title, equals: "Widget" }
- { path: /price, equals: 99 }
Live 测试(真实 HTTP:kind: site_tool_live_tests、
archive_tool_live_tests、rss_tool_live_tests)通过设置
fetch: true 来替代 fixture:。如何运行它们——见 09-operations。
4.5 指令实践
- 说明何时调用哪个工具,包括多步链路
以及按 slug/id 查找(实际示例——
config/agents/drom/agent.yaml)。 - 要求将工具输出中的
links[]复制到回复中;禁止裸 URL。 - 固定回复的语言和格式。
- 使用
__GEOIP__来替换用户所在城市(见drom)。 - 让 agent 保持「脚踏实地」:「永远不要猜测,始终通过调用验证」
(见
trace-agent)。
4.6 热重载
在 live_reload: true(默认)下,可以实时修改 agent.yaml、工具
和钩子:新会话采用新指令,重新注册的
工具/钩子从下一次调用起生效。已启动的进程
保留原始系统提示词。