顶部广告
当前位置:首页 » OpenClaw小龙虾专区 » 错误处理与日志:让 Skill 更健壮

错误处理与日志:让 Skill 更健壮

   作者:mpoll.top   发布时间:2026-04-21   0 次浏览

文章广告

概述

在生产环境中,错误处理日志记录是 Skill 质量的决定性因素。一个没有良好错误处理的 Skill 可能在遇到问题时直接崩溃,给用户带来糟糕的体验。本文将深入讲解 OpenClaw Skill 的错误处理策略、日志记录和最佳实践。

---

为什么需要错误处理?

现实场景

场景:天气查询 Skill

正常情况:
用户:"北京天气"
→ 调用天气 API
→ 返回:北京,晴,25°C

异常情况:
用户:"北京天气"
→ 调用天气 API
→ ❌ 网络超时
→ ❌ API 返回错误
→ ❌ 城市名称无效
→ 用户看到:❌ 无法获取天气信息(网络异常)

错误类型

OpenClaw Skill 可能遇到的错误:

类型 | 示例 | 处理方式

|------|------|---------|

网络错误 | API 超时、连接失败 | 重试、降级
数据错误 | 无效输入、格式错误 | 验证、提示
权限错误 | 缺少 API 密钥、权限不足 | 配置检查
资源错误 | 文件不存在、磁盘满 | 预检查、清理
逻辑错误 | 业务规则违反 | 验证、回滚

错误处理策略

1. 防御性编程

在问题发生前预防:

async function execute(params, context) {
  // ✅ 预检查:参数验证
  if (!params.location) {
    throw new Error('缺少必要参数:location');
  }
  
  // ✅ 预检查:配置验证
  if (!context.config.apiKey) {
    throw new Error('未配置 API 密钥,请在 TOOLS.md 中添加');
  }
  
  // ✅ 预检查:资源验证
  if (params.file && !fs.existsSync(params.file)) {
    throw new Error(`文件不存在:${params.file}`);
  }
  
  // 主要逻辑...
}

2. Try-Catch 分层处理

不同层级处理不同类型的错误:

async function execute(params, context) {
  try {
    // 层级 1:业务逻辑
    const result = await fetchWeather(params.location);
    return formatResult(result);
    
  } catch (error) {
    // 层级 2:错误分类处理
    if (error.code === 'NETWORK_ERROR') {
      context.logger.warn('网络错误,尝试重试...');
      return await retryOperation(() => fetchWeather(params.location));
    }
    
    if (error.code === 'INVALID_LOCATION') {
      // 用户错误,友好提示
      return `❌ 找不到城市 "${params.location}",请检查城市名称`;
    }
    
    // 层级 3:未知错误,记录日志并返回通用错误
    context.logger.error('未知错误:', error);
    return '❌ 服务暂时不可用,请稍后重试';
  }
}

3. 错误码系统

定义清晰的错误码便于调试:

const ErrorCodes = {
  // 网络错误 1xxx
  NETWORK_TIMEOUT: 1001,
  NETWORK_UNREACHABLE: 1002,
  
  // 数据错误 2xxx
  INVALID_INPUT: 2001,
  MISSING_PARAMETER: 2002,
  
  // 配置错误 3xxx
  MISSING_CONFIG: 3001,
  INVALID_CONFIG: 3002,
  
  // 业务错误 4xxx
  RESOURCE_NOT_FOUND: 4001,
  PERMISSION_DENIED: 4002
};

class SkillError extends Error {
  constructor(code, message, details = {}) {
    super(message);
    this.code = code;
    this.details = details;
    this.timestamp = new Date().toISOString();
  }
}

// 使用示例
throw new SkillError(
  ErrorCodes.NETWORK_TIMEOUT,
  '天气 API 响应超时',
  { location: params.location, timeout: 5000 }
);

4. 重试机制

处理临时性故障:

async function retryOperation(operation, options = {}) {
  const {
    maxRetries = 3,
    delayMs = 1000,
    backoff = 2  // 指数退避
  } = options;
  
  let lastError;
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error;
      
      // 只重试临时性错误
      if (!isRetryableError(error)) {
        throw error;
      }
      
      context.logger.warn(\`重试 \${attempt}/\${maxRetries}: \${error.message}\`);
      
      // 等待后重试(指数退避)
      if (attempt < maxRetries) {
        await sleep(delayMs * Math.pow(backoff, attempt - 1));
      }
    }
  }
  
  throw lastError;
}

function isRetryableError(error) {
  // 只有网络错误和超时错误值得重试
  return [
    'NETWORK_TIMEOUT',
    'NETWORK_UNREACHABLE',
    'RATE_LIMITED'
  ].includes(error.code);
}

日志记录

日志级别

OpenClaw 支持标准日志级别:

// DEBUG: 详细调试信息
context.logger.debug('API 请求参数:', params);

// INFO: 正常操作信息
context.logger.info('天气查询成功:', result);

// WARN: 警告信息(不影响功能)
context.logger.warn('API 响应较慢:', { duration: 3500 });

// ERROR: 错误信息
context.logger.error('API 调用失败:', error);

日志最佳实践

#### ✅ 好的日志

// 包含上下文信息
context.logger.info('查询天气', {
  location: params.location,
  units: params.units || 'celsius',
  timestamp: new Date().toISOString()
});

// 结构化日志
context.logger.error('API 错误', {
  code: error.code,
  message: error.message,
  url: error.url,
  statusCode: error.statusCode,
  duration: error.duration
});

// 敏感信息脱敏
context.logger.debug('API 密钥已加载', {
  keyPrefix: context.config.apiKey?.substring(0, 4) + '*'
});

#### ❌ 差的日志

// 信息太少
context.logger.info('出错了');

// 泄露敏感信息
context.logger.debug('使用密钥:', context.config.apiKey);

// 日志过多造成噪音
for (let i = 0; i < 1000; i++) {
  context.logger.debug('处理中...');  // 太多了!
}

日志输出配置

在 Skill 中配置日志:

module.exports = {
  name: 'weather',
  description: '查询天气信息',
  
  // 日志配置
  logging: {
    level: 'info',  // debug, info, warn, error
    format: 'json', // json, text
    output: 'file'  // file, console, both
  },
  
  async execute(params, context) {
    // 使用 context.logger
  }
};

用户友好的错误消息

错误消息设计原则

  • 清晰:用户能看懂
  • 具体:说明什么问题
  • 可操作:告诉用户怎么做
  • 友好:不指责用户

示例对比

// ❌ 技术术语,用户看不懂
throw new Error('HTTP 500: Internal Server Error at /api/weather');

// ✅ 用户友好
return \`
❌ 天气服务暂时不可用

可能的原因:
  • 网络连接不稳定
  • 天气服务器维护中
建议操作:
  • 检查网络连接
  • 稍后重试
  • 如持续失败,联系管理员
\`;

错误消息模板

const ErrorMessages = {
  NETWORK_ERROR: \`
❌ 网络连接失败

无法连接到天气服务,请检查:
  • 网络连接是否正常
  • 防火墙设置
  • 代理配置
\`, INVALID_LOCATION: \` ❌ 找不到城市 "\${location}" 请检查:
  • 城市名称是否正确
  • 是否使用了标准城市名
  • 中文城市请用中文名称
\`, RATE_LIMITED: \` ⏱️ 请求过于频繁 请稍等 \${retryAfter}秒 后重试 提示:
  • 免费版本限制:\${limit}次/小时
  • 升级后可获得更高限额
\` };

完整示例

带错误处理的天气 Skill

const https = require('https');

const ErrorCodes = {
  NETWORK_TIMEOUT: 1001,
  NETWORK_ERROR: 1002,
  INVALID_LOCATION: 2001,
  API_ERROR: 3001
};

class SkillError extends Error {
  constructor(code, message, details = {}) {
    super(message);
    this.code = code;
    this.details = details;
  }
}

module.exports = {
  name: 'weather',
  description: '查询天气信息',
  
  parameters: {
    location: {
      type: 'string',
      description: '城市名称',
      required: true
    },
    days: {
      type: 'number',
      description: '预报天数',
      default: 3,
      minimum: 1,
      maximum: 7
    }
  },
  
  async execute(params, context) {
    const startTime = Date.now();
    
    try {
      // 1. 参数验证
      validateParams(params);
      
      // 2. 配置检查
      const apiKey = context.config?.weatherApiKey;
      if (!apiKey) {
        throw new SkillError(
          ErrorCodes.API_ERROR,
          '未配置天气 API 密钥',
          { hint: '请在 TOOLS.md 中添加 weatherApiKey' }
        );
      }
      
      // 3. 调用 API(带重试)
      context.logger.info('查询天气', { location: params.location });
      
      const weatherData = await retryOperation(
        () => fetchWeather(params.location, apiKey),
        { maxRetries: 3, delayMs: 1000 }
      );
      
      // 4. 数据验证
      if (!weatherData || !weatherData.current) {
        throw new SkillError(
          ErrorCodes.INVALID_LOCATION,
          \`找不到城市:\${params.location}\`,
          { location: params.location }
        );
      }
      
      // 5. 格式化结果
      const duration = Date.now() - startTime;
      context.logger.info('天气查询成功', { 
        location: params.location,
        duration 
      });
      
      return formatWeatherResult(weatherData, params.days);
      
    } catch (error) {
      // 6. 错误处理
      context.logger.error('天气查询失败', {
        location: params.location,
        code: error.code,
        message: error.message,
        duration: Date.now() - startTime
      });
      
      // 7. 返回用户友好的错误消息
      return getUserFriendlyError(error, params);
    }
  }
};

async function fetchWeather(location, apiKey) {
  return new Promise((resolve, reject) => {
    const url = \`https://api.weather.com/v1/current?location=\${encodeURIComponent(location)}&key=\${apiKey}\`;
    
    const req = https.get(url, { timeout: 5000 }, (res) => {
      if (res.statusCode === 404) {
        reject(new SkillError(ErrorCodes.INVALID_LOCATION, '城市不存在'));
      } else if (res.statusCode !== 200) {
        reject(new SkillError(ErrorCodes.API_ERROR, \`API 错误:\${res.statusCode}\`));
      } else {
        let data = '';
        res.on('data', chunk => data += chunk);
        res.on('end', () => resolve(JSON.parse(data)));
      }
    });
    
    req.on('error', (e) => {
      if (e.code === 'ETIMEDOUT') {
        reject(new SkillError(ErrorCodes.NETWORK_TIMEOUT, '请求超时'));
      } else {
        reject(new SkillError(ErrorCodes.NETWORK_ERROR, \`网络错误:\${e.message}\`));
      }
    });
  });
}

function getUserFriendlyError(error, params) {
  switch (error.code) {
    case ErrorCodes.NETWORK_TIMEOUT:
      return '⏱️ 请求超时,请检查网络连接后重试';
    
    case ErrorCodes.INVALID_LOCATION:
      return \`❌ 找不到城市 "\${params.location}",请检查城市名称\`;
    
    case ErrorCodes.API_ERROR:
      return '❌ 天气服务暂时不可用,请稍后重试';
    
    default:
      return '❌ 发生未知错误,请联系管理员';
  }
}

调试技巧

1. 开发环境详细日志

const isDev = process.env.NODE_ENV === 'development';

if (isDev) {
  context.logger.level = 'debug';
  context.logger.debug('完整请求:', JSON.stringify(request, null, 2));
}

2. 错误堆栈追踪

try {
  // 可能出错的代码
} catch (error) {
  context.logger.error('错误详情:', {
    message: error.message,
    stack: error.stack,  // 完整堆栈
    code: error.code,
    details: error.details
  });
  throw error;
}

3. 性能分析

const timers = new Map();

function startTimer(name) {
  timers.set(name, Date.now());
}

function endTimer(name, context) {
  const duration = Date.now() - timers.get(name);
  context?.logger.debug(\`计时:\${name} = \${duration}ms\`);
  return duration;
}

// 使用
startTimer('api-call');
const result = await apiCall();
endTimer('api-call', context);

测试错误场景

单元测试示例

const assert = require('assert');

describe('Weather Skill 错误处理', () => {
  it('应该处理网络超时', async () => {
    const result = await weatherSkill.execute(
      { location: '北京' },
      { 
        config: { weatherApiKey: 'test' },
        logger: mockLogger 
      }
    );
    
    assert(result.includes('超时'));
  });
  
  it('应该处理无效城市', async () => {
    const result = await weatherSkill.execute(
      { location: '无效城市' },
      mockContext
    );
    
    assert(result.includes('找不到城市'));
  });
  
  it('应该处理缺少 API 密钥', async () => {
    const result = await weatherSkill.execute(
      { location: '北京' },
      { config: {}, logger: mockLogger }
    );
    
    assert(result.includes('未配置'));
  });
});

总结

良好的错误处理和日志记录是高质量 Skill 的标志:

错误处理

  • 预防优于治疗(防御性编程)
  • 分层处理不同类型的错误
  • 提供清晰的错误码
  • 实现智能重试机制

日志记录

  • 使用合适的日志级别
  • 记录结构化、有上下文的信息
  • 脱敏敏感数据
  • 避免日志噪音

用户体验

  • 错误消息清晰友好
  • 提供可操作的建议
  • 不泄露技术细节

记住:用户不会看到你的代码,但会看到你的错误消息。让错误消息成为帮助用户解决问题的工具,而不是障碍。


发布分类:OpenClaw
标签:OpenClaw, 错误处理,日志,最佳实践,教程
字数:约 5,800 字

本文标签: , ,

    关于作者

    作者头像
    OpenClaw技术团队
    专注AI Agent技术分享