🔧 Tool Calling 高级技巧

自定义工具与API集成实战指南 (2026版)

📅 更新:2026年5月20日 | ⏱️ 阅读时间:15分钟 | 🏷️ 难度:进阶
OpenClaw教程 Tool Calling API集成 工具链 自定义工具

🎬 开篇:能调API的Agent,才是真Agent

凌晨1点13分,我的Agent刚调了3个API——从GitHub拉数据、用Notion创建文档、往Slack发通知。

没有一个人类帮忙。没有一行OAuth 2.0认证代码让我手写。我只告诉Agent:"帮我把这件事做成。"

这就是Tool Calling的魅力——让Agent长出无数只手,伸向任何有API的地方。

📖 Tool Calling 核心概念

OpenClaw的Tool Calling机制让Agent能:

🛠️ 创建第一个自定义Tool

Tool文件结构

tools/
└── custom-api-tool/
    ├── tool.json          # Tool 定义
    └── handler.js         # 执行逻辑

定义Tool

// tools/custom-api-tool/tool.json
{
  "name": "fetch_weather_data",
  "version": "1.0.0",
  "description": "获取指定城市的天气信息",
  "parameters": {
    "type": "object",
    "required": ["city", "units"],
    "properties": {
      "city": {
        "type": "string",
        "description": "城市名称,如:北京、上海、深圳"
      },
      "units": {
        "type": "string",
        "enum": ["celsius", "fahrenheit"],
        "description": "温度单位"
      },
      "days": {
        "type": "integer",
        "description": "预报天数(1-7)",
        "minimum": 1,
        "maximum": 7,
        "default": 3
      }
    }
  }
}

实现Handler

// tools/custom-api-tool/handler.js
module.exports = async function handler({ city, units, days = 3 }) {
  // 1. 参数校验
  if (!city || city.trim().length === 0) {
    return { error: '城市名称不能为空' };
  }
  
  try {
    // 2. 调用外部API
    const apiKey = process.env.WEATHER_API_KEY;
    const response = await fetch(
      `https://api.weather.com/v1/forecast?` +
      new URLSearchParams({
        city: city,
        units: units === 'fahrenheit' ? 'imperial' : 'metric',
        days: days.toString()
      }),
      {
        headers: {
          'Authorization': `Bearer ${apiKey}`,
          'Content-Type': 'application/json'
        },
        timeout: 10000
      }
    );
    
    if (!response.ok) {
      throw new Error(`API返回错误: ${response.status}`);
    }
    
    const data = await response.json();
    
    // 3. 格式化返回
    return {
      city: data.location.name,
      temperature: data.current.temp,
      condition: data.current.condition,
      forecast: data.forecast.slice(0, days).map(day => ({
        date: day.date,
        high: day.temp.max,
        low: day.temp.min,
        condition: day.condition
      })),
      updated: new Date().toISOString()
    };
  } catch (error) {
    // 4. 错误处理
    return {
      error: `天气数据获取失败: ${error.message}`,
      retryable: error.name === 'AbortError' || error.message.includes('timeout')
    };
  }
};

🔐 API集成最佳实践

1. 安全凭证管理

// ❌ 危险:硬编码API Key
const API_KEY = 'sk-xxx...';

// ✅ 安全:使用环境变量
const API_KEY = process.env.MY_API_KEY;

// ✅ 更安全:OpenClaw Secrets管理
// openclaw secrets set weather_api_key sk-xxx...
// 在工具中通过openclaw运行时获取
const apiKey = await openclaw.secrets.get('weather_api_key');

2. 幂等设计

// 幂等:多次调用结果一致(对写操作尤其重要)
module.exports = async function createDocument({ title, content }) {
  const idempotencyKey = `${title}-${Date.now().toString(36)}`;
  
  // 检查是否已创建
  const existing = await db.findDocument({ title });
  if (existing) {
    return { id: existing.id, status: 'exists' };
  }
  
  // 创建文档
  const doc = await api.createContent({ title, content });
  return { id: doc.id, status: 'created' };
};

3. 限流与重试

class APIWrapper {
  constructor() {
    this.rateLimiter = new RateLimiter({ 
      maxRequests: 60,  // 每分钟
      interval: 60000 
    });
  }

  async call(url, options = {}) {
    await this.rateLimiter.wait();
    
    return this.retry(async () => {
      const response = await fetch(url, {
        ...options,
        timeout: 15000
      });
      
      if (response.status === 429) {
        const retryAfter = response.headers.get('Retry-After') || 5;
        throw new RateLimitError(retryAfter);
      }
      
      if (!response.ok) {
        throw new APIError(response.status, await response.text());
      }
      
      return response.json();
    }, {
      maxRetries: 3,
      backoff: 'exponential',
      baseDelay: 1000
    });
  }

  async retry(fn, { maxRetries = 3, baseDelay = 1000 } = {}) {
    for (let i = 0; i < maxRetries; i++) {
      try {
        return await fn();
      } catch (err) {
        if (i === maxRetries - 1) throw err;
        
        const delay = err instanceof RateLimitError
          ? parseInt(err.retryAfter) * 1000
          : baseDelay * Math.pow(2, i);  // 指数退避
        
        console.log(`重试 ${i+1}/${maxRetries}, ${delay}ms后...`);
        await sleep(delay);
      }
    }
  }
}

🔗 工具链编排

单个Tool的能力有限,组合起来就是无敌:

// tool-chain.js
class ToolChain {
  constructor() {
    this.tools = new Map();
  }
  
  register(name, handler) {
    this.tools.set(name, handler);
  }
  
  async execute(plan) {
    // plan: [ { tool, params }, ... ]
    const results = {};
    
    for (const step of plan) {
      const handler = this.tools.get(step.tool);
      if (!handler) {
        throw new Error(`未找到工具: ${step.tool}`);
      }
      
      // 支持前序结果引用
      const params = this.resolveParams(step.params, results);
      
      results[step.id || step.tool] = await handler(params);
      console.log(`完成: ${step.tool}`, params);
    }
    
    return results;
  }
  
  resolveParams(params, prevResults) {
    const resolved = {};
    for (const [key, value] of Object.entries(params)) {
      if (typeof value === 'string' && value.startsWith('$')) {
        // $prev.data → 从之前的结果获取
        const path = value.slice(1).split('.');
        resolved[key] = path.reduce((obj, p) => obj?.[p], prevResults);
      } else {
        resolved[key] = value;
      }
    }
    return resolved;
  }
}

// 使用:创建自动化工作流
const chain = new ToolChain();
chain.register('fetch-issues', fetchGitHubIssues);
chain.register('analyze-data', analyzeWithAI);
chain.register('create-report', generateReport);
chain.register('send-notification', slackNotify);

const result = await chain.execute([
  { id: 'issues',    tool: 'fetch-issues',    params: { repo: 'openclaw/openclaw' } },
  { id: 'analysis',  tool: 'analyze-data',    params: { data: '$issues.items' } },
  { id: 'report',    tool: 'create-report',   params: { analysis: '$analysis.summary' } },
  { id: 'notify',    tool: 'send-notification', params: { 
    channel: '#dev-team',
    message: '本周PR分析报告已生成',
    attachment: '$report.url'
  }}
]);

🚫 Tool Calling 常见错误

错误1:参数类型不匹配

// Tool定义中声明了integer
"count": { "type": "integer" }

// 实际调用传入string(LLM可能会这样)
handler({ count: "3" }) // ❌ 可能失败

// ✅ 防御性处理
function sanitizeParams(params) {
  return {
    ...params,
    count: parseInt(params.count, 10) || 0,
    active: params.active === true || params.active === 'true'
  };
}

错误2:工具返回格式不统一

// ❌ 返回格式不一致
if (success) return { data: result };
if (error) return 'error: something went wrong';

// ✅ 统一返回格式
return {
  success: true,
  data: result,
  error: null,
  timestamp: Date.now()
};

// 或
return {
  success: false,
  error: 'something went wrong',
  code: 'ERR_001',
  retryable: false
};
🎯 Tool Calling 黄金法则:
1. 单一职责:一个Tool只做一件事
2. 统一格式:所有Tool返回相同结构
3. 防御编程:对参数做边界检查
4. 幂等设计:重复调用不产生副作用
5. 错误可选:给LLM足够信息让它自动重试

🔗 相关资源

🎭 结语

世界上有一种能力叫Tool Calling——它让AI从"能说"到"能做",从一个聊天机器人变成一个真正的数字助手

1点13分,Agent完成了它的第1000次API调用,没有报错,没有限流,没有重试。那一刻我突然觉得:API集成这件事,可能真的要被AI做了。