当前位置: 首页 > news >正文

Grok Build 是如何工作的(一):三层循环

这是一个系列博客,剖析 Grok Build 是如何工作的。

Copiloted by Grok Build.

关于 Grok Build

Grok Build 可能是现在最值得学习的“大而全”开源 Harness(这里说的大而全主要是和 Pi 对立,Pi 不是一个完全开箱即用的 Harness,它需要你自己搭积木做配置,和 Claude Code, Grok Build 不是同一个路数)。我不知道大家会不会认为上面的结论是某种暴论,但我们可以细数一下:

  • Claude Code 很优秀但不开源(泄漏的开源版本已经被无数人做成了各种“Github 教学项目”,但绝大多数都是 AI 写的可读性很差的项目,而且经过这么长时间,泄漏版本应该已经与最新的 Claude Code 区别甚大)。
  • Codex 开源,但我主观认为它不是一个很好的 Harness,codex 的能力很大程度上依靠的是模型的智能水平,而非 Harness 框架的能力。至少我个人在社交媒体上看到的对 Codex 的吐槽远远多于对 Claude Code 的,包括 Bug、预期外的沙箱拦截、UI 设计、略显混乱的 Agents 调度等等。我个人的感受也是如此,Codex 不是一个特别优秀的 Harness 产品。当前 Codex 的竞争力似乎主要来源于优秀的模型能力和 Tibo —— 他正在不断按下额度重置按钮。
  • Cursor 实际上很优秀,但它不开源。很可惜,我听说他们为了每个(系列)模型都定制了不同的 Harness,我很想知道他们具体做了哪些区别。
  • Opencode,常常无法约束模型行为,我遇到过几次模型意外提早结束一个 Turn 的情况,并且我遇到过在一系列边界条件下导致我丢失了两个小时内我用 opencode 做的全部代码改动的情况(当时在写一个汇报 html,不是正式项目,所以没有用 git)。除了 TUI 比较好看以外,它也没有什么让我好奇的功能。Oh-my-opencode 引入了更多噱头,但我很质疑 ultrawork 是否真的带来了什么交付质量上的显著提升。绝大多数的 Harness 所支持的长程任务模式都仅代表它能产出更多交付产物,而非带来智能水平的提升。
  • 没有人在意 OpenClaw。

虽然我是大概四月份下载的 Grok Build,但当时没怎么用。五月份 Grok Build 正式发布,而它真正进入大家视野实际上是随着 Grok 4.5 的发布。大家突然发现它干活快速,可靠,稳定,简单易用,有好用的 TUI。这不完全是我的一家之言,可以看看社群评价:

img img 媒体 (3)

以上社交平台的吹捧言论均不来自本人

除社交平台的用户反馈以外,还可以看看 Artificialanalysis 的 coding-agents 榜单。在我写这篇文章的时候,Grok 4.5 + Grok Build 在 Artificial Analysis Coding Agent Index 上逊于 Claude Code + Opus 5 (max),优于Codex + GPT 5.6 Terrra (max). 在 Terminal-Bench v2 上,仅次于 Codex + GPT 5.6 Sol (max).

我其实不是一个很爱研究 coding 产品功能的人,比起研究用户侧的功能,我更关注他们如何实现了这样的优秀效果,所以就不在这里介绍 Grok Build 的功能特色了。如果你还没有用过 Grok Build,完全值得下载下来试试。

不过在正式开始之前,我还是要提示这份文档的不足。我们可以通过源代码来了解 Grok Build 是如何实现的,但却不能永远理解为什么要这样实现。总会有许多可选的技术路线、实现方式和看起来难以决断的取舍,但促使 Grok Build 变成现在这样的,往往是来自于系统内部微妙的牵制关系,捕捉到的 bad case,以及内部的完善测试集带来的持续反馈。若非亲自参与构建,难以指望彻底理解。

三层循环

Grok Build 的主流程包括三层循环。从大到小分别是:

Session 级别循环


后文将称为 L1 循环。这一层级的循环,会用 select! 多路等待多类事件源,任一就绪则进入对应分支处理,然后回到等待。

命令 channel

命令 channel 是异步消息队列,也是外部控制 session 的主入口。Session 启动时(spawn_session_actor)做以下事情:

  • 建一对 channel:cmd_tx(发送端)/ cmd_rx(接收端)
  • 把接收端交给 session,只由 L1 取消息
  • 把发送端经 SessionHandle 克隆给外部持有方(TUI、workflow、后台回调等)
  • 建立一个 ChatState,用于保存当前对话记录。

发送方只 send,不直接改 session 状态;L1 取出后串行处理。

消息类型是 SessionCommand,种类很多:Prompt、会话生命周期、模型/模式变更、压缩、队列操作、任务通知、查询回包等。Prompt 只是其中一种。

L1 在等什么

L1 不只等命令 channel。select! 的每一臂都是一个 future,来源不同:

• 若干路来自 channel 的 recv(命令 cmd_rx、turn 结束的 completion、ChatState 事件、session 事件等)
• 若干路来自 定时器(memory 空闲 flush、dream 检查)
• 一路来自 watch(模型切换)

这些不同来源的 future 挂在同一个循环上。

Turn 循环


后文将称之为 L2 循环。其入口是handle_prompt. 不过其实这一层循环大多数时候只会执行一次,原因后面会写。

L1 在 maybe_start_running_task 里另起一个异步任务跑它;跑完后结果送回 L1。同一时刻最多有一个在运行的 L2,L2 按顺序处理本轮输入。


L2 大致分几段

  1. 开场
    根据 prompt_id 判断这条输入从哪来(用户、定时任务、后台通知、goal 等),标记 turn 为活跃。!开头加命令会直接走 bash,整轮在这里结束。

  2. / 命令

    • 多数内置命令(/session-info、/yolo 等):本地执行完,本 turn 直接结束,不进模型。
    • skill 形式的 /xxx:展开 skill 信息,改写成普通 prompt,继续往下。
    • 普通文字:不当 slash 处理,继续往下。

  3. 拼本轮用户消息,写入对话
    这是 L2 的主要任务之一:

    • 解析正文、附件、图片、skill 信息
    • 过大则截断;图片做 normalize / 落盘
    • 注入一批 reminder(MCP、plan mode、任务恢复、workflow 状态、中断提示等)
    • 把用户消息写进 ChatState
    • 跑 UserPromptSubmit hook

    做完这些,对话里已有本轮用户消息,模型还没开始。

L2 的 loop

首先解释一下 L3。L3:调用模型、执行工具,直到本轮采样与工具循环正常或异常结束。

用户消息写入对话历史之后,L2 进入这一段:

循环 {进入一次 L3若 L3 结果不是正常完成 → 结束 L2若存在状态为进行中的 goal → 执行 goal 轮次结束判定需要继续 → 将续跑说明写入对话历史,再次进入 L3不需要因 goal 继续 → 进入 stop 检查执行 stop 检查允许结束 → 结束 L2不允许结束 → 将 hook 反馈写入对话历史,再次进入 L3
}

无进行中的 goal,且 stop 检查允许结束时,该循环通常只执行 1 次。

除 goal、stop 以外,L3 上的完成条件

在 Agent 定义里,会写有哪些必须要调用的工具。L2 会检查 Agent 是否调用了工具,如果没有则会拦截。

  • L3 结束时已调用该工具 → 视为满足完成条件,返回外层循环。
  • 未调用 → 写入纠正说明,在完成条件逻辑内重试 L3(含退避)。重试不一定经过外层循环。
  • 重试次数用尽,或 L3 因步数上限、动作停滞等原因结束 → 不再为完成条件重试,将结果返回外层。

完成条件来自 agent 定义。Goal 来自 session 级目标状态。二者独立。

L3 结果与外层循环:

L3 结果 外层行为
取消、错误、步数上限等非「正常完成」 结束 L2;不执行 goal 判定与 stop 检查
正常完成,且提供方返回拒答 若 goal 为进行中,则 pause goal;结束 L2
正常完成 执行 goal 判定(若适用),再执行 stop 检查

goal 的判定

前置条件:goal 功能已启用,且 goal 状态为进行中。否则跳过本段,直接执行 stop 检查。

步骤:

  1. 评估

    session 在 L3 正常结束后,额外发一次隐藏 LLM 请求:

    • 无 tools,强制 JSON schema 输出
    • 超时 30s;最多试 2 次(先小模型,失败再退到当前会话模型)
    • 用户在 TUI 里一般看不到这是“另一个 agent”,代码里 system prompt 自称 hidden completion evaluator

    System prompt 固定写死在 goal_evaluator.rs,大意:你不是 coding agent,只根据 goal + transcript 判三种结论(continue | candidate_complete | blocked),要保守,别信模型嘴上说做完了。

  2. 根据评估结果更新 goal

    • 未完成:记录进度;状态保持进行中
    • 候选完成:运行验证。验证通过可将 goal 标为已完成;未通过则记录差距,状态可仍为进行中;验证基础设施失败或次数达上限可将 goal pause
    • 阻塞:记录阻塞信息;同一阻塞连续达到阈值可将 goal pause
  3. 读取 goal 状态

    若已不是进行中(已完成、已暂停、预算受限等)→ 本 L2 不再因 goal 再次进入 L3。

  4. Token 预算

    超出预算时,停止因 goal 再次进入 L3(goal 可变为预算受限)。

  5. 状态仍为进行中

    生成续跑说明(目标摘要、计划引用、验证差距、下一步等),写入对话历史,然后再次进入 L3。

主路径下,goal 接近结束通常需要:评估为候选完成,且验证通过。

Stop 检查

Stop 检查调用用户/客户端注册的 Stop(主 session)或 SubagentStop(子 agent session)类 hook。
两类来源:文件配置的 hook,以及 client 注册的 hook。都没有启用时,直接允许结束 L2。

判定顺序:

  1. 未注册 上述 Stop / SubagentStop hook → 允许结束 L2
  2. 本 turn 内因 stop 再次进入 L3 的次数 ≥ 8 → 强制允许结束 L2(达到上限后本次不再询问 hook)
  3. 执行 hook:
    • hook 返回 强制结束(prevent continuation)→ 允许结束 L2
    • hook 返回 继续(block / deny,要求 agent 接着干)→ 将反馈写入对话历史,同一 L2 内再次进入 L3
    • hook 未要求继续 → 允许结束 L2

反馈内容来自 hook 的 block reason 与 additional context,写入后主模型在下一轮 L3 能看到。

与 goal 的关系:

  • 顺序固定:先 goal 判定,后 stop 检查
  • goal 判定为需要继续时,不执行 stop 检查
  • stop 只能在「本 turn 已不再因 goal 续跑」之后拦截结束

三种导致再次进入 L3 的机制

机制 条件 作用位置 备注
完成条件(completion requirement) agent 配置了必须调用的工具名,且本轮未调用 多在 L3 外包的 recovery 重试里 内置 agent 默认未
Goal goal 状态为进行中,且轮次结束判定为需要继续 L2 外层循环 隐藏评估器 + 可选验证器;续跑说明写入对话后再进 L3
Stop hook Stop / SubagentStop hook 要求继续 L2 外层循环 同 turn 最多因 stop 续跑 8 次

三者都发生在同一次 L2(同一条已开始处理的输入)内,不经过 L1 重新调度。

采样与工具循环


后文将称之为 L3 循环。它是一个经典的 ReAct 循环,入口是 process_conversation_turn

L3 由 L2 调用。同一时刻一个 L2 里最多同时跑一段 L3(外层 loop 串行多次进入时,是一次结束后再进下一次)。L3 的职责:在当前 ChatState 上反复 采样模型 →(有 tool call 则执行工具并写回结果)→ 再采样,直到满足结束条件之一。
主路径的结束条件是:模型某一轮输出 没有 tool call(且未触发下文的续跑分支)。

进入循环前

  1. 准备本 turn 可用的 tool 定义(含 MCP 等待与统计)
  2. 输出 JSON Schema(可选)
    调用方可为这一次 Prompt 指定最终答案的结构(Prompt.json_schema / ACP meta.outputSchema / headless --json-schema)。必须是 JSON object。普通 TUI 聊天一般为无。
    有合法 schema 时:
    • 后端原生支持 schema → 走原生 structured output
    • 否则 → 注入 reminder,要求模型最后用 StructuredOutput 工具交答案
  3. 进入 loop

每一圈大致顺序

{1. 动作停滞硬停检查(见下)2. 采样前注入 / 维护• 处理插话(用户 mid-turn 输入)• skill / monitor 等 pending 注入• 首轮 memory(本 turn 内最多注入一次,未来会讲 memory 系统)• 如有必要,告诉模型当前 MCP 服务器/工具集合发生了什么变化的 system-reminder• token 将超窗口 → auto-compact(改 ChatState 历史)3. 从 ChatState 组装请求(build_request:messages + tools 等)4. 调用模型(流式/非流式由采样层处理)5. 把 assistant(及流里带来的相关项)写入 ChatState6. 分支:• 无 tool call → 尝试结束(中间可能 TodoGate / 插话 再 continue)• 有 tool call → 执行工具,结果写入 ChatState,再进入下一圈7. 工具步数达到 max_turns(若配置了)→ MaxTurnsReached,结束 L3 
}

无 tool call:默认视为本段 L3 正常完成

模型本轮输出不含 tool call 时:

  1. TodoGate(策略开启且非拒答等情况下):若 todo 列表仍有 pending / in_progress 且判定应推进,可注入 reminder 并 continue(有次数上限,用尽后放行)
  2. 插话:若 turn 中途攒了用户插话,先写入对话再 continue
  3. 否则 → TurnOutcome::Completed,带上本段调用过的工具名列表等,返回 L2

因此:

  • 常规结束:无 tool call → Completed
  • 这不等于整次 L2 一定结束;L2 外层还可能因 goal / stop 再进 L3

有 tool call:执行后继续

  1. 将 tool call 记入本段 tools_called
  2. 动作停滞 统计(同工具同参数连续重复)
  3. 执行工具(权限、沙箱、后台任务等走工具运行时)
  4. 工具结果写回 ChatState(模型下一轮能看到)
  5. 部分工具结果会要求 follow-up 用户消息,则写入后再 continue
  6. 若配置了 max_turns,工具轮次将超过上限 → MaxTurnsReached,结束 L3,不再采样
  7. 否则进入下一圈(再次采样)

执行工具前后,L1 仍可收到 Cancel 等命令;取消会中止当前 L2/L3。

动作停滞(action stationarity)

防止模型对同一工具、同一参数(或纯 no-op)空转:

情况 阈值(当前代码) 行为
连续相同 tool call 约 8 次 注入 nudge reminder,提示停止空转轮询
连续相同 tool call 16 硬停:StationarityEnded,结束 L3
连续 true no-op 类调用 4 硬停阈值更低

硬停后 L2 按「非普通 Completed」处理:不跑 goal 续跑那条正常完成路径(与 L2 表中非正常完成一致)。

其它结束 / 特殊结果

结果 含义
Completed 正常结束(通常无 tool call,或 structured output 交卷成功)
Cancelled 用户取消或其它取消路径
MaxTurnsReached 达到 agent/session 配置的工具轮次上限
StationarityEnded 动作停滞硬停
错误 采样/工具等不可恢复错误向上返回
提供方拒答(content filter) 仍可能记为 Completed,但带 refusal;L2 侧对 goal 会 pause

和 L2、完成条件的边界

职责
L3 采样+工具循环;写 assistant / tool 结果;auto-compact;插话与 TodoGate 等 turn 内续跑
L3 外的 completion requirement L3 返回后检查是否调过规定工具;未调则注入 reminder、退避后 再跑整段 L3(多数不经过 L2 的 goa
L2 外层 L3 已 Completed 之后:goal 评估/验证、stop hook

Context 相关:

  • L2:本轮用户消息、多数 reminder 写入 ChatState
  • L3 采样前:再补 memory / MCP / 插话等,并可能 compact,再 build_request 生成发给模型的 message list

发给模型的格式:由 ChatState 中的对话条目转成 API 请求(常见为 OpenAI 兼容的 role + content 列表)。<system-reminder> 标签会作为某条 user content 里的文本。

总结

run_session(L0,session 基本的生命周期;接受外部任何信息)││  同时听几路,谁先到处理谁:│  · channel 来了 SessionCommand│  · 当前 turn 跑完(completion)│  · 空闲定时(memory flush / dream 等)│  · chat-state / 模型切换等旁路│  · channel 关了 → 收尾退出│├─ Prompt 指令│    ││    ├─ 入队(同时最多一个 running turn)│    ├─ send_now → 可取消当前 turn,优先这条│    └─ 空闲 → 队头起 handle_prompt(L1)│         ││         ├─ 开场:prompt 来源、turn 标记、lifecycle…│         ├─ / 命令?│         │    · 多数 builtin → 本地做完,本 turn 结束│         │    · skill 类 / → 化成普通 prompt 继续│         │    · 普通文字 → 往下│         ├─ 拼本 turn 的 user 消息(附件、reminder…)│         ││         ├─ loop(harness 还不让停就再来)│         │    ││         │    ├─ process_conversation_turn_with_recovery│         │    │    · 若配了 completion requirement:│         │    │      L2 正常结束但没 call 约定 tool│         │    │      → 塞 reminder、退避、再跑整段 L2│         │    │    · 否则直接进 L2│         │    ││         │    │    L2 ReAct loop│         │    │      ├─ 圈头:若同一 tool+参数已连打满 16 次│         │    │      │         → Cancelled(ActionStationarity),整 turn 停│         │    │      ├─ 每圈开头:插话 / skill / monitor / 首次 memory 等│         │    │      ├─ 采样前:可能 auto-compact(默认 ~85%,可 per-model)│         │    │      ├─ build_request(history + tools)│         │    │      ├─ 调模型│         │    │      │    · 有 tool → 权限 → 执行 → 结果入对话│         │    │      │               · 同一 tool+参数连 8 次 → 塞 nudge 再继续│         │    │      │               · 否则继续下一圈│         │    │      │    · 无 tool → TodoGate?插话?│         │    │      │               还要继续则再圈,否则 L2 Completed│         │    │      └─ 其它:取消 / 权限拒 / max_turns / 错误…│         │    ││         │    ├─ 非 Completed / refusal → 跳出 L1 loop│         │    ├─ goal 还 Active 且要 Continue│         │    │    → 塞 continuation → 再开 L2│         │    └─ Stop hook:KeepWorking → 塞 feedback 再跑│         │                 AllowStop → 跳出│         ││         └─ 记账、回包 → completion 通知 L0│├─ turn 完成(completion)│    ├─ 收尾、冲滞留插话│    ├─ 队列有下一条 → 再起 L1│    └─ 否则 drain 通知 / 标 idle│├─ 空闲定时 / 其它指令│    memory、换模型、改 mode、shutdown、│    改队列(删/改/重排/hold combine)、插话、MCP 开关…│└─ Loop

最后,我也让 AI 画了张架构图。

grok-build-three-layer-loops

预计下一章会写模型调用相关的内容

作者的邮箱:tokamak9000@163.com。如有问题,欢迎讨论

http://www.jsqmd.com/news/1335941/

相关文章:

  • 从技术分析到感受共鸣:解析当代音乐创作的价值判断
  • Unity游戏自动翻译终极指南:XUnity.AutoTranslator从入门到精通
  • Unity跨平台VR交互系统构建:基于SteamVR插件的工程化实战方案
  • 2026年上海GEO服务商选型全指南 中小微企业适用 - 筑云鲸
  • Grok 做表格落地实操指南,AI 导出鸭凭借多元导出方案,破解 AI 制表后文档导出痛点
  • AUTOSAR 存储
  • 【故障诊断】基于人工蜂鸟优化算法AHA优化双向时间卷积神经网络BiTCN实现轴承数据故障诊断附Matlab代码
  • 5分钟快速上手:PoeCharm中文版Path of Building完全指南
  • 玩转 OpenClaw 小龙虾,Windows 平台 AI 数字员工搭建全流程(含安装包)
  • 深度学习入门幻觉正在毁掉你的职业发展,资深AI研究员紧急发布5条不可逆的学习红线
  • 微信小游戏辅助神器终极指南:快速提升游戏体验的完整教程
  • JSON实战指南:从语法解析到API配置与数据转换的避坑技巧
  • 方差计算全解析:从基础概念到概率分布推导与工程应用
  • 抖音批量下载器:一键打造个人专属素材库的终极方案
  • Java Stream API中Duplicate key异常:原理、排查与解决方案全解析
  • 2026年电瓶车托运价格表:寄电动车多少钱?整车托运避坑指南 - 快递物流资讯
  • Jupyter Notebook 完整指南:从虚拟环境安装到深度定制与彻底卸载
  • python基于Django的智慧学习平台
  • 如何轻松编辑Unreal Engine游戏存档:uesave终极指南
  • FIR滤波器窗函数设计法:从矩形窗到吉布斯现象
  • Godot游戏开发:使用gd-ecs框架实现ECS架构,提升代码可维护性
  • C2000 DSP ePWM模块实战:从基础配置到互补死区与动态更新
  • RuoYi-Cloud项目登录流程+密码验证
  • AI Agent 面试题 508:如何实现Agent的约束动态更新和重规划?
  • 战略地图:从目标到执行的核心工具
  • MySQL Binlog日志保留策略与清理方法详解
  • 如何用开源工具构建个人音乐库:MusicDownload终极指南
  • Unity Wii Remote API开发指南:低成本体感交互实现与实战
  • 从H.264/H.265码流手动解析SPS:获取视频宽高与帧率的底层原理与实践
  • 从特斯拉塔到无线充电:电磁谐振原理与现代工程实践