说实话,这周被 AI Agent 的讨论刷屏了——InfoQ 上连着几篇讲智能体评估和生产部署的,Hacker News 上 NVIDIA NemoClaw 的帖子底下 200 多条评论,全在吵 Agent 安全性的问题。

但吵归吵,Agent 该写还是得写。今天就聊聊最核心的一环:Function Calling——让大模型从「只会聊天」变成「能干活」的关键能力。

什么是 Function Calling?30 秒搞懂

简单说,Function Calling 就是告诉大模型:「你除了能说话,还有这些工具可以用」。

比如你告诉 GPT-4o:「你可以查天气、查数据库、发邮件」,它就会在合适的时机返回一个结构化的 JSON,告诉你「我想调用查天气这个函数,参数是北京」。你拿到这个 JSON 去执行,把结果喂回去,模型再基于结果继续对话。

这就是 Agent 的核心循环:感知 → 思考 → 调用工具 → 获取结果 → 继续思考

没有 Function Calling,大模型就是个「嘴炮王」——什么都懂,但什么都做不了。

踩坑第一站:不是所有 API 都支持 Function Calling

这是很多人忽略的问题。

我之前用某个中转平台调 Claude 的 Function Calling,死活返回不了正确的 JSON 格式。排查了半天,发现是中转层没有正确透传 tools 参数,直接给我吞了。

后来换了 ofox.ai,试了一下发现它是完整透传 OpenAI 格式的,toolstool_choiceparallel_tool_calls 这些参数都能正常工作。才算把这个坑过了。

教训:选 API 中转的时候,一定要测 Function Calling 支持度,不能只看能不能聊天。

实战:用 Python 写一个能查天气的 Agent

不废话,直接上代码。我们要实现的效果:用户问「北京今天天气怎么样」,模型自动调用天气 API,然后用自然语言回复。

第一步:定义工具

import openai
import json

client = openai.OpenAI(
    base_url="https://api.ofox.ai/v1",  # 国内直连,支持 Function Calling
    api_key="your-api-key"
)

# 定义可用工具
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定城市的当前天气信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称,如:北京、上海、深圳"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "温度单位"
                    }
                },
                "required": ["city"]
            }
        }
    }
]

第二步:实现工具函数

def get_weather(city: str, unit: str = "celsius") -> dict:
    """模拟天气 API(实际项目替换成真实 API)"""
    # 这里用模拟数据,实际可以调和风天气、心知天气等
    weather_data = {
        "北京": {"temp": 12, "condition": "晴", "humidity": 35},
        "上海": {"temp": 16, "condition": "多云", "humidity": 62},
        "深圳": {"temp": 23, "condition": "阴", "humidity": 78},
    }
    data = weather_data.get(city, {"temp": 20, "condition": "未知", "humidity": 50})
    return {
        "city": city,
        "temperature": data["temp"],
        "unit": unit,
        "condition": data["condition"],
        "humidity": data["humidity"]
    }

第三步:Agent 主循环

这是最核心的部分——实现「思考 → 调用 → 反馈」的循环:

def run_agent(user_message: str):
    messages = [
        {"role": "system", "content": "你是一个实用的助手,可以查询天气信息。回答要简洁自然。"},
        {"role": "user", "content": user_message}
    ]
    
    # 第一轮:让模型决定是否需要调用工具
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        tools=tools,
        tool_choice="auto"  # 让模型自己判断是否需要调用
    )
    
    assistant_message = response.choices[0].message
    
    # 如果模型决定调用工具
    if assistant_message.tool_calls:
        messages.append(assistant_message)  # 把模型的决策加入对话
        
        # 执行每个工具调用
        for tool_call in assistant_message.tool_calls:
            function_name = tool_call.function.name
            function_args = json.loads(tool_call.function.arguments)
            
            print(f"🔧 模型请求调用: {function_name}({function_args})")
            
            # 分发到对应函数
            if function_name == "get_weather":
                result = get_weather(**function_args)
            else:
                result = {"error": f"未知函数: {function_name}"}
            
            # 把工具结果喂回模型
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(result, ensure_ascii=False)
            })
        
        # 第二轮:模型基于工具结果生成最终回复
        final_response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages
        )
        return final_response.choices[0].message.content
    
    # 如果不需要工具,直接返回回复
    return assistant_message.content


# 测试
print(run_agent("北京今天天气怎么样?"))
print("---")
print(run_agent("帮我看看上海和深圳的天气对比"))

运行结果大概是这样:

🔧 模型请求调用: get_weather({'city': '北京'})
北京今天天气晴朗,气温 12°C,湿度 35%,挺适合出门的。
---
🔧 模型请求调用: get_weather({'city': '上海'})
🔧 模型请求调用: get_weather({'city': '深圳'})
上海多云 16°C,深圳阴天 23°C。深圳暖和不少,但湿度 78% 会比较闷。上海湿度 62% 相对舒适一些。

注意第二个问题,模型会并行调用两次 get_weather——这就是 parallel_tool_calls 的能力,一轮请求解决多个工具调用。

进阶:多工具 Agent 架构

实际项目中,一个 Agent 往往要挂好几个工具。架构上有几个关键点:

1. 工具注册表模式

class ToolRegistry:
    def __init__(self):
        self._tools = {}
        self._schemas = []
    
    def register(self, name: str, func, schema: dict):
        """注册一个工具"""
        self._tools[name] = func
        self._schemas.append({
            "type": "function",
            "function": {"name": name, **schema}
        })
    
    def execute(self, name: str, args: dict):
        """执行指定工具"""
        if name not in self._tools:
            return {"error": f"工具 {name} 不存在"}
        return self._tools[name](**args)
    
    @property
    def schemas(self):
        return self._schemas

# 使用
registry = ToolRegistry()
registry.register("get_weather", get_weather, {
    "description": "获取天气信息",
    "parameters": {
        "type": "object",
        "properties": {
            "city": {"type": "string", "description": "城市名"}
        },
        "required": ["city"]
    }
})

2. 带重试的安全调用

import time

def safe_agent_loop(messages, tools, max_rounds=5):
    """带轮次限制的 Agent 循环,防止无限调用"""
    for round_num in range(max_rounds):
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=tools,
            tool_choice="auto"
        )
        
        msg = response.choices[0].message
        messages.append(msg)
        
        if not msg.tool_calls:
            return msg.content  # 模型决定不调用工具,返回结果
        
        # 执行所有工具调用
        for tc in msg.tool_calls:
            try:
                result = registry.execute(
                    tc.function.name,
                    json.loads(tc.function.arguments)
                )
            except Exception as e:
                result = {"error": str(e)}
            
            messages.append({
                "role": "tool",
                "tool_call_id": tc.id,
                "content": json.dumps(result, ensure_ascii=False)
            })
        
        print(f"  ⏳ 第 {round_num + 1} 轮工具调用完成")
    
    return "达到最大调用轮次,请简化问题后重试。"

这个 max_rounds 很关键——不加限制的话,模型有可能陷入循环调用,一直在「思考 → 调用 → 不满意 → 再调用」。HN 上那些说「Agent 跑了一夜把服务器跑爆」的,很多就是没做这个限制。

Function Calling 的几个坑

坑 1:JSON 解析偏差

模型返回的 function.arguments 偶尔会有格式问题,特别是用小一点的模型时。建议加个 try-except:

try:
    args = json.loads(tool_call.function.arguments)
except json.JSONDecodeError:
    # 尝试修复常见问题(多余逗号等)
    cleaned = tool_call.function.arguments.rstrip(',}') + '}'
    args = json.loads(cleaned)

坑 2:工具描述要够具体

模型是根据你写的 description 来决定什么时候调用哪个工具的。描述含糊,模型就容易误判。

# ❌ 太模糊
"description": "搜索东西"

# ✅ 够具体
"description": "在公司内部知识库中搜索技术文档,支持关键词和语义搜索,返回最相关的前5条结果"

坑 3:注意 Token 消耗

每个 tools 的 schema 定义都会占用 input token。如果你注册了 20 个工具,每次请求光工具描述就可能吃掉 2000+ token。

解决方案:根据上下文动态选择要传的工具,不要一股脑全传。

def select_relevant_tools(user_message: str, all_tools: list) -> list:
    """根据用户消息预筛选相关工具(可以用简单的关键词匹配或小模型)"""
    weather_keywords = ["天气", "温度", "下雨", "气温"]
    search_keywords = ["搜索", "查找", "帮我找"]
    
    selected = []
    for tool in all_tools:
        name = tool["function"]["name"]
        if name == "get_weather" and any(k in user_message for k in weather_keywords):
            selected.append(tool)
        elif name == "search_docs" and any(k in user_message for k in search_keywords):
            selected.append(tool)
    
    return selected if selected else all_tools  # 没匹配到就全传

不同模型的 Function Calling 能力差异

这里分享下我实测的结论:

模型 Function Calling 质量 并行调用 复杂参数处理
GPT-4o ⭐⭐⭐⭐⭐ ✅ 稳定 嵌套 JSON 没问题
Claude 3.5 Sonnet ⭐⭐⭐⭐ ✅ 支持 偶尔参数格式抖动
DeepSeek-V3 ⭐⭐⭐⭐ ✅ 支持 中文参数很稳
GPT-4o-mini ⭐⭐⭐ ✅ 支持 复杂场景偶尔漏参数
Llama 3.3 70B ⭐⭐ ❌ 不稳定 需要特殊 prompt

通过 ofox.ai 可以很方便地在这些模型之间切换测试,因为它都是 OpenAI 格式的接口,代码一行不用改,只需要换 model 参数。

安全性:Agent 不是你想放飞就放飞的

最后聊聊安全。HN 上关于 Agent 安全的讨论确实不是杞人忧天。

Function Calling 的 Agent 本质上是让 AI 操作你的系统——它可以查数据库、调 API、甚至执行代码。所以几个原则必须守住:

  1. 最小权限:工具只暴露必要的能力,不要给 Agent 一个 execute_sql 然后没有任何限制
  2. 人类在环:关键操作(发邮件、转账、删数据)必须经过人工确认
  3. 轮次限制:上面说的 max_rounds,必须有
  4. 输入校验:不要无脑信任模型传来的参数,该校验的校验
  5. 日志审计:每次工具调用都要记录,出了问题能追溯
# 关键操作需要人工确认
DANGEROUS_TOOLS = {"send_email", "delete_record", "execute_command"}

def execute_with_approval(tool_name: str, args: dict):
    if tool_name in DANGEROUS_TOOLS:
        print(f"⚠️ 高风险操作: {tool_name}({args})")
        confirm = input("确认执行?(y/n): ")
        if confirm.lower() != 'y':
            return {"error": "用户拒绝执行此操作"}
    return registry.execute(tool_name, args)

总结

Function Calling 是 AI Agent 从「能说」到「能做」的关键跳跃。核心就三步:

  1. 定义工具:告诉模型你有什么能力
  2. 处理调用:模型说要调什么就调什么,把结果喂回去
  3. 安全兜底:限制轮次、校验参数、关键操作要人工确认

选 API 的时候别只看能不能聊天,Function Calling 的支持度和稳定性才是建 Agent 的关键指标

代码都是可以直接跑的,复制到你的项目里改改就能用。有问题评论区见 👋

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐