四、AI Agent 设计模式:上下文管理

上一篇我们分析了工具编排。今天深入第三个主题:上下文管理。Agent 的上下文包含消息历史、文件状态、Token 预算、配置等。如何高效管理这些状态?

ToolUseContext:共享状态容器

Context 是 Agent 执行的"记忆":

typescript 复制代码
type ToolUseContext = {
  // === 执行控制 ===
  abortController: AbortController

  // === 文件状态 ===
  readFileState: FileStateCache

  // === 消息历史 ===
  messages: Message[]

  // === UI 状态 ===
  getAppState(): AppState
  setAppState(updater: (prev: AppState) => AppState): void

  // === 配置选项 ===
  options: {
    tools: Tools
    commands: Command[]
    mcpClients: MCPServerConnection[]
    mainLoopModel: string
    thinkingConfig: ThinkingConfig
    isNonInteractiveSession: boolean
    maxBudgetUsd?: number
    customSystemPrompt?: string
    agentDefinitions: AgentDefinitionsResult
  }

  // === Agent 信息 ===
  agentId?: AgentId
  agentType?: string
  toolUseId?: string

  // === 追踪信息 ===
  queryTracking?: QueryChainTracking

  // === 限制配置 ===
  fileReadingLimits?: { maxTokens?: number; maxSizeBytes?: number }
  globLimits?: { maxResults?: number }

  // === Hook 触发器 ===
  nestedMemoryAttachmentTriggers?: Set<string>
  loadedNestedMemoryPaths?: Set<string>
  dynamicSkillDirTriggers?: Set<string>

  // === 特殊状态 ===
  renderedSystemPrompt?: SystemPrompt
  localDenialTracking?: DenialTrackingState
  preserveToolUseResults?: boolean
}

Context 包含了执行所需的所有信息。

文件读取状态缓存

为什么需要缓存?

每次读取文件都发送内容给 API,浪费 Token。缓存避免重复发送:

typescript 复制代码
type FileReadState = {
  content: string
  timestamp: number
  offset?: number
  limit?: number
  isPartialView: boolean
}

type FileStateCache = Map<string, FileReadState>

// 读取时记录状态
const content = await fs.readFile(filePath)
readFileState.set(filePath, {
  content,
  timestamp: Date.now(),
  offset: input.offset,
  limit: input.limit,
  isPartialView: input.offset !== undefined || input.limit !== undefined,
})

缓存命中检测

typescript 复制代码
// 再次读取同一范围时
const existingState = readFileState.get(filePath)

if (existingState) {
  // 范围匹配
  const rangeMatch =
    existingState.offset === input.offset &&
    existingState.limit === input.limit

  // 文件未修改
  const mtimeMs = await getFileModificationTime(filePath)
  const unchanged = mtimeMs === existingState.timestamp

  if (rangeMatch && unchanged) {
    // 返回 stub,不发送内容
    return {
      type: 'file_unchanged',
      file: { filePath },
    }
  }
}

编辑前的状态检查

typescript 复制代码
// Edit 工具要求先读取
const readState = readFileState.get(filePath)

if (!readState) {
  return {
    result: false,
    message: 'File has not been read yet. Read it first.',
    errorCode: 6,
  }
}

// 文件可能被外部修改
if (lastWriteTime > readState.timestamp) {
  // 检查内容是否真的变了
  const currentContent = await fs.readFile(filePath)
  if (currentContent !== readState.content) {
    return {
      result: false,
      message: 'File has been modified since read',
      errorCode: 7,
    }
  }
}

缓存既是优化,也是安全机制------确保编辑的文件是已知的。

子 Agent 的缓存继承

typescript 复制代码
// Fork 模式:子 Agent 克隆缓存
const agentReadFileState = forkContextMessages !== undefined
  ? cloneFileStateCache(toolUseContext.readFileState)
  : createFileStateCacheWithSizeLimit(READ_FILE_STATE_CACHE_SIZE)

// 克隆函数
function cloneFileStateCache(source: FileStateCache): FileStateCache {
  const clone = new Map()
  for (const [key, value] of source.entries()) {
    clone.set(key, { ...value })
  }
  return clone
}

子 Agent 继承父 Agent 已读取的文件状态,但独立管理更新。

Token 预算管理

Token 计算

typescript 复制代码
// 粗略估算
function roughTokenCountEstimation(text: string): number {
  // 英文 ~4 chars/token
  // 中文 ~1.5 chars/token
  return Math.ceil(text.length / 4)
}

// 精确计算(使用 tokenizer)
function exactTokenCount(text: string, model: string): number {
  const tokenizer = getTokenizerForModel(model)
  return tokenizer.encode(text).length
}

预算分配

typescript 复制代码
// Token 预算来源
function getTokenBudget(model: string): number {
  // Claude 模型限制
  if (model.includes('claude-3')) {
    return 200_000 - SYSTEM_PROMPT_TOKENS - SAFETY_BUFFER
  }

  // 默认
  return 100_000 - SAFETY_BUFFER
}

// 预算分配
type TokenBudgetAllocation = {
  systemPrompt: number       // 系统提示固定预算
  userContext: number        // CLAUDE.md 等
  conversationHistory: number // 消息历史
  toolDescriptions: number   // 工具提示
  responseBudget: number     // 响应预留
}

function allocateTokenBudget(total: number): TokenBudgetAllocation {
  return {
    systemPrompt: 20_000,
    userContext: 30_000,
    conversationHistory: total - 20_000 - 30_000 - 10_000 - 40_000,
    toolDescriptions: 10_000,
    responseBudget: 40_000,
  }
}

Token 监控

typescript 复制代码
type TokenTracker = {
  currentCount: number
  budget: number
  warningThreshold: number  // 80%
  criticalThreshold: number // 90%
}

function checkTokenStatus(tracker: TokenTracker): TokenStatus {
  const usageRatio = tracker.currentCount / tracker.budget

  if (usageRatio >= tracker.criticalThreshold) {
    return 'critical'  // 触发压缩
  }

  if (usageRatio >= tracker.warningThreshold) {
    return 'warning'   // 显示警告
  }

  return 'ok'
}

自动压缩触发

typescript 复制代码
// 每轮结束后检查
const tokenCount = estimateTokenCount(currentMessages)
const tokenBudget = context.tokenBudget

if (tokenCount >= tokenBudget * 0.9) {
  // 触发压缩
  const compactResult = await compactConversation(
    currentMessages,
    context,
    cacheSafeParams,
    false,  // suppressFollowUpQuestions
    undefined,  // customInstructions
    true,  // isAutoCompact
  )

  currentMessages = compactResult.compactedMessages
  yield createCompactBoundaryMessage(compactResult)
}

消息历史管理

消息类型

typescript 复制代码
type Message =
  | UserMessage           // 用户输入
  | AssistantMessage      // AI 响应
  | ToolResultMessage     // 工具结果
  | SystemMessage         // 系统提示
  | AttachmentMessage     // 文件附件
  | CompactBoundaryMessage // 压缩边界

消息过滤

typescript 复制代码
// 发送给 API 时过滤
function filterMessagesForAPI(messages: Message[]): APIMessage[] {
  return messages
    .filter(m => m.type !== 'compact_boundary')
    .filter(m => m.type !== 'attachment' || m.shouldInclude)
    .map(normalizeMessageForAPI)
}

// 不完整工具调用过滤
function filterIncompleteToolCalls(messages: Message[]): Message[] {
  // 移除没有 result 的 tool_use
  const toolUseIds = new Set<string>()
  const completeMessages: Message[] = []

  for (const message of messages) {
    if (message.type === 'assistant') {
      for (const block of message.message.content) {
        if (block.type === 'tool_use') {
          toolUseIds.add(block.id)
        }
      }
    }

    if (message.type === 'tool_result') {
      if (toolUseIds.has(message.toolUseId)) {
        completeMessages.push(message)
      }
    }
  }

  return completeMessages
}

消息截断

typescript 复制代码
// 消息太大时截断
function truncateMessageContent(message: Message, maxTokens: number): Message {
  const content = getMessageContent(message)
  const tokenCount = estimateTokenCount(content)

  if (tokenCount > maxTokens) {
    // 截断并添加提示
    const truncatedContent = content.slice(0, maxTokens * 4)
    return {
      ...message,
      message: {
        ...message.message,
        content: truncatedContent + '\n[truncated]',
      },
    }
  }

  return message
}

CLAUDE.md 加载

加载流程

typescript 复制代码
async function loadClaudeMd(cwd: string): ClaudeMdResult {
  const paths = [
    join(cwd, 'CLAUDE.md'),
    join(cwd, '.claude', 'CLAUDE.md'),
  ]

  for (const path of paths) {
    if (await fs.exists(path)) {
      const content = await fs.readFile(path)

      // 解析 frontmatter
      const { frontmatter, body } = parseFrontmatter(content)

      // 估算 token
      const tokenEstimate = estimateTokenCount(body)

      return {
        path,
        content: body,
        frontmatter,
        tokenEstimate,
      }
    }
  }

  return null
}

Token 预算控制

typescript 复制代码
// CLAUDE.md 太大时警告
if (claudeMdResult.tokenEstimate > 20_000) {
  addNotification({
    type: 'warning',
    message: 'CLAUDE.md is large (~${claudeMdResult.tokenEstimate} tokens). Consider splitting.',
  })
}

// 超限时截断
if (claudeMdResult.tokenEstimate > MAX_CLAUDE_MD_TOKENS) {
  const truncated = truncateClaudeMd(claudeMdResult.content, MAX_CLAUDE_MD_TOKENS)
  claudeMdResult.content = truncated
  claudeMdResult.tokenEstimate = MAX_CLAUDE_MD_TOKENS
}

嵌套加载

typescript 复制代码
// CLAUDE.md 可以引用其他文件
function processClaudeMdReferences(content: string, cwd: string): string[] {
  const references: string[] = []

  // 检测 @file 语法
  const matches = content.matchAll(/@([^\s]+)/g)
  for (const match of matches) {
    const refPath = join(cwd, match[1])
    references.push(refPath)
  }

  return references
}

// 加载引用文件
async function loadNestedClaudeMdFiles(references: string[]): AttachmentMessage[] {
  const attachments: AttachmentMessage[] = []

  for (const refPath of references) {
    if (await fs.exists(refPath)) {
      const content = await fs.readFile(refPath)
      attachments.push({
        type: 'attachment',
        attachment: {
          type: 'file',
          path: refPath,
          content,
        },
      })
    }
  }

  return attachments
}

系统提示构建

动态构建

typescript 复制代码
async function buildSystemPrompt(context: ToolUseContext): SystemPrompt {
  const parts: string[] = []

  // 1. 基础系统提示
  parts.push(getBaseSystemPrompt())

  // 2. 工具描述
  const toolDescriptions = await buildToolDescriptions(context.options.tools)
  parts.push(toolDescriptions)

  // 3. 用户上下文(CLAUDE.md)
  if (context.userContext?.claudeMd) {
    parts.push(context.userContext.claudeMd.content)
  }

  // 4. 记忆文件
  const memoryFiles = await loadMemoryFiles(context)
  for (const memory of memoryFiles) {
    parts.push(memory.content)
  }

  // 5. Skills 提示
  const skillPrompts = await buildSkillPrompts(context.options.skills)
  parts.push(skillPrompts)

  // 6. 自定义追加
  if (context.options.appendSystemPrompt) {
    parts.push(context.options.appendSystemPrompt)
  }

  return {
    content: parts.join('\n\n'),
    tokenEstimate: estimateTokenCount(parts.join('\n\n')),
  }
}

Token 预算平衡

typescript 复制代码
function balanceSystemPromptBudget(
  allocation: TokenBudgetAllocation,
  actual: { systemPrompt: number; userContext: number },
): string {
  const overhead = actual.systemPrompt + actual.userContext - allocation.systemPrompt - allocation.userContext

  if (overhead > 0) {
    // 超限:压缩用户上下文
    const compressedUserContext = compressUserContext(
      actual.userContext,
      allocation.userContext,
    )

    return rebuildSystemPrompt({
      systemPrompt: actual.systemPrompt,
      userContext: compressedUserContext,
    })
  }

  return buildFullSystemPrompt()
}

上下文隔离

子 Agent 独立上下文

typescript 复制代码
function createSubagentContext(parent: ToolUseContext, overrides: Partial<ToolUseContext>): ToolUseContext {
  return {
    ...parent,

    // 独立的消息历史
    messages: overrides.messages ?? [],

    // 克隆的文件状态
    readFileState: cloneFileStateCache(parent.readFileState),

    // 独立的 AbortController
    abortController: new AbortController(),

    // 独立的 Agent ID
    agentId: overrides.agentId,
    agentType: overrides.agentType,

    // 可能有不同的权限模式
    getAppState: overrides.getAppState ?? parent.getAppState,

    // 独立的 MCP clients(可能不同)
    options: {
      ...parent.options,
      mcpClients: overrides.mcpClients ?? parent.options.mcpClients,
      tools: overrides.tools ?? parent.options.tools,
    },
  }
}

上下文清理

typescript 复制代码
// 子 Agent 结束时清理
async function cleanupSubagentContext(context: ToolUseContext): void {
  // 清除文件状态缓存
  context.readFileState.clear()

  // 关闭 MCP connections
  for (const client of context.options.mcpClients) {
    if (client.type === 'connected' && client.isSubagentOwned) {
      await client.cleanup()
    }
  }

  // 清除 Hook 注册
  clearSessionHooks(context.agentId)

  // 清除追踪
  unregisterPerfettoAgent(context.agentId)
}

状态持久化

会话保存

typescript 复制代码
async function saveSessionState(sessionId: string, context: ToolUseContext): void {
  const sessionDir = `.claude/sessions/${sessionId}`

  // 消息历史
  await fs.writeFile(
    join(sessionDir, 'messages.json'),
    JSON.stringify(context.messages),
  )

  // 文件状态摘要(不保存完整内容)
  const fileStateSummary = {}
  for (const [path, state] of context.readFileState.entries()) {
    fileStateSummary[path] = {
      timestamp: state.timestamp,
      offset: state.offset,
      limit: state.limit,
    }
  }
  await fs.writeFile(
    join(sessionDir, 'file-state.json'),
    JSON.stringify(fileStateSummary),
  )

  // 元数据
  await fs.writeFile(
    join(sessionDir, 'metadata.json'),
    JSON.stringify({
      sessionId,
      agentId: context.agentId,
      tokenCount: estimateTokenCount(context.messages),
      savedAt: Date.now(),
    }),
  )
}

会话恢复

typescript 复制代码
async function restoreSessionState(sessionId: string): ToolUseContext {
  const sessionDir = `.claude/sessions/${sessionId}`

  // 消息历史
  const messages = JSON.parse(await fs.readFile(join(sessionDir, 'messages.json')))

  // 文件状态(重新验证)
  const fileStateSummary = JSON.parse(await fs.readFile(join(sessionDir, 'file-state.json')))
  const readFileState = new Map()

  for (const [path, summary] of Object.entries(fileStateSummary)) {
    // 验证文件是否还存在
    if (await fs.exists(path)) {
      const mtimeMs = await getFileModificationTime(path)
      if (mtimeMs === summary.timestamp) {
        readFileState.set(path, {
          timestamp: summary.timestamp,
          offset: summary.offset,
          limit: summary.limit,
          isPartialView: summary.offset !== undefined || summary.limit !== undefined,
        })
      }
    }
  }

  return buildToolUseContext({
    sessionId,
    initialMessages: messages,
    readFileState,
  })
}

总结

上下文管理展示了状态管理的复杂设计:

状态类型 管理策略
文件读取 缓存 + 去重 + 修改检测
Token 预算分配 + 监控 + 自动压缩
消息历史 过滤 + 截断 + 类型管理
CLAUDE.md 加载 + 嵌套引用 + Token 控制
系统提示 动态构建 + 预算平衡
子 Agent 克隆 + 独立 + 清理
持久化 保存 + 恢复 + 验证

核心设计:缓存 + Token 预算 + 克隆 + 持久化

下一篇,我们将深入子 Agent 架构------Fork 模式如何工作,父子 Agent 如何通信。

相关推荐
奔跑中的小象6 小时前
统信UOS + 天数AI卡部署SGLang服务手册
人工智能·uos·sglang·天数智芯
DevSecOps选型指南6 小时前
中国版Mythos,为何是悬镜安全灵脉CodeAI?
人工智能·安全
Shockang6 小时前
LangGraph 状态机实战
人工智能
刹那芳华19926 小时前
循环神经网络的从零开始实现(RNN)
人工智能·rnn·深度学习
阿童木写作7 小时前
跨境图片翻译工具多合一,批量图片视频字幕翻译加智能抠图
人工智能·python·音视频·语音识别
科技之门7 小时前
AI3D从建模到贴图、绑骨和动画的完整流程怎么做?V2Fun完整工作流指南
人工智能·3d·贴图
宇宙第一小趴菜7 小时前
二、机器学习的应用领域和发展史
人工智能·机器学习
前端开发江鸟7 小时前
我写过 MCP Server,却一直以为 MCP 只有 Tool
人工智能
天国梦7 小时前
自习室智能化升级避坑指南:天学网AI智习室方案实测与选型建议
大数据·人工智能
阿里云云原生8 小时前
可用性从 99.9% 跃升至 99.995%:畅捷通如何用 AI 重塑运维底座?
运维·网络·人工智能