找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2762

积分

0

好友

372

主题
发表于 4 小时前 | 查看: 3| 回复: 0

相信用过 Claude Code 的开发者都遇到过这样的窘境:对话开始时一切顺畅,但随着任务深入,响应速度越来越慢,最终弹出一个令人沮丧的提示——“context window full”。这是因为Claude的上下文窗口有其上限(Sonnet模型为200K tokens)。

本文将深入解析一个在Anthropic黑客马拉松上获奖的项目:Everything Claude Code (ECC)。该项目由Affaan Mustafa历时十个月精心打磨,其核心目标正是攻克这个硬性限制:如何在有限的上文窗口中塞入更多功能,同时避免会话崩溃?

下面,我们来拆解ECC项目中几个关键的上下文压缩与优化策略。

Agent描述压缩:从26K降至2K的秘诀

问题根源何在?

ECC项目包含了27个各司其职的 specialized agents。每个agent都是一个Markdown文件,内含完整的功能说明、工具定义和使用场景。如果将这些描述全部加载到上下文中,仅agent描述本身就会占用约121KB,折合26K tokens

这意味着什么?——你的上下文窗口还没开始做正事,就已经被“说明书”吃掉了13%。

三层压缩模式

scripts/lib/agent-compress.js 中,ECC实现了一个压缩库,提供了三种压缩模式:

1. catalog模式(仅保留元数据)

function compressToCatalog(agent) {
return {
name: agent.name,
description: agent.description,
tools: agent.tools,
model: agent.model,
  };
}

此模式仅保留名称、描述、工具和模型四个核心字段。27个agent压缩后仅占用2-3K tokens,压缩率超过85%。

2. summary模式(元数据 + 首段摘要)

function extractSummary(body, maxSentences = 1) {
const lines = body.split('\n');
const paragraphs = [];
let current = [];
let inCodeBlock = false;

for (const line of lines) {
const trimmed = line.trim();

// 跟踪代码块状态
if (trimmed.startsWith('```')) {
      inCodeBlock = !inCodeBlock;
continue;
    }
if (inCodeBlock) continue;

if (trimmed === '') {
if (current.length > 0) {
        paragraphs.push(current.join(' '));
        current = [];
      }
continue;
    }

// 跳过标题、列表项、编号列表、表格行
if (
      trimmed.startsWith('#') ||
      trimmed.startsWith('- ') ||
      trimmed.startsWith('* ') ||
/^\d+\.\s/.test(trimmed) ||
      trimmed.startsWith('|')
    ) {
if (current.length > 0) {
        paragraphs.push(current.join(' '));
        current = [];
      }
continue;
    }

    current.push(trimmed);
  }

const firstParagraph = paragraphs.find(p => p.length > 0);
if (!firstParagraph) return '';

const sentences = firstParagraph.match(/[^.!?]+[.!?]+/g) || [firstParagraph];
return sentences.slice(0, maxSentences).map(s => s.trim()).join(' ').trim();
}

此模式会智能解析agent正文,跳过代码块和各种Markdown格式标记,提取第一段文本内容。压缩后约4-5K tokens。

3. full模式(完整内容)
此模式不会进行压缩。只有当某个agent被真正调用时,才会以懒加载的方式读取其完整内容。

巧妙的懒加载机制

整个设计的精髓在于 lazyLoadAgent 函数:

function lazyLoadAgent(agentsDir, agentName) {
// 安全校验:只允许字母数字下划线和横线
if (!/[\w-]+$/.test(agentName)) return null;

const filePath = path.resolve(agentsDir, `${agentName}.md`);

// 路径遍历防护
if (!filePath.startsWith(resolvedAgentsDir + path.sep)) return null;

if (!fs.existsSync(filePath)) return null;
return loadAgent(filePath);
}

这一设计确保了:

  1. 安全:包含路径遍历防护,防止恶意输入读取系统任意文件。
  2. 按需加载:在catalog模式下,会话开始时仅加载元数据;仅在需要时才加载完整的agent描述。
  3. 快速选择:在选择agent的阶段,上下文仅需承载2-3K tokens,而非最初的26K。

安全测试(tests/lib/agent-compress.test.js)验证了其可靠性:

// 测试路径遍历防护
if (test('lazyLoadAgent rejects path traversal attempts', () => {
const agent = lazyLoadAgent(tmpDir, '../etc/passwd');
  assert.strictEqual(agent, null);
}));

// 测试非法字符过滤
if (test('lazyLoadAgent rejects names with invalid characters', () => {
const agent1 = lazyLoadAgent(tmpDir, 'foo/bar');
  assert.strictEqual(agent1, null);

const agent2 = lazyLoadAgent(tmpDir, 'foo bar');  // 空格
  assert.strictEqual(agent2, null);

const agent3 = lazyLoadAgent(tmpDir, 'foo..bar'); // 双点
  assert.strictEqual(agent3, null);
}));

// 测试不存在的agent
if (test('lazyLoadAgent returns null for non-existent agent', () => {
const agent = lazyLoadAgent(tmpDir, 'does-not-exist');
  assert.strictEqual(agent, null);
}));

实际的压缩效果如何?我们来看测试验证:

// tests/lib/agent-compress.test.js
const result = buildAgentCatalog(realAgentsDir, { mode: 'catalog' });
assert.ok(result.agents.length > 0, 'Should find at least one agent');
assert.ok(result.stats.compressedBytes < result.stats.originalBytes,
'Catalog should be smaller than original');

// 验证压缩率
const ratio = result.stats.compressedBytes / result.stats.originalBytes;
assert.ok(ratio < 0.5, `Compression ratio ${ratio.toFixed(2)} should be < 0.5`);
// 实际结果: ratio ≈ 0.15 (85% 压缩率)

// 验证token估算
assert.ok(
  result.stats.compressedTokenEstimate < 5000,
`Token estimate ${result.stats.compressedTokenEstimate} exceeds 5000`
);
// 27 个 agent 压缩后约 2500 tokens

结果令人振奋:压缩率超过85%,token占用从26K骤降至2-3K。

Frontmatter解析的实现细节

Agent文件使用YAML frontmatter存储元数据,解析器需要妥善处理各种边界情况,这其中涉及了不少 Node.js 的路径和字符串处理技巧:

function parseFrontmatter(content) {
// 匹配 ---\n frontmatter \n---\n body
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n([\s\S]*))?$/);
if (!match) {
return { frontmatter: {}, body: content };
  }

const frontmatter = {};
for (const line of match[1].split('\n')) {
const colonIdx = line.indexOf(':');
if (colonIdx === -1) continue;

const key = line.slice(0, colonIdx).trim();
let value = line.slice(colonIdx + 1).trim();

// 处理 JSON 数组(如 tools: ["Read", "Grep"])
if (value.startsWith('[') && value.endsWith(']')) {
try {
        value = JSON.parse(value);
      } catch {
// 解析失败保持原字符串
      }
    }

// 去除引号包裹
if (typeof value === 'string' &&
        value.startsWith('"') && value.endsWith('"')) {
      value = value.slice(1, -1);
    }

    frontmatter[key] = value;
  }

return { frontmatter, body: match[2] || '' };
}

Context Budget:审计你的Token都去哪了

仅靠压缩还不够,你还需要知道Token被消耗在了何处。ECC贴心地提供了 /context-budget 命令,专门用于分析Token消耗构成。

四大组件扫描

该命令会扫描你的项目设置,并统计四大类组件的Token消耗估算。

核心扫描逻辑(skills/context-budget/SKILL.md):

// Agent 扫描
function scanAgents(agentsDir) {
const agents = fs.readdirSync(agentsDir)
    .filter(f => f.endsWith('.md'))
    .map(f => {
const content = fs.readFileSync(path.join(agentsDir, f), 'utf8');
const lines = content.split('\n');
const tokens = Math.ceil(lines.length * 1.3); // 每行约 1.3 tokens

// 解析 frontmatter 检查 description 长度
const { frontmatter } = parseFrontmatter(content);
const descWords = frontmatter.description?.split(/\s+/).length || 0;

return {
name: f,
lines: lines.length,
        tokens,
heavy: lines.length > 200,        // 标记重型 agent
bloated: descWords > 30,          // 标记臃肿 frontmatter
      };
    });

return {
count: agents.length,
totalTokens: agents.reduce((sum, a) => sum + a.tokens, 0),
issues: agents.filter(a => a.heavy || a.bloated),
  };
}

// MCP Server 扫描
function scanMcpServers(mcpConfig) {
const servers = Object.values(mcpConfig.mcpServers || {});
const toolCount = servers.reduce((sum, s) =>
    sum + (s.tools?.length || 0), 0);

const cliTools = ['gh', 'git', 'npm', 'aws', 'supabase', 'vercel'];
const cliReplaceable = servers.filter(s =>
    cliTools.some(cli =>
      s.name?.includes(cli) || s.description?.includes(cli)
    )
  );

return {
serverCount: servers.length,
    toolCount,
estimatedTokens: toolCount * 500,  // 每个 tool 约 500 tokens
issues: [
      servers.length > 10 && `${servers.length} MCP servers (recommend <10)`,
      ...cliReplaceable.map(s => `${s.name} is CLI-replaceable`),
    ].filter(Boolean),
  };
}

扫描细节解读:

  • *Agents (`agents/.md`)**:按每行约1.3个token估算。标记超过200行的“重型agent”和description超过30词的“臃肿frontmatter”。
  • *Skills (`skills//SKILL.md)**:同样按1.3 token/行估算。标记超过400行的大文件,并检查.agents/skills/` 目录中可能存在的重复副本。
  • MCP Servers (.mcp.json):每个tool的schema定义大约消耗500 tokens。标记tool数量超过20的server,并特别指出那些仅包装了简单CLI命令(如 gh, git, npm)的、可被替代的server。
  • CLAUDE.md:合并项目级和用户级的CLAUDE.md文件,并标记总行数超过300的情况。

输出报告示例

运行 /context-budget 后,你会得到一份清晰的审计报告:

Context Budget Report
═══════════════════════════════════════

Total estimated overhead: ~33,000 tokens
Context model: Claude Sonnet (200K window)
Effective available context: ~167,000 tokens (83%)

Component Breakdown:
┌─────────────────┬────────┬───────────┐
│ Component       │ Count  │ Tokens    │
├─────────────────┼────────┼───────────┤
│ Agents          │ 16     │ ~12,400   │
│ Skills          │ 28     │ ~6,200    │
│ Rules           │ 8      │ ~4,800    │
│ MCP tools       │ 87     │ ~43,500   │
│ CLAUDE.md       │ 2      │ ~1,200    │
└─────────────────┴────────┴───────────┘

⚠ Issues Found (3):
- 3 heavy agents (>200 lines)
- 14 MCP servers enabled (recommend <10)
- 3 CLI-replaceable MCP servers

Top 3 Optimizations:
1. Remove 3 MCP servers → save ~27,500 tokens (47% overhead reduction)
2. Use catalog mode for agents → save ~11,000 tokens
3. Compact CLAUDE.md → save ~400 tokens

这个工具的价值在于可视化。很多时候你只是在不断添加新功能,却意识不到它们已经悄然吞噬了一半的上下文预算。

Strategic Compact:在合适的时机做压缩

ECC不依赖于Claude Code内置的自动压缩机制(后者往往在不合时宜的节点触发,例如任务进行到一半时突然压缩,导致关键上下文丢失)。他们实现了一套 Strategic Compact(策略性压缩) 机制。

核心原理:在工具调用时计数

scripts/hooks/suggest-compact.js 中,通过 PreToolUse Hook(在执行Edit/Write等操作前触发)来进行调用计数:

(注:此处展示核心逻辑,完整实现包含详尽的错误处理和会话隔离)

if (count === threshold) {
  log(`[StrategicCompact] ${threshold} tool calls reached - consider /compact`);
}

if (count > threshold && (count - threshold) % 25 === 0) {
  log(`[StrategicCompact] ${count} tool calls - good checkpoint for /compact`);
}

默认阈值是50次工具调用。达到阈值后,会首次建议执行 /compact。之后每超出阈值25次,会再次提醒,为你标记出合适的压缩检查点。

策略性压缩的最佳时机

ECC建议在以下明确的任务边界进行手动压缩:

阶段转换 是否压缩 原因
Research → Planning 研究成果占用空间大,但计划是提炼后的精华
Planning → Implementation 计划可以存入TodoWrite或文件,腾出空间写代码
Implementation → Testing 看情况 如果测试需要引用刚写的代码,则先不压缩
Debugging → Next feature 调试痕迹对新功能而言是噪音
Mid-implementation 会丢失变量名、文件路径等关键中间信息

理解压缩的“幸存者”

很多人不敢压缩,是担心丢失重要信息。ECC明确了压缩后哪些内容能保留:

能够存活的:

  • CLAUDE.md 中的指令(始终加载)
  • TodoWrite 中的任务列表
  • Memory 文件 (~/.claude/memory/)
  • Git 状态(提交记录、分支信息)
  • 磁盘上的所有文件

会丢失的:

  • 中间的推理和分析过程
  • 之前读取过的文件内容(除非重新读取)
  • 多轮对话的上下文历史
  • 工具调用的历史和计数
  • 口头表达的细微偏好

因此,ECC的核心建议是:将任何你认为重要的中间状态,先保存到文件或Memory中,然后再执行压缩。

环境变量层面的优化

除了架构设计,ECC还提供了一套推荐的环境变量配置,据称最高可降低70%的token消耗

{
"model": "sonnet",
"env": {
"MAX_THINKING_TOKENS": "10000",
"CLAUDE_AUTOCOMPACT_PCT_OVERRIDE": "50",
"CLAUDE_CODE_SUBAGENT_MODEL": "haiku"
  }
}

这些环境变量在代码中被读取并应用,例如在 suggest-compact.js 中读取 COMPACT_THRESHOLD

  • MAX_THINKING_TOKENS=10000:默认值是31999,Claude会利用这些token进行内部“思考”。将其降至10000可节省约70%的“隐藏成本”。对于简单任务,甚至可以设置为0以完全关闭。
  • CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=50:默认在上下文使用率达到95%时触发自动压缩,此时响应质量可能已下降。改为50%触发,为复杂任务留出更充足的余量。
  • CLAUDE_CODE_SUBAGENT_MODEL=haiku:子agent(如负责文件读取、代码搜索的Task工具)默认继承主模型(通常是昂贵的Sonnet)。换成更轻量的Haiku模型可节省约80%的成本,且对于此类简单任务完全够用。

对MCP Server的取舍

ECC在文档中提出了一个重要警告:每个启用的MCP server都会将其所有tool的定义schema塞入上下文窗口。如果启用过多MCP,200K的窗口可能仅剩70K可用。

他们的建议是:

  1. 将启用的MCP server数量保持在10个以下
  2. 优先使用CLI工具(例如用 gh 命令行替代GitHub MCP,用 aws cli 替代AWS MCP)。
  3. 利用 disabledMcpServers 配置按项目禁用不需要的server。
  4. 评估默认配置的Memory MCP等server是否真的需要,可以考虑关闭。

/context-budget 扫描器会专门标记出那些“CLI-replaceable”的MCP server,指导你进行优化。

总结:Token优化是一个系统工程

回顾一下,ECC提供了一套组合拳来解决上下文限制问题:

  1. Agent Compression —— 懒加载 + 三层压缩模式,将agent描述从26K降至2-3K tokens。
  2. Context Budget —— 审计工具,可视化呈现token消耗分布,帮你定位“内存大户”。
  3. Strategic Compact —— 基于工具调用计数的策略性压缩建议,避免在任务中途丢失上下文。
  4. Model Routing —— 让轻量模型(Haiku)处理简单任务,重量模型(Sonnet/Opus)专注复杂推理。
  5. Thinking Tokens控制 —— 通过环境变量限制内部推理token,削减隐藏成本。
  6. MCP管理 —— 严格控制MCP server的数量和必要性,减少不必要的schema开销。

这些技术背后贯穿着同一个核心理念:不要试图一次性将所有信息都塞进有限的上下文窗口,而是采用按需加载、及时清理、在明确边界进行管理的策略。

如果你也在使用Claude Code进行复杂的项目开发,并时常受限于上下文窗口,那么ECC项目的这些 开源实战 经验和具体实现,无疑提供了极具价值的参考。更多深入的技术讨论和实践分享,也欢迎在 云栈社区 的相应板块进行交流。

关键源码参考与测试验证

Agent压缩核心实现 (scripts/lib/agent-compress.js):

function buildAgentCatalog(agentsDir, options = {}) {
const mode = options.mode || 'catalog';
let agents = loadAgents(agentsDir);
const originalBytes = agents.reduce((sum, a) => sum + a.byteSize, 0);

let compressed;
if (mode === 'catalog') {
    compressed = agents.map(compressToCatalog);
  } else if (mode === 'summary') {
    compressed = agents.map(compressToSummary);
  } else {
    compressed = agents.map(a => ({...a, body: a.body}));
  }

const compressedJson = JSON.stringify(compressed);
const compressedTokenEstimate = Math.ceil(compressedJson.length / 4);

return {
agents: compressed,
stats: {
totalAgents: agents.length,
      originalBytes,
compressedBytes: Buffer.byteLength(compressedJson, 'utf8'),
      compressedTokenEstimate,
      mode,
    },
  };
}

运行测试验证压缩效果:

# 运行Agent压缩测试
node tests/lib/agent-compress.test.js

运行Strategic Compact测试:

node tests/hooks/suggest-compact.test.js

文章基于ECC v1.9.0版本代码分析,其中Agent描述压缩是本次重点解析的特性。




上一篇:私信服务中断!一线工程师复盘Redis连接池耗尽与5K QPS突增的紧急处理
下一篇:Claude Mythos模型信息遭意外泄露,Anthropic警示其网络安全能力带来新风险
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-29 06:26 , Processed in 0.517535 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表