Context Engine 与截断策略

📑 目录

这是「GSD 全景代码解析」专题的第 41 篇。在本系列中,我们将逐层拆解 Get Shit Done (GSD) 这一 56K+ stars 的 Meta-Prompting 框架,从命令系统到工作流编排,从 Agent 设计到上下文工程,带你一览上下文驱动开发的工程全貌。


一、Context Engine 的定位与职责

在 GSD 的 SDK 架构中,Context Engine 是连接「静态参考文档」与「动态运行时状态」的核心枢纽。它负责将分散在不同目录(dreams/plans/sessions/)中的结构化信息,组装成一份完整、有序、符合预算约束的上下文报文,最终提交给 AI 模型。

如果说 GSD 的参考体系(第 32-37 篇)解决的是**"写什么"的问题,那么 Context Engine 解决的就是"怎么装""装不下怎么办"**的问题。

GSD 的上下文引擎由两个紧密协作的模块组成:

模块文件大小核心职责
Context Enginecontext-engine.ts~6KB上下文组装、模板渲染、状态注入
Truncation Enginecontext-truncation.ts~6KB预算监控、截断决策、质量保障

这两个模块遵循**"先组装后截断"**的两阶段流水线设计:Context Engine 尽可能完整地收集所有相关信息,Truncation Engine 则在预算超限时有策略地精简内容,而非简单地丢弃。


二、context-engine.ts:上下文组装流程

2.1 整体组装流水线

Context Engine 的组装流程可以概括为五个阶段:解析 → 加载 → 注入 → 渲染 → 校验

flowchart TD
    A[输入: config.json + 命令参数] --> B[阶段1: 解析配置]
    B --> C[阶段2: 加载参考文档]
    C --> D[阶段3: 注入运行时状态]
    D --> E[阶段4: 模板渲染]
    E --> F[阶段5: 预算校验]
    F -->|未超限| G[输出: 完整上下文]
    F -->|超限| H[触发截断流程]
    H --> I[调用 context-truncation.ts]
    I --> J[输出: 截断后上下文]

2.2 阶段详解

阶段 1:解析配置(Configuration Resolution)

Context Engine 首先读取 config.json 中的上下文配置,确定本次组装需要包含哪些组件:

interface ContextConfig {
  // 参考文档配置
  references: {
    core: string[];       // 核心参考文档(如 gsd-rules.md)
    project: string[];    // 项目级参考文档
    session: string[];    // 会话级参考文档
  };
  // 状态配置
  state: {
    includePlan: boolean;     // 是否注入当前 plan
    includeSession: boolean;  // 是否注入会话历史
    includeGit: boolean;      // 是否注入 Git 状态
  };
  // 模板配置
  template: string;       // 使用的上下文模板(dev/research/review)
  // 预算配置
  budget: {
    maxTokens: number;    // 最大 token 预算
    reserveTokens: number;// 为模型响应预留的 token
  };
}

配置解析遵循层级覆盖原则:项目默认值 → .planning/config.json 覆盖 → 命令行参数最终覆盖。这种设计允许用户在全局规范的基础上,针对特定项目或特定命令灵活调整上下文构成。

阶段 2:加载参考文档(Reference Loading)

配置解析完成后,Context Engine 进入文档加载阶段。这一步的核心是**@reference 解析引擎**。

flowchart LR
    A[解析 @reference 指令] --> B{文档类型}
    B -->|本地文件| C[fs.readFile 读取]
    B -->|URL| D[HTTP fetch 获取]
    B -->|内置引用| E[从 SDK bundle 读取]
    C --> F[缓存到内存]
    D --> F
    E --> F
    F --> G[解析 frontmatter]
    G --> H[提取元数据]
    H --> I[按优先级排序]

加载策略的关键细节:

  1. 路径解析@reference 支持相对路径(相对于 .planning/ 目录)、绝对路径和远程 URL。Context Engine 会依次尝试解析,失败时抛出带有修复建议的错误。

  2. 缓存机制:同一会话内的重复引用会被缓存,避免磁盘 I/O 重复开销。缓存以引用路径的规范化字符串为键。

  3. frontmatter 提取:每个参考文档的 YAML frontmatter 会被解析为结构化元数据,用于后续的优先级计算和依赖追踪。

  4. 加载顺序:文档按照 priority 字段(数值越大越优先)排序,同优先级按照配置中出现的顺序排列。这确保了核心规则文档始终位于上下文的头部——模型对上下文开头的注意力通常更强。

阶段 3:注入运行时状态(State Injection)

参考文档是静态的,而 Agent 的执行需要动态信息。Context Engine 在此阶段将运行时的活数据注入到上下文结构中。

注入的状态类型包括:

状态类型数据来源注入位置用途
Plan 状态plans/current/plan.md参考文档之后让模型了解当前目标和已完成的任务
Session 历史sessions/current/session.logPlan 之后提供多轮对话的连续性
Git 状态git status / git diffSession 之后反映代码仓库的当前变更
文件树find . -type f 过滤后可选位置帮助模型理解项目结构
环境信息Node.js 版本、OS 等尾部调试和兼容性参考

状态注入的实现逻辑(伪代码):

function injectState(context: ContextBuilder, config: ContextConfig): void {
  if (config.state.includePlan) {
    const plan = loadPlanState();
    context.addSection('## Current Plan', plan.content, {
      priority: 80,
      source: plan.path,
    });
  }

  if (config.state.includeSession) {
    const session = loadSessionHistory(config.maxHistoryRounds);
    context.addSection('## Session History', session.content, {
      priority: 70,
      source: session.path,
    });
  }

  if (config.state.includeGit) {
    const gitStatus = getGitStatus();
    if (gitStatus.hasChanges) {
      context.addSection('## Working Directory Changes', gitStatus.diff, {
        priority: 60,
        source: 'git',
      });
    }
  }
}

设计要点:

  • Plan 状态采用增量注入:只注入 plan.md 中标记为 in_progresspending 的任务,已完成任务只保留摘要,避免历史任务膨胀上下文。
  • Session 历史支持轮数限制:通过 maxHistoryRounds 控制注入的对话轮数,默认保留最近 5 轮。更早的历史会被压缩为"执行摘要"。
  • Git diff 有大小上限:如果 diff 超过 200 行,会触发 diff 摘要模式,只列出变更文件列表和每个文件的变更统计(+n/-m),而非完整 diff 内容。

阶段 4:模板渲染(Template Rendering)

所有内容加载完毕后,Context Engine 进入模板渲染阶段。GSD 使用轻量级的字符串模板引擎,而非完整的模板语言(如 EJS 或 Handlebars),以控制复杂度和避免注入风险。

模板语法:

# GSD Context

## System Reference
{{systemReference}}

## Project Reference
{{projectReference}}

## Current Plan
{{planState}}

## Session History
{{sessionHistory}}

## User Request
{{userRequest}}

## Output Instructions
{{outputInstructions}}

渲染规则:

  1. 占位符替换{{key}} 被替换为对应 Section 的渲染结果。
  2. 条件渲染{{?key}}...{{/key}} 仅在 key 存在且非空时渲染内容块。
  3. 列表渲染{{#items}}...{{/items}} 用于渲染数组型数据。
  4. 原始输出{{{key}}} 表示不进行 HTML 转义(GSD 上下文中不常用,保留给特殊场景)。

模板与模式的结合:

GSD 的上下文模板与之前讨论过的模式模板(dev.mdresearch.mdreview.md)是正交关系。上下文模板控制**"信息如何组织",模式模板控制"模型如何响应"**。最终提交给模型的上下文结构如下:

[模式模板头部: 输出风格定义]
  ↓
[上下文模板: 结构化信息]
  ↓
[模式模板尾部: 输出格式约束]

这种分离设计允许用户独立切换"信息结构"和"响应风格",例如使用 research 模式配合精简的上下文,或 dev 模式配合详尽的上下文。

阶段 5:预算校验(Budget Validation)

渲染完成后,Context Engine 计算最终上下文的 token 数量:

function validateBudget(context: string, config: ContextConfig): BudgetReport {
  const totalTokens = estimateTokenCount(context);
  const availableTokens = config.budget.maxTokens - config.budget.reserveTokens;

  return {
    totalTokens,
    availableTokens,
    isOverBudget: totalTokens > availableTokens,
    overBudgetAmount: Math.max(0, totalTokens - availableTokens),
    utilizationRate: totalTokens / config.budget.maxTokens,
  };
}

预算报告数据结构:

字段类型说明
totalTokensnumber当前上下文的估算 token 数
availableTokensnumber可用预算(总预算减去预留)
isOverBudgetboolean是否超出预算
overBudgetAmountnumber超预算的 token 数量
utilizationRatenumber预算使用率(0-1)

如果预算未超限,Context Engine 直接返回组装好的上下文。如果超限,则将上下文对象连同预算报告一并传递给 context-truncation.ts,进入截断流程。


三、context-truncation.ts:智能截断策略

3.1 截断触发条件

Truncation Engine 在以下三种情况下被激活:

  1. 硬预算超限:组装后的上下文 token 数超过 maxTokens - reserveTokens,必须截断才能提交。
  2. 软预算警告:使用率超过 85%(可配置),触发预防性截断,为后续对话增长预留空间。
  3. 渐进式披露请求:上层调用明确要求"精简上下文",通常发生在多轮对话后历史累积过多时。
flowchart TD
    A[预算报告] --> B{isOverBudget?}
    B -->|是| C[强制截断]
    B -->|否| D{utilizationRate > 0.85?}
    D -->|是| E[预防性截断]
    D -->|否| F{explicitCompact?}
    F -->|是| G[请求性截断]
    F -->|否| H[无需截断]

3.2 截断策略:三种模式

GSD 实现了三种截断策略,分别适用于不同的上下文构成场景。

策略一:尾部截断(Tail Truncation)

适用场景:历史对话、日志输出等时间敏感型内容。

原理:保留最早的内容(通常是系统指令和核心参考),从尾部开始截断。因为对话历史中,越新的消息越可能包含当前话题的关键转折,但完全丢弃尾部又会导致信息丢失,所以 GSD 的尾部截断会保留每轮对话的第一条和最后一条消息,中间的消息用摘要替代。

function truncateTail(sections: ContextSection[], targetTokens: number): ContextSection[] {
  const result: ContextSection[] = [];
  let currentTokens = 0;

  for (const section of sections) {
    if (section.type === 'session_history') {
      // 对会话历史特殊处理:保留首尾,摘要中间
      const compacted = compactSessionHistory(section.content);
      const compactTokens = estimateTokenCount(compacted);
      if (currentTokens + compactTokens <= targetTokens) {
        result.push({ ...section, content: compacted });
        currentTokens += compactTokens;
      } else {
        break;
      }
    } else {
      const sectionTokens = estimateTokenCount(section.content);
      if (currentTokens + sectionTokens <= targetTokens) {
        result.push(section);
        currentTokens += sectionTokens;
      } else {
        break;
      }
    }
  }

  return result;
}

策略二:头部截断(Head Truncation)

适用场景:过时的项目背景、早期的 plan 版本等前置信息

原理:从上下文头部开始截断,保留最新的状态信息。这种策略较少单独使用,因为头部通常是系统指令和核心规则,截断风险较高。GSD 仅在头部内容为"历史版本信息"且明确标记为 deprecated 时才会应用此策略。

策略三:中间截断(Middle Truncation)

适用场景:混合类型内容,需要保留头部规则和尾部请求。

原理:保留上下文的头部(系统指令、核心参考)和尾部(用户当前请求、输出指令),截断中间部分。中间部分通常是状态信息和历史记录,可通过摘要替代。

flowchart LR
    A[头部: 系统规则
高优先级
保留] --> B[中间: 状态/历史
中优先级
摘要或截断] --> C[尾部: 用户请求
高优先级
保留]

中间截断的实现细节:

function truncateMiddle(sections: ContextSection[], targetTokens: number): ContextSection[] {
  // 按优先级分组
  const highPriority = sections.filter(s => s.priority >= 90);   // 系统规则、用户请求
  const mediumPriority = sections.filter(s => s.priority >= 50 && s.priority < 90); // 状态信息
  const lowPriority = sections.filter(s => s.priority < 50);     // 环境信息、调试数据

  // 第一步:保留所有高优先级
  let currentTokens = highPriority.reduce((sum, s) => sum + estimateTokenCount(s.content), 0);
  const result = [...highPriority];

  // 第二步:尝试按优先级顺序纳入中优先级
  for (const section of mediumPriority) {
    const compacted = generateSummary(section);
    const compactTokens = estimateTokenCount(compacted);

    if (currentTokens + compactTokens <= targetTokens) {
      result.splice(result.length - 1, 0, { ...section, content: compacted });
      currentTokens += compactTokens;
    }
  }

  // 第三步:低优先级直接丢弃(已在预算外)
  return result;
}

3.3 优先级保留机制

截断的核心不是"删除内容",而是"按优先级保留内容"。GSD 的优先级体系分为三个层级:

第一层级:不可截断(Immutable)

  • 系统指令(## System Reference
  • 核心规则文档(gsd-rules.md
  • 当前用户请求(## User Request
  • 输出格式约束(## Output Instructions

这些内容如果单独就超过预算,Truncation Engine 会向上层抛出 BudgetExceededError,建议用户切换至更大上下文窗口的模型,而非强行截断。

第二层级:可摘要(Summarizable)

  • Plan 状态(保留 in_progress,摘要 completed)
  • Session 历史(保留最近 2 轮,摘要更早的)
  • Git diff(保留变更文件列表,摘要具体 diff)
  • 项目参考文档(保留标题和核心规则,展开详细说明)

这些内容在截断时会触发自动摘要机制。GSD 内置了基于关键句提取的轻量级摘要器,不依赖外部模型调用,确保截断流程的确定性和低延迟。

第三层级:可丢弃(Discardable)

  • 环境信息(Node.js 版本、OS 信息)
  • 调试输出(Agent 内部日志)
  • 过时的参考文档版本(标记为 deprecated

这些内容在预算紧张时会被直接移除,不产生摘要。

3.4 截断后的质量保障

截断不是简单的"剪掉多余部分",GSD 在截断后执行三项质量检查:

1. 结构完整性检查

function validateStructure(sections: ContextSection[]): ValidationResult {
  const requiredSections = ['System Reference', 'User Request', 'Output Instructions'];
  const missing = requiredSections.filter(
    name => !sections.some(s => s.title.includes(name))
  );

  return {
    isValid: missing.length === 0,
    missingSections: missing,
  };
}

确保截断后上下文仍包含所有必需的结构组件。

2. 引用一致性检查

检查上下文中是否存在对其他 Section 的交叉引用(如 "详见上面的 Plan 状态"),如果被引用的 Section 已被截断,则修复引用文本或移除该引用。

function fixCrossReferences(sections: ContextSection[]): ContextSection[] {
  const existingTitles = new Set(sections.map(s => s.title));

  return sections.map(section => {
    let content = section.content;
    // 查找 "详见 [Section Name]" 或 "如上所述" 等引用模式
    content = content.replace(/详见\s+`?([^`]+)`?/g, (match, refTitle) => {
      return existingTitles.has(refTitle) ? match : '';
    });
    return { ...section, content };
  });
}

3. Token 估算校准

截断后的上下文会重新进行 token 估算,确保实际提交时不会再次超限。GSD 使用字符数除以 4 的快速估算法作为第一近似,对接近预算阈值的上下文使用更精确的 tiktoken 计数作为二次校验。


四、上下文预算管理

4.1 预算分配模型

GSD 采用分层预算池模型,将总预算按用途分配:

pie title 上下文预算分配(以 128K 模型为例)
    "系统指令 + 核心规则" : 20480
    "项目参考文档" : 15360
    "Plan 状态" : 10240
    "Session 历史" : 15360
    "Git 状态" : 5120
    "用户请求" : 8192
    "预留响应空间" : 25600
    "缓冲池" : 29648

各预算池的默认值(基于 128K 上下文窗口):

预算池默认比例说明
系统指令池20%核心规则和模式模板
参考文档池15%项目级和会话级参考
状态池20%Plan + Session + Git
请求池8%用户当前输入
响应预留20%为模型输出保留的空间
缓冲池17%跨池调配的弹性空间

弹性调配机制:

当某一池未用尽时,剩余额度会自动流入缓冲池。当另一池超限时,优先从缓冲池借用。如果缓冲池也耗尽,才触发截断。这种设计避免了"某类信息完全无法加载"的刚性限制。

4.2 运行时预算监控

在多轮对话场景中,预算不是静态的——每轮对话都会消耗预算。GSD 通过预算监控器追踪实时使用情况:

interface BudgetMonitor {
  // 初始预算
  initialBudget: number;
  // 当前已用(累计)
  cumulativeUsed: number;
  // 当前轮次已用
  currentRoundUsed: number;
  // 剩余可用
  remaining: number;
  // 趋势预测(基于最近3轮的增长率)
  projectedRounds: number;
}

当监控器检测到"按当前增长速度,预计 2 轮内将耗尽预算"时,会提前触发预防性压缩:主动对 Session 历史进行滚动摘要,释放预算空间,避免在关键轮次被迫硬截断。


五、与渐进式披露的衔接

GSD 的上下文管理不止于"装得下",更追求"装得巧"。**渐进式披露(Progressive Disclosure)**是 GSD 上下文工程的高级策略,它与 Context Engine 和 Truncation Engine 紧密配合。

5.1 渐进式披露的三层模型

flowchart TD
    subgraph "第一层:核心上下文"
        A1[gsd-rules.md]
        A2[plan.md 摘要]
        A3[用户请求]
    end

    subgraph "第二层:按需加载"
        B1[详细 Plan 任务]
        B2[相关参考文档]
        B3[代码片段]
    end

    subgraph "第三层:动态触发"
        C1[完整 Git diff]
        C2[历史会话详情]
        C3[外部 API 文档]
    end

    A1 --> B1
    A2 --> B2
    A3 --> C1

第一层(始终加载):最核心的信息,每轮对话都必须包含。这部分内容被标记为 immutable,不受截断影响。

第二层(按需加载):根据当前任务的类型和阶段动态决定。例如,当 Agent 进入"实现模式"时,自动加载代码规范参考;进入"审查模式"时,自动加载审查清单。

第三层(动态触发):只有在特定条件满足时才加载。例如:

  • 当 Plan 中的任务涉及"数据库操作"时,自动加载 database-schema.md
  • 当 Git diff 包含测试文件变更时,自动加载 testing-guidelines.md
  • 当用户请求中包含"@doc [URL]"时,实时抓取外部文档

5.2 截断与渐进式披露的协同

截断和渐进式披露不是对立关系,而是互补策略

  • 渐进式披露解决"该不该装":从源头减少不必要的内容进入上下文。
  • 截断策略解决"装不下怎么办":在内容已经进入上下文后,智能地精简和压缩。

实际协同流程:

sequenceDiagram
    participant U as 用户
    participant CE as Context Engine
    participant PD as Progressive Disclosure
    participant TE as Truncation Engine
    participant M as AI 模型

    U->>CE: 发起请求
    CE->>PD: 请求上下文组件清单
    PD->>PD: 分析任务类型 + Plan 状态
    PD->>CE: 返回推荐组件列表(已过滤)
    CE->>CE: 组装推荐组件
    CE->>TE: 预算校验
    alt 预算充足
        TE->>CE: 通过
        CE->>M: 提交完整上下文
    else 预算超限
        TE->>TE: 按优先级截断/摘要
        TE->>CE: 返回截断后上下文
        CE->>M: 提交精简上下文
    end
    M->>U: 返回响应

5.3 从粗粒度到细粒度的演进

在 GSD 的实际使用中,上下文管理经历三个阶段的演进:

  1. V1:全量加载:早期将所有参考文档全部加载,依赖大上下文窗口(200K+)硬撑。简单但低效,模型注意力分散。

  2. V2:智能截断:引入 Truncation Engine,在窗口有限时自动精简。解决了"装不下"的问题,但截断本身有信息损失。

  3. V3:渐进式披露:结合 Progressive Disclosure 和 Truncation Engine,从"先装后砍"演进为"按需加载 + 动态精简"。这是 GSD 当前采用的策略。

演进的核心洞察:上下文管理的终极目标不是"塞进尽可能多的信息",而是"在有限的窗口内呈现最高价值的信息"。渐进式披露让"价值筛选"发生在加载之前,截断策略让"价值保留"发生在加载之后,两者结合实现了上下文质量的最大化。


六、小结

本文深入解析了 GSD SDK 中两个关键的上下文管理模块:

context-engine.ts 负责上下文的组装与渲染

  • 通过五阶段流水线(解析 → 加载 → 注入 → 渲染 → 校验)构建完整上下文
  • 支持层级配置覆盖和灵活的参考文档加载
  • 将 Plan、Session、Git 等运行时状态有机注入静态参考体系
  • 采用轻量级模板引擎实现内容与表现分离

context-truncation.ts 负责上下文的截断与保障

  • 支持尾部、头部、中间三种截断策略,适配不同内容类型
  • 通过不可截断 / 可摘要 / 可丢弃的三级优先级体系保护关键信息
  • 截断后执行结构完整性、引用一致性和预算校准三重质量检查

上下文预算管理采用分层预算池 + 弹性调配 + 运行时监控的组合方案,在保证功能完整性的同时最大化预算利用率。

与渐进式披露的衔接将上下文管理从"先装后砍"的被动模式,升级为"按需加载 + 动态精简"的主动模式,这是 GSD 在 Context Engineering 领域的重要工程创新。


下一篇预告: 第 42 篇《GSD Tools 与状态管理》——深入解析 GSD 的 Tool 系统架构,包括工具注册、权限控制、状态持久化机制,以及 Tools 如何与 Context Engine 协同完成复杂任务编排。