Zero 架构 · 第一课
剥掉流式、重试、权限这些外壳,一个编码 agent 做的事本质上就是一个循环。Zero
把它叫作 Run,正是这个函数让 Zero 成为一个 agent,而不是一个
聊天框。吃透它,其余 67 个包都只是配角。
对齐 mission:这一课交付你目标的前半部分 —— 把一个 prompt 从输入追踪到最终回答,并说出每一步对应的具体函数。学完 你应该能凭记忆把整个循环讲出来。
一个 agent 回合就是:问模型 → 如果它要求调用工具,就执行工具、把结果喂回去 → 重复;如果它没要求调用工具,那段文本就是答案。 这个循环是对回合(turns)的一次 fold,而工具结果就是每一轮不断 增长的累加器。
在 Zero 里这一切都在一个函数 Run 中,它的骨架是一个有上限的
for 循环:
loop.go:107
func Run(ctx, prompt, provider, options) (Result, error) {
messages := SeedMessagesWithImages(buildSystemPrompt(options), prompt, images) // :130 初始化对话
for turn := 0; turn < maxTurns; turn++ { // :149 回合上限
exposed, _ := partitionTools(...) // :156 本回合暴露哪些工具
messages = compactor.maybeCompact(ctx, provider, messages, exposed) // :161 压缩以守住窗口
stream, _ := streamWithReconnect(ctx, provider, request, ...) // :178 问模型
collected := CollectStreamWithOptions(ctx, stream, forwardingOpts) // :273 抽干流
messages = append(messages, assistantMessage(collected)) // :348 记录回复
if len(collected.ToolCalls) == 0 { // :357
result.FinalAnswer = collected.Text // :453 ← 出口:这就是答案
return result, nil
}
for _, call := range collected.ToolCalls { // :478
toolResult := executeToolCall(ctx, registry, call, mode, options) // :482 执行工具
messages = append(messages, toolMessage(toolResult)) // :495 把结果喂回去
}
// 继续下一回合,此时上下文里已带上工具结果
}
}
这个 2700 行文件里的其它一切 —— 重连、压缩、护栏、自我纠错、模型升级 —— 都挂在这个 骨架上。先学骨架;剩下的都是骨架上的分支。
进入循环前,Zero 先构建初始的 messages 切片:系统提示 + 用户 prompt
(+ 可能的图片)。
loop.go:130
这个切片就是对话状态 —— 循环只会往里 append。每个回合把它拷进一个
CompletionRequest,连同本回合可用的工具定义一起发出去。
loop.go:162
模型通过唯一一道窄接缝触达 —— Provider 接口,一个单一的流式方法:
types.go:209
type Provider interface {
StreamCompletion(ctx, request) (<-chan StreamEvent, error)
}
正是这一个方法,让 Zero 能支持 25+ 家 provider:循环里从不提 OpenAI 或 Anthropic ——
它只讲 StreamEvent。CollectStreamWithOptions 把 channel
抽干成一个 CollectedStream,内含最终的 Text 和任何
ToolCalls,同时把增量实时转发给 TUI。
loop.go:273
这一个分支就是「继续 vs 结束」的全部判定逻辑: loop.go:357
tool 角色的消息,然后落入下一轮迭代,让模型对它刚学到的东西作出反应。
loop.go:478注意这个不变量:每个已声明的工具调用都恰好得到一条工具结果消息, 即使护栏在批次中途叫停 —— Zero 会为剩余调用回填「已中止」占位结果,好让记录下来的 历史对严格的 provider 重放仍然有效。 loop.go:509
for turn := 0; turn < maxTurns 这个上限(默认 12)是安全网:自然完成会
经由「无工具调用」路径提前退出;触到天花板意味着 agent 在半途被截断,循环之后单独处理。
loop.go:112、
loop.go:628
messages 是一条只追加的磁带。每个回合写入一条 assistant 记录,然后是
零条或多条 tool 记录。模型每个回合都读整条磁带。所谓「agent 性」不过是:
模型的输出能把内容加进它自己的下一次输入。
先作答,再回看。费力的回忆才能让知识真正扎根。
Zero 的 Run 循环凭什么判定一次运行结束?
为什么 Zero 支持 25+ 家 provider 却不用改动循环?
一个工具执行完之后,循环拿它的结果做什么?
把 Run 从头到尾读一遍 —— 只看骨架,略过重试/压缩的分支。打开它,循着上面的
四个动作走:
less +107 ../zero/internal/agent/loop.go
# 或在你的编辑器里: zero/internal/agent/loop.go:107
别想一口气吃完整个文件。找到 for turn 那行、
len(collected.ToolCalls) == 0 那个分叉,以及内层的
for _, call := range collected.ToolCalls。这三个锚点就是整个循环。
StreamEvent 长什么样」,或者「executeToolCall 里面到底
是什么?」(那就是第二课)。