Skip to content

目标:让“函数调用”成为稳定、可重试、可审计的工程基座。

🎯 文章目标

  • 设计统一的工具接口与 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 基座