Skip to content

Latest commit

 

History

History
275 lines (208 loc) · 8.02 KB

File metadata and controls

275 lines (208 loc) · 8.02 KB

第七章 · 上下文工程

LLM 的上下文窗口是有限的,但对话是无限增长的。
Pi 通过上下文压缩在输入端和历史端建立防线,让 Agent 在长时间对话中不失忆。


7.1 问题:有限窗口 vs 无限对话

上下文窗口:200k tokens
          ┌──────────────────────────────────────────────┐
第 1 轮:  │ system prompt + user msg + assistant reply    │  ~2k
第 5 轮:  │ ████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │  ~20k
第 20 轮: │ █████████████████████████░░░░░░░░░░░░░░░░░░░ │  ~80k
第 50 轮: │ ████████████████████████████████████████████░ │  ~180k
第 60 轮: │ ██████████████████████████████████████████████│ 爆了!
          └──────────────────────────────────────────────┘

不做任何处理,对话历史会不断膨胀直到超出窗口。超出后要么报错,要么被截断——两种结果都会导致 Agent 丢失关键信息。

7.2 Pi 的双端防线

Pi 在两个方向管理上下文:

@startuml
skinparam backgroundColor transparent

rectangle "输入端" #E8F5E9 {
  rectangle "System Prompt\n构建" as SP
  rectangle "transformContext\n上下文变换" as TC
}

rectangle "历史端" #E3F2FD {
  rectangle "Compaction\n上下文压缩" as CP
  rectangle "Branch Summary\n分支摘要" as BS
}

rectangle "LLM 调用" as LLM #FFF3E0

SP --> LLM
TC --> LLM
CP --> LLM
BS --> LLM

note bottom of SP: 每轮重新构建\n注入最新工具/技能信息
note bottom of TC: 调 LLM 前\n可选的上下文变换管道
note bottom of CP: 旧消息压缩为摘要\n保留最近 N 轮
note bottom of BS: 切换分支时\n旧分支压缩为摘要

@enduml

7.3 上下文压缩(Compaction)

当消息历史接近窗口限制时,Pi 自动触发压缩。

压缩流程

@startuml
skinparam backgroundColor transparent
skinparam ActivityBackgroundColor #f8f9fa

start

:检测 token 使用量;
if (超过阈值?) then (yes)
  :prepareCompaction();
  note right
    计算哪些消息要压缩
    哪些消息要保留
  end note
  
  :分离消息;
  fork
    :待压缩消息\n(旧的对话历史);
  fork again
    :保留消息\n(最近 N 轮);
  end fork
  
  :用 LLM 生成摘要;
  note right
    把待压缩的消息
    发给 LLM 做结构化摘要
  end note
  
  :创建 CompactionEntry;
  :写入 Session 存储;
  
  :替换上下文;
  note right
    上下文 = 摘要 + 保留的最近消息
    总 token 数大幅减少
  end note
else (no)
  :继续;
endif

stop

@enduml

压缩前后对比

压缩前(180k tokens):
┌─────────────────────────────────────┐
│ [msg1][msg2][msg3]...[msg48][msg49] │
│ ← 很早的对话 ─────────── 最近的 → │
└─────────────────────────────────────┘

压缩后(~30k tokens):
┌─────────────────────────────────────┐
│ [摘要: 3k tokens][msg45]...[msg49] │
│ ← 压缩后的历史   最近保留的 5 轮 → │
└─────────────────────────────────────┘

压缩配置

interface CompactionSettings {
  enabled: boolean;
  reserveTokens: number;   // 为新的对话预留的 token 空间
  keepRecentTokens: number; // 保留最近多少 token 的消息不压缩
}

// 默认配置
const DEFAULT_COMPACTION_SETTINGS = {
  enabled: true,
  reserveTokens: 40000,    // 预留 40k
  keepRecentTokens: 20000, // 保留最近 20k
};

压缩摘要的结构

Pi 要求 LLM 生成的摘要是结构化的,不是随意的文本总结:

interface CompactResult {
  summary: string;           // 结构化摘要文本
  firstKeptEntryId?: string; // 保留的第一条消息 ID
  tokensBefore: number;      // 压缩前的 token 数
  retainedTail?: AgentMessage[]; // 保留的尾部消息
  usage?: Usage;             // 摘要生成消耗的 token
}

摘要会追踪文件操作,确保 Agent 记住自己做过什么:

interface FileOperations {
  read: Set<string>;     // 读过的文件
  written: Set<string>;  // 写过的文件
  edited: Set<string>;   // 编辑过的文件
}

7.4 transformContext:输入端变换

在调用 LLM 前,transformContext 钩子允许对上下文做最后的处理:

const agent = new Agent({
  transformContext: async (messages, signal) => {
    // 例:删除超过 100 轮前的消息
    // 例:注入实时检索结果
    // 例:压缩中间的工具结果
    return processedMessages;
  },
});

这与 Compaction 不同——transformContext每次调 LLM 前都执行的临时变换,不修改持久化的消息历史。Compaction 则是永久修改消息历史。

7.5 分支摘要(Branch Summary)

当用户通过 /tree 命令切换到历史分支时,当前分支的对话会被压缩为一条摘要:

@startuml
skinparam backgroundColor transparent

rectangle "Session Tree" {
  rectangle "Root" as R
  rectangle "Branch A\n(当前)" as A #C8E6C9
  rectangle "Branch B\n(要切换到)" as B #E3F2FD

  R --> A
  R --> B
}

note right of A
  切换前:压缩为 BranchSummaryEntry
  包含对话摘要和文件操作记录
end note

@enduml

分支摘要与 Compaction 的区别:

维度 Compaction Branch Summary
触发时机 上下文快满时 切换分支时
作用范围 当前对话的旧消息 整个分支的对话
结果 替换旧消息 存储为独立条目
目的 释放窗口空间 保留分支记忆

7.6 System Prompt 动态构建

Pi 的 system prompt 不是静态的——它在每轮对话开始前动态构建

// AgentHarness 支持函数式 system prompt
const harness = new AgentHarness({
  systemPrompt: ({ session, model, thinkingLevel, activeTools, resources }) => {
    let prompt = BASE_SYSTEM_PROMPT;

    // 注入可用工具描述
    for (const tool of activeTools) {
      prompt += `\nTool: ${tool.name} - ${tool.description}`;
    }

    // 注入可用 Skills
    if (resources.skills?.length) {
      prompt += formatSkillsForSystemPrompt(resources.skills);
    }

    return prompt;
  },
});

这确保 system prompt 始终反映 Agent 的最新状态(可用工具、技能等可能在运行时变化)。

7.7 重试策略

压缩和分支摘要都涉及 LLM 调用,可能失败。Pi 内置了重试机制:

interface RetryPolicy {
  maxAttempts: number;    // 最大重试次数
  baseDelayMs: number;    // 基础延迟
  maxDelayMs: number;     // 最大延迟
  backoffMultiplier: number; // 退避倍数
}

相关事件:

{ type: "retry_scheduled", operation: "compaction", attempt: 2, maxAttempts: 3, delayMs: 2000 }
{ type: "retry_attempt_start", operation: "compaction" }
{ type: "retry_finished", operation: "compaction" }

7.8 本章检查清单

  • 理解为什么需要上下文压缩
  • 知道 Compaction 的触发条件和流程
  • 理解压缩摘要为什么是结构化的
  • 知道 transformContext 和 Compaction 的区别
  • 理解 Branch Summary 的用途

上一章第六章 · 事件流
下一章第八章 · 会话管理 — 对话的持久化与分支