从零搭建一个基于 Function Calling 的天气查询智能体

一、问题:大模型不知道「现在的天气」

问一个 LLM「北京今天热不热」,它只能诚实地告诉你「我无法获取实时天气」。因为大模型的知识来自训练数据,天然缺三样东西:实时数据、外部系统、动作能力。Function Calling(函数调用,也称 Tool Calling)就是补上这三样的标准机制:让模型在对话中「感知」到外部工具的存在,并在需要时返回一份结构化的「调用指令」(函数名 + 参数),由你的代码真正执行,再把结果喂回给模型生成最终回答。

本实验要做的,就是把这条链路亲手跑通:一个用户说「帮我查下北京天气」,Agent 自动决定调用 get_weather 工具、拿到真实数据、组织成人话。这也是一切「AI Agent 能干活」的最小范式。

二、核心思路与调用流程

Function Calling 的完整流程分六步:

1
① 定义工具 → ② 用户提问 → ③ 模型决定是否调用→ ④ 你的代码执行真实函数 → ⑤ 结果回传(tool 消息)→ ⑥ 模型基于结果生成最终回答

关键认知:模型其实「不会」调用函数。它只是预测「接下来应该请程序帮我调用哪个函数、传什么参数」,然后输出一个 tool_calls 对象;真正执行的是你的程序。这份「指令-执行-回填」的分离,既保留了模型的推理能力,又杜绝了模型直接操作系统的安全问题。

三、第一步:定义工具 Schema(决定命中率的关键)

工具定义用 JSON Schema 描述,三要素缺一不可:名称、描述、参数结构。其中 description 是命中的关键——模型就靠它判断「什么场景该调这个工具」。

1
tools = [ { "type": "function", "function": { "name": "get_weather", "description": "获取指定城市的实时天气信息,含气温、天气现象、湿度", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,例如:北京、上海、广州" } }, "required": ["city"] } } }]

工程经验:description 写得越具体,调用准确率越高。把「获取天气」写成「获取指定城市的实时天气信息,含气温、天气现象、湿度」,模型就能区分「该调用」与「不该调用」的场景。生产环境可用 "strict": true 开启 Strict Mode,保证返回的参数 100% 符合 Schema,杜绝解析报错。若参数取值固定,用 enum 约束还能进一步提升准确率。

四、第二步:写真正的天气查询函数

模型只负责「决定调用」,执行必须由真实代码完成。最省事的做法是调用开源 API,全程零 Key 成本:

  • Open-Meteo(免费、无需 API Key):先给地理编码接口传城市名换经纬度,再给天气接口传经纬度取「气温/天气现象/湿度/风速」。

  • wttr.in(极简,一条 URL 出结果,适合快速验证)。

  • 需要更结构化数据可用和风天气 / OpenWeatherMap(需注册 Key)。

以 Open-Meteo 为例:

1
import requestsdef get_weather(city: str) -> str: # 1) 城市名 → 经纬度(Open-Meteo Geocoding,免费) geo = requests.get( "https://geocoding-api.open-meteo.com/v1/search", params={"name": city, "count": 1, "language": "zh"}, timeout=10, ).json() if not geo.get("results"): return "抱歉,没有找到该城市的坐标信息" loc = geo["results"][0] lat, lon = loc["latitude"], loc["longitude"] # 2) 经纬度 → 实时天气 w = requests.get( "https://api.open-meteo.com/v1/forecast", params={"latitude": lat, "longitude": lon, "current_weather": True}, timeout=10, ).json() cur = w.get("current_weather", {}) return (f"{city}当前天气:温度 {cur.get('temperature')}°C," f"风速 {cur.get('windspeed')} km/h," f"代码 {cur.get('weathercode')}")

把「城市名 → 经纬度」与「经纬度 → 天气」拆成两步,是因为天气 API 普遍接受坐标而非中文城市名——这也是真实 Agent 里常见的「多工具串联」雏形。建议给函数加 .env 式配置与 10 秒超时,避免外部 API 卡住主流程。

五、第三步:实现 Agent 主循环

主体逻辑分两轮请求:

1
from openai import OpenAIimport jsonclient = OpenAI() # 可配置 base_url 指向本地 vLLM / llama.cppdef weather_agent(user_message: str) -> str: # —— 第一轮:模型分析意图,决定是否要调用工具 —— response = client.chat.completions.create( model="gpt-4o", # 需支持 function calling 的模型 messages=[ {"role": "system", "content": "你是一个天气助手,使用 get_weather 回答天气问题。"}, {"role": "user", "content": user_message}, ], tools=tools, tool_choice="auto", # 让模型自己判断是否调用 ) message = response.choices[0].message # —— 模型没有要求调用工具,直接返回回答 —— if not message.tool_calls: return message.content # —— 执行工具调用 —— results = [] for tool_call in message.tool_calls: # 支持并行调用多个 name = tool_call.function.name args = json.loads(tool_call.function.arguments) results.append((tool_call.id, get_weather(args["city"]))) # —— 回填工具结果,进入第二轮生成最终回答 —— messages = [ {"role": "system", "content": "你是一个天气助手。"}, {"role": "user", "content": user_message}, message, # 带上模型此前的 tool_calls ] for tool_call_id, content in results: messages.append({ "role": "tool", "tool_call_id": tool_call_id, "content": content, }) final = client.chat.completions.create( model="gpt-4o", messages=messages, ) return final.choices[0].message.content

三个细节别漏:回填消息必须带 tool_call_id(把结果正确挂到对应调用上);第二轮请求要把模型的第一轮响应 message 原样带回去(保留 tool_calls 上下文,否则模型不知道调用谁);load_dotenv() 管理 API Key,别把密钥写死在代码里。

六、第四步:验证与使用

跑起来验证两件事:

1
if __name__ == "__main__": print(weather_agent("北京今天天气怎么样?"))
  • 调用是否正确发起:日志里应能看到模型输出了 tool_calls → get_weather(北京),随后打印出经纬度查询与天气 API 的真实返回;

  • 最终回答是否自然:模型应把结构化的温度/风速数据组织成「北京当前天气:温度 26°C,风速 12 km/h」这样的人类口吻,而不是复读原始 JSON。

进阶玩两招:并行调用——问「对比北京和上海天气」时,模型可能一次返回两个 tool_calls,用 asyncio.gather 并发执行两个查询,把两轮网络请求耗时压到一轮;多工具——再加一个 get_aqi(空气质量)函数,模型会自动在天气、空气之间按意图路由,进一步接近生产中「工具注册即用」的智能体形态。

七、踩坑清单与工程建议

  • description 敷衍导致误调用:同一函数 description 写得具体与否,实测调用准确率可差 30 个百分点。命名、参数说明都要有「模型视角」。

  • 忘记回填 tool_call_id / 原响应:第二轮 messages 缺了这些,模型会对「谁调用、结果给谁」完全错乱,输出答非所问——这也是新手最常见的报错点。

  • 外部 API 不可靠:加超时(10s)+ 重试(最多 3 次);失败时把错误信息作为 content 回传并标记 is_error,让模型优雅降级「天气服务暂时不可用」,而不是整个 Agent 崩掉。

  • 工具要少而精:别把所有函数一股脑塞给模型,生产环境只暴露必要的工具,敏感操作(发邮件、删文件)要人工确认。

  • 生产化:记录每次 tool call 的参数与结果用于审计;用 Strict Mode 保 Schema 稳定;多步任务想起完整 Agent 循环(while 一直调工具直到模型说「不用调了」)时,参考主循环把单轮扩展成循环即可。

八、一句话总结

Function Calling 天气 Agent 的本质是「决策与执行分离」:模型用结构化 tool_calls 表达意图,代码执行真实请求,再把结果回填生成回答。本文用 40 行代码串起了定义工具 Schema → 实现查询函数 → Agent 主循环 → 回填二次生成 的完整闭环,并且全程零 API Key(天气走 Open-Meteo、LLM 可指向本地模型)。掌握这个最小范式,你就能把它推广到任何「需要连外部世界」的场景——查订单、算日历、查资料,一通百通。