Skip to content

工具懒加载优化 (Tool Lazy Loading) 🚀

"从 53k tokens 到 2.6k tokens —— Claude Code 如何通过动态工具搜索实现 95% 的上下文削减"


1. 问题背景

1.1 传统 MCP 工具加载的困境

在引入工具懒加载之前,系统采用全量预加载模式:

typescript
// 传统方式:会话启动时加载所有工具
async function initializeSession() {
  const allTools = [];
  for (const server of mcpServers) {
    const tools = await server.listTools();
    allTools.push(...tools);
  }
  systemPrompt += formatToolDefinitions(allTools);
  // 结果:一个简单的 "hi" 就消耗 53,000 tokens
}

问题分析:

问题影响数据
Token 浪费大量无关工具占用上下文53k tokens
响应延迟加载时间长~5 秒首次响应
成本高昂每条消息都携带全量工具10x 成本增加
扩展性差工具数量受限最多 20-30 个工具

2. 懒加载架构设计

2.1 核心思想

渐进式工具发现:

传统方式: 启动 → 加载所有工具 → 等待用户输入 → 响应
懒加载方式: 启动 → 加载工具列表 → 用户输入 → 搜索相关工具 → 加载定义 → 响应

2.2 三层架构

Layer 1: 工具元数据 (启动时加载)
  - 工具名称
  - 简短描述 (1 句话)
  - 所属服务器

Layer 2: 工具搜索 (用户输入时)
  - 语义搜索相关工具
  - 关键词匹配
  - 返回 top-k 工具

Layer 3: 工具定义 (使用时加载)
  - 完整参数定义
  - 详细描述
  - 使用示例

3. 实现细节

3.1 元数据加载

typescript
interface ToolMetadata {
  name: string;
  description: string;  // 简短描述,≤100 字符
  server: string;
}

class ToolRegistry {
  private metadata: ToolMetadata[] = [];
  private fullDefinitions: Map<string, ToolDefinition> = new Map();

  async initialize() {
    // 只加载元数据
    for (const server of this.mcpServers) {
      const tools = await server.listTools();
      this.metadata.push(...tools.map(t => ({
        name: t.name,
        description: t.description.slice(0, 100),
        server: server.name
      })));
    }
    // 消耗: ~2k tokens (vs 53k)
  }
}

3.2 语义搜索

typescript
class ToolSearchEngine {
  private embeddings: Map<string, number[]> = new Map();

  async indexTools(tools: ToolMetadata[]) {
    for (const tool of tools) {
      const text = `${tool.name} ${tool.description}`;
      const embedding = await getEmbedding(text);
      this.embeddings.set(tool.name, embedding);
    }
  }

  async search(query: string, limit: number = 5): Promise<ToolMetadata[]> {
    const queryEmbedding = await getEmbedding(query);

    const scores = [];
    for (const [name, embedding] of this.embeddings) {
      const similarity = cosineSimilarity(queryEmbedding, embedding);
      scores.push({ name, similarity });
    }

    return scores
      .sort((a, b) => b.similarity - a.similarity)
      .slice(0, limit)
      .map(s => this.getMetadata(s.name));
  }
}

3.3 按需加载完整定义

typescript
class ToolRegistry {
  async getToolDefinition(name: string): Promise<ToolDefinition> {
    // 检查缓存
    if (this.fullDefinitions.has(name)) {
      return this.fullDefinitions.get(name)!;
    }

    // 加载完整定义
    const metadata = this.metadata.find(t => t.name === name);
    const server = this.mcpServers.get(metadata.server);
    const definition = await server.getTool(name);

    // 缓存
    this.fullDefinitions.set(name, definition);

    return definition;
  }
}

4. 工作流程

4.1 完整流程

typescript
class LazyToolAgent {
  async handleUserInput(input: string) {
    // 1. 搜索相关工具
    const relevantTools = await this.toolSearch.search(input, 5);

    // 2. 加载工具定义
    const toolDefinitions = await Promise.all(
      relevantTools.map(t => this.toolRegistry.getToolDefinition(t.name))
    );

    // 3. 注入到提示词
    const systemPrompt = this.buildSystemPrompt(toolDefinitions);

    // 4. 调用 LLM
    const response = await this.llm.chat({
      system: systemPrompt,
      messages: [{ role: 'user', content: input }],
      tools: toolDefinitions
    });

    return response;
  }

  buildSystemPrompt(tools: ToolDefinition[]): string {
    return `
You have access to the following tools:

${tools.map(t => `- ${t.name}: ${t.description}`).join('\n')}

Use these tools to help the user.
    `.trim();
  }
}

4.2 示例对话

用户: "读取 package.json 文件"

1. 搜索工具:
   search("读取 package.json 文件")
   → ["read_file", "list_files", "search_files"]

2. 加载定义:
   getToolDefinition("read_file")
   → { name: "read_file", parameters: { path: string }, ... }

3. 注入提示词:
   "You have access to: read_file, list_files, search_files"

4. LLM 响应:
   tool_use: read_file({ path: "package.json" })

5. 性能对比

5.1 Token 消耗

场景传统方式懒加载改进
启动53,0002,60095% ↓
简单对话53,0002,60095% ↓
使用 1 个工具53,0003,20094% ↓
使用 5 个工具53,0006,50088% ↓

5.2 响应时间

操作传统方式懒加载改进
首次启动5.2s0.8s85% ↓
简单查询1.5s0.3s80% ↓
工具调用2.1s0.9s57% ↓

5.3 成本节省

传统方式:
  - 1000 次对话
  - 平均 53k tokens/次
  - 总计: 53M tokens
  - 成本: $159 (按 $3/M tokens)

懒加载:
  - 1000 次对话
  - 平均 5k tokens/次
  - 总计: 5M tokens
  - 成本: $15

节省: $144 (91%)

6. 优化技巧

6.1 缓存策略

typescript
class ToolCache {
  private cache: Map<string, { definition: ToolDefinition; timestamp: number }> = new Map();
  private maxAge = 5 * 60 * 1000; // 5 分钟

  async get(name: string): Promise<ToolDefinition | null> {
    const cached = this.cache.get(name);

    if (cached && Date.now() - cached.timestamp < this.maxAge) {
      return cached.definition;
    }

    return null;
  }

  set(name: string, definition: ToolDefinition) {
    this.cache.set(name, {
      definition,
      timestamp: Date.now()
    });
  }
}

6.2 预加载常用工具

typescript
class ToolRegistry {
  private commonTools = ['read_file', 'write_file', 'run_command'];

  async initialize() {
    // 加载元数据
    await this.loadMetadata();

    // 预加载常用工具
    await Promise.all(
      this.commonTools.map(name => this.getToolDefinition(name))
    );
  }
}

6.3 批量加载

typescript
async getToolDefinitions(names: string[]): Promise<ToolDefinition[]> {
  // 分组:已缓存 vs 需要加载
  const cached = names.filter(n => this.fullDefinitions.has(n));
  const toLoad = names.filter(n => !this.fullDefinitions.has(n));

  // 批量加载
  const loaded = await Promise.all(
    toLoad.map(name => this.loadToolDefinition(name))
  );

  return [
    ...cached.map(n => this.fullDefinitions.get(n)!),
    ...loaded
  ];
}

7. 挑战与解决方案

7.1 搜索准确性

挑战: 语义搜索可能遗漏相关工具

解决方案: 混合搜索

typescript
async search(query: string): Promise<ToolMetadata[]> {
  // 1. 语义搜索
  const semanticResults = await this.semanticSearch(query, 10);

  // 2. 关键词搜索
  const keywordResults = this.keywordSearch(query, 10);

  // 3. 合并去重
  const merged = this.mergeResults(semanticResults, keywordResults);

  return merged.slice(0, 5);
}

7.2 冷启动延迟

挑战: 首次搜索需要生成 embedding

解决方案: 预计算 embeddings

typescript
async initialize() {
  await this.loadMetadata();

  // 后台预计算 embeddings
  this.indexTools(this.metadata).catch(console.error);
}

7.3 工具依赖

挑战: 某些工具依赖其他工具

解决方案: 依赖图

typescript
const toolDependencies = {
  'edit_file': ['read_file'],
  'run_tests': ['read_file', 'run_command']
};

async getToolWithDependencies(name: string): Promise<ToolDefinition[]> {
  const tools = [await this.getToolDefinition(name)];

  const deps = toolDependencies[name] || [];
  for (const dep of deps) {
    tools.push(await this.getToolDefinition(dep));
  }

  return tools;
}

8. 总结

8.1 核心收益

  • 95% Token 削减: 53k → 2.6k
  • 85% 启动加速: 5.2s → 0.8s
  • 91% 成本节省: $159 → $15
  • 无限扩展性: 支持数百个工具

8.2 设计原则

  1. 元数据优先: 启动时只加载最小信息
  2. 按需加载: 使用时才加载完整定义
  3. 智能搜索: 语义 + 关键词混合搜索
  4. 缓存优化: 缓存常用工具和最近使用的工具

8.3 通用模式

typescript
// 通用的懒加载模式
class LazyResourceLoader<T> {
  async initialize() {
    this.metadata = await this.loadMetadata();
  }

  async load(query: string): Promise<T[]> {
    const relevant = await this.search(query);
    return await this.loadFull(relevant);
  }

  private cache = new Map<string, T>();
}

参考资源

前端面试知识库