《AI Agent 自学指北》第 4 篇:从单工具到多工具协作

1. 前言

第 3 篇跑通了第一个 Agent,能查天气了。但当时用的是硬编码的模拟数据,,根本不是真正的天气。

这一篇我决定全部用真实 API——能查真实天气、能搜实时新闻、能读网页内容。这些工具写好之后,后面可以直接打包进更大的项目里。

这一篇的代码,每一行都是能真正跑起来干活的。


2. 先设计三个实用的工具

我选了三个方向:

工具用的 API是否免费需要 API Key
查天气Open-Meteo免费不需要
搜新闻NewsAPI免费版每天 100 次需要(免费注册)
读网页requests + BeautifulSoup免费不需要

这三个工具覆盖了"外部数据获取"的核心场景,后面做任何项目基本都能用上。


3. 工具函数(可以单独 import 使用)

我先把每个工具写成独立的函数,这样以后可以直接 import 到其他项目里用。

3.1 查天气(真实 API)

import requests

# 天气数据工具:可以单独 import 使用
def get_weather(city: str) -> str:
    """
    查询指定城市的实时天气
    
    使用 Open-Meteo API(免费,无需 API Key)
    步骤:城市名 → 经纬度 → 天气数据
    """
    try:
        # 第 1 步:城市名转经纬度
        geo_url = f"https://geocoding-api.open-meteo.com/v1/search?name={city}&count=1&language=zh"
        geo_resp = requests.get(geo_url, timeout=10)
        geo_data = geo_resp.json()
        
        if "results" not in geo_data or len(geo_data["results"]) == 0:
            return f"未找到城市:{city}"
        
        lat = geo_data["results"][0]["latitude"]
        lon = geo_data["results"][0]["longitude"]
        city_name = geo_data["results"][0].get("name", city)
        
        # 第 2 步:查询天气
        weather_url = (
            f"https://api.open-meteo.com/v1/forecast?"
            f"latitude={lat}&longitude={lon}"
            f"&current_weather=true"
            f"&hourly=temperature_2m,relative_humidity_2m,weathercode"
            f"&timezone=auto"
        )
        weather_resp = requests.get(weather_url, timeout=10)
        weather_data = weather_resp.json()
        
        current = weather_data.get("current_weather", {})
        temp = current.get("temperature", "N/A")
        wind_speed = current.get("windspeed", "N/A")
        
        # 天气代码转中文描述
        weather_code = current.get("weathercode", 0)
        code_map = {
            0: "晴天", 1: "大部晴朗", 2: "多云", 3: "阴天",
            45: "雾", 48: "雾凇",
            51: "小毛毛雨", 53: "中毛毛雨", 55: "大毛毛雨",
            61: "小雨", 63: "中雨", 65: "大雨",
            71: "小雪", 73: "中雪", 75: "大雪",
            80: "小阵雨", 81: "中阵雨", 82: "大阵雨",
            95: "雷暴", 96: "雷暴加冰雹", 99: "强雷暴加冰雹"
        }
        weather_desc = code_map.get(weather_code, f"代码 {weather_code}")
        
        return f"{city_name}{temp}°C,{weather_desc},风速 {wind_speed} km/h"
    
    except Exception as e:
        return f"查询天气失败:{str(e)}"

这个函数做了什么

  1. 输入城市名(如"北京")
  2. 调用 Open-Meteo Geocoding API 把城市名转成经纬度
  3. 调用 Open-Meteo Weather API 查实时天气
  4. 把天气代码转成中文描述
  5. 返回格式化结果

不需要任何 API Key,直接就能用。

3.2 搜新闻(实时新闻 API)

import os

def get_news(query: str, api_key: str = None) -> str:
    """
    搜索最新新闻
    
    使用 NewsAPI(免费版每天 100 次请求)
    需要去 https://newsapi.org/register 免费注册获取 API Key
    """
    try:
        key = api_key or os.getenv("NEWS_API_KEY")
        if not key:
            return "请先设置 NEWS_API_KEY 环境变量,或传入 api_key 参数"
        
        url = "https://newsapi.org/v2/everything"
        params = {
            "q": query,
            "language": "zh",
            "sortBy": "publishedAt",
            "pageSize": 5,
            "apiKey": key
        }
        resp = requests.get(url, params=params, timeout=10)
        data = resp.json()
        
        if data.get("status") != "ok":
            return f"新闻查询失败:{data.get('message', '未知错误')}"
        
        articles = data.get("articles", [])
        if not articles:
            return f"没有找到关于「{query}」的新闻"
        
        result = [f"关于「{query}」的最新新闻:"]
        for i, article in enumerate(articles[:5], 1):
            title = article.get("title", "无标题")
            source = article.get("source", {}).get("name", "未知来源")
            url = article.get("url", "")
            result.append(f"{i}. [{source}] {title}")
            result.append(f"   {url}")
        
        return "\n".join(result)
    
    except Exception as e:
        return f"搜索新闻失败:{str(e)}"

注意:这个需要去 newsapi.org 免费注册拿到 API Key,然后设置到环境变量 NEWS_API_KEY 里。免费版每天 100 次,学习完全够用。

3.3 读网页内容

from bs4 import BeautifulSoup

def fetch_webpage(url: str) -> str:
    """
    获取网页内容并提取正文文本
    
    使用 requests + BeautifulSoup,无需 API Key
    可以读取任意公开网页的内容
    """
    try:
        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                          "AppleWebKit/537.36 (KHTML, like Gecko) "
                          "Chrome/120.0.0.0 Safari/537.36"
        }
        resp = requests.get(url, headers=headers, timeout=15)
        resp.encoding = resp.apparent_encoding
        
        soup = BeautifulSoup(resp.text, "html.parser")
        
        # 移除 script 和 style 标签
        for tag in soup(["script", "style", "nav", "footer", "header"]):
            tag.decompose()
        
        text = soup.get_text(separator="\n", strip=True)
        # 限制返回长度,避免 Token 爆炸
        max_chars = 2000
        if len(text) > max_chars:
            text = text[:max_chars] + "...(内容已截断)"
        
        return text if text else "无法提取网页内容"
    
    except Exception as e:
        return f"读取网页失败:{str(e)}"

这个工具不需要任何 API Key,可以读取任意公开网页的内容。后面做知识库、做日报汇总、做信息采集都能用上。


4. 给 LLM 注册工具

工具函数写好了,接下来注册给 LLM。注意看 description 字段,写得不叫很详细,这是为了让 LLM 能准确判断什么时候该用哪个工具。

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查询任意城市的实时天气,包括温度、天气状况、风速。适合用户问天气、温度、冷不冷、热不热等情况时调用",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称,如北京、上海、广州、纽约、东京等"
                    }
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_news",
            "description": "搜索指定话题的最新新闻,返回新闻标题和链接。适合用户问"最近有什么新闻"、"搜索一下xxx"等情况时调用",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "搜索关键词,如人工智能、天气预报、NBA"
                    }
                },
                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "fetch_webpage",
            "description": "读取指定网页的内容并提取正文文本。适合用户问"打开这个网页"、"看看这篇文章写了什么"等情况时调用",
            "parameters": {
                "type": "object",
                "properties": {
                    "url": {
                        "type": "string",
                        "description": "网页的完整 URL,必须以 http:// 或 https:// 开头"
                    }
                },
                "required": ["url"]
            }
        }
    }
]

5. 完整代码

"""
多工具 Agent
功能:查天气(真实API)+ 搜新闻(真实API)+ 读网页
"""

from openai import OpenAI
import json
import os
import requests
from bs4 import BeautifulSoup
from dotenv import load_dotenv

# ============ 1. 加载配置 ============
load_dotenv()
client = OpenAI(
    api_key=os.getenv("OPENAI_API_KEY"),
    base_url=os.getenv("OPENAI_BASE_URL")
)

# ============ 2. 工具函数 ============

def get_weather(city: str) -> str:
    """查询指定城市的实时天气(Open-Meteo API,免费,无需 API Key)"""
    try:
        # 城市名 → 经纬度
        geo_url = f"https://geocoding-api.open-meteo.com/v1/search?name={city}&count=1&language=zh"
        geo_resp = requests.get(geo_url, timeout=10)
        geo_data = geo_resp.json()
        
        if "results" not in geo_data or len(geo_data["results"]) == 0:
            return f"未找到城市:{city}"
        
        lat = geo_data["results"][0]["latitude"]
        lon = geo_data["results"][0]["longitude"]
        city_name = geo_data["results"][0].get("name", city)
        
        # 经纬度 → 天气
        weather_url = (
            f"https://api.open-meteo.com/v1/forecast?"
            f"latitude={lat}&longitude={lon}"
            f"&current_weather=true&hourly=temperature_2m,relative_humidity_2m,weathercode"
            f"&timezone=auto"
        )
        weather_resp = requests.get(weather_url, timeout=10)
        weather_data = weather_resp.json()
        
        current = weather_data.get("current_weather", {})
        temp = current.get("temperature", "N/A")
        wind_speed = current.get("windspeed", "N/A")
        
        code_map = {
            0: "晴天", 1: "大部晴朗", 2: "多云", 3: "阴天",
            45: "雾", 51: "小毛毛雨", 53: "中毛毛雨", 55: "大毛毛雨",
            61: "小雨", 63: "中雨", 65: "大雨",
            71: "小雪", 73: "中雪", 75: "大雪",
            80: "小阵雨", 81: "中阵雨", 82: "大阵雨",
            95: "雷暴"
        }
        weather_desc = code_map.get(current.get("weathercode", 0), "未知")
        
        return f"{city_name}{temp}°C,{weather_desc},风速 {wind_speed} km/h"
    
    except Exception as e:
        return f"查询天气失败:{str(e)}"


def get_news(query: str) -> str:
    """搜索最新新闻(NewsAPI,免费版每天 100 次)"""
    try:
        api_key = os.getenv("NEWS_API_KEY")
        if not api_key:
            return "请先注册 https://newsapi.org/register 获取 API Key,并设置 NEWS_API_KEY 环境变量"
        
        url = "https://newsapi.org/v2/everything"
        params = {
            "q": query,
            "language": "zh",
            "sortBy": "publishedAt",
            "pageSize": 5,
            "apiKey": api_key
        }
        resp = requests.get(url, params=params, timeout=10)
        data = resp.json()
        
        if data.get("status") != "ok":
            return f"新闻查询失败:{data.get('message', '未知错误')}"
        
        articles = data.get("articles", [])
        if not articles:
            return f"没有找到关于「{query}」的新闻"
        
        result = [f"关于「{query}」的最新新闻:"]
        for i, article in enumerate(articles[:5], 1):
            title = article.get("title", "无标题")
            source = article.get("source", {}).get("name", "未知来源")
            url = article.get("url", "")
            result.append(f"{i}. [{source}] {title}")
            result.append(f"   {url}")
        
        return "\n".join(result)
    
    except Exception as e:
        return f"搜索新闻失败:{str(e)}"


def fetch_webpage(url: str) -> str:
    """读取网页内容(requests + BeautifulSoup,无需 API Key)"""
    try:
        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                          "AppleWebKit/537.36"
        }
        resp = requests.get(url, headers=headers, timeout=15)
        resp.encoding = resp.apparent_encoding
        
        soup = BeautifulSoup(resp.text, "html.parser")
        for tag in soup(["script", "style", "nav", "footer", "header"]):
            tag.decompose()
        
        text = soup.get_text(separator="\n", strip=True)
        if len(text) > 2000:
            text = text[:2000] + "...(内容已截断)"
        
        return text if text else "无法提取网页内容"
    
    except Exception as e:
        return f"读取网页失败:{str(e)}"


def execute_tool(name: str, args: dict) -> str:
    """根据工具名执行对应的函数"""
    if name == "get_weather":
        return get_weather(args["city"])
    elif name == "get_news":
        return get_news(args["query"])
    elif name == "fetch_webpage":
        return fetch_webpage(args["url"])
    else:
        return f"未知工具:{name}"


# ============ 3. 工具描述 ============
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查询任意城市的实时天气,包括温度、天气状况、风速。适合用户问天气、温度、冷不冷等情况时调用",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名称,如北京、上海"}
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_news",
            "description": "搜索指定话题的最新新闻,返回新闻标题和链接。适合用户问"最近有什么新闻"、"搜索一下xxx"等情况时调用",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "搜索关键词,如人工智能、NBA"}
                },
                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "fetch_webpage",
            "description": "读取指定网页的内容并提取正文文本。适合用户问"打开这个网页"、"看看这篇文章写了什么"等情况时调用",
            "parameters": {
                "type": "object",
                "properties": {
                    "url": {"type": "string", "description": "网页完整 URL,以 http:// 或 https:// 开头"}
                },
                "required": ["url"]
            }
        }
    }
]

# ============ 4. 对话循环 ============
def chat_with_agent(user_input: str) -> str:
    """和 Agent 对话,支持多轮工具调用"""
    messages = [{"role": "user", "content": user_input}]
    
    response = client.chat.completions.create(
        model="deepseek-chat",
        messages=messages,
        tools=tools,
        tool_choice="auto"
    )
    
    msg = response.choices[0].message
    
    while msg.tool_calls:
        messages.append(msg)
        
        for tool_call in msg.tool_calls:
            name = tool_call.function.name
            args = json.loads(tool_call.function.arguments)
            
            print(f"  🧠 调用工具:{name}({args})")
            result = execute_tool(name, args)
            print(f"  🛠 返回结果:{result[:100]}...")
            
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result
            })
        
        response = client.chat.completions.create(
            model="deepseek-chat",
            messages=messages,
            tools=tools,
            tool_choice="auto"
        )
        msg = response.choices[0].message
    
    return msg.content


# ============ 5. 测试 ============
if __name__ == "__main__":
    questions = [
        "北京的天气怎么样?",
        "最近人工智能有什么新闻?",
        "帮我查一下上海的天气,再搜一下机器学习的新闻",
    ]
    
    for q in questions:
        print(f"\n{'='*50}")
        print(f"👤 用户:{q}")
        print(f"{'='*50}")
        answer = chat_with_agent(q)
        print(f"🤖 Agent:{answer}")

6. 代码注意内容

  1. 安装依赖:
pip install openai python-dotenv requests beautifulsoup4
  1. .env 文件里配置:
OPENAI_API_KEY=你的API密钥
OPENAI_BASE_URL=https://api.deepseek.com  # 如果用 DeepSeek
NEWS_API_KEY=你的NewsAPI密钥  # 去 https://newsapi.org/register 免费注册
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值