Skip to content
登录

yoagent 技术笔记:从 Agent Loop 到共享 Harness

Updated View as Markdown

官方资料:Design Philosophy · Agent Loop · Extensions · Sub-Agents · 源码

注:本文基于 2026-10 的官方文档整理;文中涉及 English Studio 的部分均为架构演进与工程落地推演,非已上线实现。

先建立心智模型

yoagent 是一个用 Rust 编写的轻量级 Agent 执行底座(Harness)。它的核心定位是跑通并管好单次 Agent Loop,而非提供开箱即用的聊天产品或复杂的多 Agent 业务编排框架。

用一句话概括 Agent 的核心机制:在“模型推理 → 工具执行 → 结果回填”的闭环中持续迭代,直到模型不再发起工具调用并给出最终答案。

01 / Agent Loop模型决策与工具执行形成闭环
用户任务
准备上下文与工具列表
Agent Loop · 重复执行
模型推理
决定回答,或发起 tool_calls
工具执行
返回结果并写入上下文
结果反馈给模型,继续推理
返回最终答案
无更多工具调用,停止循环
工具结果返回模型继续推理;当模型没有更多工具调用时结束。

把上面这个 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 的内部生命周期可以拆解如下:

02 / Run Lifecycle一次 Run 可以包含多个 Turn
AgentStart
一个 Turn
TurnStart
1处理用户 steering 指令
2检查轮次、耗时与 Token 预算
3必要时压缩上下文
4流式请求模型,执行工具并回填结果
TurnEnd
有工具调用 / steering↶ 下一轮 Turn
无后续动作检查 follow-up 队列有待处理项 → 继续 Turn
队列为空 → AgentEnd
工具调用或 steering 会进入下一轮;自然结束前还会检查 follow-up。

厘清四个粒度: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 彻底结束时 记录最终执行状态、清理本次运行占用的资源

两者的生命周期关系如下:

03 / Extension & RunHooks共享扩展契约,每次 Run 隔离状态
English Studio
注册扩展
yoagent Extension
持续存活的扩展契约
独立 RunHooks
每次 Run 都重新创建
Agent Loop
在指定生命周期节点调用 Hook
Extension 持续提供能力;每次运行创建自己的 RunHooks 实例。

为什么 RunHooks 要每次重新创建? 核心原因是单次运行的状态隔离。两个并发的学习任务即使复用同一个 Extension 类型,也绝不能意外共享本次的执行轮数、临时权限和验证次数。真正需要跨 Run 共享的全局预算或计数器,应放在 Extension 持有的共享状态中,并做好并发同步保护。

Fail-Closed:异常时宁可拒绝,也绝不默认放行

在涉及安全与权限的拦截点上,必须遵循 Fail-Closed(故障关闭) 原则。例如 AI 计划调用高危工具 delete_learning_records:

  1. before_tool 确认用户已授权 → 允许执行;
  2. 明确判定没有授权 → 拦截拒绝;
  3. 权限扩展超时、崩溃或无法判定 → 仍然拒绝(Fail-Closed)。

同理,负责敏感输出过滤的 after_tool 一旦执行失败,绝不能原样泄露未经脱敏处理的工具结果。

架构边界:不是所有逻辑都应该写成 Hook

在工程实践中,应当按职责归属拆分逻辑:

  • Tool(工具):供模型调用的明确动作,例如 find_grammar_knowledge。
  • Extension(扩展):与业务解耦的横切关注点,例如安全检查、预算控制、可观测性和临时上下文补充。
  • App Service(应用服务):管理会话生命周期、长期业务状态、学习计划与 Workflow。

三、为什么多个 Agent 能共享同一套 Harness?

多 Agent 复用底座的核心在于:共享的是底层执行机制和基础设施,而不是共享同一份可变的消息历史。

可以用一个公式直观理解:

04 / Agent DefinitionAgent = 共享引擎 + 独立配置
同一套 yoagent Loop
共享执行引擎
01System Prompt独立角色设定
02Tools本次可用工具
03Model / Limits模型与执行配额
04Session独立上下文
05Extensions所需切面扩展
一个专业 Agent 实例
执行代码复用,运行状态隔离
Loop 的实现复用;角色、工具权限、会话和运行策略仍独立。

以 English Studio 中的三个专项教练 Agent 为例:

Agent 角色定位 特有工具 维护的当前上下文
Listening 听力教练 音频转写、听力错误分析 当前音频、听写结果与批改记录
Grammar 语法私教 查语法知识、生成专项题 语法薄弱点、历史错题
Vocabulary 词汇复习教练 查询到期卡片、记录复习结果 生词表、掌握度、复习计划

在运行时,三者可以调用相同的模型 Provider,也可以按需选用不同模型;它们还能共享日志管道、统一工具运行能力、记忆网关(OpenViking)和 Session Store,但各自的会话上下文应当保持逻辑隔离。

05 / Shared Harness三个教学 Agent,共用一个执行底座
Listening听力分析
Grammar语法讲解
Vocabulary词汇复习
English Studio · AgentRuntime
装配配置、管理会话、分发事件
统一执行底座yoagent Harness
Agent LoopProviderTools
Session Store会话状态
OpenViking长期记忆
Learning DB学习数据
AgentRuntime 是 English Studio 应用层的建议包装,并非 yoagent 内置编排器。

注意上图中的 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。
2. 适合使用「业务 Workflow(应用层编排)」的场景
  • 每日晨间综合训练流(听写 → 错因诊断 → 闪卡生成 → 复习排期):这是一条必须严格按序执行的业务链路。应当由 English Studio 的业务服务来编写确定性流程:
    1. 先调用确定性代码完成音频切片与转写比对(无需 Agent);
    2. 启动一次独立的 Listening Agent Run 生成错因诊断报告并落库;
    3. 根据诊断报告中的生词,调用确定性服务写入 Learning DB 闪卡表;
    4. 当用户点击“进入语法巩固”时,再由应用层启动一次全新的 Grammar Agent Run。

总结:回顾开篇三个问题

  1. Agent 是怎么循环执行的? 围绕模型推理 → 工具调用 → 结果写回循环推进;yoagent 通过无状态的 agent_loop() 与有状态的 Agent 封装,统一解决了流式输出、超时重试、上下文裁剪、配额控制与 Extension 切面拦截等工程难题。
  2. 为什么多个 Agent 可以共用 Harness? 因为执行循环与底层基建(Provider、工具执行器、日志、存储网关)是完全通用的;不同 Agent 只是在同一套 yoagent 底座之上,注入了各自独立的 System Prompt、工具子集与逻辑隔离的 Session 上下文。
  3. 什么时候用子 Agent,什么时候用业务 Workflow? 临时专家协同、隔离中间推理噪声用 SubAgentTool(让模型做动态委派);固定教学链路、跨阶段状态流转用业务 Workflow(让应用层代码驱动多次独立 Run)。
Navigation

Type to search…

↑↓ navigate↵ selectEsc close