学 AI 产品 · 专业 AI 产品经理播客第 2 章 · T2 解剖 Grok Build:Rust 写的生产级 Coding Agent · EP 02
第 2 章 · EP 02

Compaction:85% 阈值与可选 two-pass 等 4 节

时长 13:45音色 云健 · 男声

同步字幕

章节导航(点击跳转)

0:00开场 · 这一集钻四个容易写错的细节1:09
1:09压缩策略 · 五个字段和它们的默认值1:28
2:37阈值判断 · 饱和乘法和两遍压缩何时成立2:05
4:43渲染输入 · PromptContext 存的是什么1:56
6:39模板覆盖 · 三个变体管住基础模板1:18
7:58工具集注册表 · 函数指针、可见性和注册时序2:08
10:07工具分类 · 只读默认值不是权限判决1:40
11:47收尾 · 七条可以带走的结论1:57
解读全文

ep05 · Compaction 阈值、PromptContext 与工具分类:四个容易写错的实现细节

本集对应课程章节:CHAPTER 12(Compaction:85% 阈值与可选 two-pass / PromptContext:可检查的渲染输入 / 进程级外部 Toolset Preset 注册表 / ToolKind 提供默认只读语义)
内容来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》),本页为二次演绎的解读与音频稿件版本。

一、本集解决什么工程问题

上一集拆了 Grok Build 的骨架(入口分层、调用链、会话隔离)。本集往下钻一层,核对四个极容易被写错的实现细节:

  1. 上下文快满时自动压缩如何触发,阈值与超时预算是多少;
  2. 提示词渲染前的输入数据长什么样,谁负责渲染;
  3. 工具集配置在哪注册,注册晚了会怎样;
  4. 「一个工具算不算只读」由谁给出,这个判断能推出什么、不能推出什么。

本集的方法论口号:别猜默认值,去看 Default 实现和枚举定义。

能力地图

维度读完本集你能做到对应证据
上下文预算背出 CompactionPolicy 五个字段与默认值impl Default for CompactionPolicy
阈值计算手算是否触发压缩,并解释饱和乘法的用意exceeds_threshold 的 saturating_mul
两遍压缩说清 two-pass 何时成立、默认值是什么two_pass_enabled: false
提示词输入列出 PromptContext 三组字段与渲染职责边界#[derive(Debug, Clone, Serialize, Deserialize)]
模板选择区分 TemplateOverride 三种变体enum TemplateOverride
工具集注册解释函数指针、可见性、注册时序三类事实pub type ToolsetPresetBuilder = fn() -> ToolServerConfig
工具分类判断某 kind 的默认只读值,并划清与权限判决的边界ToolKind::is_read_only()

二、CompactionPolicy:五个字段与默认值


impl Default for CompactionPolicy {
    fn default() -> Self {
        Self {
            auto_compact_threshold_percent: 85,
            compact_model: None,
            memory_flush_enabled: false,
            wall_clock_budget_secs: 300,
            two_pass_enabled: false,
        }
    }
}
字段类型默认值含义
auto_compact_threshold_percentu3285自动压缩阈值百分比
compact_modelOption<String>None未指定时使用当前 Session 模型
memory_flush_enabledboolfalse启用后,压缩前才运行 memory flush turn
wall_clock_budget_secsu64300单次压缩的墙钟预算(秒)
two_pass_enabledboolfalse由配置解析后写入,默认 single-pass

关键提醒:memory_flush 与 two_pass 默认都是 false。任何「默认开启 memory flush」「默认两遍压缩」的复述都是错的。


三、阈值判断:exceeds_threshold

判断逻辑位于 xai-token-estimation,被 Agent::should_auto_compact 调用(接收 total_tokens 与 NonZeroU64 的 context_window)。


pub fn exceeds_threshold(used: u64, context_window: u64, threshold_percent: u8) -> bool {
    if context_window == 0 { return false; }
    used.saturating_mul(100) >= context_window.saturating_mul(threshold_percent as u64)
}

三个设计要点:

  1. 零窗口防御:context_window == 0 直接返回 false。配置未加载完时窗口可能为零,这行防御很关键。
  2. 整数乘法而非浮点除法:没有精度损失,也没有除零风险。
  3. 饱和乘法:saturating_mul 防止溢出。

手算边界

设 context_window = 100_000,threshold_percent = 85,触发点为 85,000:

used是否触发原因
84,999否84_999 × 100 < 100_000 × 85
85,000是比较为 >=,正好命中
90,000是超出阈值

Two-pass 何时成立

阶段触发条件动作
PASS 1 · PREFIREtwo_pass_enabled == true 且接近阈值投机地在后台总结历史前缀,得到 NOTE₁
PASS 2 · COMPACT正式压缩时把 NOTE₁ 与 recent tail 组合后再次总结

配置为 false 时保留原有 single-pass 路径。开启 two_pass_enabled 会改变压缩路径,但不会把默认值改成 true。


四、PromptContext:可检查的渲染输入

PromptContext 保存 Agent 专属的模板输入,通过 Serde derive 获得序列化能力,最终交给 ToolBridge::render_prompt() 渲染。

事实校正:可序列化能力来自 Serialize / Deserialize derive,源码未额外定义专用 JSON 转换方法。工具描述由渲染路径结合 ToolBridge 处理,不是 PromptContext 自己拼的。

三组字段

分组字段
版本与模板version、prompt_mode、audience、prompt_body、system_prompt、build_timestamp_utc、system_prompt_label
配置与身份agents_md_files、persona_summaries、role_instructions、persona_instructions、memory_enabled、memory_global_path、memory_workspace_path
用户运行环境os_name、shell_path、working_directory、current_date、is_non_interactive

第三组就是「提示词里告诉你现在什么系统、什么 shell、在哪个目录、今天几号」的来源——它们是普通字段,序列化后渲染进提示词,不是魔法。

工程细节:skip_serializing_if

部分字段使用 skip_serializing_if,空值不写进序列化结果,持久化输出更小。做对账或快照测试时要注意:空字段在输出里不出现是刻意省略,不是丢数据。

TemplateOverride 三变体

变体含义
None(#[default])标准模板:Primary 用标准 base template,Subagent 用紧凑模板
Codexapply-patch profile 的提示词模板,按需解密
Custom(String)调用方提供完整自定义模板字符串

职责边界:PromptContext 只存输入,TemplateOverride 只定基础模板,真正拼成最终字符串的是 TemplateRenderer + ToolBridge 渲染路径。


五、进程级外部 Toolset Preset 注册表

config.rs 允许 crate 外的扩展代码在当前进程内注册按名称解析的工具集构建函数。

三个必须分清的事实

事实说明
Builder 是函数指针ToolsetPresetBuilder = fn() -> ToolServerConfig。注册表保存构建函数,查询时调用生成配置——每次解析都重新构建,不复用同一对象
Visibility 只管枚举Public 进入 preset_names 与公开 preset 集合;Internal 不进入公开枚举,但仍能被 toolset_for_preset 按名称解析
Registry 属于进程OnceLock + Mutex 包住全局 HashMap,生命周期覆盖当前进程,读写受锁保护

注册时序的准确含义

  • 配置 A 已解析 → 随后注册的新 preset 不会回写到配置 A,现有 ToolServerConfig 保持原样;
  • 配置 B 之后解析 → 后续调用会重新查询全局 registry,能看到晚注册的 preset。

同一个注册动作,对已解析配置和未来解析的效果完全不同。源码注释明确要求:尽量在第一次解析前完成注册,以保证启动行为一致。

设计一个仅供测试 harness 使用的 preset:应调用 register_internal_toolset_preset,builder 类型为 ToolsetPresetBuilder,不会出现在 preset_names() 中;且若会话配置已解析,注册后该会话不自动变化。

六、ToolKind:默认只读语义

is_read_only() 是工具种类层的默认分类,具体工具可通过自己的元数据覆盖;最终是否执行还要经过规则、沙箱、Hook 与交互批准。

只读为 true

Read、Search、Lsp、ListDir、List、MemorySearch、MemoryGet、WebSearch、WebFetch、EnterPlan、ExitPlan、AskUser

只读为 false

Edit、Delete、Write、Move、Execute、Plan、Task、Skill、SearchTool、UseTool、Monitor、GoalUpdate,以及后台任务(BackgroundTaskAction / WaitTasksAction / KillTaskAction)、媒体生成(ImageGen / VideoGen / ImageToVideo / ReferenceToVideo)、DeployApp、Other 等。

三处易错事实

  1. Task 明确位于 false 分支——Task 种类默认不是只读;
  2. 源码中没有 TaskOutput 这个 kind——很多复述会写,源码里不存在;
  3. Execute 的显示层标签是 Run Command(presentation_name 中写死),不是 "Execute"。
边界提醒:is_read_only 描述的是默认副作用语义,不能单独推出「自动执行」或「必须弹窗」。命令规则、工作区权限、沙箱、Hook、用户批准都会影响最终结果。只读分类只是一个默认值、一个起点,不是权限判决。

七、审查清单

  • 能否背出 CompactionPolicy 五个字段与默认值(85 / None / false / 300 / false)?
  • 能否说明 context_window == 0 时 exceeds_threshold 返回什么、为什么?
  • 能否解释为何用整数饱和乘法而非浮点除法?
  • 能否手算 84,999 / 85,000 / 90,000 在十万窗口、阈值 85 下是否触发?
  • 能否说清 two-pass 的两遍分别做什么,以及默认值是什么?
  • 能否列出 PromptContext 三组字段,并指出谁负责最终渲染?
  • 能否区分 TemplateOverride 三个变体及 None 在不同角色下的差异?
  • 能否说明注册表存的是配置对象还是构建函数?
  • 能否说清 Public 与 Internal 的真实差异(公开枚举范围)?
  • 能否解释晚注册对「已解析配置」与「后续解析」的不同影响?
  • 能否判断 Task 的默认只读值,并指出 TaskOutput 是否存在?
  • 能否说明 is_read_only 为什么不能单独推出执行或弹窗结论?

八、约束说明

  1. 来源约束:本集全部结论来自小山学堂 CHAPTER 12 相应四节,未引入外部资料或虚构数据。
  2. 版本约束:源码核对依据本地同步副本,该副本没有 .git 元数据,因此不声称对应某个具体 commit。第三节、第四节另标注核对日期为 2026-07-17(本地仓库 grok-build-main)。
  3. 默认值约束:凡涉及默认值的陈述一律以 impl Default 为准,不得以「常见做法」或「直觉」替代。
  4. 存在性约束:should_compact 方法、TaskOutput kind、专用 JSON 转换方法——三者在源码中均不存在,不得写入复述。
  5. 语义约束:is_read_only 只描述默认副作用语义,不得单独作为权限判决依据。
  6. 音频约束:配套口播稿为二次演绎配音版,已去除不便朗读的代码块与符号;题库类内容不进入音频。

九、实践提示

提示一 · 默认值收敛到一个地方

把配置默认值统一收敛进 Default 实现,而不是散落在构造函数、环境变量和调用点里。散落写法迟早会导致「线上默认值和文档不一致」的事故。这是本集最便宜的一条改进。

提示二 · 阈值比较用整数乘法

判断是否超过百分比阈值时,用 used × 100 >= window × percent 这类整数乘法,别用浮点除法。同时显式处理分母为零的边界。这是可以直接抄的防御式写法。

提示三 · 长任务要有墙钟预算

任何可能耗时的自动流程(压缩、重建索引、批处理)都应该带一个超时预算。wall_clock_budget_secs: 300 就是这类设计的样板——没有上限的自动化流程是生产事故的高发区。

提示四 · 注册表存构建函数而非实例

注册表保存「名称 → 构建函数 + 可见性」的映射,而不是配置实例本身。好处是每次解析都能拿到新鲜配置,且调用方可以在进程启动后扩展。代价是必须明确注册时序,否则会出现「已解析配置不感知新条目」的困惑。

提示五 · 写复述前先验证存在性

should_compact、TaskOutput、专用 JSON 转换方法——这三类「看起来应该有」的东西在源码里都不存在。写复述或设计方案时,逐条回到定义处确认。源码没有的,就明确写「本系统未提供」。


十、Takeaway

  • 默认阈值是 85,wall clock 是 300 秒,memory flush 与 two-pass 均为 false。Two-pass 是显式配置能力,自动触发判断落在 Agent 与 token estimation 中。
  • PromptContext 是可序列化的渲染输入,TemplateOverride 决定基础模板,ToolBridge 与 TemplateRenderer 完成最终渲染。字段与能力都应按 derive 和真实定义描述。
  • 外部 preset registry 保存「名称到构建函数与可见性」的映射。Public 与 Internal 的核心差异是公开枚举范围。晚注册不会改变已解析配置,后续解析仍能查询到新条目。
  • ToolKind::is_read_only() 给出可复用的种类默认值。Task 为 false,源码没有 TaskOutput kind,Execute 的统一标签为 Run Command。完整权限判断需要继续读取运行环境与用户控制信号。

*来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)*

*本页为二次演绎解读稿,配套音频为配音版。*