Vibecape
Context 系统

Context Builder 实现

Context Builder 实现

概述

ContextBuilder 是 Context 系统的核心协调器,负责整合各种上下文来源,构建最优的消息列表发送给 LLM。它智能地分配 Token 预算,确保最重要的信息被包含在上下文中。

核心职责

  1. Token 预算分配:根据总预算按比例分配给各个上下文部分
  2. 上下文整合:按优先级整合 System Prompt、摘要、记忆、消息等
  3. 预算跟踪:实时跟踪各部分的 Token 使用情况
  4. 安全网机制:确保当前用户输入一定存在于消息列表中

架构设计

┌─────────────────────────────────────────────────────┐
│                  ContextBuilder                      │
│  ┌───────────────────────────────────────────────┐  │
│  │           allocateBudget()                    │  │
│  │  按比例分配 Token 预算                          │  │
│  └───────────────┬───────────────────────────────┘  │
│                  │                                   │
│  ┌───────────────▼───────────────────────────────┐  │
│  │           buildContext()                      │  │
│  │  1. System Prompt (20%)                       │  │
│  │  2. Thread Summary (10%)                      │  │
│  │  3. Long-term Memory (10%)                    │  │
│  │  4. Recent Messages (55%)                     │  │
│  │  5. Current Input (5%)                        │  │
│  │  6. Instruction Pinning (安全网)              │  │
│  └───────────────────────────────────────────────┘  │
│                  │                                   │
│  ┌───────────────▼───────────────────────────────┐  │
│  │           BuiltContext                        │  │
│  │  - messages: Message[]                        │  │
│  │  - tokenStats: TokenStats                     │  │
│  │  - metadata: Metadata                         │  │
│  └───────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────┘

代码实现

类定义

// package/src/main/context/ContextBuilder.ts

class ContextBuilderService {
  private config: ContextBuilderConfig;

  constructor(config: Partial<ContextBuilderConfig> = {}) {
    this.config = { ...DEFAULT_CONTEXT_CONFIG, ...config };
  }

  /**
   * 分配 Token 预算
   * 基于总预算按比例分配给各个上下文部分
   */
  private allocateBudget(total: number): ContextBudget {
    return {
      systemPrompt: Math.floor(total * 0.2),   // 20%
      summary: Math.floor(total * 0.1),         // 10%
      retrieved: Math.floor(total * 0.1),       // 10%
      recentMessages: Math.floor(total * 0.55), // 55%
      currentInput: Math.floor(total * 0.05),   // 5%
      reserved: 0,
    };
  }

  /**
   * 构建完整的上下文
   */
  async buildContext(
    thread: ChatThread | null,
    currentPrompt: string,
    systemPrompt: string,
    options: {
      includeCurrentInput?: boolean;
      debug?: boolean;
    } = {}
  ): Promise<BuiltContext>;
}

上下文构建流程

async buildContext(
  thread: ChatThread | null,
  currentPrompt: string,
  systemPrompt: string,
  options = {}
): Promise<BuiltContext> {
  const { includeCurrentInput = false, debug = true } = options;
  const budget = this.allocateBudget(this.config.totalBudget);
  const tracker = new TokenBudgetTracker(this.config.totalBudget);
  const messages: ContextMessage[] = [];

  const metadata = {
    includedMessageCount: 0,
    summarizedMessageCount: 0,
    usedSummary: false,
    retrievedMemoryCount: 0,
  };

  // 1. System Prompt (始终包含)
  const truncatedSystemPrompt = truncateToTokens(
    systemPrompt,
    budget.systemPrompt
  );
  const systemTokens = estimateTokens(truncatedSystemPrompt);
  tracker.allocate("systemPrompt", systemTokens);

  messages.push({
    role: "system",
    content: truncatedSystemPrompt,
  });

  // 2. Thread Summary (如果启用且存在)
  if (this.config.enableSummary && thread) {
    const summary = await ThreadSummary.get(thread.id);
    if (summary && summary.content) {
      const summaryContent = `[对话背景摘要]\n${summary.content}`;
      const truncatedSummary = truncateToTokens(
        summaryContent,
        budget.summary
      );
      const summaryTokens = estimateTokens(truncatedSummary);

      if (tracker.allocate("summary", summaryTokens)) {
        messages.push({
          role: "system",
          content: truncatedSummary,
        });
        metadata.usedSummary = true;
        metadata.summarizedMessageCount = summary.last_message_seq;
      }
    }
  }

  // 3. Long-term Memory (RAG 检索) - 带超时
  if (this.config.enableMemoryRetrieval && currentPrompt) {
    const MEMORY_SEARCH_TIMEOUT = 500;
    const searchPromise = LongTermMemory.search(currentPrompt, {
      limit: 3,
      minSimilarity: 0.6,
      category: "user_preference",
    });
    const timeoutPromise = new Promise<never>((_, reject) =>
      setTimeout(() => reject(new Error("timeout")), MEMORY_SEARCH_TIMEOUT)
    );

    const memories = await Promise.race([searchPromise, timeoutPromise]);

    if (memories.length > 0) {
      metadata.retrievedMemoryCount = memories.length;
      const memoryContent = memories
        .map((m) => `- ${m.content} (相关度: ${m.similarity.toFixed(2)})`)
        .join("\n");

      const memoryMessage = `[相关历史记忆]\n${memoryContent}`;
      const tokens = estimateTokens(memoryMessage);

      if (tracker.allocate("retrieved", tokens)) {
        messages.push({
          role: "system",
          content: memoryMessage,
        });
      }
    }
  }

  // 4. Recent Messages (滑动窗口)
  if (thread && thread.messages.length > 0) {
    // 异步更新摘要(不阻塞)
    if (this.config.enableSummary) {
      ThreadSummary.updateIfNeeded(thread.id).catch(() => {
        logger.warn("摘要更新失败");
      });
    }

    const windowResult = SlidingWindow.applyWindow(
      thread.messages,
      summaryLastSeq + 1,
      budget.recentMessages
    );

    const recentTokens = windowResult.totalTokens;
    if (tracker.allocate("recentMessages", recentTokens)) {
      for (const msg of windowResult.messages) {
        messages.push({
          role: msg.role,
          content: msg.content,
        });
      }
      metadata.includedMessageCount = windowResult.messages.length;
    }
  }

  // 5. Current Input (仅在需要时添加)
  if (includeCurrentInput && currentPrompt) {
    const truncatedInput = truncateToTokens(
      currentPrompt,
      budget.currentInput
    );
    const inputTokens = estimateTokens(truncatedInput);
    tracker.allocate("currentInput", inputTokens);

    messages.push({
      role: "user",
      content: truncatedInput,
    });
  } else {
    // 安全检查:确保当前用户输入一定存在
    const lastMsg = messages[messages.length - 1];
    const isLastMsgUser = lastMsg && lastMsg.role === "user";
    const isContentMatch = lastMsg &&
      lastMsg.content.includes(currentPrompt.slice(0, 20));

    if (!isLastMsgUser || !isContentMatch) {
      logger.debug(`[Safety] 检测到最新消息可能丢失,强制追加用户输入`);
      const truncatedInput = truncateToTokens(
        currentPrompt,
        budget.currentInput || 500
      );
      messages.push({
        role: "user",
        content: truncatedInput,
      });
    }
  }

  // 6. Instruction Pinning (Active Instruction Slot)
  if (this.config.enableInstructionPinning && currentPrompt) {
    let taskStatus = "";
    if (thread?.id) {
      ActiveTaskService.extractGoalFromMessage(thread.id, currentPrompt);
      taskStatus = ActiveTaskService.generateStatusSummary(thread.id);
    }

    const instructionContent = `
[CRITICAL INSTRUCTION]
User's Current Goal: "${currentPrompt.slice(0, 200)}${currentPrompt.length > 200 ? "..." : ""}"
Status: ACTIVE
Guide:
1. If you have just received tool outputs, briefly ANALYZE them first.
2. If the goal is complex, form a concise PLAN before executing.
3. Prioritize answering the User's Goal immediately.
${taskStatus ? `\n${taskStatus}` : ""}
`;

    messages.push({
      role: "system",
      content: instructionContent.trim(),
    });
  }

  return {
    messages,
    tokenStats: {
      systemPrompt: breakdown.systemPrompt || 0,
      summary: breakdown.summary || 0,
      retrieved: breakdown.retrieved || 0,
      recentMessages: breakdown.recentMessages || 0,
      currentInput: breakdown.currentInput || 0,
      total: totalTokens,
    },
    metadata,
  };
}

关键设计决策

1. Token 预算分配策略

比例设计原则

  • System Prompt (20%):Hero 的角色和行为定义,不可压缩
  • Summary (10%):压缩的历史对话,提供背景
  • Retrieved (10%):长期记忆检索,增强个性化
  • Recent Messages (55%):最大的占比,确保对话连贯性
  • Current Input (5%):通常已在历史中,保留小预算

动态调整

// 如果某部分超出预算,可选择截断或跳过
if (tracker.allocate("summary", summaryTokens)) {
  // 预算充足,添加摘要
  messages.push({ role: "system", content: truncatedSummary });
} else {
  // 预算不足,跳过摘要
  logger.debug("摘要超出预算,跳过");
}

2. 异步非阻塞设计

记忆检索超时

const MEMORY_SEARCH_TIMEOUT = 500; // 500ms
const memories = await Promise.race([
  LongTermMemory.search(currentPrompt, options),
  timeoutPromise,
]);

摘要更新异步

// 不等待摘要更新完成,立即返回上下文
ThreadSummary.updateIfNeeded(thread.id).catch(() => {
  logger.warn("摘要更新失败");
});

3. 安全网机制

强制包含当前输入

// 检查最新消息是否是用户输入
const lastMsg = messages[messages.length - 1];
const isLastMsgUser = lastMsg && lastMsg.role === "user";
const isContentMatch = lastMsg &&
  lastMsg.content.includes(currentPrompt.slice(0, 20));

// 如果可能丢失,强制追加
if (!isLastMsgUser || !isContentMatch) {
  messages.push({
    role: "user",
    content: truncateToTokens(currentPrompt, budget.currentInput || 500),
  });
}

4. 指令钉选(Instruction Pinning)

问题:工具调用结果可能在上下文中间,冲刷原始指令

解决方案:在上下文末尾再次附加指令

messages.push({
  role: "system",
  content: `
[CRITICAL INSTRUCTION]
User's Current Goal: "${currentPrompt.slice(0, 200)}..."
Status: ACTIVE
Guide:
1. Analyze tool outputs first
2. Form a plan if complex
3. Prioritize answering the goal
${taskStatus}
  `.trim(),
});

配置选项

interface ContextBuilderConfig {
  /** 总 Token 预算 */
  totalBudget: number;
  /** 滑动窗口大小 (消息轮数) */
  slidingWindowSize: number;
  /** 是否启用会话摘要 */
  enableSummary: boolean;
  /** 是否启用长期记忆检索 */
  enableMemoryRetrieval: boolean;
  /** 是否启用工具结果压缩 */
  enableToolResultCompression: boolean;
  /** 是否启用指令钉选 */
  enableInstructionPinning: boolean;
}

const DEFAULT_CONTEXT_CONFIG: ContextBuilderConfig = {
  totalBudget: 48000,
  slidingWindowSize: 20,
  enableSummary: true,
  enableMemoryRetrieval: true,
  enableToolResultCompression: true,
  enableInstructionPinning: true,
};

使用示例

基本使用

import { ContextBuilder } from "./context";

const result = await ContextBuilder.buildContext(
  thread,        // ChatThread 或 null
  "解释 Context 系统", // 当前用户输入
  heroPrompt,    // Hero 的系统提示词
  { debug: true }
);

console.log("消息数量:", result.messages.length);
console.log("Token 使用:", result.tokenStats);
console.log("元数据:", result.metadata);

自定义配置

ContextBuilder.setConfig({
  totalBudget: 32000,              // 降低总预算
  enableSummary: false,            // 禁用摘要
  enableMemoryRetrieval: false,    // 禁用记忆检索
});

const result = await ContextBuilder.buildContext(
  thread,
  currentPrompt,
  systemPrompt
);

简单上下文(无历史)

const messages = ContextBuilder.buildSimpleContext(
  systemPrompt,
  userPrompt,
  previousMessages // 可选
);

性能优化

Token 估算

使用简化的字符计数,避免调用 tokenizer:

// 中文约 1.8 字符/token,英文约 4 字符/token
const chineseRatio = getChineseRatio(text);
const charsPerToken = chineseRatio * 1.8 + (1 - chineseRatio) * 4;
const tokens = Math.ceil(text.length / charsPerToken);

预算跟踪

class TokenBudgetTracker {
  private used: number = 0;
  private breakdown: Map<string, number> = new Map();

  allocate(category: string, tokens: number): boolean {
    if (this.used + tokens > this.budget) {
      return false; // 预算不足
    }
    this.used += tokens;
    this.breakdown.set(category, (this.breakdown.get(category) || 0) + tokens);
    return true;
  }
}

调试日志

const log = debug ? logger.debug.bind(logger) : () => {};

log("上下文构建开始");
log(`配置: totalBudget=${this.config.totalBudget}`);
log(`[1] System Prompt: ${systemTokens} tokens`);
log(`[2] 使用摘要: ${summaryTokens} tokens`);
log(`========== 上下文构建完成 ==========`);

错误处理

记忆检索失败

try {
  const memories = await Promise.race([searchPromise, timeoutPromise]);
} catch (err) {
  const errMsg = err instanceof Error ? err.message : String(err);
  if (errMsg === "timeout") {
    log(`[3] 记忆检索超时,跳过`);
  } else {
    log(`[3] 检索失败: ${errMsg}`);
  }
  // 继续执行,不阻塞主流程
}

摘要查询失败

try {
  const summary = await ThreadSummary.get(thread.id);
} catch (error) {
  log(`[2] 摘要查询失败 (表可能不存在): ${error}`);
  // 摘要查询失败不应阻止对话继续
}

与其他组件的集成

与 ThreadSummary

// 获取现有摘要
const summary = await ThreadSummary.get(thread.id);

// 异步更新摘要
ThreadSummary.updateIfNeeded(thread.id).catch(() => {});

与 LongTermMemory

// 语义检索
const memories = await LongTermMemory.search(currentPrompt, {
  limit: 3,
  minSimilarity: 0.6,
  category: "user_preference",
});

与 SlidingWindow

// 应用滑动窗口
const windowResult = SlidingWindow.applyWindow(
  thread.messages,
  summaryLastSeq + 1,
  budget.recentMessages
);

与 ActiveTaskService

// 生成任务状态摘要
const taskStatus = ActiveTaskService.generateStatusSummary(thread.id);

相关文档