Grok Build 如何用 Agent 循环压缩上下文

本文基于 xAI 开源仓库 xai-org/grok-build 的 2026-07-16 master 快照(原始拆解所记录的 commit 为 c68e39f)撰写。它是从内部 monorepo 周期性同步出来的镜像仓库,采用 Apache-2.0 许可证,不接收外部 PR。代码和产品都会变,文中涉及文件路径、阈值、默认值和命令时,请以你本地安装版本为准。

很多人第一次打开一个编码 Agent 的源码,都会掉进同一个坑:看见几十个 crate、几百个配置项、三套工具协议,马上开始背名词。看完只留下一个模糊印象:”它好复杂。”

Grok Build 的重点在于把一件很朴素的事做得很彻底:让模型在真实电脑上持续完成任务,同时尽量不失控。模型会误用工具,会重复读文件,会把长日志塞满上下文,会在一条 shell 命令里夹进危险操作,也会把已经失败的方案再试一遍。一个能在线上长期使用的编码 Agent,必须把这些不体面的情况当成正常输入。

这篇文章不打算逐个翻译 Rust 模块名。我会带着一个具体任务来读代码:”测试失败了,找原因、修改代码、跑验证。” 先看它如何一轮轮推进,再解释提示词、工具、压缩、记忆、权限和沙箱各自在解决什么问题。即使你从没做过 Agent,也可以把它当成一张生产级 Harness 的设计图来读。

先给结论:Grok Build 不是模型本身,而是一套运行环境

大语言模型只能根据已看见的文本生成下一个 token。它不能直接打开文件、执行 cargo test,更不能凭意念修改你的代码。真正把这些动作接起来的是模型外面的程序,业内常叫 Harness 或 Agent Runtime。Grok Build 就是这层运行环境。

把它放进一次修复任务里,角色分工会非常清楚:

图里的模型只负责 C:在当前信息下提出下一个动作。B、D、F、G 都是 Harness 的工作。这个区分非常重要,因为很多初学者会把 Agent 的所有表现都归因于模型。实际上,模型即使足够聪明,工具给错、权限过宽、上下文被日志淹没,照样会做出糟糕结果。

例如,模型先调用 read_file 查看失败测试,接着调用 grep 找到处理函数,提出一个 search_replace 编辑,最后运行测试。Harness 必须依次做四件事:验证工具参数;判断这次编辑或命令是否需要批准;在受限环境里执行;把结果以模型能理解的消息格式写回历史。测试仍然失败,模型才有机会依据真实输出再试一次。没有这个闭环,”会写代码”只是一次文本续写。

Grok Build 的 crate 名以 xai-grok- 为前缀,二进制发布名是 grok。从仓库组织方式看,采样、会话、工具、权限、上下文和终端后端各自独立。拆开不代表抽象更优雅,首先是为了让每一层都有明确的失败处理。

一、提示词为什么要尽量不变

1.1 System prompt 是程序的一部分

在普通聊天里,system prompt 常被当作一段角色设定。在编码 Agent 里,它更接近运行时的操作规程:什么时候先阅读再修改,怎样汇报风险,工具调用后该继续还是停止,遇到用户插话怎么处理,最终回答要包含哪些验证信息。

Grok Build 的 xai-grok-agent/src/prompt/prompt_encrypted.rs 里有三份提示词模板。它们以 XOR 方式混淆,算法可概括为:

1
plain_byte = encrypted_byte XOR ((seed + index) & 0xFF)

种子本身就在仓库中,注释也直说这是 “obfuscation, not security”。它只能防止别人用 strings 随手扫出提示词,不能保护秘密。程序把解密后的字符串放进 Zeroizing<String>,用完尽量清零。这是两个不同目标:前者降低二进制中可见性,后者缩短敏感内容在内存中的停留时间。

这里真正值得学的不是 XOR,而是三份 prompt 的职责划分:短的 base 版提供身份、工具调用和安全底线;较长的 codex 版包含计划、前置说明、任务范围与最终回复的细则;subagent 版则给子 Agent 一个较小的工作基座。不要把这种划分误读成”提示词越长越好”。不同工作场景需要不同约束密度,探索子任务若带着主会话全部规则,往往只是浪费上下文。

1.2 静态前缀和 KV Cache

源码中有一个很工程化的选择:操作系统、shell、工作目录、日期等会变化的环境信息,不放在 system prompt,而是在首条用户消息的 <user_info> 里发送。项目的 AGENTS.md 注入路径旁还有一句很直白的注释:一旦放入前缀,就不要替换,否则会破坏 KV Cache 前缀。

先解释这句话。模型每次推理都要处理前面的输入 token。许多服务会缓存完全相同的长前缀对应的注意力键值,也就是 KV Cache。下一次请求若仍以同样的系统提示开头,服务端能复用此前的计算;如果你把当天日期、当前目录、git 状态混进这个前缀,前缀每天甚至每轮都不同,缓存命中就没了。

因此可以把输入拆成两层:

内容 是否应该稳定
静态策略层 身份、工具使用规范、长期项目规则 尽量稳定
运行时事实层 cwd、日期、shell、当前任务、当前 git 状态 可以变化

这个分层看起来只是省钱,实际还影响延迟和行为一致性。静态策略一会儿被替换、一会儿被附加,模型看到的指令结构也在漂移。Grok Build 连日期跨天都不重发整段前缀,而是追加一次性 <system-reminder>。日期格式被固定成常量,因为改格式本身就是模型可观察到的变化。

如果你在自己项目里设计 Agent,先问一个问题:哪些文字是每回合都必须重新算一遍的事实,哪些只是长期规则?把两者混在同一段大 prompt 里,既费 token,也让调试变得困难。

1.3 读这类源码,先追一条状态流

刚开始读 Agent 源码时,不建议从 crate 名称一个个点开。很容易在配置结构和 trait 定义里绕半天,最后仍说不出一次任务到底从哪里开始、在哪里结束。更有效的办法是挑一个你关心的动作,沿着它的状态流往下追。

以”编辑一个文件”为例,可以按下面顺序搜索:先找工具注册表,确认模型实际看见的工具名与参数;再找工具调用的分发处,看 JSON 参数怎样变成内部请求;接着找权限检查和 hook 在调用前后各做什么;最后找到真正写文件的实现,以及结果怎样封装回 tool 消息。这样读完一条链,你就能回答四个比模块名更有用的问题:模型能请求什么,运行时在哪儿拒绝,它实际修改了什么,下一轮模型究竟看到了什么。

读压缩和记忆也用同一招。不要先盯着总结提示词,先找”什么时候触发”这个判断,再看触发后有哪些消息被保留、哪些字段重新注入、失败时状态如何写回。你会发现真正决定行为的常常是阈值判断和状态重建,而不是那段看起来最显眼的 prompt。

我一般会边读边画一条很简陋的链:输入 → 状态 → 模型请求 → 策略检查 → 执行 → 结果 → 新状态。任何一个看不懂的模块,都把它放回这条链问一句:它读取什么状态,写回什么状态,失败会怎样?如果答不上来,继续沿调用方找;如果答得上来,细枝末节可以稍后再看。这个方法同样适用于 Claude Code、Codex 或你自己的 Agent 项目。

实际操作时,最好把源码版本固定下来,再把每条结论旁边记上路径和触发条件。比如不要只记”它会自动压缩”,要记成”会话层在使用量严格超过阈值时触发,随后由压缩模块重建消息历史”。前一种笔记下周就会忘,后一种可以直接回到代码核对。遇到看起来很漂亮的注释,也别急着把它当行为事实。继续找调用点、单元测试和错误分支。生产代码里最有价值的信息经常藏在 if 条件、超时参数和测试用例里,它们写的是系统实际允许发生什么。

1.4 外部输入必须有出口

启动会话时读 git 状态似乎没有风险,实际上也可能卡住或吐出很长的内容。Grok Build 对这类输入设置了 2 秒超时、10,000 字符上限,并尽量在最后一个换行处截断;为空就不放这个区块。

这条规则适用于任何外部输入:命令输出、网页抓取、MCP 返回、IDE 诊断。它们都可能慢、空、格式异常或巨大。可靠的运行时不会假定”这条辅助信息一定能拿到”,而是明确写出拿不到时怎样继续。一个简单的伪代码是:

1
2
3
status = run("git status --short", timeout=2)
if status.ok and status.stdout:
context.add(truncate_at_newline(status.stdout, limit=10_000))

这里的重点不是 10_000 这个数字,而是超时、上限、空值三个出口都存在。没有这些出口,Agent 可能在任务开始前就被一个坏掉的 git hook、网络盘或超长输出拖死。

二、工具不是函数列表,而是模型的操作手册

2.1 同一能力,为什么会有多套工具皮肤

Grok Build 按风格提供多套工具集,例如 codexopencodegrok_buildgrok_build_concisegrok_build_hashline。它们都接在同一套 Harness 上,却可以给不同模型或兼容目标不同名称、参数形状和描述。比如一个工具集偏向 apply_patch,另一个提供完整的 read_filesearch_replacegrep、终端、LSP、web 等原生工具。

这说明一个常被忽略的事实:工具协议是模型体验的一部分。两个模型都能”编辑文件”,但其中一个更擅长精确字符串替换,另一个更适合补丁格式,给它们同一把工具未必更稳定。

为了避免模板和工具名脱节,提示词不直接写死函数名称,而是通过类似 ${{ tools.by_kind.* }} 的占位符在最终组装时渲染为当前工具集的名称。这样,策略中说的”先读文件再编辑”不会因为把 read_file 换名而变成失效指令。

这比看上去重要。很多 Agent 的 prompt 还写着旧工具名,函数注册表早已换了新协议。模型就会一边看到 A,一边只能调用 B。排查这种问题时,别只检查函数有没有注册,也要检查最终发给模型的工具描述与提示词是否来自同一个配置。

2.2 好的 description 会教模型怎样成功

工具的 JSON Schema 只解决参数类型,不告诉模型如何在真实任务中使用。Grok Build 的工具 description 会写调用例子、反例和出错后的修复方式。hashline 编辑工具尤其明显:当锚点过期时,它会返回新的锚点、附近带锚点的上下文,并告诉模型无需重新读取整个文件,而不只抛出一句笼统的 “stale”。

对模型而言,工具错误是下一轮的训练样本。错误信息如果只给人看,模型仍要猜怎么恢复;如果把可执行的恢复路径写进去,少一次读文件就少一次调用、少一段上下文,也少一次偏离任务的机会。

所以可以用一个很实用的标准审查工具:一个第一次调用失败的模型,能否仅凭错误消息完成第二次正确调用?如果不行,description 和错误文案还不够好。

2.3 MCP 工具多到装不下时怎么办

MCP 允许连接很多外部服务。若把几百个 MCP 工具的名称、说明和 Schema 全塞进系统提示,模型还没开始写代码,窗口就被工具定义占了。Grok Build 的做法是把 MCP 工具移出常驻上下文:先用 search_tool 通过 BM25 搜索工具描述,再用 use_tool 按限定名调用,例如 linear__save_issue

BM25 是传统文本检索算法,它更看重词是否出现、出现得多不多、是不是稀有词。这里不需要先理解向量数据库:把它看成终端里的高质量关键词搜索即可。模型先说”我需要一个创建 Linear issue 的能力”,系统只把相关工具找出来,之后才能调用。

这是渐进式披露的典型用法。常驻上下文保留目录,细节在需要时再拿。它同样适合项目文档、Skills 和大型 API 集合。

三、hashline 编辑:把模型常犯的错写进协议

普通的 old_string → new_string 替换十分直观,却有两个老问题:文件里相同片段太多,替错位置;模型读完文件后,用户或格式化器已经修改了它,旧文本不再可靠。

Grok Build 的 hashline 工具给每一行增加一个锚点,阅读结果类似:

1
2
22:abc:rst→return normalize(user_input)
23:kqd:rst→save(result)

其中行号用于定位,行 hash 用于确认那一行是否还是当时看到的内容,chunk 指纹用于确认周围片段是否仍新鲜。源码使用经过空白归一化的 FNV-1a 32-bit hash,并把字节映射为小写字母。默认锚点只有 3 个字母,空间约为 26³ = 17,576,所以碰撞并不被当作不可能事件。它靠行号、分块指纹和恢复逻辑共同工作,而不是迷信一个短 hash 能唯一标识全文件。

这种设计很值得初学者借鉴,因为它在区分两件不同的事:”我找到了长得像的文本”和”我确认这是模型上次读到的那块上下文”。后者才是安全编辑真正需要的保证。

3.1 为什么要先校验整批,再一次写入

假设模型要同时改三处校验逻辑。若工具按顺序应用,第一处成功,第二处因锚点过期失败,文件就进入只改了一半的状态。Grok Build 先基于编辑前快照校验全部锚点,任何一个失败,整批都拒绝,并明确回复 “none were applied”。

这就是原子性。它不是数据库专属概念,只要一次操作中的多个变更必须一起成立,就值得用。对 Agent 更重要,因为模型经常会把一组相关修改当作一个推理步骤。半成功状态会让下一轮模型误以为整体已经完成,之后的补救成本很高。

锚点过期时,工具会在附近约 ±15 行内寻找唯一候选。如果找到,会把新的锚点和约 ±5 行的上下文回给模型;如果找不到,才要求重新读取。这种”先局部恢复,后全量重读”的策略非常节省:它承认文件会变,但不把每次小变动都升级成重读整份文件。

3.2 容错不是放松校验

源码还专门处理了模型的几种错误输出:把 →内容 后缀一起带回来;把锚点前缀混进真正要写入的内容;漏掉行号只剩 hash。对于最后一种,若 hash 在全文件唯一,工具可恢复;对于缺少 chunk 段的锚点,工具直接判定 stale,避免悄悄退化为只校验单行。

这是一条很成熟的产品原则:根据真实失败分布增加容错,但不能让容错悄悄降低安全语义。比如只要模型省略一段字段就自动放行,看似成功率提高,实际可能让原本的并发保护失效。

hashline 目前不是默认工具集,需显式设置 file_toolset = "hashline" 才启用。仓库同时保留了 ContentOnly、ChunkFingerprint、CheckpointChain 等候选方案和离线 benchmark。这里最应该学的是方法:先定义你想抵抗的编辑冲突,再拿真实 edit trace 衡量误拒、误改和恢复成本,而不是凭直觉挑一个”更复杂的 hash”。

四、上下文满了,不能只把前半段删掉

长任务一定会遇到上下文窗口上限。文件内容、测试日志、工具结果和多轮对话不断累积。很多简单 Agent 的处理是保留最后 N 条消息,早期历史全部丢掉。这能避免请求报错,却常把任务的目标、关键约束和已失败的尝试一起丢掉。

Grok Build 的自动压缩阈值是窗口使用量严格超过 85%。token 估算在仓库中统一按 bytes / 4,图片固定按 765 token 计。这不是精确计费器,而是运行时必须使用的一把共同尺子。若每个模块各估各的,压缩触发、预算显示和异常恢复会互相矛盾。

4.1 full-replace:摘要后重建会话

它不采用简单的尾部滑窗,而是先总结整段对话,再重建一份结构明确的历史。压缩后的消息大致包含:

  1. 原始 system prompt;
  2. 重新注入的 user_info 与项目 AGENTS.md
  3. 最后一条真实用户请求;
  4. 当前回合尚未结束的尾部消息;
  5. 对旧历史的结构化总结;
  6. 记录当前编辑文件、后台任务、待办等内容的状态提醒。

你可以把它想成接力赛换棒。摘要模型负责把”前面发生了什么”写成接力纸条,但比赛规则、用户现在到底要什么、以及运行时已经改过哪些文件,不能交给它自由改写。这些高价值信息必须从确定性来源重新注入。

这正是生产级压缩和”请总结一下聊天记录”的分界线。总结模型会遗漏,也可能把不确定内容写得很肯定。Grok Build 因此把 AGENTS.md 原文、最后一条用户 query、活动状态等放在摘要外面。摘要只承担历史叙述,不承担权威事实。

4.2 一份能接力的摘要要写什么

源码的总结模板包含九类内容:Primary Request、技术概念、涉及文件与代码、错误和修复、问题解决过程、所有用户消息、待办、当前工作、下一步。把 “All User Messages” 单独列出来看似笨,却能防止模型在压缩数次后悄悄忘掉用户中途增加的限制。

假设用户开始时说”修复登录超时”,第十轮又补了一句”不要改数据库 schema”。如果摘要只留一个抽象的”已解决部分登录问题”,后续模型就可能发起迁移。好的摘要至少要留下:目标、不可违背的限制、已确认事实、已做变更、失败路径,以及下一步要验证什么。

你可以用下面这个模板做自己的手工压缩或 Agent 状态文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
## 当前任务
- 修复 OAuth 回调偶发超时;不改数据库 schema。

## 已确认
- 超时发生在 `src/auth/callback.ts:84` 的串行 token 刷新。
- `tests/auth/callback.test.ts` 已能稳定复现。

## 已做修改
- 将两个独立请求改为并发;尚未提交。

## 失败与限制
- 直接提高超时时间没有解决根因,不能采用。

## 下一步
- 运行 `pnpm test auth/callback`,再检查重试分支。

它不需要写得漂亮,只要让下一位接手的人或下一次模型调用知道不能重做什么、不能忘掉什么。

4.3 压缩失败也要设计

自动压缩本身是一轮模型调用,会超时、被服务端拒绝或生成过短的废摘要。Grok Build 为输入准备了三档降级:原始输入、能装下窗口的 verbatim 输入、丢弃旧工具结果和 reasoning 的 lossy 输入。带签名的 thinking 块在某些提供商那里可能导致 400,因此不能盲目原样转发。

生成结果清理后少于 500 字符,会被视为退化结果而重试。仓库中还设置了最多 3 次重试、每次之间 3 秒间隔、单次调用最长 300 秒。若确认是确定性失败,后续自动压缩会被抑制,避免每一轮都触发同一颗定时炸弹。

这一段对自己做 Agent 的人尤其有用:任何自动化 LLM 步骤都要有”失败后如何不重复伤害系统”的设计。重试不是答案,分类失败才是。网络抖动可以重试,输入格式永久不兼容就应该停止并留下可见状态。

4.4 旧细节该放在哪里

摘要总会丢细节。Grok Build 没有假装一段总结能记住全部历史,而是保留回查后门:transcript 模式会给出完整 updates.jsonl 路径;segments 模式将每次压缩的原始内容写到 compaction/segment_*.mdINDEX.md,并提醒模型需要细节时主动 read_filegrep,不要修改这些记录。

这是一种很务实的取舍。主上下文只保存当前工作所需的高密度信息,完整证据留在文件系统,需要时再检索。它比把所有东西都塞回 prompt 更接近人类工作:我们不会背下所有终端输出,但会记得日志在哪。

五、记忆和上下文不是一回事

上下文解决的是”这一项任务还没做完,模型下一轮要记住什么”。记忆解决的则是”下次打开新会话,哪些长期信息值得找回来”。把两者混为一谈,通常会导致一份不断膨胀、最终谁也不想读的 MEMORY.md

Grok Build 的记忆设计有一个清晰边界:模型只拥有 memory_searchmemory_get 两个只读工具,没有 memory_write。写入由生命周期触发,例如压缩前 flush、会话结束时沉淀,以及 autoDream 在满足条件时把多个会话巩固为长期记忆。

为什么要限制写入?如果模型每轮都能随手记一条,记忆库很快变成它的草稿本,充满短期猜测、重复结论和错误前提。Grok Build 在入口做质量闸:内容必须包含 ## 标题,NO_REPLY 不存,超过 8,000 字符截断,并以余弦相似度 ≥ 0.92 做去重。门槛不一定适合所有项目,但方向是对的:写入比搜索更稀缺。

存储层使用人可读 Markdown 加 SQLite 索引。FTS5 的 BM25 检索是基础能力,sqlite-vec 的 KNN 向量检索是可选项。默认并不建向量表,embedding model 默认 None,因此只开基础功能也不依赖外部 embedding 服务。混合检索启用时,源码权重为向量 0.7、文本 0.3;会话片段按 7 天半衰期衰减,长期的 MEMORY.md 不衰减。

对初学者来说,这里有两个可直接照抄的判断:

  • 需要马上驱动下一步的事实,例如”测试还没跑”,属于会话状态或上下文。
  • 跨很多任务仍稳定成立的事实,例如”这个仓库用 pnpm,提交前必须跑 pnpm lint“,才值得进长期记忆或项目规则。

读取也不必每轮都做。Grok Build 会在首轮拿第一条用户消息搜索记忆,跳过纯问候;已注入过的内容不再重搜,以保留 prompt cache。压缩后再用最后一条用户请求找回少量相关记忆。少量、按需、可追溯,比”每次启动把所有记忆贴进去”可靠得多。

该功能默认处于实验开关下,例如 --experimental-memoryGROK_MEMORY=1。如果 cwd 在临时目录,项目级写入会静默跳过。这个细节也很好理解:临时目录通常代表一次性环境,把长期知识绑在它上面会产生难以解释的污染。

六、子 Agent 的价值是隔离过程,不是多开几个模型

用户常把子 Agent 等同于”并行所以更快”。并行只是一个可能结果。更基础的价值是上下文隔离。

设想主 Agent 正在修复登录超时,同时需要确认项目里全部重试策略。让主 Agent 自己 grep 上百个文件,会把大量过程性内容塞进主历史;而子 Agent 可以在独立上下文里完成搜索,只回传”涉及 4 个文件,当前回调链已经重试两次,因此不该再在外层叠加重试”。主线仍然干净。

Grok Build 的子 Agent 是隐藏的子 session,拥有独立上下文窗口,与父会话共享文件系统和终端后端。它限制嵌套深度为 1,子 Agent 不能继续生成孙 Agent。这个限制听起来保守,实际上在防一种常见事故:树状委派会让成本、权限、取消、结果收集和责任归属迅速失去控制。

前台 spawn 超过等待预算时,任务会自动转后台继续跑,不会让父回合永久卡住。父回合结束前最多等 120 秒回收结果;如果没收齐,usage 会标记为不完整,并省略所有成本数字。这个”宁可不给总价,也不报假总价”的选择和它的成本会计一致。

还有一条细小却重要的不变量:只有项目级 agent 文件可以覆盖内置的 general-purposeexploreplan 子 Agent;用户级同名文件会被丢弃。原因是模型看到的可调用列表必须等于实际允许调用的列表。要是 UI 或 description 里出现了一个同名 Agent,运行时却因为优先级规则拒绝它,模型就拥有了一个幻影能力。

自己设计子 Agent 时,可以从下面四个问题开始,不必急着做并行:

  1. 这个任务能独立完成吗?如果必须不断追问主任务细节,别分出去。
  2. 它的过程输出很多、最终结论很短吗?如果是,隔离通常有收益。
  3. 它能否再委派?多数场景先禁止递归,等取消和预算做成熟。
  4. 回传什么才够?要求返回结论、证据路径、完成状态和未解决问题,而不是整段思维过程。

七、权限清单只能提高便利,不能代替安全边界

一个编码 Agent 必须能运行命令,但 shell 命令比文件工具危险得多。git status 很安全,git status && rm -rf / 不是。安全设计要把命令语言当成有结构、有组合、有绕过方式的程序,而不是维护一张”好命令”名单。

Grok Build 的授权链路可概括为五层:PreToolUse hooks、权限规则、项目记住的授权、内建只读规则和 prompt policy。规则合并时遵循 deny > ask > allow,且跨来源按严重度决定,不靠配置文件出现顺序碰运气。TOML 规则的默认 action 是 Deny,这是为了避免漏写一个字段时意外变成兜底允许。

7.1 为什么正则不够

它使用 tree-sitter 解析 shell 的结构。deny 与 ask 会逐段检查一条链式命令,allow 则要求匹配整条命令。仓库文档专门点出一个危险例子:若只按前缀放行 Bash(git *),下面这条会被误认为是 git 命令:

1
git status && rm -rf /

&& 两边是两条独立命令。正则若只看字符串开头,很容易漏掉后半段。对于命令替换、反引号和控制流等难解析构造,Grok Build 选择整体请求确认。这会多打扰几次用户,却比解析失败后默认放行要稳得多。

内建只读清单同样有很多不显眼的坑。匹配要有词边界,不能让 ls 意外匹配 lsofrg --pre 被排除,因为它会为每个文件启动预处理进程;tee 也不在只读清单,因为它可以通过管道写文件。这些不是”安全团队过度谨慎”,而是命令行里每个看似无害的组合都可能改变效果。

7.2 记住批准,不等于永远批准

Grok Build 即使在用户选择 Always allow 后,也会对 rmchmodchownkillgit push 等危险命令再次提示。原理很简单:一次批准应当理解为对某类低风险、可预期操作的便利授权,而不是一张可转让的空白支票。

Auto 模式也不是魔法。它本质上会先做启发式分类:不能解析为纯单词命令序列时就 fail-closed;环境变量赋值默认拒绝,只有少数显示类变量在白名单中。PATHLD_PRELOADNODE_OPTIONS 会被拦住,因为环境变量可以影响接下来实际执行的程序。

如果你要为自己的 Agent 制定规则,可以先按可撤销性和影响半径分级:读工作区通常低风险,改工作区可回滚,访问网络会把边界扩展到外部,删除、权限变化和远程推送的后果最难撤销。审批应当对准后两类,不要用一个总开关解决全部问题。

7.3 沙箱才是执行时的最后一道线

权限策略决定 Harness 是否愿意执行一条请求,沙箱决定即使策略或模型出错,进程实际能碰到什么。Grok Build 基于 nono:Linux 使用 Landlock,macOS 使用 Seatbelt。沙箱会在进程启动时一次性施加,覆盖它和子进程,之后无法在进程内撤销。

它提供 offworkspacedevboxread-onlystrict 等 profile。Linux 上还可通过 seccomp BPF 拦截 connectbind 实现网络隔离;macOS 上这部分是 no-op,不能假设两端具有同样网络边界。

这件事提醒我们不要把”配置了 sandbox”说得太笼统。你至少要问清平台、文件可写范围、网络是否真的隔离、子进程是否继承限制。安全边界最终由操作系统执行,不由 prompt 执行。

folder trust 也采取了较窄的做法:只对真正可能执行代码的仓库配置设门,例如 .mcp.json.grok/hooks/.envrc 等。未信任项目会丢弃项目级 hooks、plugins、MCP、LSP 和环境配置。反过来,对 $HOME/ 这类不能合理持久化确认的根路径直接放行,避免每次启动都弹无意义提示,最后把用户训练成闭眼确认。

八、终端和成本这两件小事,最能看出是否上过生产

8.1 “持久 shell”可以不是真的长命进程

用户希望终端有连续状态:上一条 cd 后,下一条命令还在新目录;前一条 export 的变量仍存在。最直接的做法是维护一个一直活着的 shell 或 PTY,但这会带来状态泄漏、终端仿真和重启恢复问题。

Grok Build 选了另一种实现:每条命令仍在新进程中执行,结束时把 cwd、环境变量、函数和 alias 序列化,下一条命令前再 replay。状态通过 base64 和额外文件描述符传递,避免污染正常 stdout。对用户而言像持续 shell,对运行时而言每次命令仍可以独立管理。

这并不是说无状态总比 PTY 好。需要交互式 TUI 时,长命 PTY 仍有价值。仓库也提供 ptyctl,基于无头终端控制器生成进程、发送按键、读取屏幕文本和样式,并开放 HTTP API 给 TUI 测试使用。关键在于根据任务选择状态模型,而不是为了”更真实”无条件保留进程。

长命令也不能一直占住主循环。前台运行超过约 15 秒会转后台,输出先完整落盘,模型只拿头尾各一部分,总量约 20,000 字符,并获得完整文件路径以便按需读取。输出达到 5 GiB 会终止进程,完成后最多保留 64 MiB。这里同样贯彻一条原则:给模型足够判断下一步的摘要,完整材料放在可回查的位置。

8.2 金额不要用浮点数相加

Grok Build 的 headless JSON 输出包含 total_cost_usd_ticks,定义为 1 USD = 10¹⁰ ticks。为什么不直接把每次调用费用用 f64 相加?因为二进制浮点数无法精确表示很多十进制小数,累积后容易和服务端账单对不上。

概念上,下面两种写法的差别就像金额与分:

1
2
不稳定:total_usd += 0.0000017
稳定: total_ticks += 17000

真正的生产细节在于完整性:只要任一调用缺少成本数据,它会整体省略所有 cost 字段,并标记 cost_is_partial。同样,子 Agent 未能在收集窗口内完成时,会标记 usage 不完整,而不是把已拿到的一部分包装成总数。指标不完整时,”没有总价”比”一个精确但错误的总价”更诚实。

九、把它用起来:先建立安全而有效的习惯

Grok Build 的具体命令和界面会演进,不过原始源码与用户指南展示的使用思路很稳定。初学者不必在第一天就把所有模式都打开,先把任务边界说清,观察 Agent 的读、改、测顺序。

日常做小型改动时,优先使用会先规划、只读探索后再请求修改批准的模式。需要快速补充条件时,可以把信息排队为 follow-up;真正要让当前工作转向时,才使用立即插话。会话分叉、回滚、手动压缩等功能适合探索式任务,但不要把它们当成”修改没关系”的许可,代码改动仍要靠 diff 和测试确认。

在 CI 或无人值守脚本里,先缩小工具和权限集合。例如只做审查时,只暴露读文件和搜索工具,不给 shell;需要执行命令时,把允许规则写得具体,并确保 deny 优先。原文提到的 --tools--disallowed-tools--allow--deny--max-turns--check--worktree--agents 都属于这类控制面。它们让运行结果更可预测、可复现。

还有一个反直觉细节:headless 模式不一定会从 stdin 管道读取提示内容。自动化时,不要想当然地把 git diff | grok ... 当成唯一办法,优先查看当前版本是否提供 --prompt-file 或命令替换等受支持的输入路径。CLI 的输入边界是接口契约,凭终端直觉猜错后,Agent 可能从一开始就没有看到你想给的内容。

项目配置也应分层。长期、所有任务都适用的规则写进项目级规则文件;一次性的任务限制放在当前请求;只有你个人偏好的行为放进用户级配置。仓库对 Claude Code 的兼容扫描默认覆盖 skills、rules、agents、MCP、hooks、sessions,并提供 /import-claude 一类导入机制。这降低迁移成本,但也意味着你需要逐项检查旧配置里有没有本不想自动带进新工具的 hook 或环境变量。

十、从 Grok Build 可以带走的七条设计规则

读完这些模块,最容易犯的错误是挑一个酷功能照搬,例如给自己的编辑器加 hashline、给 Agent 加向量记忆。更有价值的是先记住它们背后的约束。

  1. 让模型负责判断,让运行时负责执行和边界。模型输出不是系统调用。
  2. 静态规则与动态事实分开。这样既方便缓存,也让行为更容易复现。
  3. 工具的失败信息要能指导下一次调用。工具 description 和错误文案都是模型接口。
  4. 长任务不要只截断历史。保留目标、限制、状态等确定性事实,并让旧证据可回查。
  5. 记忆写入要比读取更克制。会话草稿不该自动变成长期知识。
  6. 权限规则提高便利,沙箱提供最终执行约束。两者缺一个都不完整。
  7. 对成本、usage、测试结果这类数字,宁可标注不完整,也不要报一个看似精确的假结论。

这些规则没有一条只属于 xAI。你用 LangGraph、Claude Code、Codex,或者自己写一个 200 行 Python Agent,都会遇到同样的问题。区别只在规模小的时候,问题还藏在角落;规模变大后,它们会以超时、误改、循环、账单异常和用户不敢批准命令的形式一起冒出来。

结语:生产级 Agent 的难点在模型之外

Grok Build 的源码给人的最大感受是它把失败当作常态:工具会调用错,文件会在编辑期间变化,压缩会失败,统计会缺数据,shell 会被拼接,子任务会跑不完,平台能力也不完全一致。

这也是为什么一个玩具 Agent 可以用几十行代码跑起来,而可靠的编码 Agent 要有这么多层。前者只要让模型会调用工具,后者还要让每一次调用在权限内、在上下文预算内、能恢复、可观察,并且最终能回答一句最朴素的问题:”它真的把任务完成了吗?”

如果你准备自己做 Agent,我建议不要一开始追求全套能力。先做一条可以被测试的闭环:受限地读一个文件,做一次明确修改,跑一个固定验证命令,记录结果。然后再加入回合预算、工具错误恢复、上下文状态、最小权限和沙箱。Grok Build 可供借鉴的是这个顺序:每增加一种自由度,就补上相应的约束和恢复路径。

延伸阅读与核对入口

本文的源码观察以文章开头所列快照为边界。若你在更新版本中发现默认阈值、工具名或配置路径不同,优先相信当前代码和官方文档,再把本文当作理解其设计取舍的地图。