Qwen-Agent的使用示例-天气查询(function calling)

关于心知天气

心知天气(Seniverse)是一家提供气象数据服务的平台。该服务适合开发者或企业集成到应用或系统中。
网址:https://seniverse.yuque.com
心知天气API官方文档

访问频限说明
  • 免费版限制:每分钟最多20次API调用,覆盖国内370个主要城市,可提供3项基础天气数据(如实时天气、预报等)。
  • 适用场景:适合低频需求或测试用途,如需更高频次需升级付费套餐。

关于 function call

function call(函数调用):指的是大模型(如Qwen、GPT等)在推理过程中,自动识别用户意图并调用本地或注册的Python函数(如工具函数、插件等)。调用过程在本地Python环境内完成,数据流不经过外部服务。

本示例的实现方式分析

  • WeatherTool 通过 @register_tool('get_weather') 注册为Qwen-Agent的工具(tool),并在call方法中用asyncio.run(weather_controller.get_weather(args['city']))实现了本地的异步天气查询。
  • 该工具的实现逻辑完全在本地Python进程内完成,虽然底层用到了httpx异步请求外部API(心知天气),但工具的注册、调用和数据流都在本地Python环境内,并未通过MCP协议暴露为独立服务。
  • Qwen-Agent在推理时会自动识别需要调用get_weather,并通过function call机制调用本地注册的WeatherTool

function call调用范式的典型实现:

 - 工具注册在本地  
 - 工具调用在本地  
 - 工具内部可访问外部API,但对大模型来说是本地函数
 - **不是MCP调用**。如果要实现MCP,需要将天气查询功能通过MCP协议暴露为服务,模型通过MCP协议远程调用该服务(如通过FastMCP、mcp-server等框架实现)。

如何区分function call和MCP?

  • function call:模型直接调用本地注册的Python函数/类,调用链不出本地进程。
  • MCP:模型通过协议(如MCP、gRPC、HTTP等)调用外部服务,通常需要单独运行MCP服务进程,模型与服务解耦。

实现代码如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Qwen-Agent的使用示例 - 心知天气MCP实现
"""

import os
import json
import time
import hmac
import base64
import urllib
import asyncio
from typing import Dict, Any, Optional
from dataclasses import dataclass
from abc import ABC, abstractmethod
import httpx
from qwen_agent.agents import Assistant

# ============ Model Layer ============
@dataclass
class WeatherInfo:
    """天气信息数据模型"""
    temperature: str
    condition: str
    city: str
    humidity: str
    wind_direction: str
    wind_speed: str

    def to_dict(self) -> Dict[str, str]:
        return {
            "temperature": self.temperature,
            "condition": self.condition,
            "city": self.city,
            "humidity": self.humidity,
            "wind_direction": self.wind_direction,
            "wind_speed": self.wind_speed
        }

# ============ Provider Layer ============
class WeatherProvider(ABC):
    """天气数据提供者抽象基类"""
    @abstractmethod
    async def get_weather(self, city: str) -> Optional[WeatherInfo]:
        pass

class SeniverseWeatherProvider(WeatherProvider):
    """心知天气数据提供者"""
    def __init__(self):
        self.api_secret = "Sh7XVAAnSWd5TNcET"  # 替换为您的心知天气API私钥
        self.base_url = "https://api.seniverse.com/v3"
        self.unit = "c"  # 温度单位: c 摄氏度, f 华氏度
        self.language = "zh-Hans"  # 语言: zh-Hans 简体中文, en 英文

    def _generate_signature(self, params: Dict[str, str]) -> str:
        """生成API签名"""
        params_str = '&'.join([f"{k}={params[k]}" for k in sorted(params.keys())])
        message = 'GET\napi.seniverse.com\n/v3/weather/now.json\n' + params_str
        signature = base64.b64encode(
            hmac.new(
                self.api_secret.encode(),
                message.encode(),
                digestmod='sha1'
            ).digest()
        ).decode()
        return signature

    async def get_weather(self, city: str) -> Optional[WeatherInfo]:
        """获取天气信息"""
        try:
            params = {
                'location': city,
                'key': self.api_secret,
                'unit': self.unit,
                'language': self.language
            }
            
            # 生成签名
            params['sig'] = self._generate_signature(params)
            
            # 构建请求URL
            url = f"{self.base_url}/weather/now.json?{urllib.parse.urlencode(params)}"
            print(f"Request URL: {url}")
            
            async with httpx.AsyncClient() as client:
                response = await client.get(url)
                response.raise_for_status()
                data = response.json()
                
                if 'results' in data and len(data['results']) > 0:
                    result = data['results'][0]
                    now = result['now']
                    
                    return WeatherInfo(
                        temperature=f"{now['temperature']}°C",
                        condition=now['text'],
                        city=result['location']['name'],
                        humidity=now.get('humidity', 'N/A'),
                        wind_direction=now.get('wind_direction', 'N/A'),
                        wind_speed=f"{now.get('wind_speed', 'N/A')}km/h"
                    )
                return None
                
        except Exception as e:
            print(f"获取天气数据失败: {str(e)}")
            return None

# ============ Controller Layer ============
class WeatherController:
    """天气查询控制器"""
    def __init__(self, provider: WeatherProvider):
        self._provider = provider

    async def get_weather(self, city: str) -> Dict[str, Any]:
        """获取天气信息"""
        weather_info = await self._provider.get_weather(city)
        if weather_info:
            return weather_info.to_dict()
        return {"error": f"没有找到{city}的天气信息"}

# 初始化天气控制器
weather_controller = WeatherController(SeniverseWeatherProvider())

# ============ Tool Layer ============
from qwen_agent.tools.base import BaseTool, register_tool

@register_tool('get_weather')
class WeatherTool(BaseTool):
    description = "获取指定城市的天气信息"
    parameters = {
        'type': 'object',
        'properties': {
            'city': {
                'type': 'string',
                'description': '城市名称'
            }
        },
        'required': ['city']
    }

    def call(self, params: str, **kwargs) -> Dict[str, Any]:
        """同步调用接口"""
        try:
            args = json.loads(params)
            # 使用asyncio运行异步函数
            return asyncio.run(weather_controller.get_weather(args['city']))
        except json.JSONDecodeError:
            return {"error": "参数解析失败"}
        except Exception as e:
            return {"error": f"查询天气失败: {str(e)}"}

# 创建Assistant实例
assistant = Assistant(
    llm={
        'model': 'qwen-max',
        'model_type': 'qwen_dashscope',
        'api_key': os.getenv("DASHSCOPE_API_KEY")   # 若没有配置环境变量,请用百炼API Key替换
    },
    function_list=['get_weather']
)


if __name__ == "__main__":
    messages = []

    # 测试Agent对话
    user_query = "我应该带伞出门吗?我在广州。"
    print(f"\n用户: {user_query}")
    messages.append({"role": "user", "content": user_query})

    final_response = None
    for response in assistant.run(messages):
        if isinstance(response, list):
            final_response = response[-1]
            if isinstance(final_response, dict) and "content" in final_response:
                messages.append(final_response)
    print(f"Agent: {final_response['content']}")


代码详解

1. 启动与初始化阶段

  • 1.1 脚本入口

    • 代码从if __name__ == "__main__":开始执行,初始化对话消息列表messages = []
  • 1.2 工具注册

    • 通过@register_tool('get_weather'),将WeatherTool注册为Qwen-Agent可调用的工具(function call工具)。
    • 工具描述、参数schema等信息被Qwen-Agent框架收集,用于后续大模型推理时的函数调用决策。
  • 1.3 Assistant实例化

    • 创建Assistant对象,指定大模型(如qwen-max)、API Key等参数,并声明可用工具function_list=['get_weather']

2. 用户输入与对话流程

  • 2.1 用户输入

    • 用户输入一句自然语言问题(如“我应该带伞出门吗?我在广州。”),被加入messages列表。
  • 2.2 Assistant.run(messages)

    • 调用assistant.run(messages),进入主对话流程。

3. 大模型推理与function call决策

  • 3.1 LLM理解意图

    • Qwen大模型分析用户输入,判断需要调用天气查询工具(get_weather)。
  • 3.2 生成function call请求

    • 大模型自动生成function call请求,参数为{"city": "广州"},并调用本地注册的WeatherTool

4. 工具调用与数据获取

  • 4.1 WeatherTool.call方法执行

    • Qwen-Agent框架调用WeatherTool.call(params),params为{"city": "广州"}的JSON字符串。
  • 4.2 参数解析

    • call方法内部用json.loads(params)解析参数,提取城市名。
  • 4.3 异步天气查询

    • 通过asyncio.run(weather_controller.get_weather(args['city']))同步调用异步天气查询控制器。
  • 4.4 Controller调度Provider

    • WeatherController.get_weather调用SeniverseWeatherProvider.get_weather,实际发起HTTP请求。
  • 4.5 HTTP请求心知天气API

    • 构造API请求参数(含签名),用httpx.AsyncClient异步请求心知天气API,获取实时天气数据。
  • 4.6 数据解析与模型化

    • 解析API返回的JSON,提取温度、天气预报等,封装为WeatherInfo数据对象。
  • 4.7 数据返回

    • WeatherInfo.to_dict()转为字典,逐层返回到WeatherTool.call,再返回给Qwen-Agent主流程。

5. 大模型生成回复

  • 5.1 工具结果注入上下文

    • 工具返回的天气信息被注入到大模型的上下文中。
  • 5.2 LLM生成自然语言回复

    • Qwen大模型基于工具返回结果,生成最终的自然语言回复(如“广州当前天气为小雨,温度30°C,建议带伞出门。”)。

6. 输出与对话循环

  • 6.1 回复输出

    • 最终回复通过print(f"Agent: {final_response['content']}")输出到终端。
  • 6.2 多轮对话(可选)

    • 若有多轮对话需求,可继续追加用户输入与Agent回复,循环上述流程。

总结流程图

  1. 用户输入 → 2. Assistant.run → 3. LLM推理 → 4. function call触发 → 5. WeatherTool.call → 6. Controller/Provider异步API → 7. 数据返回 → 8. LLM生成回复 → 9. 输出

系统思维补充

  • 本流程实现了“自然语言→智能工具调用→外部API→结构化数据→自然语言”的完整闭环。
  • function call机制极大提升了大模型的工具能力和可扩展性。
  • 代码结构清晰,便于后续扩展更多工具或对接不同API。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值