目标:让“函数调用”成为稳定、可重试、可审计的工程基座。
🎯 文章目标
- 设计统一的工具接口与 JSON Schema
- 定义错误/重试/幂等语义,便于治理与回放
- 给出客户端与服务端的最小实现
📚 背景/前置
- LLM 调用工具的关键不是“能不能”,而是“稳定可控、越界可防”
- 不同模型/供应商在工具调用细节上可能不同,需统一抽象
🔧 核心内容
1) 接口与协议
- 请求:
- 响应:{ok, data, error: {code, message, retryable, hint}}
- 幂等键:idempotencyKey(防重复调用)
2) 服务端最小实现(Node.js/Express)
javascript
import express from 'express'
const app = express(); app.use(express.json())
app.post('/tools/searchTickets', async (req,res)=>{
const { q } = req.body
if (!q) return res.json({ ok:false, error:{ code:'INVALID_ARG', retryable:false, hint:'q required' } })
// ...查询逻辑
return res.json({ ok:true, data: [{ id:'T123', title:'退款请求' }] })
})
app.post('/tools/closeTicket', async (req,res)=>{
const { id, idempotencyKey } = req.body
// 幂等:根据 idempotencyKey 记录/回放
return res.json({ ok:true, data:{ closed:true, ticketId:id } })
})
app.listen(3000)
3) 客户端与 LLM 集成
javascript
import OpenAI from 'openai'
const client = new OpenAI({ baseURL: process.env.OPENAI_API_BASE, apiKey: process.env.OPENAI_API_KEY })
async function callTool(name, params){
const r = await fetch(`https://api.example.com/tools/\${name}`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(params) })
return r.json()
}
const tools = [{ type:'function', function:{ name:'searchTickets', parameters:{ type:'object', properties:{ q:{type:'string'} }, required:['q'] } } }]
const messages = [{ role:'user', content:'帮我查找退款工单' }]
const rsp = await client.chat.completions.create({ model: process.env.CHAT_MODEL, messages, tools })
const call = rsp.choices[0].message.tool_calls?.[0]
if (call) {
const args = JSON.parse(call.function.arguments)
const out = await callTool(call.function.name, { ...args, idempotencyKey: crypto.randomUUID() })
// 记录审计:入参/出参/错误/耗时
}
4) 错误与重试策略
- 分类:参数错误(不重试)、网络/超时(可重试)、权限/配额(退避 + 降级)
- 退避:指数退避 + 抖动;设置最大尝试次数
- 幂等:相同 idempotencyKey 多次调用只执行一次
5) 安全与权限
- 工具白名单/黑名单;按会话/用户/组织授权
- 参数校验与输出脱敏;限制危险操作(删除/资金)
💡 实战示例:最小工具注册表
javascript
const registry = {
searchTickets: { url:'/tools/searchTickets', schema:{/*...*/} },
closeTicket: { url:'/tools/closeTicket', schema:{/*...*/} }
}
📊 对比/取舍(速查)
- 直连三方 API vs 统一工具服务:治理与审计优先时,统一服务更好
- 客户端直调 vs 服务端代理:安全与成本可控性更佳
🧪 踩坑与经验
- 未设计幂等导致重复提交/扣费
- 错误码语义不清,无法回答“能否重试/何时重试”
- 工具权限过宽,存在越权风险
📎 参考与延伸
- OpenAI 函数调用、工具参数 JSON Schema
- 指数退避与重试策略
- API 网关/审计日志与合规
💭 总结
- 用“统一工具接口 + 错误/重试/幂等语义 + 权限与审计”构建可治理的 Function Call 基座