设计哲学
English · 中文
状态:现行。日期:2026-10-09。
yoagent 是一个循环库:最小而正确的 agent 循环,加上一份扩展它的契约。 它不是 agent,不是框架,也不是应用。agent、框架和应用建立在它之上。
本页说明这个出发点、它导出的形态、它的代价,以及由此而来的原则。具体机制见各概念页面和架构概览。
1. agent 就是一个循环;难的是把循环写对
agent 把消息发给模型,拿回文本和工具调用,执行工具,然后重复,直到模型停下。这个循环人人一个下午就能写出来,而几乎人人都会在同样的地方出错:
- 流结束得不好。 提供商的流在回答中途断开;容量错误伪装成上下文溢出;重试前那次尝试的半截文字已经到了用户眼前。
- 工具出状况。 工具 panic、卡死、输出刷屏,或者逃出沙箱。
- 上下文用完。 历史超出窗口;压缩毁掉了已缓存的前缀;摘要丢掉了唯一要紧的那个事实。
- 运行停不下来,或无声无息地停下。 失败的调用无限重复;限制只检查一次;取消之后留下写了一半的状态。
- 花费没人算得清。 子 agent、决策模型和摘要的花费从不出现在总数里。
yoagent 的存在,就是为了让这个循环只写一次、写对,并且每一项保证都有测试钉住——然后被所有建立在它上面的 agent 复用。这就是为什么 agent_loop 是无状态的自由函数,为什么 Agent 只是它外面一层可选的包装,也是为什么代码库的大部分是循环的失败处理,而不是正常路径。
2. 是库,不是 agent
yoagent 有意停在循环这里。产品在循环之外需要的东西,归产品所有:
| 属于 yoagent | 属于建立在它之上的应用 |
|---|---|
| 流式传输、工具执行、重试、取消 | UI:终端、网页、聊天渠道 |
| 上下文管理、限制、循环检测 | 命令、设置、对话框 |
| 成本统计、结构化输出、遥测 span | 作为产品的会话:列出、恢复、分享 |
| 提供商协议(七种) | 用户可以选哪些模型,以及它们的密钥 |
| 一份扩展契约 | 用户可以安装哪些插件和生态 |
界线由一个问题划定:是不是每个 agent 都需要它,而且需要的方式相同? 重试逻辑是;命令面板不是。当答案是"只有部分 agent 需要"时,这项能力就做成扩展或配套 crate,绝不放进循环。
对提供商也是同样的克制。yoagent 接入模型 API——Anthropic、OpenAI 的两种协议、Azure、Gemini、Vertex、Bedrock,以及 OpenAI 兼容的长尾——但不托管、也不包装模型。没有 API 的模型,yoagent 不支持。
3. 一份契约扩展循环
应用(yo、yoyo、你的 agent)
│ 构建、配置、拥有 UI
▼
┌───────────── yoagent ──────────────┐
│ 循环 ◀── Extension / RunHooks │ tools, on_input, before_model,
│ │ before_tool, after_tool, on_stop,
│ │ on_event, finish
└───────────────────┬─────────────────┘
│ 和其他扩展一样的一个 Extension
▼
yoagent-rutis(配套 crate):插件宿主
Rust · TypeScript · Python 处理器、图片、日志
│
┌──────────────┼──────────────┐
▼ ▼ ▼
DSH rutis-agent pi (生态适配器)
改变循环行为的每一种方式都经过 Extension:提供工具、审查输入、在模型请求前附加说明、允许、拒绝或改写工具调用、修改结果、校验回答、观察事件、收尾清理。这份契约有几个性质,比钩子列表本身更重要:
- 它以运行为作用域。 扩展是一个工厂;每次运行拿到全新的钩子,所以每次运行的状态从干净开始,子 agent 的运行也有自己的一份。跨运行共享的东西放在扩展本身,由你选择(
Budget可以跨运行计数)。 - 失败有明确含义。 扩展要么是 advisory(失败被记录,跳过该钩子),要么是 required(失败结束运行)。
ClonedHooks和 rutis 桥接等包装让宿主来选。 - 循环自己的功能也用它。 工具闸门、输入守卫、决策顾问和美元
Budget都是扩展。如果这份契约对它们不够用,那对谁都不够用。 - 它不是 UI。 UI 必须在运行之前和运行之间存在;扩展只存在于一次运行之内。所以命令、对话框和历史留在应用里。
4. 失败即关闭(fail closed)
agent 会在真实世界里行动,所以一个可能失败的扩展点,必须朝安全的方向失败。规则处处一致:
- 无法执行的策略即拒绝。 panic 的
before_tool钩子会拒绝该调用;运行被取消时仍未返回的也一样。跨插件桥接时,超时、被卸载或返回格式错误的处理器同样拒绝。 - 失败的脱敏即扣留。 失败的
after_tool钩子绝不放原始输出通过。 - panic 的工具是一个错误结果,带着它的消息,而不是让循环崩溃(在 panic 会展开的平台上;wasm32 上 panic 会中止)。
- 任何做决定的东西都不会被悄悄丢掉。 一个插件处理器若要决定或改写宿主从不做的事——比如改写对话——就拒绝加载;其余部分会被报告出来。适配器契约为每个生态适配器写明了这一点。
失败即关闭会牺牲一些便利:一个在别处能"半工作"的插件,在这里会拒绝加载。这是有意的取舍。
5. 插件和其他生态,在核心之外
核心从不依赖任何插件系统。yoagent-rutis 是一个独立的 crate,它把 rutis 插件——进程内的 Rust,以及在各自运行时中的 TypeScript 和 Python——变成一个 Extension。插件注册一个处理器;桥接把它变成钩子;循环分辨不出差别。
其他 agent 生态就是通过这座桥接入 yoagent 的。DeepSeek harness(DSH)的工具插件、rutis-agent 的工具和 pi 的扩展,都通过小型适配器接入,循环不做任何改动。每个适配器映射循环能兑现的部分(工具、策略、说明、图片、日志),其余部分拒绝或报告。
两个推论:
- 要证明的是契约,而不是适配器。 三个外部生态都能通过同一个
Extension接入,这就是契约具有通用性的证据。适配器本身跟随其他项目的版本发布;这些生态中需要应用服务的部分(命令、对话框、会话),留给承载 agent 的应用。 - 穿过桥接的,是每个生态都能得到的东西: 工具结果中双向传递的图片;进入宿主
tracing的插件日志,带有run_id标记,让 GASP 这样的记录器能把它们归到对应的运行;以及失败即关闭的规则。
6. 正确优先于巧妙——并对成本和缓存保持诚实
有几条原则贯穿整个循环:
- 提示缓存是正确性问题。 已缓存的前缀让长时间的 agent 运行负担得起,所以没有任何东西会背着用户改写它:每轮的说明放在最新一条用户消息上,绝不放进系统提示;来自工具源的工具按名字排序,相同的集合永远不会换序;压缩是分级的,并按真实用量校准。
- 成本只报告,从不猜测。 价格是需要主动启用的数据,不是写死在代码里的字面量;没有定价的调用是
None,不是零;一次运行自身的各轮、子 agent、决策模型和摘要各有各的账,最后汇入一个总数。 - 一切都能在没有网络的情况下测试。
MockProvider确定性地驱动循环;这里的每项保证都有测试,重要的还经过变异测试检验。 - agent 在哪里跑,它就在哪里跑。 同一个循环通过一层小小的运行时垫片构建到
wasm32(Cloudflare Workers),agent 搬到边缘时不必改动。
7. 代价
- 产品要你自己组装。 yoagent 不提供 UI、命令系统和会话产品。做一个完整的 agent 应用,比从自带这些的框架起步要多花功夫。
- 有些事在扩展里刻意做不到。 扩展不能改写历史、替换系统提示,也不能在运行之间维持一个 UI。需要其他生态中这些部分的插件(pi 的 plan 模式、DSH 的网页应用)在这里只能部分工作。
- 失败即关闭拒绝得更多。 无法兑现的安全策略会阻止加载或调用。这更安全,偶尔也不方便。
- 跨进程插件有延迟成本。 TypeScript 或 Python 处理器实现的每个钩子,都是一次对另一个进程的调用;热路径应该用 Rust 写。
- 生态适配器是局部的,而且变化快。 它们覆盖循环能兑现的部分,并跟随其他项目的版本发布。
8. 原则,以及判断新能力的标准
原则
- 循环只写一次,写在 yoagent 里,每项保证都有测试钉住。
- 其余一切通过一份契约
Extension来扩展;新钩子只为具体的使用者、窄范围地添加。 - 在每个可能失败的扩展点上失败即关闭。
- 绝不背着调用方改写已缓存的前缀。
- 诚实报告成本:未知就是
None,绝不是零。 - 核心不依赖任何可选的东西:插件系统、记录器和决策模型都是 feature 或配套 crate。
- 接入 API,不托管模型。
- 写下不保证的东西,而不只是保证的东西。
一项能力应该放进 yoagent 吗?
- 是不是每个 agent 都需要它,而且方式相同?
- 它能不能做成扩展、feature 或配套 crate,而不是循环代码?
- 它能保持提示缓存不变吗?
- 它会失败即关闭吗?
- 它的保证能在没有网络的情况下测试吗?
- 它的成本计入了吗?
如果第一个问题的答案是否定的,结论几乎总是"做成扩展"——如果做不成,那可能是契约缺了一个钩子,这本身就值得单独讨论。
9. 开放问题
- 给插件的应用服务。 命令、对话框和会话历史是应用的事,但插件需要它们。目前的方向:由接入的客户端以插件级服务的形式提供,在循环之外。
- 对插件的信任。 插件拥有其所在进程的权限。更细的授权——哪个插件可以提供或评判哪些工具——还没有设计。
- 稳定的 1.0 契约。
Extension和桥接的处理器形态还很年轻;它们的稳定性承诺随 1.0 一起给出。
相关
- Agent 循环 · 扩展 · 上下文管理 · 提示缓存 · 模型定价 · GASP
- 架构概览 · WebAssembly 与 Cloudflare Workers · 测试你的 agent
- yoagent-rutis · rutis 设计哲学
(除 rutis 设计哲学外,链接的页面为英文。)