【6】kilo 上下文管理与压缩机制

Kilo 上下文管理与压缩机制

本文档详细介绍 Kilo 上下文管理和压缩机制,包括核心实现代码、工作原理和实际示例。


一、概述

1.1 为什么需要上下文管理

在与 AI 的长时间对话中,会话历史会不断增长,最终可能超出 LLM 的上下文窗口限制。Kilo 通过智能的上下文管理机制解决这个问题:

  • 自动监控:实时跟踪对话的 token 使用量
  • 智能压缩:当接近上下文窗口限制时,自动压缩历史对话
  • 保持连续性:压缩时保留关键信息,确保对话流畅

1.2 核心特性

  1. 双重触发机制:支持自动触发和手动触发
  2. 智能保留策略:保留最近的消息和关键上下文
  3. LLM 生成摘要:使用 AI 生成结构化的对话摘要
  4. 兜底保障:截断兜底处理,保证上下文尺寸不超限制
  5. 双轨制历史:分离 API 层和 UI 层的消息管理

二、核心架构

2.1 管理手段

上下文管理采用两种核心手段来控制token使用量,确保在有限的上下文窗口内维持高效的对话体验:

  1. 压缩策略:智能压缩历史对话,通过AI生成摘要保留关键信息
  2. 截断策略:滑动窗口机制,移除超出限制的旧消息作为兜底保障

上下文管理主要手段:
上下文管理机制
压缩策略
截断策略
智能压缩
AI摘要
保留关键信息
优先使用,保持上下文连续性和关键信息完整性
滑动窗口截断
移除旧消息
Token限制保障
兜底保障,确保不超限

2.2 分层策略

Kilo 的上下文管理基于上面的手段采用三层策略,确保在任何情况下都能正常运行:

复制代码
第一层:智能语义压缩(LLM 生成摘要)
   ↓ 失败或不可用
第二层:滑动窗口截断(机械删除)
   ↓ 仍然超限
第三层:拒绝请求(返回错误)

优先级说明

  1. 智能压缩优先:保留语义信息,压缩效果最好,用户体验最佳
  2. 滑动窗口兜底:简单粗暴但可靠,能保证不超限,确保系统可用性
  3. 拒绝请求:最后手段,提示用户手动处理或调整配置

对比分析

特性 智能压缩 滑动窗口
实现方式 LLM 生成摘要 机械删除消息
语义保留 优秀 可能丢失关键信息
Token 节省 70-80% 约 50%
执行速度 较慢(需调用 LLM) 极快(本地计算)
成本 有成本 无成本
可靠性 可能失败 100% 成功
适用场景 正常对话 紧急兜底

设计理念

这种分层设计体现了 Kilo 的工程哲学:

  • 用户体验优先:优先使用智能压缩,保留语义信息
  • 系统可用性保障:滑动窗口作为兜底,确保系统不会因压缩失败而中断
  • 渐进式降级:从最优方案逐步降级到可用方案

2.3 核心配置

核心配置参数

配置项 默认值 描述
N_MESSAGES_TO_KEEP 3 保留最近不压缩的消息数量
MIN_CONDENSE_THRESHOLD 5 压缩阈值最小百分比
MAX_CONDENSE_THRESHOLD 100 压缩阈值最大百分比
TOKEN_BUFFER_PERCENTAGE 0.1 Token缓冲区百分比

Condense配置参数

配置项 类型 描述
autoCondenseContext boolean 是否启用自动压缩
autoCondenseContextPercent number 自动压缩的阈值百分比(5-100)
customCondensingPrompt string (可选) 自定义压缩提示词
condensingApiConfigId string (可选) 专用的压缩API配置ID
profileThresholds Record<string, number> (可选) 配置文件级别的阈值覆盖

三、压缩策略

3.1 概述

触发方式

Kilo 的上下文压缩支持三种触发方式,每种方式都有其特定的使用场景和优势:

  1. 自动触发压缩:系统自动检测上下文使用情况,达到阈值时自动压缩
  2. 用户手动触发压缩:用户主动点击 UI 按钮触发压缩
  3. 用户批准 AI 压缩:AI 检测到需要压缩时,请求用户批准后执行

关键设计点

  1. 存储与传输分离:本地存储完整历史,API 调用时只传递必要上下文
  2. 智能消息选择getMessagesSinceLastSummary() 决定实际传递的消息范围
  3. Token 管理:通过摘要消息实现语义压缩,减少重复内容的 token 消耗
  4. 上下文连续性:基于摘要的上下文理解,避免重复处理历史消息
  5. 多种触发方式的互补
    • 自动触发:保证系统稳定运行,避免超限
    • 手动触发:用户主动管理,灵活控制
    • AI 建议:智能检测,提供最佳时机建议
  6. 共同的核心逻辑
    • 都调用 summarizeConversation() 执行实际压缩
    • 都更新 apiConversationHistory
    • 都会在 UI 显示压缩结果
    • 都支持自定义压缩提示词和专用 API Handler

3.2 触发方式详解

3.2.1 自动触发压缩

特点

  1. 无需干预:完全自动化,用户无感知
  2. 智能判断:基于 token 使用率和绝对数量双重判断
  3. 及时触发:在达到阈值时立即执行,避免超限
  4. 适用场景:日常对话,长时间连续开发
3.2.1.1 触发方式

系统在每次 API 请求前自动检测上下文使用情况,当满足压缩条件时自动执行压缩,无需用户干预。

3.2.1.2 触发条件

自动触发需要满足以下任一条件:

  1. 百分比阈值(当前 token 数 / 上下文窗口) >= 阈值百分比
  2. 绝对数量当前 token 数 > 允许的 token 数

条件计算公式

typescript 复制代码
// 允许的 token 数 = 上下文窗口 × (1 - 缓冲区百分比) - 保留 token 数
const allowedTokens = contextWindow * (1 - TOKEN_BUFFER_PERCENTAGE) - reservedTokens

// 触发条件
const contextPercent = (100 * prevContextTokens) / contextWindow
if (contextPercent >= effectiveThreshold || prevContextTokens > allowedTokens) {
    // 触发压缩
}

配置参数

  • autoCondenseContext : 是否启用自动压缩(默认 true
  • autoCondenseContextPercent : 压缩阈值百分比(默认 80,范围 5-100)
  • TOKEN_BUFFER_PERCENTAGE : Token 缓冲区百分比(固定 0.1,即 10%)
3.2.1.3 执行链条
复制代码
用户发送消息
    ↓
Task.attemptApiRequest()
    ↓
检查 contextTokens 是否存在
    ↓
如果存在,调用 willManageContext() 判断是否需要压缩
    ↓
如果需要压缩,则调用 manageContext() 执行压缩
    ↓
计算 token 使用情况和阈值
    ↓
判断是否启用自动压缩 (autoCondenseContext)
    ↓
如果启用:
    ↓
计算上下文百分比 (contextPercent)
    ↓
判断是否达到阈值 (contextPercent >= effectiveThreshold)
    ↓
如果达到阈值:
    ↓
调用 summarizeConversation() 进行智能压缩
    ↓
消息分割策略:
    ↓
messages.slice(0, -N_MESSAGES_TO_KEEP) → 需要压缩的消息
    ↓
messages.slice(-N_MESSAGES_TO_KEEP) → 保留的最近3条消息
    ↓
调用 getMessagesSinceLastSummary() 确定实际压缩范围
    ↓
如果压缩成功:
    ↓
构建摘要消息 (isSummary: true)
    ↓
重组消息结构:[前面的所有消息...] + [摘要消息] + [保留的最近3条消息]
    ↓
验证压缩效果 (newContextTokens < prevContextTokens)
    ↓
更新 apiConversationHistory
    ↓
设置 skipPrevResponseIdOnce = true
    ↓
通知用户压缩完成
    ↓
继续正常流程
    ↓
如果压缩失败:
    ↓
记录错误信息
    ↓
继续流程(可能触发滑动窗口)
    ↓
如果未达到阈值:
    ↓
检查是否超过允许的token数 (prevContextTokens > allowedTokens)
    ↓
如果超过:
    ↓
调用 truncateConversation() 进行滑动窗口截断
    ↓
如果未超过:
    ↓
无需压缩,继续正常流程
    ↓
如果未启用自动压缩:
    ↓
直接检查是否超过允许的token数
    ↓
如果超过,进行滑动窗口截断
    ↓
继续正常流程
3.2.1.4 压缩后的数据传递流程
复制代码
压缩完成后的数据流:
    ↓
apiConversationHistory 更新(包含所有原始消息 + 摘要消息)
    ↓
Task.attemptApiRequest() 继续执行
    ↓
调用 getMessagesSinceLastSummary(this.apiConversationHistory)
    ↓
返回传递给大模型的消息:
    - 首次压缩:所有消息(包括摘要)
    - 后续压缩:从最后一个摘要开始的消息
    ↓
maybeRemoveImageBlocks() 清理图像内容
    ↓
构建 cleanConversationHistory
    ↓
调用 this.api.createMessage(systemPrompt, cleanConversationHistory, metadata)
    ↓
大模型接收到的实际消息内容
3.2.1.5 核心代码详解
3.2.1.5.1 核心入口点:Task.attemptApiRequest()

attemptApiRequest() 是 Task 类中负责发起 API 请求的核心方法,它是整个上下文管理和压缩机制的入口点。该方法是一个异步生成器函数,集成了速率限制控制、上下文自动压缩、错误处理和重试、GPT-5 特殊处理以及流式响应处理等关键功能。

核心代码

typescript 复制代码
public async *attemptApiRequest(
        retryAttempt: number = 0,
        options: { skipProviderRateLimit?: boolean } = {},
): ApiStream {
   // ========== 第一阶段:配置获取和初始化 ==========

   // 1. 获取当前状态配置
   const state = await this.providerRef.deref()?.getState()
   const {
      apiConfiguration,
      autoApprovalEnabled,
      requestDelaySeconds,
      mode,
      autoCondenseContext = true,
      autoCondenseContextPercent = 100,
      profileThresholds = {},
   } = state ?? {}

   // 2. 获取上下文压缩配置
   const customCondensingPrompt = state?.customCondensingPrompt
   const condensingApiConfigId = state?.condensingApiConfigId
   const listApiConfigMeta = state?.listApiConfigMeta

   // 3. 确定用于压缩的 API 处理器
   let condensingApiHandler: ApiHandler | undefined
   if (condensingApiConfigId && listApiConfigMeta && Array.isArray(listApiConfigMeta)) {
      const matchingConfig = listApiConfigMeta.find((config) => config.id === condensingApiConfigId)
      if (matchingConfig) {
         const profile = await this.providerRef.deref()?.providerSettingsManager.getProfile({
            id: condensingApiConfigId,
         })
         if (profile && profile.apiProvider) {
            condensingApiHandler = buildApiHandler(profile)
         }
      }
   }

   // ========== 第二阶段:速率限制处理 ==========

   // 4. 处理提供商速率限制(除非显式跳过)
   if (!options.skipProviderRateLimit) {
      await this.maybeWaitForProviderRateLimit(retryAttempt)
   }

   // 5. 更新最后一次请求时间
   Task.lastGlobalApiRequestTime = performance.now()

   // ========== 第三阶段:系统提示词和上下文管理 ==========

   // 6. 获取系统提示词
   const systemPrompt = await this.getSystemPrompt()
   const { contextTokens } = this.getTokenUsage()

   // 7. 上下文管理(如果需要)
   if (contextTokens) {
      // KiloCode 特定处理:初始化和调整虚拟配额回退处理器
      if (this.api instanceof VirtualQuotaFallbackHandler) {
         await this.api.initialize()
         await this.api.adjustActiveHandler("Pre-Request Adjustment")
      }

      // 获取模型信息和配置
      const modelInfo = this.api.getModel().info
      const maxTokens = getModelMaxOutputTokens({
         modelId: this.api.getModel().id,
         model: modelInfo,
         settings: this.apiConfiguration,
      })
      const contextWindow = this.api.contextWindow ?? modelInfo.contextWindow
      const currentProfileId = this.getCurrentProfileId(state)
      const useNativeTools = isNativeProtocol(this._taskToolProtocol ?? "xml")

      // 计算最后一消息的令牌数
      const lastMessage = this.apiConversationHistory[this.apiConversationHistory.length - 1]
      const lastMessageContent = lastMessage?.content
      let lastMessageTokens = 0
      if (lastMessageContent) {
         lastMessageTokens = Array.isArray(lastMessageContent)
                 ? await this.api.countTokens(lastMessageContent)
                 : await this.api.countTokens([{ type: "text", text: lastMessageContent as string }])
      }

      // 检查是否需要进行上下文管理
      const contextManagementWillRun = willManageContext({
         totalTokens: contextTokens,
         contextWindow,
         maxTokens,
         autoCondenseContext,
         autoCondenseContextPercent,
         profileThresholds,
         currentProfileId,
         lastMessageTokens,
      })

      // 如果需要上下文管理,通知前端显示进度指示器
      if (contextManagementWillRun && autoCondenseContext) {
         await this.providerRef
                 .deref()
                 ?.postMessageToWebview({ type: "condenseTaskContextStarted", text: this.taskId })
      }

      // 执行上下文管理(压缩或截断)
      const truncateResult = await manageContext({
         messages: this.apiConversationHistory,
         totalTokens: contextTokens,
         maxTokens,
         contextWindow,
         apiHandler: this.api,
         autoCondenseContext,
         autoCondenseContextPercent,
         systemPrompt,
         taskId: this.taskId,
         customCondensingPrompt,
         condensingApiHandler,
         profileThresholds,
         currentProfileId,
         useNativeTools,
      })

      // 更新对话历史并发送相关消息
      if (truncateResult.messages !== this.apiConversationHistory) {
         await this.overwriteApiConversationHistory(truncateResult.messages)
      }

      if (truncateResult.error) {
         await this.say("condense_context_error", truncateResult.error)
      } else if (truncateResult.summary) {
         // 上下文压缩成功
         const { summary, cost, prevContextTokens, newContextTokens = 0, condenseId } = truncateResult
         const contextCondense: ContextCondense = {
            summary,
            cost,
            newContextTokens,
            prevContextTokens,
            condenseId,
         }
         await this.say("condense_context", undefined, undefined, false, undefined, undefined,
                 { isNonInteractive: true }, contextCondense)
      } else if (truncateResult.truncationId) {
         // 滑动窗口截断(压缩失败或禁用时的备用方案)
         const contextTruncation: ContextTruncation = {
            truncationId: truncateResult.truncationId,
            messagesRemoved: truncateResult.messagesRemoved ?? 0,
            prevContextTokens: truncateResult.prevContextTokens,
            newContextTokens: truncateResult.newContextTokensAfterTruncation ?? 0,
         }
         await this.say("sliding_window_truncation", undefined, undefined, false, undefined, undefined,
                 { isNonInteractive: true }, undefined, contextTruncation)
      }

      // 通知前端上下文管理完成
      if (contextManagementWillRun && autoCondenseContext) {
         await this.providerRef
                 .deref()
                 ?.postMessageToWebview({ type: "condenseTaskContextResponse", text: this.taskId })
      }
   }

   // ========== 第四阶段:构建请求上下文 ==========

   // 8. 获取有效的 API 历史(过滤掉已压缩的消息)
   const effectiveHistory = getEffectiveApiHistory(this.apiConversationHistory)
   const messagesSinceLastSummary = getMessagesSinceLastSummary(effectiveHistory)
   const messagesWithoutImages = maybeRemoveImageBlocks(messagesSinceLastSummary, this.api)
   const cleanConversationHistory = this.buildCleanConversationHistory(messagesWithoutImages as ApiMessage[])

   // 9. KiloCode 特定处理:获取项目配置
   const kiloConfig = this.providerRef.deref()?.getKiloConfig()

   // ========== 第五阶段:自动批准限制检查 ==========

   // 10. 检查自动批准限制
   const approvalResult = await this.autoApprovalHandler.checkAutoApprovalLimits(
           state,
           this.combineMessages(this.clineMessages.slice(1)),
           async (type, data) => this.ask(type, data),
   )

   if (!approvalResult.shouldProceed) {
      throw new Error("Auto-approval limit reached and user did not approve continuation")
   }

   // ========== 第六阶段:工具配置 ==========

   // 11. 确定是否包含原生工具
   const modelInfo = this.api.getModel().info
   const taskProtocol = this._taskToolProtocol ?? "xml"
   const shouldIncludeTools = taskProtocol === TOOL_PROTOCOL.NATIVE && (modelInfo.supportsNativeTools ?? false)

   // 12. 构建工具数组
   let allTools: OpenAI.Chat.ChatCompletionTool[] = []
   let allowedFunctionNames: string[] | undefined
   const supportsAllowedFunctionNames = apiConfiguration?.apiProvider === "gemini"

   if (shouldIncludeTools) {
      const provider = this.providerRef.deref()
      if (!provider) {
         throw new Error("Provider reference lost during tool building")
      }

      const toolsResult = await buildNativeToolsArrayWithRestrictions({
         provider,
         cwd: this.cwd,
         mode,
         customModes: state?.customModes,
         experiments: state?.experiments,
         apiConfiguration,
         maxReadFileLine: state?.maxReadFileLine ?? 500,
         maxConcurrentFileReads: state?.maxConcurrentFileReads ?? 5,
         browserToolEnabled: state?.browserToolEnabled ?? true,
         state,
         modelInfo,
         diffEnabled: this.diffEnabled,
         includeAllToolsWithRestrictions: supportsAllowedFunctionNames,
      })
      allTools = toolsResult.tools
      allowedFunctionNames = toolsResult.allowedFunctionNames
   }

   // ========== 第七阶段:构建元数据和发起请求 ==========

   // 13. 构建请求元数据
   const parallelToolCallsEnabled = false // 并行工具调用目前被禁用
   const metadata: ApiHandlerCreateMessageMetadata = {
      mode: mode,
      taskId: this.taskId,
      suppressPreviousResponseId: this.skipPrevResponseIdOnce,
      ...(shouldIncludeTools
              ? {
                 tools: allTools,
                 tool_choice: "auto",
                 toolProtocol: taskProtocol,
                 parallelToolCalls: parallelToolCallsEnabled,
                 ...(allowedFunctionNames ? { allowedFunctionNames } : {}),
              }
              : {}),
      projectId: (await kiloConfig)?.project?.id,
   }

   // 14. 创建 AbortController 用于取消请求
   this.currentRequestAbortController = new AbortController()
   const abortSignal = this.currentRequestAbortController.signal
   this.skipPrevResponseIdOnce = false

   // 15. 发起 API 请求
   const stream = this.api.createMessage(
           systemPrompt,
           cleanConversationHistory as unknown as Anthropic.Messages.MessageParam[],
           metadata,
   )
   const iterator = stream[Symbol.asyncIterator]()

   // 16. 设置中止处理
   const abortCleanupListener = () => {
      console.log(`[Task#${this.taskId}.${this.instanceId}] AbortSignal triggered for current request`)
      this.currentRequestAbortController = undefined
   }
   abortSignal.addEventListener("abort", abortCleanupListener)

   // ========== 第八阶段:流式响应处理和错误处理 ==========

   try {
      // 17. 等待第一个 chunk 以检测错误
      this.isWaitingForFirstChunk = true

      const firstChunkPromise = iterator.next()
      const abortPromise = new Promise<never>((_, reject) => {
         if (abortSignal.aborted) {
            reject(new Error("Request cancelled by user"))
         } else {
            const firstChunkAbortListener = () => reject(new Error("Request cancelled by user"))
            abortSignal.addEventListener("abort", firstChunkAbortListener)
         }
      })

      const firstChunk = await Promise.race([firstChunkPromise, abortPromise])
      yield firstChunk.value
      this.isWaitingForFirstChunk = false
   } catch (error) {
      this.isWaitingForFirstChunk = false

      // 18. KiloCode 特定错误处理
      if (apiConfiguration?.apiProvider === "kilocode" && isAnyRecognizedKiloCodeError(error)) {
         const { response } = await (isPaymentRequiredError(error)
                 ? this.ask("payment_required_prompt", JSON.stringify({
                    title: error.error?.title ?? t("kilocode:lowCreditWarning.title"),
                    message: error.error?.message ?? t("kilocode:lowCreditWarning.message"),
                    balance: error.error?.balance ?? "0.00",
                    buyCreditsUrl: error.error?.buyCreditsUrl ?? getAppUrl("/profile"),
                 }))
                 : this.ask("invalid_model", JSON.stringify({
                    modelId: apiConfiguration.kilocodeModel,
                    error: {
                       status: error.status,
                       message: error.message,
                    },
                 })))

         this.currentRequestAbortController = undefined

         if (response === "retry_clicked") {
            yield* this.attemptApiRequest(retryAttempt + 1)
         } else {
            throw error
         }
         return
      }

      // 19. 自动重试处理
      if (autoApprovalEnabled) {
         // 应用指数退避和倒计时 UX
         await this.backoffAndAnnounce(retryAttempt, error)

         // 检查任务是否被中止
         if (this.abort) {
            throw new Error(`[Task#attemptApiRequest] task ${this.taskId}.${this.instanceId} aborted during retry`)
         }

         // 递归调用进行重试
         yield* this.attemptApiRequest(retryAttempt + 1)
         return
      } else {
         // 20. 用户确认重试
         const { response } = await this.ask(
                 "api_req_failed",
                 error.message ?? JSON.stringify(serializeError(error), null, 2),
         )

         if (response !== "yesButtonClicked") {
            throw new Error("API request failed")
         }

         await this.say("api_req_retried")
         yield* this.attemptApiRequest()
         return
      }
   }

   // ========== 第九阶段:流式响应传递和清理 ==========

   // 21. 传递剩余的所有 chunks
   yield* iterator

   // 22. KiloCode 特定处理:更新请求时间
   if (apiConfiguration?.rateLimitAfter) {
      Task.lastGlobalApiRequestTime = performance.now()
   }

   // 23. 清理中止监听器
   abortSignal.removeEventListener("abort", abortCleanupListener)
}

关键设计点

  1. Token 使用检查 :只有当 contextTokens 存在时才进行压缩检查,避免不必要的计算
  2. 压缩触发 :通过 willManageContext() 函数判断是否需要压缩,如果需要压缩则调用 manageContext() 执行压缩,这些函数会检查百分比阈值和绝对数量
  3. 压缩结果处理
    • 如果消息历史发生变化,更新 apiConversationHistory
    • 如果压缩失败,显示错误消息给用户
    • 如果压缩成功,显示压缩信息并设置 skipPrevResponseIdOnce 标志
  4. GPT-5 兼容性 :压缩后跳过 previous_response_id,因为上下文已经改变,避免 GPT-5 的上下文追踪出现问题
  5. 消息筛选 :使用 getMessagesSinceLastSummary() 函数确保只传递最后一个摘要及其后续消息,而不是完整的对话历史,从而实现 token 节省
3.2.1.5.2 上下文检查和处理:willManageContext 和 manageContext

willManageContext 是被 attemptApiRequest() 调用的核心函数,负责检查上下文长度并决定是否需要压缩。

typescript 复制代码
/**
 * 判断是否需要进行上下文管理(压缩或截断)
 * 
 * @param options - 上下文管理配置选项
 * @returns 是否需要进行上下文管理
 */
export function willManageContext({
    totalTokens,
    contextWindow,
    maxTokens,
    autoCondenseContext,
    autoCondenseContextPercent,
    profileThresholds,
    currentProfileId,
    lastMessageTokens,
}: WillManageContextOptions): boolean {
    // 当自动压缩功能关闭时,仅检查是否需要截断
    if (!autoCondenseContext) {
        // 计算预留的响应 token 数量
        const reservedTokens = maxTokens || ANTHROPIC_DEFAULT_MAX_TOKENS
        // 计算包含最新消息后的总 token 数
        const prevContextTokens = totalTokens + lastMessageTokens
        // 计算允许的最大 token 数(上下文窗口 * (1 - 缓冲区百分比) - 预留 token)
        const allowedTokens = contextWindow * (1 - TOKEN_BUFFER_PERCENTAGE) - reservedTokens
        // 当实际 token 数超过允许值时需要截断
        return prevContextTokens > allowedTokens
    }

    // 计算预留的响应 token 数量
    const reservedTokens = maxTokens || ANTHROPIC_DEFAULT_MAX_TOKENS
    // 计算包含最新消息后的总 token 数
    const prevContextTokens = totalTokens + lastMessageTokens
    // 计算允许的最大 token 数
    const allowedTokens = contextWindow * (1 - TOKEN_BUFFER_PERCENTAGE) - reservedTokens

    // 确定生效的压缩阈值
    let effectiveThreshold = autoCondenseContextPercent
    const profileThreshold = profileThresholds[currentProfileId]
    if (profileThreshold !== undefined) {
        // -1 表示使用全局设置
        if (profileThreshold === -1) {
            effectiveThreshold = autoCondenseContextPercent
        } else if (profileThreshold >= MIN_CONDENSE_THRESHOLD && profileThreshold <= MAX_CONDENSE_THRESHOLD) {
            // 使用配置文件中的有效阈值
            effectiveThreshold = profileThreshold
        }
        // 无效值回退到全局设置(effectiveThreshold 已设置)
    }

    // 计算当前上下文占用的百分比
    const contextPercent = (100 * prevContextTokens) / contextWindow
    // 当上下文占用百分比达到阈值或超过允许的 token 数时需要管理
    return contextPercent >= effectiveThreshold || prevContextTokens > allowedTokens
}

manageContext 是被 attemptApiRequest() 调用的核心函数,负责执行上下文管理

typescript 复制代码
// manageContext 函数用于执行上下文管理(压缩或截断)
export async function manageContext({
										messages,
										totalTokens,
										contextWindow,
										maxTokens,
										apiHandler,
										autoCondenseContext,
										autoCondenseContextPercent,
										systemPrompt,
										taskId,
										profileThresholds,
										currentProfileId,
										customCondensingPrompt,
										condensingApiHandler,
									}: TruncateOptions): Promise<TruncateResult> {
	// 计算可用 token 数量
	const reservedTokens = maxTokens + 1000
	const allowedTokens = contextWindow * (1 - TOKEN_BUFFER_PERCENTAGE) - reservedTokens

	// 确定有效阈值
	let effectiveThreshold = autoCondenseContextPercent
	// ... 阈值处理逻辑

	// 1. 如果启用了自动压缩,先尝试智能压缩
	if (autoCondenseContext) {
		const contextPercent = (100 * prevContextTokens) / contextWindow
		if (contextPercent >= effectiveThreshold || prevContextTokens > allowedTokens) {
			// 尝试智能压缩
			const result = await summarizeConversation(
				messages,
				apiHandler,
				systemPrompt,
				taskId,
				prevContextTokens,
				true,
				customCondensingPrompt,
				condensingApiHandler,
			)

			// 2. 如果智能压缩失败,记录错误但继续流程
			if (result.error) {
				return {
					messages,
					error: result.error,
					cost: result.cost,
					summary: "",
					prevContextTokens,
				}
			}

			// 3. 智能压缩成功,直接返回
			return { ...result, prevContextTokens }
		}
	}

	// 4. 如果 token 数仍然超过允许值,使用滑动窗口截断
	if (prevContextTokens > allowedTokens) {
		const truncatedMessages = truncateConversation(messages, 0.5, taskId)
		return {
			messages: truncatedMessages,
			error: "",
			cost: 0,
			summary: "",
			prevContextTokens,
		}
	}

	// 5. 不需要任何处理,返回原始消息
	return {
		messages,
		error: "",
		cost: 0,
		summary: "",
		prevContextTokens,
	}
}

关键点

  1. 双重判断:同时检查百分比和绝对数量
  2. 配置灵活性:支持全局和配置文件级别的阈值
  3. 预留空间:为模型响应预留 token 空间
  4. 错误处理:压缩失败时返回原始消息

3.2.2 用户手动触发压缩

特点

  1. 用户主动:用户完全掌控压缩时机
  2. 执行快速:无需确认,直接执行
  3. 路径直接:UI → Extension → Task,响应更快
  4. 适用场景:用户明确知道需要压缩的时候,如切换任务前、长时间暂停后
3.2.2.1 触发方式

用户主动点击 TaskHeader 的折叠按钮(FoldVertical 图标)触发压缩。

3.2.2.2 执行链条
复制代码
用户点击 TaskHeader 的折叠按钮(FoldVertical 图标)
    ↓
TaskHeader 组件调用 handleCondenseContext(currentTaskItem.id)
    ↓
handleCondenseContext 函数(在 ChatView 中定义)
    ↓
vscode.postMessage({ type: "condenseContext", taskId })
    ↓
VSCode Extension 接收消息(src/extension.ts)
    ↓
provider.condenseContext(taskId)
    ↓
Task.condenseContext() 方法
    ↓
调用 summarizeConversation() 执行压缩
    ↓
更新 apiConversationHistory
    ↓
通知 UI 显示压缩结果
3.2.2.3 关键代码实现
3.2.2.3.1 UI 层按钮(webview-ui/src/components/chat/TaskHeader.tsx
typescript 复制代码
const condenseButton = (
    <StandardTooltip content={t("chat:task.condenseContext")}>
        <button
            disabled={buttonsDisabled}
            onClick={() => currentTaskItem && handleCondenseContext(currentTaskItem.id)}
            className="shrink-0 min-h-[20px] min-w-[20px] p-[2px] cursor-pointer disabled:cursor-not-allowed opacity-85 hover:opacity-100 bg-transparent border-none rounded-md">
            <FoldVertical size={16} />
        </button>
    </StandardTooltip>
)
3.2.2.3.2 消息发送(webview-ui/src/components/chat/ChatView.tsx
typescript 复制代码
const handleCondenseContext = (taskId: string) => {
    vscode.postMessage({ 
        type: "condenseContext", 
        taskId 
    })
}
3.2.2.3.3 Extension 消息处理(src/extension.ts
typescript 复制代码
case "condenseContext": {
    const { taskId } = message
    await provider.condenseContext(taskId)
    break
}
3.2.2.3.4 Task 压缩执行(src/core/task/Task.ts
typescript 复制代码
/**
 * 执行上下文压缩的核心方法
 * 通过调用 summarizeConversation 来压缩对话历史,减少 token 使用量
 */
public async condenseContext(): Promise<void> {
	// CRITICAL: 压缩前必须刷新所有待处理的工具结果
	// 确保 tool_use/tool_result 对在历史记录中是完整的
	await this.flushPendingToolResultsToHistory()

	// 获取系统提示词,用于压缩时的上下文
	const systemPrompt = await this.getSystemPrompt()

	// 获取压缩配置参数
	const state = await this.providerRef.deref()?.getState()
	// 这些属性可能在状态类型中尚不存在,但用于压缩配置
	const customCondensingPrompt = state?.customCondensingPrompt  // 自定义压缩提示词
	const condensingApiConfigId = state?.condensingApiConfigId    // 压缩专用 API 配置 ID
	const listApiConfigMeta = state?.listApiConfigMeta           // API 配置元数据列表

	// 确定要使用的 API 处理器
	let condensingApiHandler: ApiHandler | undefined
	if (condensingApiConfigId && listApiConfigMeta && Array.isArray(listApiConfigMeta)) {
		// 根据 ID 查找匹配的配置
		const matchingConfig = listApiConfigMeta.find((config) => config.id === condensingApiConfigId)
		if (matchingConfig) {
			const profile = await this.providerRef.deref()?.providerSettingsManager.getProfile({
				id: condensingApiConfigId,
			})
			// 确保配置文件和 apiProvider 存在后再尝试构建处理器
			if (profile && profile.apiProvider) {
				condensingApiHandler = buildApiHandler(profile)
			}
		}
	}

	// 获取压缩前的上下文 token 数量
	const { contextTokens: prevContextTokens } = this.getTokenUsage()

	// 确定是否使用原生工具协议进行正确的消息处理
	// 使用任务锁定的协议,而不是当前设置(如果未设置则回退到 xml)
	const useNativeTools = isNativeProtocol(this._taskToolProtocol ?? "xml")

	// 调用 summarizeConversation 执行实际的压缩操作
	const {
		messages,           // 压缩后的消息历史
		summary,            // 生成的摘要内容
		cost,               // 压缩操作的成本
		newContextTokens = 0, // 压缩后的新上下文 token 数
		error,              // 错误信息(如果有)
		condenseId,         // 压缩操作 ID
	} = await summarizeConversation(
		this.apiConversationHistory,    // 要压缩的对话历史
		this.api,                       // 主 API 处理器(备用)
		systemPrompt,                   // 默认摘要提示词(备用)
		this.taskId,                    // 任务 ID
		prevContextTokens,              // 压缩前的 token 数
		false,                          // 手动触发标志
		customCondensingPrompt,         // 用户的自定义提示词
		condensingApiHandler,           // 专门用于压缩的处理器
		useNativeTools,                 // 传递原生工具标志以进行正确的消息处理
	)
	
	// 如果压缩过程中出现错误,发送错误消息并返回
	if (error) {
		this.say(
			"condense_context_error",
			error,
			undefined /* images */,
			false /* partial */,
			undefined /* checkpoint */,
			undefined /* progressStatus */,
			{ isNonInteractive: true } /* options */,
		)
		return
	}
	
	// 更新 API 对话历史为压缩后的内容
	await this.overwriteApiConversationHistory(messages)

	// 构建压缩上下文信息对象
	const contextCondense: ContextCondense = {
		summary,             // 摘要内容
		cost,                // 压缩成本
		newContextTokens,    // 压缩后的 token 数
		prevContextTokens,   // 压缩前的 token 数
		condenseId: condenseId!, // 压缩 ID
	}
	
	// 发送压缩完成的通知消息
	await this.say(
		"condense_context",
		undefined /* text */,
		undefined /* images */,
		false /* partial */,
		undefined /* checkpoint */,
		undefined /* progressStatus */,
		{ isNonInteractive: true } /* options */,
		contextCondense,
	)

	// 压缩完成后处理任何排队的消息
	this.processQueuedMessages()
}

3.2.3 用户批准 AI 压缩(condense 工具)

特点

  1. 智能触发:AI 主动检测最佳压缩时机
  2. 用户交互:用户可以提供反馈指导压缩方向
  3. 灵活性:用户可以拒绝或修改压缩建议
  4. 适用场景:AI 判断需要压缩但希望用户确认的场景
3.2.3.1 触发方式

AI 在对话过程中主动检测到上下文过长时,会调用 condense 工具建议进行压缩。

3.2.3.2 执行链条
复制代码
AI 检测到上下文过长,决定调用 condense 工具
    ↓
AI 输出工具调用(在响应中)
    ↓
Task 解析工具调用,识别为 condense 工具
    ↓
调用 condenseTool 函数(src/core/tools/condenseTool.ts)
    ↓
询问用户是否接受压缩(cline.ask("condense", context))
    ↓
用户响应(接受或提供反馈)
    ↓
如果用户接受:
    ↓
调用 summarizeConversation() 执行压缩
    ↓
更新 apiConversationHistory(cline.overwriteApiConversationHistory)
    ↓
返回工具结果给 AI
3.2.3.3 关键代码实现

文件路径 : src/core/tools/condenseTool.ts

typescript 复制代码
export const condenseTool = async (
    cline: Task,
    block: ToolUse,
    askApproval: AskApproval,
    handleError: HandleError,
    pushToolResult: PushToolResult,
    removeClosingTag: RemoveClosingTag,
) => {
    const context: string | undefined = block.params.message
    
    try {
        // 处理部分工具调用
        if (block.partial) {
            await cline.ask("condense", removeClosingTag("message", context), block.partial).catch(() => {})
            return
        }
        
        // 验证参数
        if (!context) {
            cline.consecutiveMistakeCount++
            pushToolResult(await cline.sayAndCreateMissingParamError("condense", "context"))
            return
        }
        
        cline.consecutiveMistakeCount = 0

        // 询问用户是否接受压缩
        const { text, images } = await cline.ask("condense", context, false)

        // 如果用户提供了反馈
        if (text || images?.length) {
            await cline.say("user_feedback", text ?? "", images)
            pushToolResult(
                formatResponse.toolResult(
                    `The user provided feedback on the condensed conversation summary:\n<feedback>\n${text}\n</feedback>`,
                    images,
                )
            )
        } else {
            // 用户接受压缩,执行压缩
            pushToolResult(formatResponse.toolResult(formatResponse.condense()))

            const { contextTokens: prevContextTokens } = cline.getTokenUsage()

            // 调用 summarizeConversation 创建压缩版本
            const summarizedMessages = await summarizeConversation(
                cline.apiConversationHistory,
                cline.api,
                await cline.getSystemPrompt(),
                "TaskId condenseTool",
                prevContextTokens,
            )

            // 更新对话历史
            await cline.overwriteApiConversationHistory(summarizedMessages.messages)
        }
        return
    } catch (error) {
        await handleError("condensing context window", error)
        return
    }
}

3.3 三种压缩方式对比

特性 自动触发压缩 用户手动触发压缩 用户批准 AI 压缩
触发者 系统自动 用户主动 AI 主动
用户交互 无需交互 点击按钮 需要确认
触发时机 达到阈值时 用户决定 AI 检测时
灵活性 配置阈值 完全自主 可提供反馈
适用场景 日常对话 主动管理 智能建议
执行路径 attemptApiRequest → willManageContext/manageContext UI → Extension → Task.condenseContext() AI 响应 → condenseTool → summarizeConversation
isAutomaticTrigger true false 未传递(默认 undefined
响应速度 快速 最快 较慢(需等待用户)
用户感知

3.4 压缩核心源码解析

3.4.1 压缩执行核心:summarizeConversation()

文件路径 : src/core/condense/index.ts (第 78-212 行)

typescript 复制代码
export async function summarizeConversation(
    messages: ApiMessage[],
    apiHandler: ApiHandler,
    systemPrompt: string,
    taskId: string,
    prevContextTokens: number,
    isAutomaticTrigger?: boolean,
    customCondensingPrompt?: string,
    condensingApiHandler?: ApiHandler,
): Promise<SummarizeResponse> {
    // 1. 记录遥测事件
    TelemetryService.instance.captureContextCondensed(
        taskId,
        isAutomaticTrigger ?? false,
        !!customCondensingPrompt?.trim(),
        !!condensingApiHandler,
    )

    const response: SummarizeResponse = { messages, cost: 0, summary: "" }
    
    // 2. 获取需要压缩的消息(排除最后 3 条)
    const messagesToSummarize = getMessagesSinceLastSummary(
        messages.slice(0, -N_MESSAGES_TO_KEEP)
    )

    // 3. 检查消息数量是否足够
    if (messagesToSummarize.length <= 1) {
        const error = messages.length <= N_MESSAGES_TO_KEEP + 1
            ? t("common:errors.condense_not_enough_messages", {
                prevContextTokens,
                messageCount: messages.length,
                minimumMessageCount: N_MESSAGES_TO_KEEP + 2,
            })
            : t("common:errors.condensed_recently")
        return { ...response, error }
    }

    // 4. 保留最近的消息
    const keepMessages = messages.slice(-N_MESSAGES_TO_KEEP)
    
    // 5. 检查最近是否有摘要
    const recentSummaryExists = keepMessages.some((message) => message.isSummary)
    if (recentSummaryExists) {
        const error = t("common:errors.condensed_recently")
        return { ...response, error }
    }

    // 6. 构建请求消息
    const finalRequestMessage: Anthropic.MessageParam = {
        role: "user",
        content: "Summarize the conversation so far, as described in the prompt instructions.",
    }

    const requestMessages = maybeRemoveImageBlocks(
        [...messagesToSummarize, finalRequestMessage],
        apiHandler
    ).map(({ role, content }) => ({ role, content }))

    // 7. 选择压缩提示词
    const promptToUse = customCondensingPrompt?.trim() 
        ? customCondensingPrompt.trim() 
        : SUMMARY_PROMPT

    // 8. 选择 API Handler
    let handlerToUse = condensingApiHandler || apiHandler
    
    // 验证 handler 有效性
    if (!handlerToUse || typeof handlerToUse.createMessage !== "function") {
        console.warn("Chosen API handler for condensing is invalid, falling back to main apiHandler.")
        handlerToUse = apiHandler
        
        if (!handlerToUse || typeof handlerToUse.createMessage !== "function") {
            const error = t("common:errors.condense_handler_invalid")
            return { ...response, error }
        }
    }

    // 9. 调用 LLM 生成摘要
    const stream = handlerToUse.createMessage(promptToUse, requestMessages)

    let summary = ""
    let cost = 0
    let outputTokens = 0

    for await (const chunk of stream) {
        if (chunk.type === "text") {
            summary += chunk.text
        } else if (chunk.type === "usage") {
            cost = chunk.totalCost ?? 0
            outputTokens = chunk.outputTokens ?? 0
        }
    }

    summary = summary.trim()

    // 10. 验证摘要是否生成成功
    if (summary.length === 0) {
        const error = t("common:errors.condense_failed")
        return { ...response, cost, error }
    }

    // 11. 构建摘要消息
    const summaryMessage: ApiMessage = {
        role: "assistant",
        content: summary,
        ts: keepMessages[0].ts,
        isSummary: true, // 标记为摘要消息
    }

    // 12. 构建新的消息历史
    const newMessages = [
        ...messages.slice(0, -N_MESSAGES_TO_KEEP), // 早期消息
        summaryMessage,                            // 摘要消息
        ...keepMessages                            // 最近 3 条消息
    ]

    // 13. 验证压缩效果
    const systemPromptMessage: ApiMessage = { role: "user", content: systemPrompt }
    const contextMessages = outputTokens
        ? [systemPromptMessage, ...keepMessages]
        : [systemPromptMessage, summaryMessage, ...keepMessages]

    const contextBlocks = contextMessages.flatMap((message) =>
        typeof message.content === "string" 
            ? [{ text: message.content, type: "text" as const }] 
            : message.content
    )

    const newContextTokens = outputTokens + (await apiHandler.countTokens(contextBlocks))
    
    // 14. 检查压缩是否有效
    if (newContextTokens >= prevContextTokens) {
        const error = t("common:errors.condense_context_grew", { 
            prevContextTokens, 
            newContextTokens 
        })
        return { ...response, cost, error }
    }
    
    // 15. 返回成功结果
    return { messages: newMessages, summary, cost, newContextTokens }
}

关键步骤解析

  1. 遥测记录:记录压缩操作的元数据
  2. 消息筛选:排除最近 3 条消息,只压缩历史消息
  3. 数量检查:确保有足够的消息可以压缩
  4. 保留策略:保留最近 3 条消息不压缩
  5. 重复检测:避免短时间内重复压缩
  6. 请求构建:构建发送给 LLM 的请求
  7. 提示词选择:支持自定义压缩提示词
  8. Handler 选择:支持专用的压缩 API Handler
  9. 流式处理:实时接收 LLM 生成的摘要
  10. 摘要验证:确保摘要生成成功
  11. 消息构建 :创建带 isSummary 标记的摘要消息
  12. 历史重构:构建新的消息历史结构
  13. 效果验证:计算压缩后的 token 数
  14. 有效性检查:确保 token 数确实减少
  15. 结果返回:返回新的消息历史和元数据

这里有一个关键的设计理念需要特别强调:压缩并不意味着删除消息!

实际的压缩逻辑是:

  1. 保留所有原始消息messages.slice(0, -N_MESSAGES_TO_KEEP) 中的所有消息都被保留
  2. 添加摘要消息:在保留的消息后插入一个 LLM 生成的摘要消息
  3. 消息数量增加:最终消息数量 = 原始消息数量 + 1(摘要消息)

示例说明

typescript 复制代码
// 原始消息(10条)
[消息1, 消息2, 消息3, 消息4, 消息5, 消息6, 消息7, 消息8, 消息9, 消息10]

// 压缩后(11条)
[消息1, 消息2, 消息3, 消息4, 消息5, 消息6, 消息7, 摘要消息, 消息8, 消息9, 消息10]

// 其中:
// - 消息1-7 被保留(没有被删除)
// - 消息1-7 的内容被 LLM 压缩成摘要消息
// - 消息8-10 保持不变
// - 最终消息数量:7 + 1 + 3 = 11条

这种设计的优势:

  • 本地存储完整:保留所有历史消息,便于回溯
  • API 传输优化 :通过 getMessagesSinceLastSummary() 只传递必要的上下文
  • 语义压缩:摘要消息提供了历史对话的语义理解
  • Token 节省:虽然存储了所有消息,但传递给 LLM 时只传递摘要后的内容

3.4.2 第二次压缩的处理

当进行第二次压缩时,系统会特别处理已有的摘要消息。

关键代码

typescript 复制代码
// 在 summarizeConversation() 中
const messagesToSummarize = getMessagesSinceLastSummary(
    messages.slice(0, -N_MESSAGES_TO_KEEP)
)

处理流程

复制代码
假设第一次压缩后的消息结构(10条):
[消息1, 消息2, 消息3, 消息4, 摘要1(isSummary: true), 消息5, 消息6, 消息7, 消息8, 消息9]

第二次压缩时:
1. messages.slice(0, -3) 获取除最后3条之外的消息
   → [消息1, 消息2, 消息3, 消息4, 摘要1, 消息5, 消息6]

2. getMessagesSinceLastSummary() 处理后
   → [
       { role: "user", content: "Please continue from the following summary:" },
       摘要1,
       消息5,
       消息6
     ]

3. LLM 基于摘要1和后续对话生成新的摘要2

4. 最终消息结构(11条)
   → [消息1, 消息2, 消息3, 消息4, 摘要1, 消息5, 消息6, 摘要2(isSummary: true), 消息7, 消息8, 消息9]

关键结论

  • 第一个摘要消息会被包含在第二次压缩的输入中
  • LLM 会基于第一个摘要和后续对话生成新的摘要
  • 这种设计确保了上下文的连续性和完整性
  • 每次压缩都会增加一条摘要消息,但不会删除任何原始消息

3.4.3 压缩提示词

文件路径 : src/core/condense/index.ts (第 21-67 行)

typescript 复制代码
const SUMMARY_PROMPT = `\
Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing with the conversation and supporting any continuing tasks.

Your summary should be structured as follows:
Context: The context to continue the conversation with. If applicable based on the current task, this should include:
  1. Previous Conversation: High level details about what was discussed throughout the entire conversation with the user. This should be written to allow someone to be able to follow the general overarching conversation flow.
  2. Current Work: Describe in detail what was being worked on prior to this request to summarize the conversation. Pay special attention to the more recent messages in the conversation.
  3. Key Technical Concepts: List all important technical concepts, technologies, coding conventions, and frameworks discussed, which might be relevant for continuing with this work.
  4. Relevant Files and Code: If applicable, enumerate specific files and code sections examined, modified, or created for the task continuation. Pay special attention to the most recent messages and changes.
  5. Problem Solving: Document problems solved thus far and any ongoing troubleshooting efforts.
  6. Pending Tasks and Next Steps: Outline all pending tasks that you have explicitly been asked to work on, as well as list the next steps you will take for all outstanding work, if applicable. Include code snippets where they add clarity. For any next steps, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no information loss in context between tasks.

Example summary structure:
1. Previous Conversation:
  [Detailed description]
2. Current Work:
  [Detailed description]
3. Key Technical Concepts:
  - [Concept 1]
  - [Concept 2]
  - [...]
4. Relevant Files and Code:
  - [File Name 1]
    - [Summary of why this file is important]
    - [Summary of the changes made to this file, if any]
    - [Important Code Snippet]
  - [File Name 2]
    - [Important Code Snippet]
  - [...]
5. Problem Solving:
  [Detailed description]
6. Pending Tasks and Next Steps:
  - [Task 1 details & next steps]
  - [Task 2 details & next steps]
  - [...]

Output only the summary of the conversation so far, without any additional commentary or explanation.
`

提示词特点

  1. 结构化输出:要求 6 个明确的部分
  2. 技术细节:强调保留技术概念和代码片段
  3. 任务连续性:要求包含待办任务和下一步计划
  4. 原文引用:要求直接引用最近的对话内容

3.5 消息选择策略:getMessagesSinceLastSummary()

这是压缩机制中最关键的函数之一,它决定了实际传递给 LLM 的消息内容。

文件路径 : src/core/condense/index.ts (第 239-264 行)

typescript 复制代码
export function getMessagesSinceLastSummary(messages: ApiMessage[]): ApiMessage[] {
    // 1. 查找最后一个摘要消息的索引(从后往前查找)
    let lastSummaryIndexReverse = [...messages]
        .reverse()
        .findIndex((message) => message.isSummary)

    if (lastSummaryIndexReverse === -1) {
        // 如果没有摘要,返回所有消息
        return messages
    }

    // 2. 计算从最后一个摘要开始的消息
    const lastSummaryIndex = messages.length - lastSummaryIndexReverse - 1
    const messagesSinceSummary = messages.slice(lastSummaryIndex)

    // 3. 添加用户提示消息(Bedrock 兼容性要求)
    const userMessage: ApiMessage = {
        role: "user",
        content: "Please continue from the following summary:",
        ts: messages[0]?.ts ? messages[0].ts - 1 : Date.now(),
    }

    return [userMessage, ...messagesSinceSummary]
}

核心逻辑

  1. 查找摘要位置 :从消息数组末尾开始,查找最后一个 isSummary: true 的消息
  2. 确定消息范围
    • 如果没有摘要 → 返回所有消息
    • 如果有摘要 → 只返回摘要及其后续消息
  3. 添加用户提示:在返回的消息前添加一个用户提示消息

不同场景的处理

场景 1:首次压缩(无历史摘要)
typescript 复制代码
// 输入消息
[消息1, 消息2, 消息3, 消息4, 消息5, 消息6, 消息7]

// getMessagesSinceLastSummary() 返回
[消息1, 消息2, 消息3, 消息4, 消息5, 消息6, 消息7]

// 说明:没有摘要,返回所有消息
场景 2:第二次压缩(已有摘要)
typescript 复制代码
// 输入消息(包含第一次压缩的摘要)
[消息1, 消息2, 消息3, 消息4, 摘要1, 消息5, 消息6, 消息7]

// getMessagesSinceLastSummary() 返回
[
    { role: "user", content: "Please continue from the following summary:" },
    摘要1,
    消息5,
    消息6,
    消息7
]

// 说明:只返回最后一个摘要及其后续消息

关键设计点

  1. 增量压缩:每次压缩只处理摘要之后的新消息,避免重复压缩
  2. 上下文连续性:通过保留最后一个摘要,确保 LLM 能够理解对话的完整上下文
  3. 用户提示优化:添加提示消息,帮助 LLM 理解当前状态
  4. 时间戳管理:用户提示消息的时间戳设置为比第一条消息早 1 毫秒,确保正确的消息顺序

四、截断策略

当智能压缩失败或不可用时,系统会使用滑动窗口截断作为兜底方案。这是一个关键的保障机制,确保即使在压缩失败的情况下,系统也能继续正常运行。

4.1 触发条件

文件路径 : src/core/context-management/index.ts (第 100-200 行左右)

manageContext() 函数中,如果压缩失败或压缩后,仍然超限,则触发滑动窗口兜底机制

关键逻辑

  1. 优先智能压缩:如果启用了自动压缩且达到阈值,先尝试智能压缩
  2. 记录失败信息:智能压缩失败时,记录错误但不中断流程
  3. 兜底截断:如果 token 数仍然超限,使用滑动窗口截断
  4. 保证可用性:确保在任何情况下都能返回有效的消息历史

4.2 截断实现

文件路径 : src/core/context-management/index.ts (第 66 行左右)

typescript 复制代码
/**
 * 通过标记消息为隐藏而不是删除它们来截断对话
 *
 * 第一条消息总是保留,从开始处(不包括第一条)指定比例的消息(四舍五入到偶数)
 * 被标记为 truncationParent。插入一个截断标记来跟踪截断发生的位置。
 *
 * 这实现了非破坏性的滑动窗口截断,如果用户倒回到截断点之前,允许恢复消息。
 *
 * @param {ApiMessage[]} messages - 对话消息数组
 * @param {number} fracToRemove - 要隐藏的消息比例(介于0和1之间,不包括第一条消息)
 * @param {string} taskId - 对话的任务ID,用于遥测
 * @returns {TruncationResult} 包含标记的消息、截断ID和移除消息数的对象
 */
export function truncateConversation(messages: ApiMessage[], fracToRemove: number, taskId: string): TruncationResult {
   // 记录滑动窗口截断的遥测数据
   TelemetryService.instance.captureSlidingWindowTruncation(taskId)

   // 生成唯一的截断ID,用于标识这次截断操作
   const truncationId = crypto.randomUUID()

   // 过滤出可见的消息(即尚未被截断的消息)
   // 需要跟踪原始索引来正确地标记完整数组中的消息
   const visibleIndices: number[] = []
   messages.forEach((msg, index) => {
      // 如果消息既没有父截断也没有是截断标记,则视为可见消息
      if (!msg.truncationParent && !msg.isTruncationMarker) {
         visibleIndices.push(index)
      }
   })

   // 计算要截断的可见消息数量(排除第一个可见消息)
   const visibleCount = visibleIndices.length
   // 根据指定的比例计算需要移除的原始消息数
   const rawMessagesToRemove = Math.floor((visibleCount - 1) * fracToRemove)
   // 将消息数调整为偶数,确保成对移除消息
   const messagesToRemove = rawMessagesToRemove - (rawMessagesToRemove % 2)

   // 如果没有消息需要移除,则直接返回原消息数组
   if (messagesToRemove <= 0) {
      return {
         messages,
         truncationId,
         messagesRemoved: 0,
      }
   }

   // 获取要截断的可见消息的索引(跳过第一个可见消息,取接下来的N个)
   const indicesToTruncate = new Set(visibleIndices.slice(1, messagesToRemove + 1))

   // 标记被"截断"的消息(从API调用中隐藏)
   const taggedMessages = messages.map((msg, index) => {
      // 如果当前消息索引在要截断的索引集合中,则添加截断父ID标记
      if (indicesToTruncate.has(index)) {
         return { ...msg, truncationParent: truncationId }
      }
      return msg
   })

   // 找到实际的边界 - 最后一个被截断消息之后的索引
   const lastTruncatedVisibleIndex = visibleIndices[messagesToRemove] // 最后一个被截断的可见消息
   // 如果除了第一个可见消息外的所有消息都被截断,则在末尾插入标记
   const firstKeptVisibleIndex = visibleIndices[messagesToRemove + 1] ?? taggedMessages.length

   // 在实际边界处插入截断标记(位于最后截断和首个保留消息之间)
   const firstKeptTs = messages[firstKeptVisibleIndex]?.ts ?? Date.now()
   // 创建截断标记消息,说明有多少条消息被隐藏以减少上下文
   const truncationMarker: ApiMessage = {
      role: "user",
      content: `[滑动窗口截断: ${messagesToRemove} 条消息已被隐藏以减少上下文]`,
      ts: firstKeptTs - 1,
      isTruncationMarker: true,
      truncationId,
   }

   // 在边界位置插入标记
   // 找到插入位置:刚好在第一个保留的可见消息之前
   const insertPosition = firstKeptVisibleIndex
   // 构建结果数组:截断标记前的消息 + 截断标记 + 截断标记后的消息
   const result = [
      ...taggedMessages.slice(0, insertPosition),
      truncationMarker,
      ...taggedMessages.slice(insertPosition),
   ]

   // 返回截断结果,包括标记后的消息、截断ID和移除的消息数
   return {
      messages: result,
      truncationId,
      messagesRemoved: messagesToRemove,
   }
}

截断策略

  • 保留第一条:系统提示词或初始消息始终保留
  • 移除中间 :移除 50% 的消息(默认 fracToRemove = 0.5
  • 保持偶数:确保移除偶数条消息,保持对话的完整性(user-assistant 配对)
  • 保留最后:保留最近的对话,确保上下文连续性

截断示例

复制代码
原始消息(11 条):
[系统提示, 消息1, 消息2, 消息3, 消息4, 消息5, 消息6, 消息7, 消息8, 消息9, 消息10]

计算移除数量:
rawMessagesToRemove = floor((11 - 1) * 0.5) = 5
messagesToRemove = 5 - (5 % 2) = 4  (确保偶数)

截断后(12 条):
[
  系统提示,                                    // 保留(第一条始终保留)
  消息1 (truncationParent: "uuid-xxx"),       // 被标记隐藏
  消息2 (truncationParent: "uuid-xxx"),       // 被标记隐藏
  消息3 (truncationParent: "uuid-xxx"),       // 被标记隐藏
  消息4 (truncationParent: "uuid-xxx"),       // 被标记隐藏
  {                                          // 插入的截断标记消息
    role: "user",
    content: "[滑动窗口截断: 4 条消息已被隐藏以减少上下文]",
    isTruncationMarker: true,
    truncationId: "uuid-xxx"
  },
  消息5,                                     // 保留
  消息6,                                     // 保留
  消息7,                                     // 保留
  消息8,                                     // 保留
  消息9,                                     // 保留
  消息10                                     // 保留
]

说明:
- 保留了系统提示(第 1 条)
- 消息 1-4 被标记为隐藏(添加 truncationParent 标识)
- 在消息 5 前插入了截断标记消息
- 保留了消息 5-10(6 条)
- 总消息数变为 12 条(原始 11 条 + 1 条截断标记)

4.3 Token 计数实现

4.3.1 基础实现

文件路径 : src/utils/tiktoken.ts

typescript 复制代码
import { Tiktoken } from "tiktoken/lite"
import o200kBase from "tiktoken/encoders/o200k_base"

const TOKEN_FUDGE_FACTOR = 1.5  // 1.5 倍的容错因子
let encoder: Tiktoken | null = null

export async function tiktoken(
    content: Anthropic.Messages.ContentBlockParam[]
): Promise<number> {
    if (content.length === 0) {
        return 0
    }

    let totalTokens = 0

    // 懒加载并缓存 encoder
    if (!encoder) {
        encoder = new Tiktoken(
            o200kBase.bpe_ranks,
            o200kBase.special_tokens,
            o200kBase.pat_str
        )
    }

    // 处理每个内容块
    for (const block of content) {
        if (block.type === "text") {
            const text = block.text || ""
            if (text.length > 0) {
                const tokens = encoder.encode(text, undefined, [])
                totalTokens += tokens.length
            }
        } else if (block.type === "image") {
            // 图片 token 估算
            const imageSource = block.source
            if (imageSource && typeof imageSource === "object" && "data" in imageSource) {
                const base64Data = imageSource.data as string
                totalTokens += Math.ceil(Math.sqrt(base64Data.length))
            } else {
                totalTokens += 300  // 保守估计
            }
        }
    }

    // 添加容错因子
    return Math.ceil(totalTokens * TOKEN_FUDGE_FACTOR)
}

关键点

  1. 懒加载:首次使用时才初始化 encoder
  2. 缓存复用:encoder 实例全局复用
  3. 图片处理:基于 base64 长度估算图片 token
  4. 容错因子:乘以 1.5 倍避免低估
4.3.2 Worker Pool 封装

文件路径 : src/utils/countTokens.ts

typescript 复制代码
import workerpool from "workerpool"

let pool: workerpool.Pool | null | undefined = undefined

export type CountTokensOptions = {
    useWorker?: boolean
}

export async function countTokens(
    content: Anthropic.Messages.ContentBlockParam[],
    { useWorker = true }: CountTokensOptions = {},
): Promise<number> {
    // 懒创建 Worker Pool
    if (useWorker && typeof pool === "undefined") {
        pool = workerpool.pool(__dirname + "/workers/countTokens.js", {
            maxWorkers: 1,      // 最大 1 个 worker
            maxQueueSize: 10,   // 队列大小 10
        })
    }

    // 不使用 worker 或 pool 不存在时,使用同步实现
    if (!useWorker || !pool) {
        return tiktoken(content)
    }

    try {
        const data = await pool.exec("countTokens", [content])
        const result = countTokensResultSchema.parse(data)

        if (!result.success) {
            throw new Error(result.error)
        }

        return result.count
    } catch (error) {
        pool = null  // 失败时重置 pool
        console.error(error)
        return tiktoken(content)  // 回退到同步实现
    }
}

优势

  1. 异步计数:不阻塞主线程
  2. 自动回退:Worker 失败时回退到同步实现
  3. 资源控制:限制 Worker 数量和队列大小
4.3.3 Task 中的 Token 使用统计

文件路径 : src/core/task/Task.ts

typescript 复制代码
public getTokenUsage(): { contextTokens: number; totalCost: number } {
    // 统计所有消息的 token 数
    const contextTokens = this.apiConversationHistory.reduce((sum, message) => {
        // 计算每条消息的 token 数
        return sum + estimateTokenCount(message.content)
    }, 0)
    
    // 统计总成本
    const totalCost = this.totalCost
    
    return { contextTokens, totalCost }
}

五、上下文处理后对消息影响

5.1 压缩 & 截断后的消息获取

在压缩以及截断处理后,会摘取符合规则的消息,作为本次 LLM 的最终输入。

特点:非破坏性上下文

  • 可逆性:用户回滚操作时,被隐藏的消息可以恢复
  • 数据完整性:所有原始消息都被保留,只是在传输时被过滤
  • 高效性:减少了发送给 LLM 的消息数量,降低了 token 使用和响应时间
5.1.1 消息筛选规则
  1. 压缩消息筛选

    • 消息包含 condenseParent 字段,指向某个摘要的 condenseId
    • 如果对应的摘要消息(isSummary: true)仍然存在于历史中,则该消息被过滤掉
    • 如果摘要已被删除(用户回滚操作),则 condenseParent 消息重新变为可见
  2. 截断消息筛选

    • 消息包含 truncationParent 字段,指向某个截断标记的 truncationId
    • 如果对应的截断标记(isTruncationMarker: true)仍然存在于历史中,则该消息被过滤掉
    • 如果截断标记已被删除(用户回滚操作),则 truncationParent 消息重新变为可见

ApiMessage 关于压缩和截断的扩展字段:

  • condenseParent?: string - 指向摘要的 ID,表示该消息已被压缩
  • condenseId?: string - 摘要消息的唯一标识符
  • isSummary?: boolean - 标识该消息为压缩摘要
  • truncationParent?: string - 指向截断标记的 ID,表示该消息已被截断隐藏
  • truncationId?: string - 截断标记的唯一标识符
  • isTruncationMarker?: boolean - 标识该消息为截断标记
5.1.2 在 Task.ts 中的实际调用

src/core/task/Task.ts 文件中,每当需要向 LLM 发送消息时,都会调用这几个函数:

调用链如下:

  1. getEffectiveApiHistory - 筛选出有效的消息(移除被压缩和截断隐藏的消息)
  2. getMessagesSinceLastSummary - 获取从最后一个摘要开始的所有消息(进一步优化)
  3. maybeRemoveImageBlocks - 移除图片块以减少 token 使用
  4. buildCleanConversationHistory - 清理并构建最终的对话历史
5.1.3 源码实现原理

核心函数:getEffectiveApiHistory

该函数位于 src/core/condense/index.ts 文件中,负责从完整的 API 对话历史中筛选出实际需要发送给 LLM 的消息:

typescript 复制代码
/**
 * 过滤API对话历史以获取应发送给API的"有效"消息。
 * 具有指向现有摘要的condenseParent的消息将被过滤掉,
 * 因为它们已被该摘要替换。
 * 具有指向现有截断标记的truncationParent的消息也会被过滤掉,
 * 因为它们已被滑动窗口截断隐藏。
 *
 * 这允许非破坏性的压缩和截断,其中消息被打标记但不被删除,
 * 从而在仍向API发送压缩/截断的历史记录的同时启用准确的倒回操作。
 *
 * @param messages - 包含标记消息的完整API对话历史
 * @returns 应发送给API的过滤后的历史记录
 */
export function getEffectiveApiHistory(messages: ApiMessage[]): ApiMessage[] {
	// 收集当前历史中存在的所有摘要的condenseId
	const existingSummaryIds = new Set<string>()
	// 收集当前历史中存在的所有截断标记的truncationId
	const existingTruncationIds = new Set<string>()

	for (const msg of messages) {
		if (msg.isSummary && msg.condenseId) {
			existingSummaryIds.add(msg.condenseId)
		}
		if (msg.isTruncationMarker && msg.truncationId) {
			existingTruncationIds.add(msg.truncationId)
		}
	}

	// 过滤掉condenseParent指向现有摘要的消息
	// 或truncationParent指向现有截断标记的消息。
	// 具有孤立父级(摘要/标记已被删除)的消息将被包含
	return messages.filter((msg) => {
		// 如果摘要存在,则过滤掉压缩消息
		if (msg.condenseParent && existingSummaryIds.has(msg.condenseParent)) {
			return false
		}
		// 如果截断标记存在,则过滤掉截断消息
		if (msg.truncationParent && existingTruncationIds.has(msg.truncationParent)) {
			return false
		}
		return true
	})
}

**核心函数:getMessagesSinceLastSummary **

该函数位于 src/core/condense/index.ts 文件中,负责处理摘要信息

typescript 复制代码
/**
 * 获取自上次摘要消息以来的所有消息,包括摘要本身。
 * 如果没有摘要,则返回所有消息。
 * 
 * 此函数通过只包含最近的消息和最新的摘要来优化发送给LLM的上下文,
 * 而不是整个对话历史。
 * 
 * @param messages - 完整的API对话历史
 * @returns 自上次摘要以来的消息(如果没有摘要则返回所有消息)
 */
export function getMessagesSinceLastSummary(messages: ApiMessage[]): ApiMessage[] {
	let lastSummaryIndexReverse = [...messages].reverse().findIndex((message) => message.isSummary)

	if (lastSummaryIndexReverse === -1) {
		return messages
	}

	const lastSummaryIndex = messages.length - lastSummaryIndexReverse - 1
	const messagesSinceSummary = messages.slice(lastSummaryIndex)
   
	if (messagesSinceSummary.length > 0 && messagesSinceSummary[0].role !== "user") {
		const originalFirstMessage = messages[0]
		if (originalFirstMessage && originalFirstMessage.role === "user") {
			return [originalFirstMessage, ...messagesSinceSummary]
		} else {
			const userMessage: ApiMessage = {
				role: "user",
				content: "Please continue from the following summary:",
				ts: messages[0]?.ts ? messages[0].ts - 1 : Date.now(),
			}
			return [userMessage, ...messagesSinceSummary]
		}
	}

	return messagesSinceSummary
}

5.2 压缩对两套消息的影响

Kilo 维护两套独立的消息历史:

  1. API 层历史 (apiConversationHistory)

    • 用途:发送给 LLM 的实际对话
    • 特点:经过压缩优化,节省 token
    • 格式:标准的 MessageParam[]
  2. UI 层历史 (clineMessages)

    • 用途:在界面显示给用户
    • 特点:保留完整的交互记录
    • 格式:包含更多元信息的 ClineMessage[]

压缩前

typescript 复制代码
// API 层历史(53 条消息)
apiConversationHistory = [
    { role: "user", content: "消息 1" },
    { role: "assistant", content: "回复 1" },
    // ... 50 条消息
    { role: "user", content: "消息 53" },
]

// UI 层历史(53 条消息 + 工具执行记录等)
clineMessages = [
    { type: "say", say: "text", text: "消息 1", ts: ... },
    { type: "say", say: "text", text: "回复 1", ts: ... },
    { type: "say", say: "tool", text: "执行工具...", ts: ... },
    // ... 更多消息和工具记录
]

压缩后

typescript 复制代码
// API 层历史(5 条消息)- 被压缩
apiConversationHistory = [
    { role: "user", content: "消息 1" },
    { role: "assistant", content: "摘要...", isSummary: true },
    { role: "user", content: "消息 51" },
    { role: "assistant", content: "回复 51" },
    { role: "user", content: "消息 53" },
]

// UI 层历史(54 条消息)- 添加了压缩提示
clineMessages = [
    { type: "say", say: "text", text: "消息 1", ts: ... },
    { type: "say", say: "text", text: "回复 1", ts: ... },
    { type: "say", say: "tool", text: "执行工具...", ts: ... },
    // ... 所有原始消息(不变)
    { 
        type: "say", 
        say: "condense_context",
        contextCondense: {
            cost: 0.05,
            prevContextTokens: 45000,
            newContextTokens: 12000,
            summary: "摘要内容..."
        },
        ts: ...
    },  // 新增的压缩提示
]

关键点

  1. API 层:消息数量大幅减少(53 → 5)
  2. UI 层:保留所有原始消息,只增加一条压缩提示
  3. 用户体验:用户可以看到完整的对话历史
  4. Token 优化:发送给 LLM 的消息大幅减少

六、压缩示例

6.1 压缩场景说明

假设用户与 AI 进行了长时间对话,讨论并实现用户认证系统:

  • 对话时长:约 90 分钟
  • 消息数量:53 条(包括用户消息和 AI 回复)
  • Token 使用:45,000 tokens
  • 上下文窗口:50,000 tokens
  • 压缩阈值:80%(即 40,000 tokens)
  • 触发条件:当前使用 45,000 tokens,超过阈值,自动触发压缩

6.2 压缩前的存储结构

apiConversationHistory(53 条消息)

typescript 复制代码
[
    { role: "user", content: "帮我创建一个用户登录功能", ts: 1704700000000 },
    { role: "assistant", content: "好的,我会帮你创建用户登录功能...", ts: 1704700001000 },
    { role: "user", content: "使用 JWT 进行身份验证", ts: 1704700002000 },
    { role: "assistant", content: "明白了,我会使用 JWT...", ts: 1704700003000 },
    // ... 中间 45 条消息(消息 5-49)
    { role: "user", content: "现在添加密码重置功能", ts: 1704700050000 },
    { role: "assistant", content: "好的,让我添加密码重置功能...", ts: 1704700051000 },
    { role: "user", content: "发送重置邮件时使用 SendGrid", ts: 1704700052000 },
]

关键点

  • 总共 53 条消息
  • 占用 45,000 tokens
  • 所有消息都保存在本地

6.3 压缩执行过程

步骤 1:确定压缩范围
typescript 复制代码
// 保留最近 3 条消息(N_MESSAGES_TO_KEEP = 3)
const keepMessages = messages.slice(-3)  // 最后 3 条消息

// 需要压缩的消息
const messagesToCompress = messages.slice(0, -3)  // 前 50 条消息
步骤 2:LLM 生成摘要

将前 50 条消息发送给 LLM,生成结构化摘要:

typescript 复制代码
const summaryMessage = {
    role: "assistant",
    content: `## 对话摘要

### 1. Previous Conversation
用户请求创建一个完整的用户认证系统,包括登录、注册和密码管理功能。

### 2. Current Work
正在实现密码重置功能,使用 SendGrid 发送重置邮件。

### 3. Key Technical Concepts
- JWT (JSON Web Token) 用于身份验证
- bcrypt 用于密码哈希
- SendGrid API 用于邮件发送
- Express.js 作为后端框架
- MongoDB 作为数据库

### 4. Relevant Files and Code
- src/routes/auth.ts: 认证路由
  - 实现了 POST /login 和 POST /register 端点
  - 使用 JWT 生成和验证 token

- src/models/User.ts: 用户模型
  - 定义了用户 schema
  - 包含 email、password、resetToken 字段

### 5. Problem Solving
- 解决了密码哈希的性能问题(调整 bcrypt rounds 为 10)
- 修复了 JWT token 过期时间设置(现在为 7 天)

### 6. Pending Tasks and Next Steps
- 实现密码重置功能,使用 SendGrid 发送邮件
- 下一步:创建 POST /reset-password 端点
- 用户原话:"发送重置邮件时使用 SendGrid"`,
    ts: 1704700050000,
    isSummary: true  // 标记为摘要消息
}
步骤 3:重组消息历史

重要 :压缩不删除 原始消息,而是在原有基础上插入摘要消息

typescript 复制代码
const newMessages = [
    ...messages.slice(0, -3),  // 保留前 50 条原始消息
    summaryMessage,             // 插入摘要消息
    ...keepMessages            // 保留最后 3 条消息
]

6.4 压缩后的存储结构

apiConversationHistory(54 条消息 = 53 + 1)

typescript 复制代码
[
    // 前 50 条原始消息(保持不变)
    { role: "user", content: "帮我创建一个用户登录功能", ts: 1704700000000 },
    { role: "assistant", content: "好的,我会帮你创建用户登录功能...", ts: 1704700001000 },
    { role: "user", content: "使用 JWT 进行身份验证", ts: 1704700002000 },
    { role: "assistant", content: "明白了,我会使用 JWT...", ts: 1704700003000 },
    // ... 中间 45 条消息(消息 5-49)
    { role: "user", content: "添加用户注册功能", ts: 1704700049000 },
    
    // 新增的摘要消息
    { 
        role: "assistant", 
        content: "## 对话摘要\n\n### 1. Previous Conversation\n...", 
        ts: 1704700050000,
        isSummary: true  // 标记为摘要
    },
    
    // 保留的最后 3 条消息(保持不变)
    { role: "user", content: "现在添加密码重置功能", ts: 1704700050000 },
    { role: "assistant", content: "好的,让我添加密码重置功能...", ts: 1704700051000 },
    { role: "user", content: "发送重置邮件时使用 SendGrid", ts: 1704700052000 },
]

关键变化

  • 所有 53 条原始消息都保留
  • 新增 1 条摘要消息
  • 总消息数:54 条(53 + 1)
  • 本地存储完整的对话历史

6.5 传递给 LLM 的消息

虽然本地存储了 54 条消息,但通过 getMessagesSinceLastSummary() 函数,实际传递给 LLM 的消息大幅减少

typescript 复制代码
// 传递给 LLM 的消息(只有 5 条)
[
    { role: "user", content: "Please continue from the following summary:" },  // 提示消息(根据第一条是否是用户消息添加)
    { role: "assistant", content: "## 对话摘要\n...", isSummary: true },      // 摘要消息
    { role: "user", content: "现在添加密码重置功能" },                          // 最近消息 1
    { role: "assistant", content: "好的,让我添加密码重置功能..." },            // 最近消息 2
    { role: "user", content: "发送重置邮件时使用 SendGrid" },                  // 最近消息 3
]

6.6 压缩效果对比

维度 压缩前 压缩后 说明
本地存储
消息数量 53 条 54 条 ↑ 增加 1 条摘要消息
存储完整性 ✅ 完整 ✅ 完整 所有原始消息都保留
传递给 LLM
消息数量 53 条 5 条 ↓ 减少 48 条(90.6%)
Token 使用 45,000 12,000 ↓ 减少 33,000(73.3%)
上下文使用率 90% 24% ↓ 降低 66%
语义信息
关键信息 ✅ 完整 ✅ 完整 通过摘要保留
技术细节 ✅ 完整 ✅ 完整 摘要包含代码和文件
任务上下文 ✅ 完整 ✅ 完整 摘要包含待办任务

七、最佳实践

7.1 配置建议

json 复制代码
{
    "autoCondenseContext": true,
    "autoCondenseContextPercent": 80,
    "customCondensingPrompt": "",
    "condensingApiConfigId": ""
}

不同场景的阈值建议

场景 推荐阈值 理由
日常对话 80% 平衡性能和成本
长时间开发 75% 更频繁压缩,避免突然中断
复杂调试 85% 保留更多历史,便于回溯
成本敏感 70% 更早压缩,节省成本

自定义压缩提示词

如果默认提示词不满足需求,可以自定义:

typescript 复制代码
const customPrompt = `
请总结对话,重点关注:
1. 用户的核心需求
2. 已实现的功能列表
3. 当前正在进行的任务
4. 下一步计划

保持简洁,不超过 500 字。
`

7.2 使用技巧

手动触发时机

在以下情况下,建议手动触发压缩:

  1. 切换任务前:完成一个大任务,准备开始新任务
  2. 长时间暂停后:重新开始对话前
  3. 感觉响应变慢:可能是上下文过长
避免频繁压缩

问题:短时间内多次压缩会导致信息丢失

解决方案

  • 系统会自动检测最近是否有摘要
  • 如果最近 3 条消息中有摘要,会拒绝压缩
  • 建议至少间隔 10 条消息再压缩
保留关键信息

技巧:在对话中明确标记关键信息

复制代码
用户:请记住,我们使用的数据库是 PostgreSQL 13,
      连接池大小设置为 20。这个配置很重要,
      后续所有数据库相关的代码都要遵循这个设置。

八、常见问题

8.1 压缩失败

问题:压缩后 token 数没有减少

原因

  1. 摘要过长,超过了原始消息
  2. 保留的最近消息占用了大量 token
  3. 系统提示词过长

解决方案

  1. 使用更简洁的自定义压缩提示词
  2. 减少 N_MESSAGES_TO_KEEP 的值(需修改代码)
  3. 优化系统提示词

8.2 信息丢失

问题:压缩后 AI 忘记了某些重要信息

原因

  1. 压缩提示词没有强调保留该类信息
  2. 信息在被压缩的消息中,但摘要中未提及

解决方案

  1. 自定义压缩提示词,明确要求保留特定类型的信息
  2. 在对话中明确标记关键信息
  3. 增加 N_MESSAGES_TO_KEEP 的值

8.3 压缩成本过高

问题:频繁压缩导致成本增加

原因

  1. 阈值设置过低,触发过于频繁
  2. 每次压缩都需要调用 LLM

解决方案

  1. 提高压缩阈值(如从 75% 提高到 85%)
  2. 使用更便宜的模型进行压缩(通过 condensingApiConfigId 配置)
  3. 优化对话方式,减少不必要的消息

8.4 压缩速度慢

问题:压缩过程耗时较长

原因

  1. 需要压缩的消息过多
  2. LLM 生成摘要需要时间
  3. 网络延迟

解决方案

  1. 降低压缩阈值,更早触发压缩
  2. 使用更快的模型进行压缩
  3. 使用本地部署的模型(如果支持)

九、总结

9.1 核心优势

  1. 自动化:无需用户干预,自动监控和压缩
  2. 智能化:LLM 生成的摘要保留关键信息
  3. 灵活性:支持自定义阈值、提示词和 API Handler
  4. 可靠性:严格的验证机制,确保压缩有效
  5. 用户友好:双轨制历史,UI 显示完整对话

9.2 技术亮点

  1. 双重触发机制:百分比 + 绝对数量
  2. Worker Pool:异步 token 计数,不阻塞主线程
  3. 双轨制历史:分离 API 层和 UI 层
  4. 结构化摘要:6 个部分的详细摘要
  5. 压缩验证:确保 token 数确实减少

9.3 适用场景

  • ✅ 长时间对话(超过 1 小时)
  • ✅ 复杂的多步骤任务
  • ✅ 需要大量上下文的调试
  • ✅ 大规模代码重构
  • ✅ 持续的迭代开发

相关推荐
康谋自动驾驶2 小时前
高校自动驾驶研究新基建:“实测 - 仿真” 一体化数据采集与验证平台
人工智能·机器学习·自动驾驶·科研·数据采集·时间同步·仿真平台
shangjian0072 小时前
AI-大语言模型LLM-Transformer架构1-整体介绍
人工智能·语言模型·transformer
己亥孟陬2 小时前
【安全+高效+低成本】尝鲜Moltbot(原Clawdbot)
ai·agent·clawdbot·moltbot
机 _ 长2 小时前
YOLO26 蒸馏改进全攻略:从理论到实战 (Response + Feature + Relation)
人工智能·深度学习·yolo·目标检测·计算机视觉
shangjian0072 小时前
AI-大语言模型LLM-Transformer架构2-自注意力
人工智能·语言模型·transformer
2501_941507942 小时前
基于YOLOv26的文档手写文本与签名识别系统·从模型改进到完整实现
人工智能·yolo·目标跟踪
_ziva_2 小时前
Layer Normalization 全解析:LLMs 训练稳定的核心密码
人工智能·机器学习·自然语言处理
莫潇羽2 小时前
Midjourney AI图像创作完全指南:从零基础到精通提示词设计与风格探索
人工智能·midjourney
加加今天也要加油2 小时前
Oinone × AI Agent 落地指南:元数据即 Prompt、BPM 状态机护栏、SAGA 补偿、GenUI
人工智能·低代码·prompt