Function Calling 实战:用大模型 API 构建你的第一个 AI Agent
摘要(149字): 本文探讨了AI Agent的核心技术Function Calling,解析其如何让大模型从对话转向实际任务执行。文章指出Function Calling的关键在于结构化JSON交互,实现"感知-思考-调用工具-反馈"的闭环。通过Python代码演示了天气查询Agent的实现过程,包括工具定义、API调用和主循环设计,特别强调并行调用(parallel_too
说实话,这周被 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 格式的,tools、tool_choice、parallel_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、甚至执行代码。所以几个原则必须守住:
- 最小权限:工具只暴露必要的能力,不要给 Agent 一个
execute_sql然后没有任何限制 - 人类在环:关键操作(发邮件、转账、删数据)必须经过人工确认
- 轮次限制:上面说的
max_rounds,必须有 - 输入校验:不要无脑信任模型传来的参数,该校验的校验
- 日志审计:每次工具调用都要记录,出了问题能追溯
# 关键操作需要人工确认
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 从「能说」到「能做」的关键跳跃。核心就三步:
- 定义工具:告诉模型你有什么能力
- 处理调用:模型说要调什么就调什么,把结果喂回去
- 安全兜底:限制轮次、校验参数、关键操作要人工确认
选 API 的时候别只看能不能聊天,Function Calling 的支持度和稳定性才是建 Agent 的关键指标。
代码都是可以直接跑的,复制到你的项目里改改就能用。有问题评论区见 👋
更多推荐



所有评论(0)