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能:
- 声明能力:告诉LLM"我能调用这些函数"
- 解析意图:LLM理解用户请求后,决定调用哪个Tool
- 执行调用:自动处理参数校验、调用、结果返回
- 反馈循环:工具执行结果返回给LLM,继续推理
🛠️ 创建第一个自定义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足够信息让它自动重试
1. 单一职责:一个Tool只做一件事
2. 统一格式:所有Tool返回相同结构
3. 防御编程:对参数做边界检查
4. 幂等设计:重复调用不产生副作用
5. 错误可选:给LLM足够信息让它自动重试
🔗 相关资源
🎭 结语
世界上有一种能力叫Tool Calling——它让AI从"能说"到"能做",从一个聊天机器人变成一个真正的数字助手。
1点13分,Agent完成了它的第1000次API调用,没有报错,没有限流,没有重试。那一刻我突然觉得:API集成这件事,可能真的要被AI做了。