Context 系统
Context Builder 实现
Context Builder 实现
概述
ContextBuilder 是 Context 系统的核心协调器,负责整合各种上下文来源,构建最优的消息列表发送给 LLM。它智能地分配 Token 预算,确保最重要的信息被包含在上下文中。
核心职责
- Token 预算分配:根据总预算按比例分配给各个上下文部分
- 上下文整合:按优先级整合 System Prompt、摘要、记忆、消息等
- 预算跟踪:实时跟踪各部分的 Token 使用情况
- 安全网机制:确保当前用户输入一定存在于消息列表中
架构设计
┌─────────────────────────────────────────────────────┐
│ 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);