官方资料:Design Philosophy · Agent Loop · Extensions · Sub-Agents · 源码
注:本文基于 2026-10 的官方文档整理;文中涉及 English Studio 的部分均为架构演进与工程落地推演,非已上线实现。
先建立心智模型
yoagent 是一个用 Rust 编写的轻量级 Agent 执行底座(Harness)。它的核心定位是跑通并管好单次 Agent Loop,而非提供开箱即用的聊天产品或复杂的多 Agent 业务编排框架。
用一句话概括 Agent 的核心机制:在“模型推理 → 工具执行 → 结果回填”的闭环中持续迭代,直到模型不再发起工具调用并给出最终答案。
把上面这个 while 循环写出来并不难,真正棘手的是生产环境下的工程边界处理:流式响应中断、工具执行超时、重试引发的重复输出、上下文 Token 超限、中途取消任务、成本统计以及异常终止判定。
yoagent 的设计哲学非常纯粹:把这套通用的运行时机制(Harness)做深、做透,且只实现一次。
一张表看清职责边界
在实际工程中,我们需要将 yoagent(底层执行层)与 English Studio(业务应用层)清晰解耦:
| 核心能力 | yoagent 执行底座(Harness) | English Studio 应用层(App) |
|---|---|---|
| 模型交互 | 封装请求、流式解析、指数退避重试 | 选择具体模型、管理 API 凭据 |
| 工具调用与循环 | 驱动 Tool Call 循环与结果回填 | 定义英语教学业务工具与调用权限 |
| 上下文与限流 | 提供分层上下文裁剪与执行配额限制 | 决定哪些核心学习事实需要长期保留 |
| 扩展与可观测性 | 暴露统一的生命周期 Hook 与事件流 | 挂载业务扩展、向前端推送学习进度 |
| 教学策略与计划 | 不介入具体业务 | 制定学习计划、编排教学策略 |
| 产品形态与交互 | 不包含 UI 与会话持久化实现 | 管理 UI 渲染、会话列表、复习排期表 |
| 长期记忆(Memory) | 不内置特定记忆存储服务 | 对接 OpenViking 等外部记忆服务 |
一、Agent Loop 是如何运行的?
在 API 设计上,yoagent 分为高低两层:
- 高层有状态封装(Agent):内部维护会话消息、工具注册表与消息队列,适合快速开箱使用。
- 底层无状态入口(函数式 API):由调用方显式传入上下文
AgentContext与配置AgentLoopConfig,官方提供两个核心函数:agent_loop():将新输入追加到上下文,启动一次全新的 Run。agent_loop_continue():基于当前已有的上下文状态,继续向后推进执行。
一次完整 Run 的内部生命周期可以拆解如下:
厘清四个粒度:Turn、Run、Session 与 Workflow
在设计应用层架构时,必须严格区分以下四个由小到大的执行粒度:
| 概念 | 粒度定义与职责边界 |
|---|---|
| Turn(轮次) | 最小循环单元。包含一次模型响应,以及该次响应触发的所有关联工具执行。 |
| Run(单次运行) | 单次执行闭环。从接收一条 prompt 开始,历经 1 到多个 Turn,直到 Agent 停止输出。 |
| Session(会话) | 跨 Run 的状态容器。由多次连续的 Run 组成,需由应用层管理身份绑定、持久化与状态恢复。 |
| Workflow(业务流) | 面向业务目标的编排。为完成特定业务目标组织的一连串步骤,可能包含 0 次、1 次或多次 Agent Run。 |
切记:Run 不等于 Workflow。 一次简单的日常答疑只需一个 Run 即可完成;而“语法薄弱点专项训练”则可能是跨越数天、包含多个阶段(诊断 → 讲练 → 复测)的业务 Workflow。
二、Extension:如何在不侵入循环代码的前提下扩展能力?
为了保持核心 Loop 的精简,yoagent 通过 Extension(统一扩展契约)与 RunHooks(运行期介入点)对外提供切面扩展能力:
| Hook 名称 | 触发时机 | 典型工程用途 |
|---|---|---|
on_input |
用户输入刚进入本次 Run 时 | 拦截并拒绝不合规输入 |
before_model |
每次向大模型发起请求之前 | 动态补充临时学习上下文、限制模型调用 |
before_tool |
具体工具执行之前 | 权限校验、拒绝调用或修正入参 |
after_tool |
工具执行完毕返回结果之后 | 敏感数据过滤、结果清洗与格式修正 |
on_stop |
模型准备自然结束输出时 | 校验答案质量,若不达标则打回要求继续推理 |
on_event |
运行期产生各类状态事件时 | 日志采集、监控指标上报与前端 UI 事件推送 |
finish |
整个 Run 彻底结束时 | 记录最终执行状态、清理本次运行占用的资源 |
两者的生命周期关系如下:
为什么 RunHooks 要每次重新创建? 核心原因是单次运行的状态隔离。两个并发的学习任务即使复用同一个 Extension 类型,也绝不能意外共享本次的执行轮数、临时权限和验证次数。真正需要跨 Run 共享的全局预算或计数器,应放在 Extension 持有的共享状态中,并做好并发同步保护。
Fail-Closed:异常时宁可拒绝,也绝不默认放行
在涉及安全与权限的拦截点上,必须遵循 Fail-Closed(故障关闭) 原则。例如 AI 计划调用高危工具 delete_learning_records:
before_tool确认用户已授权 → 允许执行;- 明确判定没有授权 → 拦截拒绝;
- 权限扩展超时、崩溃或无法判定 → 仍然拒绝(Fail-Closed)。
同理,负责敏感输出过滤的 after_tool 一旦执行失败,绝不能原样泄露未经脱敏处理的工具结果。
架构边界:不是所有逻辑都应该写成 Hook
在工程实践中,应当按职责归属拆分逻辑:
- Tool(工具):供模型调用的明确动作,例如
find_grammar_knowledge。 - Extension(扩展):与业务解耦的横切关注点,例如安全检查、预算控制、可观测性和临时上下文补充。
- App Service(应用服务):管理会话生命周期、长期业务状态、学习计划与 Workflow。
三、为什么多个 Agent 能共享同一套 Harness?
多 Agent 复用底座的核心在于:共享的是底层执行机制和基础设施,而不是共享同一份可变的消息历史。
可以用一个公式直观理解:
以 English Studio 中的三个专项教练 Agent 为例:
| Agent | 角色定位 | 特有工具 | 维护的当前上下文 |
|---|---|---|---|
| Listening | 听力教练 | 音频转写、听力错误分析 | 当前音频、听写结果与批改记录 |
| Grammar | 语法私教 | 查语法知识、生成专项题 | 语法薄弱点、历史错题 |
| Vocabulary | 词汇复习教练 | 查询到期卡片、记录复习结果 | 生词表、掌握度、复习计划 |
在运行时,三者可以调用相同的模型 Provider,也可以按需选用不同模型;它们还能共享日志管道、统一工具运行能力、记忆网关(OpenViking)和 Session Store,但各自的会话上下文应当保持逻辑隔离。
注意上图中的 AgentRuntime 是 English Studio 自建的应用层运行时包装,而非 yoagent 开箱提供的完整编排器。它负责加载 AgentDefinition、恢复会话、装配工具、调用 yoagent 执行 Run、分发事件以及持久化最终结果。
Rust 示例:让主 Agent 委派语法子 Agent
下面的代码基于官方的 SubAgentTool 和 Agent Builder 编写;请注意,这里演示的是运行期的动态任务委派(Delegation),而非完整的业务 Workflow:
use std::sync::Arc;
use yoagent::agent::Agent;
use yoagent::provider::{AnthropicProvider, ModelConfig};
use yoagent::sub_agent::SubAgentTool;
pub fn build_tutor_coordinator(
provider: Arc<AnthropicProvider>,
grammar_tools: Vec<Box<dyn yoagent::tools::Tool>>,
) -> Agent {
// 1. 定义语法专项子 Agent:拥有独立的 System Prompt、专属工具与轮次上限
let grammar_sub_agent = SubAgentTool::new("grammar_specialist", provider.clone())
.with_description("当用户在听力或阅读中遇到复杂长难句、虚拟语气或特定语法盲区时,委派该专家进行深度拆解与出题。")
.with_system_prompt(
"你是 English Studio 的语法私教。请仅针对传入的句子和错因进行语法结构剖析,并生成 1 道同类变式题。",
)
.with_model(ModelConfig::claude_sonnet())
.with_tools(grammar_tools)
.with_max_turns(5);
// 2. 构建主教练 Agent:将子 Agent 作为 Tool 挂载,按需动态委派
Agent::new(provider)
.with_system_prompt(
"你是用户的英语主教练。负责推进日常学习对话;遇到需要深度语法专项解析的场景时,请调用 grammar_specialist 工具。",
)
.with_model(ModelConfig::claude_sonnet())
.with_sub_agent(grammar_sub_agent)
.with_max_turns(10)
}在这段代码中:
- 主 Agent 与
grammar_specialist底层跑的都是同一套 yoagent Loop; - 当主模型决定调用
grammar_specialist时,SubAgentTool会在内部启动一个子级 Loop(Child Loop),使用独立的上下文和max_turns(5)限制完成推理,最后只将提炼后的结论作为工具输出返回给主 Agent,从而避免子任务的中间推理过程污染主会话上下文。
四、什么时候用子 Agent,什么时候用业务 Workflow?
理解了 SubAgentTool 的委派机制后,工程落地中最容易踩的坑就是:把所有多步业务流程都交给主 Agent 用 SubAgentTool 自由调度。
在架构选型上,请用“控制权归属”来划分两者的边界:
| 对比维度 | 子 Agent 委派(Sub-Agent) | 业务工作流(Workflow) |
|---|---|---|
| 调度决策者 | 大模型(LLM)在单次 Run 中自主决定是否调用、何时调用 | 应用层代码(Rust 业务逻辑)按确定性状态机或编排规则驱动 |
| 确定性要求 | 探索性、开放式、分支取决于对话实时上下文 | 强确定性、步骤固定、每步输出有严格的 Schema 与落库要求 |
| 上下文传递 | 父 Agent 生成自然语言 prompt 传给子 Agent,或通过 SharedState 按 Key 引用大对象 |
由应用层查询数据库/状态机,精确组装每一步 Run 所需的最小结构化上下文 |
| 生命周期跨度 | 短周期,共生于父 Agent 的同一次 Run 内部 | 可长可短,可跨越多次请求、多个页面交互甚至数天的学习计划 |
| 失败恢复成本 | 依赖外层重试或模型自我修正,无法精确从中间某一步断点续跑 | 具备明确的业务状态检查点(Checkpoint),可针对单步精确重试或回滚 |
在 English Studio 中的选型实践
1. 适合使用「子 Agent(SubAgentTool)」的场景- 伴学对话中的临时专家会诊:用户在做精听训练时突然追问“这句话里的
were it not for为什么倒装?”,听力主 Agent 识别出这属于深度语法问题,在当前 Run 内通过SubAgentTool临时委派给 Grammar Agent 解答,拿回解析后再继续原来的听力训练。 - 长文本/大产物局部下钻:结合 yoagent 的
SharedState引用机制,让子 Agent 针对整篇外刊文章的某个段落做词根拆解,只把汇总结果返回主对话,节省主上下文 Token。
- 每日晨间综合训练流(听写 → 错因诊断 → 闪卡生成 → 复习排期):这是一条必须严格按序执行的业务链路。应当由 English Studio 的业务服务来编写确定性流程:
- 先调用确定性代码完成音频切片与转写比对(无需 Agent);
- 启动一次独立的 Listening Agent Run 生成错因诊断报告并落库;
- 根据诊断报告中的生词,调用确定性服务写入 Learning DB 闪卡表;
- 当用户点击“进入语法巩固”时,再由应用层启动一次全新的 Grammar Agent Run。
总结:回顾开篇三个问题
- Agent 是怎么循环执行的? 围绕模型推理 → 工具调用 → 结果写回循环推进;yoagent 通过无状态的
agent_loop()与有状态的 Agent 封装,统一解决了流式输出、超时重试、上下文裁剪、配额控制与 Extension 切面拦截等工程难题。 - 为什么多个 Agent 可以共用 Harness? 因为执行循环与底层基建(Provider、工具执行器、日志、存储网关)是完全通用的;不同 Agent 只是在同一套 yoagent 底座之上,注入了各自独立的 System Prompt、工具子集与逻辑隔离的 Session 上下文。
- 什么时候用子 Agent,什么时候用业务 Workflow? 临时专家协同、隔离中间推理噪声用
SubAgentTool(让模型做动态委派);固定教学链路、跨阶段状态流转用业务 Workflow(让应用层代码驱动多次独立 Run)。