本集对应课程章节:CHAPTER 12(Compaction:85% 阈值与可选 two-pass / PromptContext:可检查的渲染输入 / 进程级外部 Toolset Preset 注册表 / ToolKind 提供默认只读语义)
内容来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》),本页为二次演绎的解读与音频稿件版本。
上一集拆了 Grok Build 的骨架(入口分层、调用链、会话隔离)。本集往下钻一层,核对四个极容易被写错的实现细节:
本集的方法论口号:别猜默认值,去看 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() |
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_percent | u32 | 85 | 自动压缩阈值百分比 |
compact_model | Option<String> | None | 未指定时使用当前 Session 模型 |
memory_flush_enabled | bool | false | 启用后,压缩前才运行 memory flush turn |
wall_clock_budget_secs | u64 | 300 | 单次压缩的墙钟预算(秒) |
two_pass_enabled | bool | false | 由配置解析后写入,默认 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)
}
三个设计要点:
context_window == 0 直接返回 false。配置未加载完时窗口可能为零,这行防御很关键。saturating_mul 防止溢出。设 context_window = 100_000,threshold_percent = 85,触发点为 85,000:
| used | 是否触发 | 原因 |
|---|---|---|
| 84,999 | 否 | 84_999 × 100 < 100_000 × 85 |
| 85,000 | 是 | 比较为 >=,正好命中 |
| 90,000 | 是 | 超出阈值 |
| 阶段 | 触发条件 | 动作 |
|---|---|---|
| PASS 1 · PREFIRE | two_pass_enabled == true 且接近阈值 | 投机地在后台总结历史前缀,得到 NOTE₁ |
| PASS 2 · COMPACT | 正式压缩时 | 把 NOTE₁ 与 recent tail 组合后再次总结 |
配置为 false 时保留原有 single-pass 路径。开启 two_pass_enabled 会改变压缩路径,但不会把默认值改成 true。
PromptContext 保存 Agent 专属的模板输入,通过 Serde derive 获得序列化能力,最终交给 ToolBridge::render_prompt() 渲染。
事实校正:可序列化能力来自Serialize/Deserializederive,源码未额外定义专用 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,空值不写进序列化结果,持久化输出更小。做对账或快照测试时要注意:空字段在输出里不出现是刻意省略,不是丢数据。
| 变体 | 含义 |
|---|---|
None(#[default]) | 标准模板:Primary 用标准 base template,Subagent 用紧凑模板 |
Codex | apply-patch profile 的提示词模板,按需解密 |
Custom(String) | 调用方提供完整自定义模板字符串 |
职责边界:PromptContext 只存输入,TemplateOverride 只定基础模板,真正拼成最终字符串的是 TemplateRenderer + ToolBridge 渲染路径。
config.rs 允许 crate 外的扩展代码在当前进程内注册按名称解析的工具集构建函数。
| 事实 | 说明 |
|---|---|
| Builder 是函数指针 | ToolsetPresetBuilder = fn() -> ToolServerConfig。注册表保存构建函数,查询时调用生成配置——每次解析都重新构建,不复用同一对象 |
| Visibility 只管枚举 | Public 进入 preset_names 与公开 preset 集合;Internal 不进入公开枚举,但仍能被 toolset_for_preset 按名称解析 |
| Registry 属于进程 | OnceLock + Mutex 包住全局 HashMap,生命周期覆盖当前进程,读写受锁保护 |
ToolServerConfig 保持原样;同一个注册动作,对已解析配置和未来解析的效果完全不同。源码注释明确要求:尽量在第一次解析前完成注册,以保证启动行为一致。
设计一个仅供测试 harness 使用的 preset:应调用register_internal_toolset_preset,builder 类型为ToolsetPresetBuilder,不会出现在preset_names()中;且若会话配置已解析,注册后该会话不自动变化。
is_read_only() 是工具种类层的默认分类,具体工具可通过自己的元数据覆盖;最终是否执行还要经过规则、沙箱、Hook 与交互批准。
Read、Search、Lsp、ListDir、List、MemorySearch、MemoryGet、WebSearch、WebFetch、EnterPlan、ExitPlan、AskUser
Edit、Delete、Write、Move、Execute、Plan、Task、Skill、SearchTool、UseTool、Monitor、GoalUpdate,以及后台任务(BackgroundTaskAction / WaitTasksAction / KillTaskAction)、媒体生成(ImageGen / VideoGen / ImageToVideo / ReferenceToVideo)、DeployApp、Other 等。
Task 明确位于 false 分支——Task 种类默认不是只读;TaskOutput 这个 kind——很多复述会写,源码里不存在;Execute 的显示层标签是 Run Command(presentation_name 中写死),不是 "Execute"。边界提醒:is_read_only 描述的是默认副作用语义,不能单独推出「自动执行」或「必须弹窗」。命令规则、工作区权限、沙箱、Hook、用户批准都会影响最终结果。只读分类只是一个默认值、一个起点,不是权限判决。
CompactionPolicy 五个字段与默认值(85 / None / false / 300 / false)?context_window == 0 时 exceeds_threshold 返回什么、为什么?PromptContext 三组字段,并指出谁负责最终渲染?TemplateOverride 三个变体及 None 在不同角色下的差异?Public 与 Internal 的真实差异(公开枚举范围)?Task 的默认只读值,并指出 TaskOutput 是否存在?is_read_only 为什么不能单独推出执行或弹窗结论?.git 元数据,因此不声称对应某个具体 commit。第三节、第四节另标注核对日期为 2026-07-17(本地仓库 grok-build-main)。impl Default 为准,不得以「常见做法」或「直觉」替代。should_compact 方法、TaskOutput kind、专用 JSON 转换方法——三者在源码中均不存在,不得写入复述。is_read_only 只描述默认副作用语义,不得单独作为权限判决依据。把配置默认值统一收敛进 Default 实现,而不是散落在构造函数、环境变量和调用点里。散落写法迟早会导致「线上默认值和文档不一致」的事故。这是本集最便宜的一条改进。
判断是否超过百分比阈值时,用 used × 100 >= window × percent 这类整数乘法,别用浮点除法。同时显式处理分母为零的边界。这是可以直接抄的防御式写法。
任何可能耗时的自动流程(压缩、重建索引、批处理)都应该带一个超时预算。wall_clock_budget_secs: 300 就是这类设计的样板——没有上限的自动化流程是生产事故的高发区。
注册表保存「名称 → 构建函数 + 可见性」的映射,而不是配置实例本身。好处是每次解析都能拿到新鲜配置,且调用方可以在进程启动后扩展。代价是必须明确注册时序,否则会出现「已解析配置不感知新条目」的困惑。
should_compact、TaskOutput、专用 JSON 转换方法——这三类「看起来应该有」的东西在源码里都不存在。写复述或设计方案时,逐条回到定义处确认。源码没有的,就明确写「本系统未提供」。
PromptContext 是可序列化的渲染输入,TemplateOverride 决定基础模板,ToolBridge 与 TemplateRenderer 完成最终渲染。字段与能力都应按 derive 和真实定义描述。ToolKind::is_read_only() 给出可复用的种类默认值。Task 为 false,源码没有 TaskOutput kind,Execute 的统一标签为 Run Command。完整权限判断需要继续读取运行环境与用户控制信号。*来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)*
*本页为二次演绎解读稿,配套音频为配音版。*