返回首页

接真实天气 API:一个跑得通的天气 Agent

用 Open-Meteo 的免费接口替代假数据,讲清楚工具函数怎么写、参数怎么校验、错误怎么兜底。

为什么不用假数据

很多教程的工具函数长这样:return f"{city}今天晴,25度"。 这种例子能让你跑通流程,但学不到真实开发里最麻烦的部分: 网络会失败、接口会限流、城市名可能查不到、返回的 JSON 嵌套五层。

这篇用一个真实的免费接口——Open-Meteo, 把这些问题一次性暴露出来并解决掉。

为什么选 Open-Meteo

  • 完全免费,无需 API Key——省掉注册和密钥管理的麻烦
  • 无需信用卡,无每日调用配额限制(合理使用范围内)
  • 返回标准 JSON,结构清晰

它需要两步:先把城市名转成经纬度(geocoding),再查天气。

第一步:城市名 → 经纬度

import requests

        GEO_URL = "https://geocoding-api.open-meteo.com/v1/search"

        def geocode(city: str):
            """把城市名解析成 (纬度, 经度, 标准名称)。"""
            resp = requests.get(
                GEO_URL,
                params={"name": city, "count": 1, "language": "zh", "format": "json"},
                timeout=8,
            )
            resp.raise_for_status()
            results = resp.json().get("results") or []
            if not results:
                return None
            r = results[0]
            return r["latitude"], r["longitude"], r.get("name", city)
        

这里已经出现三个真实工程要素:timeout(不设超时可能永久挂住)、 raise_for_status(HTTP 错误要显式抛出)、 以及查不到时的空值处理。

第二步:查天气

WEATHER_URL = "https://api.open-meteo.com/v1/forecast"

        # Open-Meteo 用 WMO 编码表示天气,需要自己映射成人话
        WMO = {
            0: "晴", 1: "基本晴", 2: "局部多云", 3: "阴",
            45: "雾", 48: "雾凇", 51: "小毛毛雨", 53: "毛毛雨", 55: "大毛毛雨",
            61: "小雨", 63: "中雨", 65: "大雨",
            71: "小雪", 73: "中雪", 75: "大雪",
            80: "阵雨", 81: "强阵雨", 82: "暴雨",
            95: "雷阵雨", 96: "雷阵雨伴冰雹", 99: "强雷阵雨伴冰雹",
        }


        def fetch_weather(lat: float, lon: float) -> dict:
            resp = requests.get(
                WEATHER_URL,
                params={
                    "latitude": lat,
                    "longitude": lon,
                    "current": "temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m",
                    "timezone": "auto",
                },
                timeout=8,
            )
            resp.raise_for_status()
            return resp.json()
        

第三步:包装成工具

现在用 LangChain 的 @tool 把它包起来。重点是 docstring:

from langchain.tools import tool


        @tool
        def get_weather(city: str) -> str:
            """查询指定中国城市的当前实时天气。

            当用户询问天气、气温、是否下雨、适合穿什么等问题时使用本工具。
            不要用它查询历史天气或未来多天的预报。

            Args:
                city: 城市名称,用中文,例如 "北京"、"杭州"。不要带"市"字。
            """
            try:
                geo = geocode(city)
            except requests.RequestException as e:
                return f"查询失败:网络异常({e.__class__.__name__})"

            if geo is None:
                return f"没有找到城市「{city}」,请确认名称是否正确,或用更大的城市名重试。"

            lat, lon, name = geo

            try:
                data = fetch_weather(lat, lon)
            except requests.RequestException as e:
                return f"查询失败:天气服务暂时不可用({e.__class__.__name__})"

            cur = data.get("current") or {}
            temp = cur.get("temperature_2m")
            humid = cur.get("relative_humidity_2m")
            wind = cur.get("wind_speed_10m")
            code = cur.get("weather_code")

            if temp is None:
                return "天气服务返回的数据不完整,请稍后重试。"

            desc = WMO.get(code, "未知天气")
            return (
                f"{name}当前{desc},气温 {temp}℃,"
                f"相对湿度 {humid}%,风速 {wind} km/h。"
            )
        

docstring 到底有多重要

注意我写了三件事:

  1. 做什么:查询中国城市的实时天气
  2. 什么时候用 / 什么时候不用:天气相关问题用,历史天气不用
  3. 参数怎么填:中文、不带"市"字

第二条最容易被忽略,但它直接影响准确率。如果模型不知道边界在哪, 用户问"明天下雨吗",它也会拿当前天气去回答,然后你就得到一个错误的答案。

把 docstring 当成写给模型的说明书,而不是写给同事的注释。

第四步:组装 Agent

from langchain.chat_models import init_chat_model
        from langchain.agents import create_agent

        model = init_chat_model(
            "qwen-plus",
            model_provider="openai",
            base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
            api_key="sk-xxx",
        )

        agent = create_agent(model=model, tools=[get_weather])

        result = agent.invoke({
            "messages": [{"role": "user", "content": "杭州现在适合出门跑步吗?"}]
        })
        print(result["messages"][-1].content)
        

输出:

杭州当前局部多云,气温 24℃,相对湿度 62%,风速 8 km/h。
        气温适宜、湿度不高、风力较小,比较适合出门跑步,建议穿透气的运动服。

模型不只是转述数据,还结合数据给出了判断。这就是把工具结果 回填给模型、让它再生成一次回答的价值。

三个必须处理的错误分支

错误类型原因处理方式
城市查不到 用户输入了不存在的城市名 返回提示语,让模型告诉用户换一个名字
网络异常 超时、DNS 失败、连接被拒 捕获 RequestException,返回可读的失败信息
数据不完整 接口返回结构变化或字段缺失 用 .get() + 空值判断,绝不直接下标取值

关键原则:工具函数永远返回字符串,永远不抛异常。 因为异常会中断整个 Agent 循环,而返回一句"查询失败,原因是……" 能让模型自己决定怎么跟用户解释。

再加一个工具试试

只加一个 get_weather,Agent 的价值还不明显。再补一个搜索工具:

from langchain_community.tools import DuckDuckGoSearchRun

        search = DuckDuckGoSearchRun()
        agent = create_agent(model=model, tools=[get_weather, search])

        result = agent.invoke({
            "messages": [{"role": "user",
                          "content": "北京今天天气怎么样?顺便查一下今天有什么科技新闻。"}]
        })
        

这时模型需要自己判断:天气用 get_weather, 新闻用 search。判断得准不准,直接取决于两个工具的 docstring 有没有把各自的边界写清楚。

小结

  • 真实 API 的价值在于暴露网络、限流、数据异常这些"脏活"
  • timeout 和 raise_for_status 是必须的,不是可选的
  • 工具函数应返回字符串而非抛异常,把决定权交给模型
  • docstring 要写清"什么时候不用",边界比功能更重要
  • 多工具协作时,描述之间的区分度决定选择准确率