Function Calling 工程化封装:把"工具"安全可靠地交给大模型

Function Calling 工程化封装:把“工具”安全可靠地交给大模型

上一篇文章讲了 Agent 为什么离不开工具。但把“函数调用”从 demo 变成生产系统,中间差着十条沟:模型可能调错参数、工具可能抛异常、一个大输出能撑爆上下文、恶意输入能诱导模型乱调危险接口。Function Calling 工程化就是填这些沟。

一、工具 Schema 设计原则

工具描述就是模型的“使用说明书”,写得好坏直接决定调用准确率。

{
  "name": "query_order",
  "description": "按订单号查询订单状态与物流信息。仅接受已授权的订单号。",
  "parameters": {
    "type": "object",
    "properties": {
      "order_id": { "type": "string", "description": "订单号,形如 ORD2026xxxxx" },
      "with_logistics": { "type": "boolean", "description": "是否一并返回物流轨迹", "default": false }
    },
    "required": ["order_id"]
  }
}

三条铁律:

  1. description 写“什么时候用 + 约束”,不只是功能。模型靠它决策调不调。
  2. 参数强类型 + 必要项显式声明,能用 enum 就别用自由字符串。
  3. 一个工具只做一件事,别搞“万能函数”——模型容易传错参。

二、并行调用

现代模型支持一次返回多个 tool_calls(并行)。比如“查 A 和 B 两个订单”,模型会同时发起两个调用。执行器必须并发执行而非串行,否则延迟翻倍:

import concurrent.futures

def run_tools(calls, handlers):
    with concurrent.futures.ThreadPoolExecutor() as ex:
        futs = {ex.submit(handlers[c.name], **json.loads(c.arguments)): c for c in calls}
        results = {}
        for fut in concurrent.futures.as_completed(futs):
            call = futs[fut]
            results[call.id] = safe(fut)   # 见下文错误处理
        return results

三、错误处理与重试

工具可能超时、抛异常、返回超长。执行器要兜住,不能让异常直接崩掉 Agent Loop:

def safe(fut):
    try:
        return {"ok": True, "content": fut.result(timeout=30)}
    except Exception as e:
        return {"ok": False, "content": f"TOOL_ERROR: {type(e).__name__}: {e}"}

def maybe_retry(call, attempt=0):
    try:
        return handlers[call.name](**json.loads(call.arguments))
    except TransientError:
        if attempt < 2:
            return maybe_retry(call, attempt + 1)
        raise

关键点:工具报错要原样回填给模型(“TOOL_ERROR: …”),让模型自己决定重试、换参数还是放弃——这才是 Agent 自我修复的能力来源。

四、结果截断与压缩

一个 SQL 查询可能返回 10 万行。直接塞进上下文既烧钱又稀释注意力。做法:

  • 超限截断 + 提示“结果过长,已截断至前 N 行”;
  • 或让工具返回聚合结果(count/sum)而非明细;
  • 或把大结果落盘,只回传文件路径。

五、安全边界(最重要)

  1. 白名单:只允许调用预注册的工具,未知 name 一律拒绝。
  2. 权限最小化:给 Agent 的数据库账号只给只读视图;shell 工具限定工作目录。
  3. 人类确认:删除、支付、发邮件等不可逆动作,必须先弹确认再执行。
  4. 参数校验:order_id 必须是 ORD 开头,否则拒绝——防注入绕过。

一句话总结:模型决定“调什么、传什么”,执行权永远在宿主手里。工程化的全部努力,就是让这把“权”既好用又不可越界。

下一篇聊聊怎么把话“说”进模型心里——提示工程进阶。

← 返回首页