普通视图

发现新文章,点击刷新页面。
昨天 — 2026年2月8日首页

多模型支持的架构设计:如何集成 10+ AI 模型

作者 echoVic
2026年2月8日 10:47

Blade 多模型 AI 系统架构:从统一接口到智能路由

blade-code 系列第 3 篇。本文基于 Blade 源码(2026年2月)探讨多模型 AI 系统的架构设计。

为什么要支持多模型?

构建 AI 应用时,第一个问题往往是:"用哪个模型?"

  • OpenAI GPT-4.5?强大但贵
  • Claude Opus 4?代码能力强但有速率限制
  • DeepSeek V3?便宜但稳定
  • 智谱 GLM-4.7?需配置

我的答案是:全都要

blade-code 从第一天就设计为多模型架构,基于 Vercel AI SDK 构建统一接口,支持 80+ 主流模型无缝切换。

单一模型的问题

问题 影响
成本高 GPT-4.5 输入 $15/M tokens
速率限制 Claude 每分钟请求数有限
服务中断 OpenAI 宕机时干瞪眼
能力差异 不同任务需要不同模型

多模型架构能解决这些问题:

成本优化 — 简单任务用便宜模型,复杂任务用强大模型:

// 简单任务
const summary = await model.generate('总结这段文字', {
  model: 'deepseek-chat' // $0.28/M input
});

// 复杂任务
const architecture = await model.generate('设计系统架构', {
  model: 'claude-opus-4' // $15/M input
});

高可用 — 主模型挂了自动切换:

const response = await model.generate(prompt, {
  model: 'gpt-4.5',
  fallback: ['claude-sonnet-4', 'deepseek-chat']
});

统一接口设计

架构概览

graph TB
    subgraph "Blade ModelManager"
        MM[ModelManager]
        MM --> |管理| P1[OpenAI Provider]
        MM --> |管理| P2[Anthropic Provider]
        MM --> |管理| P3[Google/Gemini Provider]
        MM --> |管理| P4[DeepSeek Provider]
        MM --> |管理| P5[Azure Provider]
    end
    
    subgraph "Vercel AI SDK"
        AI[AI SDK Core]
        AI --> |抽象| CP[createOpenAICompatible]
        AI --> |抽象| CA[createAnthropic]
        AI --> |抽象| CD[createDeepSeek]
        AI --> |抽象| CG[createGoogleGenerativeAI]
    end
    
    subgraph "统一接口"
        PI[IChatService Interface]
        PI --> |chat| GEN[生成响应]
        PI --> |streamChat| STR[流式响应]
    end
    
    P1 -.-> |实现| PI
    P2 -.-> |实现| PI
    P3 -.-> |实现| PI
    P4 -.-> |实现| PI

IChatService 统一接口

所有模型提供商都实现统一的 IChatService 接口:

// packages/agent-sdk/src/services/ChatServiceInterface.ts

export interface IChatService {
  chat(messages: Message[], tools?: Tool[], signal?: AbortSignal): Promise<ChatResponse>;
  streamChat(messages: Message[], tools?: Tool[], signal?: AbortSignal): AsyncGenerator<StreamChunk>;
  getConfig(): ChatConfig;
  updateConfig(newConfig: Partial<ChatConfig>): void;
}

export interface ChatResponse {
  content: string;
  reasoningContent?: string;  // Thinking 模型的推理过程
  toolCalls?: ChatCompletionMessageToolCall[];
  usage?: UsageInfo;
}

export interface UsageInfo {
  promptTokens: number;
  completionTokens: number;
  totalTokens: number;
  reasoningTokens?: number;
  cacheCreationInputTokens?: number;  // Anthropic: 缓存创建
  cacheReadInputTokens?: number;      // Anthropic: 缓存读取
}

Provider 工厂

基于 Vercel AI SDK 的 Provider 工厂:

// packages/agent-sdk/src/services/VercelAIChatService.ts

private createModel(config: ChatConfig): LanguageModel {
  const { provider, apiKey, baseUrl, model } = config;

  switch (provider) {
    case 'anthropic':
      return createAnthropic({ apiKey, baseURL: baseUrl })(model);
    case 'gemini':
      return createGoogleGenerativeAI({ apiKey, baseURL: baseUrl })(model);
    case 'deepseek':
      return createDeepSeek({ apiKey, baseURL: baseUrl })(model);
    default:
      // OpenAI 兼容接口(覆盖 100+ 模型)
      return createOpenAICompatible({ name: 'custom', apiKey, baseURL: baseUrl })(model);
  }
}

模型管理

ModelManager

// packages/agent-sdk/src/agent/ModelManager.ts

export class ModelManager {
  private chatService!: IChatService;
  private currentModelId?: string;

  async initialize(modelId?: string): Promise<IChatService> {
    const modelConfig = this.resolveModelConfig(modelId);
    await this.applyModelConfig(modelConfig);
    return this.chatService;
  }

  async switchModelIfNeeded(modelId: string): Promise<boolean> {
    if (modelId === this.currentModelId) return false;
    
    const modelConfig = this.config.models?.find((m) => m.id === modelId);
    if (!modelConfig) return false;

    await this.applyModelConfig(modelConfig);
    return true;
  }
}

运行时切换

# 启动时指定模型
blade --model=claude-opus-4 "优化代码"

# 运行时切换
> /model claude-sonnet-4
✅ 已切换到 claude-sonnet-4

# 仅当前轮使用指定模型
> /model once deepseek-chat 总结这段代码

成本优化

Token 追踪

Blade 实时追踪每次请求的 Token 使用:

// packages/agent-sdk/src/services/ChatServiceInterface.ts

export interface UsageInfo {
  promptTokens: number;
  completionTokens: number;
  totalTokens: number;
  reasoningTokens?: number;
  cacheCreationInputTokens?: number;
  cacheReadInputTokens?: number;
}

模型成本对比

模型 输入 ($/M) 输出 ($/M) 特点
gpt-4.5 $15.00 $75.00 旗舰
gpt-4o $2.50 $10.00 均衡
gpt-4o-mini $0.15 $0.60 轻量
claude-opus-4 $15.00 $75.00 代码最强
claude-sonnet-4 $3.00 $15.00 性价比
deepseek-chat $0.28 $1.10 性价比王
deepseek-reasoner $0.55 $2.19 推理
groq-llama-4 $0.20 $0.20 高速

Prompt Caching

Anthropic/DeepSeek 支持 prompt caching,缓存命中时成本降低 90%:

提供商 正常输入 缓存命中 节省
Anthropic $5/M $0.50/M 90%
OpenAI $1.75/M $0.175/M 90%
DeepSeek $0.28/M $0.028/M 90%

核心设计原则

  1. 抽象优于具体 — Vercel AI SDK 隔离具体实现
  2. 组合优于继承 — 通过组合不同 Provider 实现多模型支持
  3. 配置优于硬编码 — 模型选择、降级策略都可配置
  4. 监控优于盲目 — 实时追踪 Token 使用和成本

参考资源


本文由 青雲 (echoVic) 撰写,基于 blade-code 的实践经验。 如有问题或建议,欢迎在 GitHub Issues 讨论。

AI Agent 安全权限设计:blade-code 的 5 种权限模式与三级控制

作者 echoVic
2026年2月8日 09:52

blade-code 技术深度系列第 2 篇。本文基于源码剖析 AI Agent 的权限设计——5 种权限模式、allow/ask/deny 三级控制、基于签名的精确匹配。

问题

把 AI Agent 接入开发环境,第一个问题不是"它能做什么",而是"它不能做什么"。

场景:

  • Agent 想执行 rm -rf /
  • Agent 想读取 .env 里的密钥
  • Agent 想 curl 下载脚本并执行
  • Agent 想 sudo 提权

你会让它直接跑吗?

blade-code 从设计之初就在解决这个问题:赋予 Agent 能力的同时,保证安全

本文内容:

  • 工具分类(ReadOnly / Write / Execute)
  • 5 种权限模式
  • allow/ask/deny 三级控制
  • 基于签名的精确匹配

一、工具分类:三种 ToolKind

blade-code 把所有工具分成三类,这是权限控制的基础:

export enum ToolKind {
  ReadOnly = 'readonly',  // 只读,无副作用
  Write = 'write',        // 文件写入
  Execute = 'execute',    // 命令执行,可能有副作用
}

ReadOnly 工具

只读操作,无副作用,最安全:

工具 功能
Read 读取文件
Glob 路径匹配
Grep 文本搜索
WebFetch 获取网页
WebSearch 网络搜索
TaskOutput 子任务输出
Plan 生成计划

Write 工具

文件写入,有副作用但可控:

工具 功能
Edit 编辑文件
Write 写入文件
NotebookEdit 编辑 Notebook

Execute 工具

命令执行,副作用不可预测:

工具 功能
Bash Shell 命令
Task 子任务
Skill 调用技能
SlashCommand 斜杠命令

二、5 种权限模式

export enum PermissionMode {
  DEFAULT = 'default',
  AUTO_EDIT = 'autoEdit',
  YOLO = 'yolo',
  PLAN = 'plan',
  SPEC = 'spec',
}

DEFAULT(默认)

平衡安全与效率:

  • ✅ 自动批准:ReadOnly 工具
  • ❌ 需要确认:Write 工具
  • ❌ 需要确认:Execute 工具
blade "帮我分析这个项目"

AUTO_EDIT

频繁编码场景:

  • ✅ 自动批准:ReadOnly 工具
  • ✅ 自动批准:Write 工具
  • ❌ 需要确认:Execute 工具
blade --mode=autoEdit "重构这个模块"

日常开发中,文件编辑最频繁。AUTO_EDIT 让 Agent 自由改代码,但执行命令仍需确认。

YOLO(危险)

完全信任 AI:

  • ✅ 自动批准:所有工具
  • ⚠️ 跳过所有确认
blade --mode=yolo "自动修复所有 lint 错误"

适用场景:沙箱环境、演示、已验证安全的自动化脚本。

源码实现:

if (permissionMode === PermissionMode.YOLO) {
  return {
    result: PermissionResult.ALLOW,
    matchedRule: 'mode:yolo',
    reason: 'YOLO mode: automatically approve all tool invocations',
  };
}

PLAN

只读模式,用于调研:

  • ✅ 自动批准:ReadOnly 工具
  • ❌ 拦截:Write 和 Execute 工具
  • 🔵 特殊工具:ExitPlanMode(提交方案)
blade --mode=plan "分析这个项目的架构"

适用场景:代码审查、架构分析、生成方案后用户批准再执行。

源码实现:

if (permissionMode === PermissionMode.PLAN) {
  if (!isReadOnlyKind(toolKind)) {
    return {
      result: PermissionResult.DENY,
      matchedRule: 'mode:plan',
      reason: 'Plan mode: modification tools are blocked',
    };
  }
}

SPEC(Spec-Driven Development)

结构化功能开发:

  • ✅ 自动批准:ReadOnly + Spec 专用工具
  • ❌ 需要确认:其他 Write 和 Execute 工具
  • 🔵 特殊工具:InitSpec, UpdateSpec, ValidateSpec, GetSpecContext, ExitSpecMode
  • 📁 持久化:.blade/specs/<feature>/
blade --mode=spec "实现用户认证功能"

适用场景:复杂功能开发,需要 Requirements → Design → Tasks → Implementation 工作流。

模式对比

模式 ReadOnly Write Execute 场景
DEFAULT ✅ 自动 ❌ 确认 ❌ 确认 日常开发
AUTO_EDIT ✅ 自动 ✅ 自动 ❌ 确认 频繁编码
YOLO ✅ 自动 ✅ 自动 ✅ 自动 沙箱/演示
PLAN ✅ 自动 ❌ 拦截 ❌ 拦截 调研/审查
SPEC ✅ 自动 ❌ 确认 ❌ 确认 复杂功能

三、三级权限控制:allow / ask / deny

权限模式之外,blade-code 还有更细粒度的控制:

export interface PermissionConfig {
  allow: string[];  // 自动批准
  ask: string[];    // 需要确认
  deny: string[];   // 直接拒绝
}

优先级

deny > allow > ask > 默认(ask)

// 1. 检查 deny(最高优先级)
const denyMatch = this.matchRules(signature, this.config.deny);
if (denyMatch) {
  return { result: PermissionResult.DENY, ... };
}

// 2. 检查 allow
const allowMatch = this.matchRules(signature, this.config.allow);
if (allowMatch) {
  return { result: PermissionResult.ALLOW, ... };
}

// 3. 检查 ask
const askMatch = this.matchRules(signature, this.config.ask);
if (askMatch) {
  return { result: PermissionResult.ASK, ... };
}

// 4. 默认:需要确认
return { result: PermissionResult.ASK, ... };

默认配置

blade-code 内置了一套安全配置:

allow 列表(自动批准):

allow: [
  // 系统信息命令
  'Bash(pwd)', 'Bash(which *)', 'Bash(whoami)',
  'Bash(hostname)', 'Bash(uname *)', 'Bash(date)', 'Bash(echo *)',

  // 目录列表
  'Bash(ls *)', 'Bash(tree *)',

  // Git 只读
  'Bash(git status)', 'Bash(git log *)', 'Bash(git diff *)',
  'Bash(git branch *)', 'Bash(git show *)', 'Bash(git remote *)',

  // 包管理器只读
  'Bash(npm list *)', 'Bash(npm view *)', 'Bash(npm outdated *)',
  'Bash(pnpm list *)', 'Bash(yarn list *)',
  'Bash(pip list *)', 'Bash(pip show *)',
]

ask 列表(需要确认):

ask: [
  // 网络下载(可能下载恶意代码)
  'Bash(curl *)', 'Bash(wget *)', 'Bash(aria2c *)', 'Bash(axel *)',

  // 危险删除
  'Bash(rm -rf *)', 'Bash(rm -r *)', 'Bash(rm --recursive *)',

  // 网络连接
  'Bash(nc *)', 'Bash(netcat *)', 'Bash(telnet *)', 'Bash(ncat *)',
]

deny 列表(直接拒绝):

deny: [
  // 敏感文件
  'Read(./.env)', 'Read(./.env.*)',

  // 危险命令
  'Bash(rm -rf /)', 'Bash(rm -rf /*)', 'Bash(sudo *)', 'Bash(chmod 777 *)',

  // Shell 嵌套(可绕过安全检测)
  'Bash(bash *)', 'Bash(sh *)', 'Bash(zsh *)', 'Bash(fish *)', 'Bash(dash *)',

  // 代码注入
  'Bash(eval *)', 'Bash(source *)',

  // 系统级操作
  'Bash(mkfs *)', 'Bash(fdisk *)', 'Bash(dd *)', 'Bash(format *)', 'Bash(parted *)',

  // 浏览器(可打开恶意链接)
  'Bash(open http*)', 'Bash(open https*)',
  'Bash(xdg-open http*)', 'Bash(xdg-open https*)',
]

设计原则

allow:只读命令无副作用,可以自动批准。pwdlsgit status 不会改变任何东西。

askcurlwget 可能下载恶意代码,rm -rf 可能删数据。需要确认,但不完全禁止。

deny.env 包含密钥,sudo 风险太高,Shell 嵌套可能绕过检测,mkfsdd 可能造成不可逆损害。


四、基于签名的精确匹配

blade-code 的权限系统支持多种匹配模式。

签名格式

ToolName(content)

例如:

  • Bash(git status) — 执行 git status
  • Read(src/index.ts) — 读取文件
  • Edit(src/utils.ts) — 编辑文件

匹配模式

  1. 精确匹配Read(src/index.ts)
  2. 前缀匹配Read(匹配所有 Read 调用)
  3. 通配符匹配Read(*)Bash(git *)
  4. Glob 模式Read(**/*.env)
规则 匹配 不匹配
Bash(git status) Bash(git status) Bash(git log)
Bash(git *) Bash(git status), Bash(git log) Bash(npm install)
Bash 所有 Bash 命令 Read(...)
Read(*.env) Read(.env), Read(.env.local) Read(config.json)
Read(**/*.ts) Read(src/index.ts) Read(package.json)

实现

blade-code 用 picomatch 库做 glob 匹配:

private matchRule(signature: string, rule: string): MatchType | null {
  // 精确匹配
  if (signature === rule) return 'exact';

  // 通配符匹配所有
  if (rule === '*' || rule === '**') return 'wildcard';

  // 工具名 glob 匹配
  if (ruleToolName.includes('*')) {
    if (!picomatch.isMatch(sigToolName, ruleToolName, { dot: true, bash: true })) {
      return null;
    }
  }

  // 参数 glob 匹配
  if (rule.includes('*')) {
    const isMatch = picomatch.isMatch(sigValue, ruleValue, { dot: true, bash: true });
    if (isMatch) return ruleValue.includes('**') ? 'glob' : 'wildcard';
  }

  return null;
}

五、权限执行管道

blade-code 的权限检查在 PipelineStages 中实现。

优先级

YOLO 模式 > PLAN 模式 > DENY 规则 > ALLOW 规则 > 模式规则 > ASK

private applyModeOverrides(
  toolKind: ToolKind,
  checkResult: PermissionCheckResult,
  permissionMode: PermissionMode
): PermissionCheckResult {
  // 1. YOLO:全部放开
  if (permissionMode === PermissionMode.YOLO) {
    return { result: PermissionResult.ALLOW, ... };
  }

  // 2. PLAN:拒绝非只读
  if (permissionMode === PermissionMode.PLAN) {
    if (!isReadOnlyKind(toolKind)) {
      return { result: PermissionResult.DENY, ... };
    }
  }

  // 3. deny 规则已拒绝,不覆盖
  if (checkResult.result === PermissionResult.DENY) return checkResult;

  // 4. allow 规则已批准,不覆盖
  if (checkResult.result === PermissionResult.ALLOW) return checkResult;

  // 5. 只读工具:自动批准
  if (isReadOnlyKind(toolKind)) {
    return { result: PermissionResult.ALLOW, ... };
  }

  // 6. AUTO_EDIT + Write:自动批准
  if (permissionMode === PermissionMode.AUTO_EDIT && toolKind === ToolKind.Write) {
    return { result: PermissionResult.ALLOW, ... };
  }

  // 7. 其他:保持原结果(通常是 ASK)
  return checkResult;
}

流程图

flowchart TD
    A[工具调用请求] --> B{YOLO 模式?}
    B -->|是| C[✅ 直接批准]
    B -->|否| D{PLAN 模式?}
    D -->|是| E{只读工具?}
    E -->|否| F[❌ 直接拒绝]
    E -->|是| G[✅ 批准]
    D -->|否| H{匹配 deny 规则?}
    H -->|是| F
    H -->|否| I{匹配 allow 规则?}
    I -->|是| C
    I -->|否| J{只读工具?}
    J -->|是| C
    J -->|否| K{AUTO_EDIT + Write?}
    K -->|是| C
    K -->|否| L[⚠️ 请求用户确认]

六、实战配置

项目级配置

在项目根目录创建 .blade/settings.json

{
  "permissionMode": "default",
  "permissions": {
    "allow": [
      "Bash(npm run *)",
      "Bash(pnpm *)",
      "Bash(git commit *)",
      "Bash(git push *)"
    ],
    "ask": [],
    "deny": [
      "Read(config/secrets.json)",
      "Bash(rm -rf node_modules)"
    ]
  }
}

命令行切换

# 启动时指定
blade --mode=autoEdit "重构这个模块"
blade --mode=plan "分析项目架构"
blade --mode=yolo "自动修复所有问题"

# 运行时切换
> /mode autoEdit
> /mode plan
> /mode default

场景推荐

场景 模式 原因
日常开发 DEFAULT 平衡安全与效率
频繁编码 AUTO_EDIT 减少文件编辑确认
代码审查 PLAN 只读不写
自动化脚本 YOLO 无需人工干预(确保安全)
复杂功能 SPEC 结构化工作流

总结

  1. 工具分类:ReadOnly / Write / Execute
  2. 5 种权限模式:DEFAULT / AUTO_EDIT / YOLO / PLAN / SPEC
  3. 三级权限控制:deny > allow > ask
  4. 精确匹配:精确、前缀、通配符、glob

设计原则:

  • 默认安全(DEFAULT 模式)
  • 灵活可控(用户可切换)
  • 细粒度(精确到命令级别)
  • 可扩展(项目级配置覆盖全局)

参考

昨天以前首页

从零实现 MCP 客户端:blade-code 的 MCP 集成实战

作者 echoVic
2026年2月7日 17:57

本文是 blade-code 技术深度系列的第 1 篇,深入剖析如何从零实现一个生产级的 MCP(Model Context Protocol)客户端,包括连接管理、OAuth 认证、健康监控、工具注册等核心功能。

目录


什么是 MCP?

MCP(Model Context Protocol)是 Anthropic 推出的开放协议,用于 AI 应用与外部工具/数据源的标准化通信。它解决了以下问题:

  • 工具碎片化:每个 AI 应用都要重新实现工具集成
  • 协议不统一:没有标准的工具调用格式
  • 扩展性差:添加新工具需要修改核心代码

MCP 提供了:

  • 📡 标准传输层:stdio、SSE、HTTP
  • 🔧 工具发现机制:动态获取可用工具列表
  • 🔐 认证支持:OAuth 2.0 集成
  • 📊 资源管理:统一的资源读取接口

架构设计

blade-code 的 MCP 集成采用三层架构:

graph TB
    A[Agent Runtime 调用层] --> B[McpRegistry 管理层]
    B --> C[McpClient 通信层]
    
    A1[工具调用<br/>参数验证] -.-> A
    B1[服务器注册/注销<br/>工具冲突处理<br/>状态监控] -.-> B
    C1[连接管理<br/>协议通信<br/>错误重试<br/>OAuth 认证] -.-> C
    
    style A fill:#e1f5ff
    style B fill:#fff4e1
    style C fill:#ffe1f5

设计原则:

  1. 单一职责:每层只负责自己的核心功能
  2. 事件驱动:通过 EventEmitter 解耦组件
  3. 容错优先:网络错误自动重试,永久错误快速失败
  4. 可观测性:完整的状态机和事件日志

核心实现

1. McpClient:连接与通信

McpClient 是 MCP 集成的核心,负责与单个 MCP 服务器的通信。

1.1 传输层抽象

MCP 支持三种传输方式,blade-code 通过工厂模式统一创建:

private async createTransport(): Promise<Transport> {
  const { type, command, args, env, url, headers } = this.config;

  if (type === 'stdio') {
    // 子进程通信(本地工具)
    return new StdioClientTransport({
      command,
      args: args || [],
      env: { ...process.env, ...env },
      stderr: 'ignore', // 忽略子进程的 stderr 输出
    });
  } else if (type === 'sse') {
    // Server-Sent Events(远程服务)
    return new SSEClientTransport(new URL(url), {
      requestInit: { headers },
    });
  } else if (type === 'http') {
    // HTTP 长轮询
    const { StreamableHTTPClientTransport } = await import(
      '@modelcontextprotocol/sdk/client/streamableHttp.js'
    );
    return new StreamableHTTPClientTransport(new URL(url), {
      requestInit: { headers },
    });
  }

  throw new Error(`不支持的传输类型: ${type}`);
}

关键点:

  • stdio 适合本地工具(如文件系统、数据库)
  • sse 适合远程服务(实时推送)
  • http 适合 RESTful API

1.2 连接管理与重试

生产环境中,网络不稳定是常态。blade-code 实现了智能重试机制:

async connectWithRetry(maxRetries = 3, initialDelay = 1000): Promise<void> {
  let lastError: Error | null = null;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      await this.doConnect();
      this.reconnectAttempts = 0; // 重置重连计数
      return; // 成功连接
    } catch (error) {
      lastError = error as Error;
      const classified = classifyError(error);

      // 如果是永久性错误,不重试
      if (!classified.isRetryable) {
        console.error('[McpClient] 检测到永久性错误,放弃重试:', classified.type);
        throw error;
      }

      // 指数退避
      if (attempt < maxRetries) {
        const delay = initialDelay * Math.pow(2, attempt - 1);
        console.warn(`[McpClient] 连接失败(${attempt}/${maxRetries}),${delay}ms 后重试...`);
        await new Promise((resolve) => setTimeout(resolve, delay));
      }
    }
  }

  throw lastError || new Error('连接失败');
}

错误分类:

enum ErrorType {
  NETWORK_TEMPORARY = 'network_temporary', // 临时网络错误(可重试)
  NETWORK_PERMANENT = 'network_permanent', // 永久网络错误
  CONFIG_ERROR = 'config_error',           // 配置错误
  AUTH_ERROR = 'auth_error',               // 认证错误
  PROTOCOL_ERROR = 'protocol_error',       // 协议错误
  UNKNOWN = 'unknown',                     // 未知错误
}

function classifyError(error: unknown): ClassifiedError {
  const msg = error.message.toLowerCase();

  // 永久性配置错误(不应重试)
  const permanentErrors = [
    'command not found',
    'no such file',
    'permission denied',
    'invalid configuration',
  ];

  if (permanentErrors.some((permanent) => msg.includes(permanent))) {
    return { type: ErrorType.CONFIG_ERROR, isRetryable: false, originalError: error };
  }

  // 临时网络错误(可重试)
  const temporaryErrors = [
    'timeout',
    'connection refused',
    'econnreset',
    'etimedout',
    '503',
    '429',
  ];

  if (temporaryErrors.some((temporary) => msg.includes(temporary))) {
    return { type: ErrorType.NETWORK_TEMPORARY, isRetryable: true, originalError: error };
  }

  // 默认视为临时错误(保守策略:允许重试)
  return { type: ErrorType.UNKNOWN, isRetryable: true, originalError: error };
}

为什么这样设计?

  • 快速失败:配置错误立即抛出,避免无意义的重试
  • 指数退避:避免雪崩效应,给服务器恢复时间
  • 保守策略:未知错误默认可重试,提高容错性

1.3 意外断连处理

MCP 服务器可能随时断开(进程崩溃、网络中断),blade-code 通过监听 onclose 事件自动重连:

this.sdkClient.onclose = () => {
  this.handleUnexpectedClose();
};

private handleUnexpectedClose(): void {
  if (this.isManualDisconnect) {
    return; // 手动断开,不重连
  }

  if (this.status === McpConnectionStatus.CONNECTED) {
    console.warn('[McpClient] 检测到意外断连,准备重连...');
    this.setStatus(McpConnectionStatus.ERROR);
    this.emit('error', new Error('MCP服务器连接意外关闭'));
    this.scheduleReconnect();
  }
}

private scheduleReconnect(): void {
  if (this.reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) {
    console.error('[McpClient] 达到最大重连次数,放弃重连');
    this.emit('reconnectFailed');
    return;
  }

  // 指数退避:1s, 2s, 4s, 8s, 16s(最大30s)
  const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
  this.reconnectAttempts++;

  this.reconnectTimer = setTimeout(async () => {
    try {
      await this.doConnect();
      console.log('[McpClient] 重连成功');
      this.reconnectAttempts = 0;
      this.emit('reconnected');
    } catch (error) {
      const classified = classifyError(error);
      if (classified.isRetryable) {
        this.scheduleReconnect(); // 继续重连
      } else {
        this.emit('reconnectFailed'); // 永久失败
      }
    }
  }, delay);
}

关键点:

  • 区分手动断开和意外断连
  • 最多重连 5 次,避免无限循环
  • 重连成功后重置计数器

2. McpRegistry:服务器管理

McpRegistry 是单例模式的注册中心,管理多个 MCP 服务器。

2.1 服务器注册

async registerServer(name: string, config: McpServerConfig): Promise<void> {
  if (this.servers.has(name)) {
    throw new Error(`MCP服务器 "${name}" 已经注册`);
  }

  const client = new McpClient(config, name, config.healthCheck);
  const serverInfo: McpServerInfo = {
    config,
    client,
    status: McpConnectionStatus.DISCONNECTED,
    tools: [],
  };

  // 设置客户端事件处理器
  this.setupClientEventHandlers(client, serverInfo, name);

  this.servers.set(name, serverInfo);
  this.emit('serverRegistered', name, serverInfo);

  try {
    await this.connectServer(name);
  } catch (error) {
    console.warn(`MCP服务器 "${name}" 连接失败:`, error);
  }
}

2.2 工具冲突处理

多个 MCP 服务器可能提供同名工具,blade-code 通过前缀解决冲突:

async getAvailableTools(): Promise<Tool[]> {
  const tools: Tool[] = [];
  const nameConflicts = new Map<string, number>();

  // 第一遍:检测冲突
  for (const [_serverName, serverInfo] of this.servers) {
    if (serverInfo.status === McpConnectionStatus.CONNECTED) {
      for (const mcpTool of serverInfo.tools) {
        const count = nameConflicts.get(mcpTool.name) || 0;
        nameConflicts.set(mcpTool.name, count + 1);
      }
    }
  }

  // 第二遍:创建工具(冲突时添加前缀)
  for (const [serverName, serverInfo] of this.servers) {
    if (serverInfo.status === McpConnectionStatus.CONNECTED) {
      for (const mcpTool of serverInfo.tools) {
        const hasConflict = (nameConflicts.get(mcpTool.name) || 0) > 1;
        const toolName = hasConflict
          ? `${serverName}__${mcpTool.name}`
          : mcpTool.name;

        const tool = createMcpTool(serverInfo.client, serverName, mcpTool, toolName);
        tools.push(tool);
      }
    }
  }

  return tools;
}

命名策略:

  • 无冲突:toolName
  • 有冲突:serverName__toolName

示例:

服务器 A: read_file
服务器 B: read_file
 最终工具: A__read_file, B__read_file

3. OAuth 认证

MCP 支持 OAuth 2.0 认证,blade-code 实现了完整的 OAuth 流程。

3.1 令牌存储

export class OAuthTokenStorage {
  private readonly tokenFilePath: string;

  constructor() {
    const homeDir = os.homedir();
    const configDir = path.join(homeDir, '.blade');
    this.tokenFilePath = path.join(configDir, 'mcp-oauth-tokens.json');
  }

  async saveToken(
    serverName: string,
    token: OAuthToken,
    clientId?: string,
    tokenUrl?: string
  ): Promise<void> {
    const credentials = await this.loadAllCredentials();

    const credential: OAuthCredentials = {
      serverName,
      token,
      clientId,
      tokenUrl,
      updatedAt: Date.now(),
    };

    credentials.set(serverName, credential);
    await this.saveAllCredentials(credentials);
  }

  isTokenExpired(token: OAuthToken): boolean {
    if (!token.expiresAt) {
      return false; // 没有过期时间,认为不过期
    }

    // 提前 5 分钟视为过期,留出刷新时间
    const buffer = 5 * 60 * 1000;
    return Date.now() >= token.expiresAt - buffer;
  }
}

安全措施:

  • 令牌文件权限设置为 0o600(仅所有者可读写)
  • 提前 5 分钟刷新令牌,避免过期
  • 支持 refresh_token 自动续期

3.2 OAuth 流程

export class OAuthProvider {
  private tokenStorage = new OAuthTokenStorage();

  async getValidToken(
    serverName: string,
    oauthConfig: OAuthConfig
  ): Promise<string | null> {
    const credentials = await this.tokenStorage.getCredentials(serverName);

    if (!credentials) {
      return null; // 没有令牌,需要认证
    }

    // 检查是否过期
    if (this.tokenStorage.isTokenExpired(credentials.token)) {
      // 尝试刷新
      if (credentials.token.refreshToken) {
        try {
          const newToken = await this.refreshToken(credentials, oauthConfig);
          await this.tokenStorage.saveToken(
            serverName,
            newToken,
            credentials.clientId,
            credentials.tokenUrl
          );
          return newToken.accessToken;
        } catch (error) {
          console.error('[OAuthProvider] 刷新令牌失败:', error);
          return null; // 刷新失败,需要重新认证
        }
      }
      return null; // 没有 refresh_token,需要重新认证
    }

    return credentials.token.accessToken;
  }

  async authenticate(
    serverName: string,
    oauthConfig: OAuthConfig
  ): Promise<OAuthToken> {
    // 1. 生成授权 URL
    const authUrl = this.buildAuthUrl(oauthConfig);
    console.log(`请访问以下 URL 进行授权:\n${authUrl}`);

    // 2. 启动本地回调服务器
    const code = await this.startCallbackServer(oauthConfig.redirectUri);

    // 3. 用授权码换取令牌
    const token = await this.exchangeCodeForToken(code, oauthConfig);

    // 4. 保存令牌
    await this.tokenStorage.saveToken(
      serverName,
      token,
      oauthConfig.clientId,
      oauthConfig.tokenUrl
    );

    return token;
  }
}

流程图:

graph TD
    A[用户请求] --> B{检查令牌}
    B -->|有效| C[返回令牌]
    B -->|无效/过期| D{有 refresh_token?}
    D -->|是| E[刷新令牌]
    E --> F[返回新令牌]
    D -->|否| G[启动 OAuth 流程]
    G --> H[返回新令牌]
    
    style C fill:#90EE90
    style F fill:#90EE90
    style H fill:#90EE90

4. 健康监控

生产环境中,MCP 服务器可能"僵死"(连接正常但不响应)。blade-code 实现了主动健康检查:

export class HealthMonitor extends EventEmitter {
  private intervalTimer: NodeJS.Timeout | null = null;
  private consecutiveFailures = 0;

  constructor(
    private client: McpClient,
    private config: HealthCheckConfig
  ) {
    super();
  }

  start(): void {
    if (this.intervalTimer) {
      return; // 已经启动
    }

    this.intervalTimer = setInterval(async () => {
      try {
        await this.performHealthCheck();
        this.consecutiveFailures = 0; // 重置失败计数
      } catch (error) {
        this.consecutiveFailures++;
        console.warn(
          `[HealthMonitor] 健康检查失败 (${this.consecutiveFailures}/${this.config.maxFailures}):`,
          error
        );

        if (this.consecutiveFailures >= this.config.maxFailures) {
          this.emit('unhealthy', this.consecutiveFailures, error);
          await this.attemptReconnect();
        }
      }
    }, this.config.intervalMs);
  }

  private async performHealthCheck(): Promise<void> {
    const timeout = this.config.timeoutMs || 5000;

    await Promise.race([
      this.client.listTools(), // 调用一个轻量级方法
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error('健康检查超时')), timeout)
      ),
    ]);
  }

  private async attemptReconnect(): Promise<void> {
    console.log('[HealthMonitor] 尝试重连...');
    try {
      await this.client.disconnect();
      await this.client.connect();
      this.consecutiveFailures = 0;
      this.emit('reconnected');
    } catch (error) {
      console.error('[HealthMonitor] 重连失败:', error);
    }
  }
}

配置示例:

{
  enabled: true,
  intervalMs: 30000,    // 每 30 秒检查一次
  timeoutMs: 5000,      // 超时时间 5 秒
  maxFailures: 3        // 连续失败 3 次触发重连
}

工具动态注册

MCP 工具需要转换为 blade-code 的 Tool 接口:

export function createMcpTool(
  client: McpClient,
  serverName: string,
  mcpTool: McpToolDefinition,
  toolName?: string
): Tool {
  return {
    name: toolName || mcpTool.name,
    description: mcpTool.description || `MCP工具: ${mcpTool.name}`,
    parameters: mcpTool.inputSchema || { type: 'object', properties: {} },
    metadata: {
      source: 'mcp',
      serverName,
      originalName: mcpTool.name,
    },

    async execute(args: Record<string, unknown>): Promise<ToolResult> {
      try {
        const response = await client.callTool(mcpTool.name, args);

        return {
          success: true,
          output: formatMcpResponse(response),
          metadata: {
            serverName,
            toolName: mcpTool.name,
          },
        };
      } catch (error) {
        return {
          success: false,
          error: error instanceof Error ? error.message : String(error),
          metadata: {
            serverName,
            toolName: mcpTool.name,
          },
        };
      }
    },
  };
}

关键点:

  • 保留原始工具名(originalName)用于调试
  • 统一错误处理格式
  • 支持元数据传递

错误处理与重连

blade-code 的错误处理遵循以下原则:

1. 错误分类

临时错误(可重试):
- 网络超时
- 连接被拒绝
- 速率限制(429)
- 服务不可用(503)

永久错误(不重试):
- 配置错误(命令不存在)
- 认证失败(401)
- 权限不足(403)
- 协议错误(格式错误)

2. 重试策略

指数退避:
- 第 1 次:1 秒后重试
- 第 2 次:2 秒后重试
- 第 3 次:4 秒后重试
- 第 4 次:8 秒后重试
- 第 5 次:16 秒后重试
- 最大延迟:30

3. 状态机

stateDiagram-v2
    [*] --> DISCONNECTED
    DISCONNECTED --> CONNECTING: connect()
    CONNECTING --> CONNECTED: 成功
    CONNECTING --> ERROR: 失败
    ERROR --> CONNECTING: 重试
    CONNECTED --> ERROR: 意外断连
    CONNECTED --> DISCONNECTED: disconnect()

实战案例

案例 1:集成文件系统 MCP 服务器

// 配置文件
{
  "mcpServers": {
    "filesystem": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/workspace"],
      "env": {
        "NODE_ENV": "production"
      }
    }
  }
}

// 使用
const registry = McpRegistry.getInstance();
await registry.registerServer('filesystem', config.mcpServers.filesystem);

const tools = await registry.getAvailableTools();
// 输出: [{ name: 'read_file', ... }, { name: 'write_file', ... }]

案例 2:集成远程 API(带 OAuth)

{
  "mcpServers": {
    "github": {
      "type": "sse",
      "url": "https://api.github.com/mcp",
      "oauth": {
        "enabled": true,
        "authUrl": "https://github.com/login/oauth/authorize",
        "tokenUrl": "https://github.com/login/oauth/access_token",
        "clientId": "your-client-id",
        "clientSecret": "your-client-secret",
        "scopes": ["repo", "user"],
        "redirectUri": "http://localhost:3000/callback"
      }
    }
  }
}

// 首次使用会自动触发 OAuth 流程
await registry.registerServer('github', config.mcpServers.github);
// 输出: 请访问以下 URL 进行授权: https://github.com/login/oauth/authorize?...

案例 3:健康监控与自动恢复

{
  "mcpServers": {
    "database": {
      "type": "stdio",
      "command": "mcp-database-server",
      "healthCheck": {
        "enabled": true,
        "intervalMs": 30000,
        "timeoutMs": 5000,
        "maxFailures": 3
      }
    }
  }
}

// 监听健康事件
registry.on('serverError', (name, error) => {
  console.error(`服务器 ${name} 出错:`, error);
});

registry.on('healthMonitorReconnected', () => {
  console.log('健康监控触发重连成功');
});

总结

blade-code 的 MCP 集成实现了以下核心功能:

已实现

  1. 多传输层支持:stdio、SSE、HTTP
  2. 智能重试:错误分类 + 指数退避
  3. 自动重连:意外断连自动恢复
  4. OAuth 认证:完整的 OAuth 2.0 流程
  5. 健康监控:主动检测僵死连接
  6. 工具冲突处理:自动添加前缀
  7. 事件驱动:完整的状态机和事件系统

设计亮点

  • 容错优先:网络错误不会导致整个系统崩溃
  • 可观测性:丰富的日志和事件,便于调试
  • 扩展性:新增 MCP 服务器只需修改配置文件
  • 安全性:令牌加密存储,权限最小化

相关资源:

讨论:欢迎在 GitHub Issues 或我的博客评论区交流!

Cursor 500MB 太重?试试这个 5MB 的 CLI 方案

作者 echoVic
2026年2月7日 11:55

Cursor 500MB 太重?试试这个 5MB 的 CLI 方案

为什么我放弃了 Cursor

上个月团队让我试用 Cursor。下载完 500MB 安装包后,我开始怀疑人生。

启动要 10 秒,打开大项目要 30 秒,内存占用 2GB+。我只是想让 AI 帮我写个脚本,为什么要装个这么重的 IDE?

后来发现了 Blade Code。

Blade Code 是什么

一个 5MB 的 Node.js CLI 工具,专门做一件事:让 AI 快速完成编程任务。

不是 IDE,不是编辑器,就是个命令行工具。

对比数据

维度 Cursor Blade Code
安装包大小 500MB 5MB (npm 包)
启动速度 10秒 1秒
内存占用 2GB+ 50MB
适用场景 完整开发环境 快速任务、脚本、自动化
学习成本 需要适应新 IDE 会用命令行就行
价格 $20/月 MIT 开源

真实场景

场景 1:快速重构代码

blade "把这个文件的所有 var 改成 let/const"

3 秒完成,不用打开 IDE。

场景 2:批量处理文件

blade "把 src/ 下所有 .js 文件加上 'use strict'"

20 个文件,5 秒搞定。

场景 3:生成测试用例

blade "给 utils.ts 生成单元测试"

自动分析代码,生成完整测试文件。

为什么这么快

  1. 无 GUI 开销 - 纯命令行,没有渲染负担
  2. 按需加载 - 只加载需要的工具
  3. 流式响应 - 边生成边输出,不等全部完成
  4. 轻量设计 - 核心只有几 MB

20+ 内置工具

Blade Code 不只是个 AI 对话工具,它内置了 20+ 实用工具:

  • 文件操作:读、写、搜索、批量处理
  • 代码分析:AST 解析、依赖分析
  • Shell 执行:安全的命令执行
  • Git 集成:提交、分支、历史查询
  • Web 搜索:实时查询最新信息

安全设计

很多人担心 AI 工具会误删代码。Blade Code 有三层保护:

  1. 权限控制 - 危险操作需要确认
  2. 工具白名单 - 只能用预定义的工具
  3. 操作日志 - 所有操作可追溯

5 分钟上手

安装

npm install -g blade-code

配置 API Key

blade config

支持 OpenAI、Claude、Gemini、国产大模型。

开始使用

blade "帮我重构这个函数"

就这么简单。

适合谁用

适合:

  • 需要快速完成小任务的开发者
  • 喜欢命令行的极客
  • 想要轻量级 AI 工具的人
  • 需要脚本化 AI 能力的场景

不适合:

  • 需要完整 IDE 功能的人
  • 不习惯命令行的人
  • 需要图形界面的场景

和其他工具对比

vs Cursor

  • Cursor:完整 IDE,适合长时间开发
  • Blade Code:快速任务,适合脚本化场景

vs GitHub Copilot

  • Copilot:代码补全,需要在编辑器里用
  • Blade Code:独立工具,可以批量处理

vs OpenCode

  • OpenCode:95K stars,功能全面但复杂
  • Blade Code:专注 CLI,简单直接

开源 + 可扩展

Blade Code 是 MIT 开源的,代码在 GitHub: github.com/echoVic/bla…

支持 MCP (Model Context Protocol),可以自己写插件扩展功能。

总结

如果你觉得 Cursor 太重,需要快速完成小任务,喜欢命令行,想要免费的 AI 编程工具,试试 Blade Code。

5MB,1 秒启动,MIT 开源。

项目地址:github.com/echoVic/bla…

❌
❌