尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

AI编程代理上下文压缩:突破大模型Token限制的智能摘要方案

AI编程代理上下文压缩:突破大模型Token限制的智能摘要方案 1. 项目概述当AI编程代理的“记忆”不堪重负最近在折腾各种AI编程助手和代理Agent比如让它们帮我重构代码库或者分析一个复杂的项目。一个绕不开的坎很快就出现了上下文长度限制。无论是使用云端大模型的API还是部署本地的开源模型那个宝贵的上下文窗口比如4K、8K、16K甚至128K tokens总感觉不够用。当你把整个项目的源代码、文档、依赖关系图一股脑儿塞给AI时很容易就触顶了导致后续的对话质量急剧下降AI开始“胡言乱语”或者干脆忘记了之前讨论过的关键约束。这背后的核心问题就是“上下文管理”。我们人类在解决复杂问题时也不会在脑子里同时加载所有细节而是依靠笔记、摘要和聚焦关键信息。对于AI编程代理来说context-mode上下文模式就是它的工作记忆管理策略。而“压缩上下文”本质上就是为AI代理打造一个高效的“记忆摘要”系统让它能在有限的“脑容量”里记住最相关、最重要的信息从而持续、稳定地输出高质量的代码和建议。这个需求在TypeScript/Node.js这类现代JavaScript生态中尤为突出。一个中等规模的Node.js后端服务加上其node_modules文件数量轻松破万。更别提现在流行的Monorepo项目了。直接把这些全喂给AI是不现实的。因此我们需要一套智能的机制在AI代理运行过程中动态地决定哪些文件是当前任务必须的哪些代码片段是关键函数哪些依赖信息可以摘要这就是context-mode要解决的问题。2. 核心思路从“全量转储”到“智能摘要”传统的、简单粗暴的上下文使用方式我称之为“全量转储”模式。比如你想让AI修复一个Bug你的指令可能是“这是server.ts的代码这是userModel.ts的代码这是相关的package.json……请修复。” 这种模式有几个致命缺点令牌浪费大量与当前任务弱相关的代码占用了宝贵空间。信息过载AI难以从海量文本中精准定位核心问题。成本高昂对于按Token计费的API每一次交互都在烧钱。context-mode的压缩思路就是要转向“智能摘要”模式。它的目标不是传输所有原始数据而是传输数据的“精华”和“索引”。具体来说可以分解为以下几个层次2.1 静态分析与依赖图谱构建在任务开始前先对目标代码库进行一次静态分析。这不仅仅是简单的文件列表而是构建一个轻量级的项目图谱。文件关系图通过解析import/require语句建立文件之间的依赖关系。知道auth.ts引用了user.ts那么在处理认证逻辑时user.ts的相关部分优先级就应该提高。符号Symbol提取提取每个文件中的关键符号——类名、函数名、接口名、类型别名。这构成了一个项目的“词汇表”。抽象语法树AST轻量级遍历不需要完整的AST分析但可以快速识别函数签名、类定义、接口结构为后续的代码块分割和摘要做准备。这个图谱是后续所有压缩决策的基础。它回答了“项目里有什么”以及“它们之间如何关联”的问题。2.2 动态上下文窗口管理AI代理在执行任务时其上下文窗口应该是一个动态的、有状态的队列而不是一个静态的文本块。我们可以借鉴操作系统的内存分页或缓存置换算法如LRU的思想。核心上下文Working Set始终保留与当前执行步骤最相关的代码片段。例如AI正在编辑login函数那么login函数所在的文件、它直接调用的几个辅助函数的代码就应该留在窗口内。历史上下文摘要对于已经被移出核心窗口的旧对话或已处理过的代码不再保留全文而是保留一个“摘要”。这个摘要可以是自然语言摘要用一两句话描述之前讨论过什么、决定了什么。关键决策点记录下已经做出的技术选择比如“决定使用bcrypt而不是argon2进行密码哈希原因是依赖更简单”。代码变更摘要对于AI已经修改过的文件记录下修改的意图和位置而不是把修改后的整个文件再塞回去。外部引用索引对于像node_modules中的第三方库代码绝对不应该直接包含。取而代之的是建立一个“知识库”或“索引”。当AI需要知道express.Request的类型定义时它可以通过一个查询接口比如RAG检索获取到相关的类型签名片段而不是把整个types/express包都加载进来。2.3 分层压缩策略针对不同类型的内容采用不同的压缩粒度全量保留仅针对当前焦点文件正在编辑的文件的核心部分。签名保留对于非焦点但相关的文件只保留函数/类/接口的签名名称、参数、返回类型省略函数体实现。这能让AI知道有什么“工具”可用而不必关心其内部细节。路径引用对于更边缘的文件只保留文件路径并在需要时告诉AI“如果需要utils/logger.ts的详细内容请提出请求。” 这实现了按需加载。自然语言描述对于复杂的业务逻辑或已经讨论过的架构用一段简洁的描述替代大段代码。3. 技术实现方案拆解理论说完了我们来点实际的。如何用TypeScript/Node.js实现这样一个context-mode压缩系统下面是一个可落地的架构设计。3.1 项目分析与图谱构建模块这个模块是离线预处理的只需要在代理启动时或项目变更时运行一次。// 示例使用 TypeScript 编译器 API 进行轻量级分析 import * as ts from typescript; import * as fs from fs/promises; import * as path from path; interface ProjectGraphNode { filePath: string; symbols: string[]; // 导出的符号名 dependencies: string[]; // 依赖的其他项目文件路径 exports?: string; // 主要导出内容的摘要 } export class ProjectAnalyzer { private program: ts.Program; private graph: Mapstring, ProjectGraphNode new Map(); constructor(private tsConfigPath: string) {} async buildGraph(): PromiseMapstring, ProjectGraphNode { const configFile ts.readConfigFile(this.tsConfigPath, (p) fs.readFile(p, utf-8) ); const parsedConfig ts.parseJsonConfigFileContent( configFile.config, ts.sys, path.dirname(this.tsConfigPath) ); this.program ts.createProgram(parsedConfig.fileNames, parsedConfig.options); for (const sourceFile of this.program.getSourceFiles()) { if (sourceFile.isDeclarationFile) continue; // 跳过.d.ts文件 const filePath sourceFile.fileName; const symbols: string[] []; const dependencies: string[] []; // 遍历AST收集导出符号和导入依赖 ts.forEachChild(sourceFile, (node) { // 收集导出声明 if (ts.isExportDeclaration(node)) { // 处理导出 } if (ts.isFunctionDeclaration(node) node.name) { symbols.push(node.name.text); } if (ts.isClassDeclaration(node) node.name) { symbols.push(node.name.text); } // 收集导入声明 if (ts.isImportDeclaration(node)) { const moduleSpecifier node.moduleSpecifier.getText(sourceFile); // 简单处理实际需要解析为项目内路径 if (moduleSpecifier.startsWith(.)) { dependencies.push(moduleSpecifier); } } }); // 生成一个非常简单的摘要文件的前50个字符通常是注释或导出语句 const content sourceFile.getFullText(); const excerpt content.substring(0, 50).replace(/\n/g, ); this.graph.set(filePath, { filePath, symbols, dependencies, exports: excerpt, }); } return this.graph; } // 根据当前文件找到相关度高的文件 getRelevantFiles(focusFilePath: string, depth: number 2): string[] { const visited new Setstring(); const queue: { path: string; level: number }[] [ { path: focusFilePath, level: 0 }, ]; const relevant: string[] []; while (queue.length 0) { const { path, level } queue.shift()!; if (visited.has(path) || level depth) continue; visited.add(path); relevant.push(path); const node this.graph.get(path); if (node) { for (const dep of node.dependencies) { // 需要将相对路径解析为绝对路径 const absDep path.resolve(path.dirname(path), dep); if (this.graph.has(absDep)) { queue.push({ path: absDep, level: level 1 }); } } } } return relevant; } }注意这是一个高度简化的示例。生产环境需要考虑路径解析、循环依赖、动态导入、忽略node_modules等复杂情况。可以使用更成熟的工具如ts-morph来简化AST操作。3.2 上下文管理器与压缩策略这是系统的核心负责维护当前的上下文状态并应用压缩规则。interface ContextChunk { id: string; type: code | summary | instruction | output; content: string; priority: number; // 优先级用于置换决策 metadata: { filePath?: string; symbol?: string; timestamp: number; }; } export class ContextManager { private chunks: ContextChunk[] []; private maxTokens: number; private tokenEstimator: (text: string) number; // 一个估算token的函数 constructor(maxTokens: number) { this.maxTokens maxTokens; // 可以使用简单的启发式方法1个token ≈ 4个英文字符或0.75个单词 this.tokenEstimator (text) Math.ceil(text.length / 4); } // 添加上下文块 addChunk(chunk: OmitContextChunk, id | metadata.timestamp): void { const fullChunk: ContextChunk { ...chunk, id: chunk_${Date.now()}_${Math.random()}, metadata: { ...chunk.metadata, timestamp: Date.now(), }, }; this.chunks.push(fullChunk); this.compressContext(); } // 核心压缩逻辑 private compressContext(): void { let totalTokens this.chunks.reduce( (sum, chunk) sum this.tokenEstimator(chunk.content), 0 ); // 如果未超限直接返回 if (totalTokens this.maxTokens) { return; } // 按优先级排序优先级低的先被考虑压缩或移除 this.chunks.sort((a, b) a.priority - b.priority); const compressedChunks: ContextChunk[] []; for (const chunk of this.chunks) { if (chunk.type code chunk.metadata.filePath) { // 对代码块进行压缩只保留签名 const compressedCode this.compressCodeChunk(chunk); const newTokenCount this.tokenEstimator(compressedCode); if (totalTokens - this.tokenEstimator(chunk.content) newTokenCount this.maxTokens) { chunk.content compressedCode; totalTokens totalTokens - this.tokenEstimator(chunk.content) newTokenCount; compressedChunks.push(chunk); } else { // 即使压缩后还是放不下则移除并可能添加一个路径引用摘要 compressedChunks.push(this.createPathReferenceChunk(chunk)); totalTokens - this.tokenEstimator(chunk.content); // 添加路径引用摘要的token totalTokens this.tokenEstimator(compressedChunks[compressedChunks.length-1].content); } } else if (chunk.type output || chunk.type instruction) { // 对旧的输出或指令进行摘要 if (chunk.metadata.timestamp Date.now() - 5 * 60 * 1000) { // 5分钟前的 const summary this.summarizeTextChunk(chunk); chunk.content summary; chunk.type summary; } compressedChunks.push(chunk); } else { compressedChunks.push(chunk); } // 重新计算总tokens如果满足条件则提前退出循环 const currentTotal compressedChunks.reduce( (sum, c) sum this.tokenEstimator(c.content), 0 ); if (currentTotal this.maxTokens * 0.9) { // 留10%余量把剩下的高优先级chunk加回来 break; } } this.chunks compressedChunks; } private compressCodeChunk(chunk: ContextChunk): string { // 简易实现尝试提取函数/类签名 const content chunk.content; // 这是一个非常简单的正则示例生产环境应用AST解析 const functionSignatureMatch content.match( /(export\s)?(async\s)?function\s\w\s*\([^)]*\)\s*(:\s*\w)?/g ); const classSignatureMatch content.match( /(export\s)?class\s\w(\sextends\s\w)?(\simplements\s[^{])?/g ); let compressed // File: ${chunk.metadata.filePath}\n; if (functionSignatureMatch) { compressed // Functions:\n${functionSignatureMatch.join(\n)}\n; } if (classSignatureMatch) { compressed // Classes:\n${classSignatureMatch.join(\n)}\n; } if (!functionSignatureMatch !classSignatureMatch) { compressed // Content omitted for brevity. Full path: ${chunk.metadata.filePath}; } return compressed; } private createPathReferenceChunk(originalChunk: ContextChunk): ContextChunk { return { id: ref_${originalChunk.id}, type: summary, content: [Reference to code file: ${originalChunk.metadata.filePath}. Details omitted due to context limit. Request if needed.], priority: originalChunk.priority 10, // 引用比完整代码优先级低 metadata: { timestamp: Date.now() }, }; } private summarizeTextChunk(chunk: ContextChunk): string { // 简易文本摘要取前100个字符 return [Previous context summary]: ${chunk.content.substring(0, 100)}...; } // 获取当前压缩后的完整上下文 getContext(): string { return this.chunks .sort((a, b) b.priority - a.priority) // 按优先级高到低排列 .map((chunk) --- ${chunk.type.toUpperCase()} ---\n${chunk.content}) .join(\n\n); } }3.3 与AI代理的集成最后我们需要将这套上下文管理系统与AI代理比如使用OpenAI API、Claude API或本地LLM集成。代理的工作流程将变为接收任务例如“在src/auth/login.ts中添加JWT刷新令牌功能”。初始上下文加载ContextManager通过ProjectAnalyzer获取login.ts及其相关文件如user.ts,jwt.ts加载它们的完整内容作为高优先级初始块。交互循环代理根据当前完整上下文生成回答代码建议、解释等。代理的输出被作为一个新的ContextChunk(type: ‘output’) 添加到管理器。如果代理在回答中引用了新文件例如“参考config.ts中的密钥设置”系统可以自动将config.ts的签名或摘要作为中优先级块加入上下文。ContextManager在每次添加后自动运行compressContext()确保总token数不超过限制。持续维护随着对话进行最早的、优先级低的代码块会被压缩成签名或路径引用最新的指令、代码变更和关键输出始终保持在上下文中。// 简化的代理集成示例 class AICodingAgent { private contextManager: ContextManager; private projectAnalyzer: ProjectAnalyzer; private llmClient: any; // 你的LLM客户端 async processTask(task: string, entryFile: string) { // 1. 分析项目获取相关文件 const relevantFiles this.projectAnalyzer.getRelevantFiles(entryFile); for (const file of relevantFiles) { const content await fs.readFile(file, utf-8); this.contextManager.addChunk({ type: code, content, priority: file entryFile ? 100 : 70, // 入口文件优先级最高 metadata: { filePath: file }, }); } // 2. 添加任务指令 this.contextManager.addChunk({ type: instruction, content: Task: ${task}, priority: 90, metadata: {}, }); // 3. 开始交互循环 let conversationActive true; while (conversationActive) { const currentContext this.contextManager.getContext(); const prompt ${currentContext}\n\nAssistant:; const response await this.llmClient.complete(prompt); // ... 处理响应例如提取代码块、解析建议 // 4. 将AI响应加入上下文 this.contextManager.addChunk({ type: output, content: response, priority: 80, metadata: {}, }); // 判断任务是否完成... } } }4. 实操要点与避坑指南在实际实现和应用这套系统时我踩过不少坑这里分享几个关键点4.1 优先级策略的设计priority字段是压缩算法的关键。设计不当会导致关键信息被过早丢弃。我的经验是当前焦点用户明确指定的文件、AI正在编辑的函数所在的文件优先级最高例如95-100。最新输出AI最近一次的回答优先级高80-90因为它包含了最新的思路和决策。直接依赖被焦点文件导入的文件优先级中高70-80。间接依赖/历史代码优先级中低50-70。系统指令/任务描述虽然重要但通常很简短可以赋予高优先级90但不必担心被压缩因为它本身就很短。路径引用/摘要优先级最低10-30。一个常见的错误是给所有“代码”类型统一的优先级。需要根据代码与当前任务的相关性进行动态调整。例如当AI开始处理config.ts时该文件的优先级应立即提升。4.2 Token估算的准确性示例中用了简单的字符长度除以4的方法这对于英文比较粗略对中文或其他语言不准确。更可靠的做法是如果使用OpenAI API可以直接调用其提供的tiktoken库进行精确计算。对于其他模型寻找对应的tokenizer库或者使用一个保守的估计系数如中文1个token≈2个字符。一定要预留缓冲不要将maxTokens用到100%。我通常预留15-20%的空间以防止估算误差和模型输出不可预测的长度。例如API限制是8000token我的上下文管理器目标就设在6500token左右。4.3 代码压缩的保真度compressCodeChunk函数中的正则表达式方法非常脆弱。它无法正确处理嵌套括号、泛型、装饰器等复杂语法。强依赖AST生产环境必须使用TypeScript编译器API(ts)或ts-morph这样的包装库来可靠地提取函数/类签名。这能确保提取出的“签名”在语法上是完整的可以被AI正确理解。保留关键注释在压缩时可以考虑保留函数上方的JSDoc注释或重要的单行注释这些注释往往包含了参数说明和业务逻辑要点对AI理解代码意图至关重要。处理未导出的内部函数对于当前焦点文件有时未导出的内部函数也很重要。压缩策略可能需要为“焦点文件”和“非焦点文件”设置不同的规则焦点文件保留更多内部细节非焦点文件只保留导出接口。4.4 与向量检索RAG的结合对于超大型代码库即使经过压缩项目图谱也可能太大。这时可以将向量检索引入系统。建立代码向量库将每个函数、类或模块的代码和其文档字符串嵌入成向量。动态检索当AI代理需要了解某个特定概念或寻找类似功能时ContextManager不是直接加载可能相关的文件而是将当前问题或上下文作为查询从向量库中检索出最相关的几个代码片段然后将这些片段作为高优先级上下文块插入。混合模式对于明确的、强依赖的文件通过静态分析得出使用路径加载对于模糊的、探索性的需求使用向量检索。这种混合模式能极大提高上下文利用的智能度。5. 常见问题与排查技巧在开发和调试上下文压缩系统时你可能会遇到以下典型问题5.1 AI开始“胡言乱语”或循环输出这是上下文管理失效的最典型标志。排查步骤检查上下文长度在每次调用AI前输出当前ContextManager估算的token数和getContext()的最后500个字符。看看是不是已经爆了或者末尾被塞入了大量无意义的压缩摘要。审查压缩输出检查被压缩成// File: ...或[Reference ...]的块是否过多。如果核心逻辑代码都被替换成了引用AI自然失去了工作依据。需要调整优先级算法确保核心文件的完整代码不被过度压缩。检查摘要质量summarizeTextChunk生成的摘要是否过于模糊丢失了关键决策信息考虑改进摘要算法或者对于重要的决策点强制保留为instruction类型并赋予高优先级禁止被摘要。5.2 性能瓶颈项目分析耗时过长对于巨型Monorepo首次构建项目图谱可能很慢。优化策略增量分析监听文件系统变化只重新分析变更的文件及其影响边界。缓存图谱将构建好的项目图谱序列化到磁盘。只有当package.json或tsconfig.json等关键文件改变时才触发全量重建。使用更快的工具ts-morph在易用性和性能之间取得了很好的平衡比直接使用底层的TypeScript编译器API更方便且其底层缓存机制能提升性能。5.3 依赖解析错误导致相关文件未加载静态分析可能误判文件间的依赖关系。调试方法可视化图谱实现一个简单的函数将ProjectGraph输出为DOT格式用Graphviz生成依赖图。直观检查login.ts是否真的连接到了你认为它应该依赖的jwt.ts。处理动态导入import(‘./’ moduleName)这类动态导入静态分析无法处理。需要在ContextManager中建立一种“回退机制”当AI在对话中明确提及某个模块时即使图谱中没有也尝试去加载它。路径别名Path Aliases如果项目配置了/之类的路径别名必须在静态分析阶段就通过tsconfig.json的compilerOptions.paths配置正确解析否则所有依赖分析都会失效。5.4 与特定AI模型或API的兼容性问题不同模型对上下文格式的敏感度不同。经验结构化分隔符像--- CODE ---这样的分隔符对大多数模型都有效能帮助它区分上下文的不同部分。但有些模型可能对特定的标记格式有偏好。指令位置有些模型对系统指令或任务描述在上下文中的位置很敏感。通常放在最开头是安全的。确保你的instruction类型块有足够高的优先级不会被挤到后面去。测试、测试、再测试用一组标准任务如“添加一个函数”、“修复一个已知Bug”来测试不同的压缩策略和参数观察哪种组合下AI的输出最稳定、最准确。这需要积累大量的测试案例。实现一个智能的context-mode压缩系统初期投入的精力不小但它带来的回报是巨大的更长的有效对话轮次、更低的API调用成本、以及更专注和准确的AI辅助编程体验。它迫使你更深入地思考项目结构和代码组织这本身也是一个很好的编程实践。
返回列表