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"]
}
}
三条铁律:
- description 写“什么时候用 + 约束”,不只是功能。模型靠它决策调不调。
- 参数强类型 + 必要项显式声明,能用
enum就别用自由字符串。 - 一个工具只做一件事,别搞“万能函数”——模型容易传错参。
二、并行调用
现代模型支持一次返回多个 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)而非明细;
- 或把大结果落盘,只回传文件路径。
五、安全边界(最重要)
- 白名单:只允许调用预注册的工具,未知 name 一律拒绝。
- 权限最小化:给 Agent 的数据库账号只给只读视图;shell 工具限定工作目录。
- 人类确认:删除、支付、发邮件等不可逆动作,必须先弹确认再执行。
- 参数校验:
order_id必须是ORD开头,否则拒绝——防注入绕过。
一句话总结:模型决定“调什么、传什么”,执行权永远在宿主手里。工程化的全部努力,就是让这把“权”既好用又不可越界。
下一篇聊聊怎么把话“说”进模型心里——提示工程进阶。