数据截至 (上游 commit 1eb2bb1ec895)
总览:笼子而非动物
30 秒导读: Yao 是一个单二进制的 AI 应用引擎,
agent/是它内部的 agent 子系统。它自己不训练、不托管模型(那是"动物"),而是给外部模型套一个可控的笼子:一条Stream()主管道,把权限校验、历史拼装、沙箱、Create/Next钩子、LLM 流式调用、工具循环、多智能体委派串成一次可观测、可拦截、可委派的对话。你写的不是模型,是笼子的形状。
本章只讲全景与导航——这是什么、大盘怎么转、各部件干什么、六章怎么读。任一机制的实现细节都留给后续章节,本章不深入。
1. 这是什么(零基础也能懂)
一句话定义
Yao agent 是单二进制 AI 运行时里的 agent 子系统:用几个配置文件(DSL)描述一个"助手",引擎把它装载成一个能对话、能调工具、能委派给别的助手的可运行对象。
"笼子而非动物"的心智模型
这是理解整个子系统最重要的一句话。
- 动物 = 模型本身。它的智能、它会说什么,来自外部 LLM(通过
connector接入,如gpt-4o)。Yao 不生产这份智能。 - 笼子 = 这套 agent 运行时。它决定动物能看到什么(拼进 prompt 的历史与检索)、能碰什么(暴露给它的 MCP 工具)、说的话如何落地(工具执行、沙箱隔离)、什么时候该换一只动物(委派给子 agent)。
所以你作为开发者,写的从来不是"更聪明的模型",而是笼子的形状:喂什么上下文、开哪些工具、在模型开口前后插什么钩子、越界了怎么拦。模型是可替换的租客,笼子是你的资产。
三种执行器模式(一个 agent 到底"跑"什么)
同一条 Stream() 主管道,按 agent 配了什么,分岔成三种执行形态。判断依据都在 agent/assistant/agent.go 的 Stream 里:
| 模式 | 触发条件(配了什么) | 笼子里关的是谁 | 典型用途 |
|---|---|---|---|
| LLM 模式 | 配了 Prompts 或 MCP | 一个云端聊天模型,逐 token 流式吐字、按需调 MCP 工具 | 常规对话助手、带工具的 RAG |
| CLI-Agent 沙箱模式 | 配了 Sandbox V2(HasSandboxV2()) | 一个跑在隔离盒子里的命令行编码 agent(claude / opencode / yaocode / tai),工具由它自己内部消化 | 让 AI 在沙箱里读写代码、跑命令 |
| 纯 Hook 模式 | 只配了 HookScript,没有 Prompts/MCP/沙箱 | 没有模型,只 有你写的 JS 逻辑 | 纯路由/编排 agent:不问模型,直接按规则委派 |
三者的共同点是同一条主管道、同一套 Create/Next 钩子、同一套流式输出——这正是"钩子统一三模式"的巧妙(见 §5)。
依据:LLM 块 gated on ast.Prompts != nil || ast.MCP != nil(agent/assistant/agent.go:284);沙箱分支 ast.HasSandboxV2()(agent/assistant/agent.go:318);纯 Hook 靠 ast.HookScript != nil 而其余为空。
给谁用
- 应用开发者:想在自己的产品里塞一个会用工具、会查库、能编排的 AI 助手,但不想从零搭运行时。
- 不想被单一模型锁死的人:
connector一换就换模型,笼子不动。 - 要做多智能体协作的人:一个主 agent 委派给若干专家 agent,引擎负责隔离与汇聚。
用起来什么样(最小示例)
一个 agent 就是一个目录,最少三个文件(引自 agent/README.md):
assistants/
└── my-assistant/
├── package.yao # 配置:名字、用哪个 connector(模型)、开哪些能力
├── prompts.yml # 系统提示词
└── src/index.ts # (可选)Create/Next 钩子,给笼子加机关
package.yao —— 指定这只笼子关哪只动物(agent/README.md):
{
"name": "{{ name }}",
"connector": "gpt-4o",
"description": "{{ description }}"
}
prompts.yml —— 系统提示词:
- role: system
content: |
You are a helpful assistant.
src/index.ts —— 可选钩子,在模型开口前拦一道,把"退款"类请求直接甩给专家 agent( 引自 agent/README.md,# 示意):
import { agent } from "@yao/runtime";
// Create 钩子:LLM 调用前运行,可改消息、可直接委派
function Create(ctx: agent.Context, messages: agent.Message[]): agent.Create {
const last = messages[messages.length - 1]?.content || "";
if (last.includes("refund")) {
// 不问模型,直接把这轮交给退款专家 agent
return { delegate: { agent_id: "refund-specialist", messages } };
}
return null; // 返回 null = 什么都不改,照常走 LLM
}
跑起来后,对外是一个 OpenAI 兼容端点(agent/README.md):POST /v1/chat/completions。
一句话直觉:你写的三个文件就是笼子的三面墙——一面选动物(connector)、一面定它听到的话(prompts)、一面装拦截机关(hooks)。
2. 顶层全景(一次请求怎么走完)
整个子系统的价值主线是一次请求的一生——Stream() 方法(agent/assistant/agent.go:21,func (ast *Assistant) Stream)。下面这张图从上到下就是它的执行顺序;虚线是"工具循环"的回边。
怎么读这张图: 从上往下是主流程,每个方框旁标了它所在的文件;右边虚线箭头是"模型调了工具、把结果喂回去再问一轮"的循环。
POST /v1/chat/completions
│
▼
┌──────────────────────────────────────────────┐
│ Stream() agent/assistant/agent.go:21 │ ← 主管道入口
└──────────────────────────────────────────────┘
│
① 权限校验 checkPermissions assistant/permission.go
│
② 进栈 + 缓冲 EnterStack context/stack.go:200
InitBuffer context/buffer.go
│
③ 拼历史 WithHistory assistant/history.go
│
④ 起沙箱(可选)initSandboxV2 assistant/sandbox_v2.go
│
⑤ Create 钩子 ───────────────► 若 delegate,直接转子 agent(跳过 LLM)
hook/create.go:13 │
│ └─── handleDelegation → 子 agent 的 Stream()
▼
⑥ 建请求 BuildRequest + 自动检索 shouldAutoSearch
assistant/llm.go / search.go
│
⑦ ┌── LLM 流 executeLLMStream ──────┐ assistant/llm.go
└── 或 沙箱流 executeSandboxV2Stream │◄──┐ assistant/sandbox_v2.go
│ │
⑧ 工具调用 executeToolCalls(带重试×3) │ assistant/mcp.go:223
│ │
⑨ ┌ 有 Next 钩子 → processNextResponse │ 循环回边
├ 无钩子+有工具结果 → executeToolLoop ┘ assistant/loop.go:32
│ (max_turn 到顶 → loop_fallback 委派)
└ 其余 → buildStandardResponse
│
⑩ Next 钩子 hook/next.go:13(可再改、可再委派)
│
▼
⑪ 流式响应 + [DONE](仅 root 栈关闭输出)
context/output.go
主线走一遍(高层,不进代码):
- 请求进来,先校验权限,再进栈(为多智能体建立调用树)并开一个缓冲区(整轮的消息/步骤攒着,退出时一次落库)。
- 拼历史:把这轮输入和数据库里的会话历史合成
fullMessages。 - 若配了沙箱,先起沙箱(必须在钩子之前,好让钩子能访问沙箱上下文)。
Create钩子先跑——它可以改消息、改选项,甚至当场委派给别的 agent(那就跳过后面的 LLM 直接返回)。- 进 LLM 流(或沙箱流),边生成边流式吐给前端;要工具就产生 tool_calls。
- 执行工具(带最多 3 次纠错重试),结果或交给
Next钩子,或进工具循环再问模型几轮。 Next钩子做最后加工/再委派,root 栈关闭输出发[DONE]。
3. 部件一句话职责表
agent/ 下每个子包干一件事。下面是选章时的"部件地图":
| 部件 | 一句话职责 | 在哪(agent/ 下) |
|---|---|---|
| assistant | 笼子本体:装载 DSL、跑 Stream() 主管道、钩子/LLM/工具/循环/委派全在这 | assistant/(核心 agent.go) |
| context | 一次请求的随身上下文:调用栈、缓冲、输出、以及注入给 JS 钩子的 JSAPI(V8 桥) | context/ |
| llm | 模型接入层:解析 connector、探测能力(vision 等)、把消息喂给底层 LLM 库 | llm/ |
| memory | 四层记忆(user/team/chat/context),给 agent 跨请求/跨会话的 KV 记忆 | memory/ |
| sandbox | CLI-Agent 执行器:在隔离盒子里跑 claude/opencode/yaocode/tai 等命令行 agent | sandbox/v2/ |
| mcp | 把 MCP(Model Context Protocol)工具收集、暴露给模型、并执行模型点名的工具 | assistant/mcp.go |
| store | 会话/消息/助手模型的持久化(xun/mongo/redis 多后端) | store/ |
| output | 流式输出适配:把内部事件转成 SSE/OpenAI 兼容 chunk 发给前端 | output/ |
| search | Web / 知识库 / 数据库三路检索,喂给自动 RAG | search/ |
4. 阅读地图(六章建议顺序)
按"由浅入深、先主干后分支"排,建议顺序如下。每章一句话导读(本页即索引,不重复列自身):
-
01-loading.md — 装载:从 DSL 到可运行的 Assistant
package.yao+prompts.yml+index.ts三个文件,是怎么被LoadPath/LoadStore读成一个内存里的Assistant对象的。先懂"笼子怎么建"。 -
02-pipeline.md — 主管道:一次请求的一生(Stream) 逐段拆
Stream():权限→栈→缓冲→历史→沙箱→Create→LLM→工具→Next→输出。这是全系统的主干,建议重点读。 -
03-hooks-jsapi.md — Hooks 与 Context JSAPI:V8 桥与边界注入
Create/Next钩子怎么在 V8 里跑你的 TS,ctx.Send/ctx.agent/ctx.memory/ctx.mcp这些 JSAPI 怎么把 Go 能力注入到 JS 边界。 -
04-toolloop-mcp.md — 工具循环与 MCP:把模型的话落到真实工具 MCP 工具怎么收集与执行、工具调用的 3 次纠错重试、以及
max_turn兜底的多轮工具循环。 -
05-multiagent-stack.md — 多智能体编排:Stack、委派与 A2A 调用栈(Stack)怎么建调用树,
delegate委派与ctx.agent.Call/All/Any/Race的 A2A 并发怎么隔离历史。 -
06-memory-sandbox.md — 记忆与沙箱:四层记忆与 CLI-Agent 执行器 四层记忆(user/team/chat/context)的作用域与生命周期,以及 Sandbox V2 怎么在隔离盒子里跑命令行编码 agent。
5. 巧妙之处速览
后面各章会展开,这里先给"要带走的四个精华",建立预期:
-
钩子统一三模式。 不管是 LLM 模式、CLI-Agent 沙箱模式还是纯 Hook 模式,
Create/Next两个钩子的位置与语义完全一样——都夹在主管道同一处。于是"改上下文、拦截、委派"的心智模 型只需学一遍,三种执行器通用。 依据:Create在 LLM/沙箱分岔之前(agent/assistant/agent.go:218),Next在之后(agent/assistant/agent.go:502)。 -
工具循环
max_turn+loop_fallback双重兜底。 无Next钩子但有工具结果时,进多轮工具循环(默认 5 轮,读mcp.options.max_turn);轮数耗尽或循环报错,不是硬崩,而是委派给内置__yao.loop_fallbackagent兜底收尾。 依据:getMaxToolLoopTurns默认 5(agent/assistant/loop.go:178);buildLoopFallbackDelegate指向__yao.loop_fallback(agent/assistant/loop.go:204)。 -
四层记忆按作用域分层。
user(跨该用户所有会话)、team(团队共享)、chat(单会话内)、context(单请求临时)——同一套 KV 接口,四种生命周期,按需选层。 依据:SpaceUser/SpaceTeam/SpaceChat/SpaceContext(agent/memory/types.go:15-27)。 -
A2A fork 隔离历史。 主线委派(delegate)算正常对话流、会存历史;但
ctx.agent.Call/All/Any/Race这类并发 fork 调用会被识别为 forked A2A,自动跳过历史落库,避免子 agent 的中间对话污染主会话。 依据:IsForkedA2ACall()触发opts.ForceA2A()(agent/assistant/agent.go:70;agent/context/context.go:546)。
6. 顶层代码地图(导航索引)
三列表:主题 → 文件 → 真实符号名。行号会随上游漂移,优先用符号名 grep 定位。所有引用 as-of sourceCommit。
| 主题 | 文件(agent/ 下) | 关键符号 |
|---|---|---|
| 主管道入口(一次请求的一生) | assistant/agent.go:21 | Stream |
| connector/能力解析 | assistant/agent.go:646 | GetConnector、initializeCapabilities |
| 装载 DSL → Assistant | assistant/load.go:317、:222 | LoadPath、LoadStore |
| Assistant 内存结构 | assistant/types.go:29 | Assistant、HookScript |
| Create 钩子 | assistant/hook/create.go:13 | (*Script).Create |
| Next 钩子 | assistant/hook/next.go:13 | (*Script).Next |
| 早退委派(Create 里 delegate) | assistant/next.go:43(调用点 assistant/agent.go:252) | handleDelegation |
| MCP 工具收集/执行 | assistant/mcp.go:75、:223 | buildMCPTools、executeToolCalls |
| 工具循环 + 轮数/兜底 | assistant/loop.go:32、:178、:204 | executeToolLoop、getMaxToolLoopTurns、buildLoopFallbackDelegate |
| 调用栈(多智能体树) | context/stack.go:200、:115 | EnterStack、(*Stack).IsRoot |
| A2A fork 判定 | context/context.go:546 | (*Context).IsForkedA2ACall |
| A2A 并发调用(JSAPI) | context/jsapi_agent.go:83 | agentCallMethod、agentAllMethod、agentAnyMethod、agentRaceMethod |
| JSAPI 注入(V8 桥) | context/jsapi.go:49、:66、:72、:151 | Send、newMCPObject、newAgentObject、memory |
| 四层记忆作用域 | memory/types.go:15 | SpaceUser、SpaceTeam、SpaceChat、SpaceContext |
| CLI-Agent 执行器 | agent/sandbox/v2/runners.go:13 | SupportedRunners(yaocode/tai/claude/opencode/dsh/pi) |
| 沙箱初始化(主管道内) | assistant/agent.go:167 | HasSandboxV2、initSandboxV2 |
| 会话缓冲(整轮攒着再落库) | assistant/chat.go:98、:195;缓冲类型 context/buffer.go | (*Assistant).InitBuffer、FlushBuffer、ChatBuffer |