LangChain系列文章超链接:

《Python+LangChain+大模型实战:使用通用配置加载器的Few‑Shot小样本提示词教程》

《使用Python版LangChain调用外部函数实战:实现智能天气查询》

《Python + LangChain Agent 实战:从单城市查询到多城市天气智能对比》

《Python+LangChain Agent 工具选择逻辑深度解析》

在LangChain开发中,Agent的核心能力之一就是“根据用户提问,自动选择合适的工具执行任务”,这也是Agent区别于普通Chain的关键。本文将围绕Agent工具选择逻辑展开,从实战案例入手,拆解工具选择的核心原理、关键配置,重点剖析工具选择的源码逻辑,帮你彻底搞懂Agent是如何“判断”该调用哪个工具、何时不调用工具,同时提供可直接运行的实战代码(兼容LangChain 1.x版本),适合想要深入理解Agent底层逻辑的开发者。

一、实战案例:Agent工具选择demo(天气+股票工具)

先通过一个完整的实战案例,直观感受Agent的工具选择能力:实现两个自定义工具(天气查询、股票查询),Agent能自动根据用户提问匹配对应工具,不命中工具时直接用大模型兜底回答,为后续源码解析铺垫,废话不多说,直接上代码。

1.天气工具包

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Project :pythonProject
@File    :weather_util.py
@Author  :BillFang
@Date    :2026/4/15 09:19
@Description :
"""

import requests
from langchain_core.tools import tool

from config_loader import ConfigLoader


class OpenWeather():
    def __init__(self, APPID):
        self.app_id = APPID

    def get_lat_lon(self, city_name):
        geo_url = "http://api.openweathermap.org/geo/1.0/direct"
        params = {
            "q": city_name,
            "limit": 1,
            "appid": self.app_id,
        }
        try:
            res = requests.get(geo_url, params=params)
            res.raise_for_status()
            data = res.json()
            if not data:
                print("未找到该城市")
                return None, None
            lat = data[0]["lat"]
            lon = data[0]["lon"]
            name = data[0]["name"]
            country = data[0]["country"]
            print(f"定位成功:{name}, {country}")
            return lat, lon
        except Exception as e:
            print("获取经纬度失败:", e)
            return None, None


@tool
def get_city_weather_data(city_name) -> dict:
    """
        调用 OpenWeatherMap API 获取指定城市的天气信息
        :param city_name: 城市名称
        :return: 天气数据字典 / None
        """
    print("调用天气查询工具,查询城市:" + city_name + "的天气。")
    config_loader = ConfigLoader()
    openweather_config = config_loader.get_openweather_config()
    print(openweather_config)
    print(openweather_config["app_id"])
    weather_client = OpenWeather(APPID=openweather_config["app_id"])
    base_url = "https://api.openweathermap.org/data/2.5/weather"
    lat, lon = weather_client.get_lat_lon(city_name)
    # 请求参数
    params = {
        "lat": lat,
        "lon": lon,
        "appid": weather_client.app_id,
        "units": "metric",  # 单位:metric=摄氏度,imperial=华氏度,默认开尔文
        "lang": "zh_cn"  # 返回中文描述
    }

    try:
        # 发送 GET 请求
        response = requests.get(base_url, params=params)
        # 检查请求是否成功
        response.raise_for_status()
        # 解析 JSON 数据
        weather_data = response.json()
        return weather_data

    except requests.exceptions.RequestException as e:
        print(f"请求出错:{e}")
        return None


if __name__ == '__main__':
    # 方法加上@tool时必须使用.invoke方法调用
    print(get_city_weather_data.invoke('扬州'))

调用 OpenWeatherMap API 查询指定城市的实时天气数据。它接收城市名称作为参数,先通过经纬度转换接口定位该城市,再调用天气接口获取温度、湿度、天气状况等信息,最终以字典格式返回完整的天气数据。该方法被 @tool 装饰器标记,使其可以作为工具被 LangChain 智能体(Agent)直接调用。先单独测一下这个工具方法,输出如下:

E:\pycharm_code\.venv\Scripts\python.exe E:\pycharm_code\langchain_test\utils\weather_util.py 
调用天气查询工具,查询城市:扬州的天气。
{'app_id': 'ab203b794f7b27b777bb48f4eb30838c'}
ab203b794f7b27b777bb48f4eb30838c
定位成功:Yangzhou City, CN
{'coord': {'lon': 119.4078, 'lat': 32.3969}, 'weather': [{'id': 804, 'main': 'Clouds', 'description': '阴,多云', 'icon': '04d'}], 'base': 'stations', 'main': {'temp': 26.33, 'feels_like': 26.33, 'temp_min': 26.33, 'temp_max': 26.33, 'pressure': 1011, 'humidity': 53, 'sea_level': 1011, 'grnd_level': 1009}, 'visibility': 10000, 'wind': {'speed': 3.05, 'deg': 245, 'gust': 4.62}, 'clouds': {'all': 86}, 'dt': 1778117306, 'sys': {'country': 'CN', 'sunrise': 1778101850, 'sunset': 1778150822}, 'timezone': 28800, 'id': 1787227, 'name': 'Yangzhou', 'cod': 200}

进程已结束,退出代码为 0

2.股票工具包

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Project :pythonProject
@File    :weather_util.py
@Author  :BillFang
@Date    :2026/5/02 09:19
@Description :
"""
import sys
import re
import io
from datetime import datetime
from urllib import request
from urllib.error import URLError, HTTPError

from langchain_core.tools import tool

STOCK_DB: dict[str, tuple[str, str]] = {
    # ── 金融 ──
    "工商银行": ("601398", "sh"),
    "建设银行": ("601939", "sh"),
    "农业银行": ("601288", "sh"),
    "中国银行": ("601988", "sh"),
    "招商银行": ("600036", "sh"),
    "交通银行": ("601328", "sh"),
    "兴业银行": ("601166", "sh"),
    "金龙鱼": ("300999", "sz"),
    # ── 汽车 ──
    "比亚迪": ("002594", "sz"),
    "上汽集团": ("600104", "sh"),
    "蔚来": ("09866", "hk"),
    # ── 新能源 / 光伏 / 电池 ──
    "宁德时代": ("300750", "sz"),
    "隆基绿能": ("601012", "sh"),
    "阳光电源": ("300274", "sz"),
    "通威股份": ("600438", "sh"),

    "赣锋锂业": ("002460", "sz"),
    "天齐锂业": ("002466", "sz"),
    # ── 医药 ──
    "恒瑞医药": ("600276", "sh"),
    "药明康德": ("603259", "sh"),

    "长春高新": ("000661", "sz"),
    "爱尔眼科": ("300015", "sz"),
    "智飞生物": ("300122", "sz"),
    "复星医药": ("600196", "sh"),
    "同仁堂": ("600085", "sh"),
    # ── 房地产 ──
    "万科A": ("000002", "sz"),
    "保利发展": ("600048", "sh"),
    "招商蛇口": ("001979", "sz"),
    "绿地控股": ("600606", "sh"),
    # ── 建筑 / 工程机械 ──
    "中国建筑": ("601668", "sh"),
    "中国中铁": ("601390", "sh"),
    "中国铁建": ("601186", "sh"),
    "中国交建": ("601800", "sh"),
    "三一重工": ("600031", "sh"),
    "中联重科": ("000157", "sz"),
    "徐工机械": ("000425", "sz"),
}

# 反向索引:代码 → 名称,启动时构建
CODE_TO_NAME: dict[str, str] = {}


def _build_reverse_index():
    for name, (code, market) in STOCK_DB.items():
        CODE_TO_NAME[f"{market}{code}"] = name


_build_reverse_index()


# ── 股票查找 ─────────────────────────────────────────────────────────

def lookup_stock(keyword: str) -> list[dict]:
    """根据名称或代码查找股票。返回匹配列表。"""
    results = []
    kw_lower = keyword.strip().lower()

    # 1. 精确名称匹配
    if kw_lower in STOCK_DB:
        code, market = STOCK_DB[kw_lower]
        results.append({"name": kw_lower, "code": code, "market": market, "match": "exact"})
        return results

    # 2. 模糊名称匹配
    for name, (code, market) in STOCK_DB.items():
        if kw_lower in name or name in kw_lower:
            results.append({"name": name, "code": code, "market": market, "match": "fuzzy"})

    if results:
        return results

    # 3. 纯数字代码匹配
    if kw_lower.isdigit():
        if len(kw_lower) <= 4:
            for name, (code, market) in STOCK_DB.items():
                if kw_lower in code:
                    results.append({"name": name, "code": code, "market": market, "match": "code_fuzzy"})
        else:
            for name, (code, market) in STOCK_DB.items():
                if code == kw_lower:
                    results.append({"name": name, "code": code, "market": market, "match": "code_exact"})
                    break
            if results:
                return results
            code = kw_lower
            market = "sh" if code.startswith("6") else "sz"
            results.append({"name": code, "code": code, "market": market, "match": "code_guess"})

    return results


# ── 实时股价查询 ─────────────────────────────────────────────────────

def fetch_realtime_price(code: str, market: str) -> dict | None:
    """通过新浪财经接口获取实时股价。"""
    full_code = f"{market}{code}"
    url = f"https://hq.sinajs.cn/list={full_code}"
    headers = {"Referer": "https://finance.sina.com.cn"}

    try:
        req_obj = request.Request(url, headers=headers)
        with request.urlopen(req_obj, timeout=8) as resp:
            text = resp.read().decode("gbk")
    except (URLError, HTTPError, OSError):
        return None

    match = re.search(r'"([^"]+)"', text)
    if not match:
        return None

    fields = match.group(1).split(",")
    if len(fields) < 32 or fields[0] == "" or fields[3] == "":
        return None

    try:
        name = fields[0]
        today_open = float(fields[1])
        yesterday_close = float(fields[2])
        current = float(fields[3])
        high = float(fields[4])
        low = float(fields[5])
        volume = int(fields[8])
        amount = float(fields[9])
        date_str = fields[30]
        time_str = fields[31]

        change = current - yesterday_close
        change_pct = (change / yesterday_close) * 100 if yesterday_close != 0 else 0.0

        return {
            "name": name,
            "code": code,
            "market": market,
            "current": current,
            "change": change,
            "change_pct": change_pct,
            "open": today_open,
            "high": high,
            "low": low,
            "yesterday_close": yesterday_close,
            "volume": volume,
            "amount": amount,
            "datetime": f"{date_str} {time_str}",
        }
    except (ValueError, ZeroDivisionError):
        return None


# ── 格式化输出 ───────────────────────────────────────────────────────


def format_price(info: dict) -> str:
    """将股票信息格式化为可读字符串。"""
    arrow = "↑" if info["change"] >= 0 else "↓"
    label = "涨" if info["change"] >= 0 else "跌"

    lines = [
        "",
        "=" * 46,
        f"  {info['name']}  [{info['market']}{info['code']}]",
        "  " + "-" * 42,
        f"  当前价:{info['current']:.2f} 元",
        f"  涨跌:  {arrow} {label} {info['change_pct']:+.2f}%({info['change']:+.2f} 元)",
        f"  今开:  {info['open']:.2f}     昨收:{info['yesterday_close']:.2f}",
        f"  最高:  {info['high']:.2f}     最低:{info['low']:.2f}",
        f"  成交量:{info['volume']:,} 手",
        f"  数据时间:{info['datetime']}",
        "=" * 46,
        "",
    ]
    return "\n".join(lines)


# ── 主入口 ───────────────────────────────────────────────────────────
@tool
def query_stock(stock_name: str) -> str:
    """
    查询股票实时信息,返回格式化结果。

    Args:
        stock_name: 股票名称,例如 "比亚迪"
    """
    print("调用股票查询工具,查询" + stock_name + "的股价。")
    stocks = lookup_stock(stock_name)

    if not stocks:
        return f'\n未找到与「{stock_name}」相关的股票。请尝试输入完整名称或6位代码。\n'

    target = stocks[0]

    price_info = fetch_realtime_price(target["code"], target["market"])
    if not price_info:
        return f'\n获取 {target["name"]} 实时股价失败,请稍后重试。\n'

    result = format_price(price_info)

    if len(stocks) > 1:
        result += f'提示:找到 {len(stocks)} 个匹配项,已显示最佳匹配「{target["name"]}」。\n'
        result += "      其他匹配: " + "、".join(s["name"] for s in stocks[1:6]) + "\n"
    elif target.get("match") != "exact":
        result += f'提示:已匹配到「{target["name"]}」。\n'

    return result

if __name__ == "__main__":
    stock_name = "比亚迪"
    print(query_stock.invoke(stock_name))

query_stock 是一个被 @tool 装饰器标记的股票查询工具函数,作用是接收股票名称或代码,返回该股票的实时行情信息

执行流程分为四步:

  1. 股票查找:调用 lookup_stock 函数,支持精确名称匹配、模糊名称匹配和代码匹配(6位数字),从内置的 STOCK_DB 字典中定位股票。

  2. 获取实时数据:使用查到的股票代码和市场标识(sh/sz),通过新浪财经接口 hq.sinajs.cn 获取实时股价数据,包括当前价、涨跌幅、开盘价、最高最低价、成交量等。

  3. 格式化输出:将获取的数据通过 format_price 函数整理成美观的字符串,用箭头(↑↓)和文字(涨/跌)直观展示价格变动。

  4. 返回结果:输出格式化后的股价信息;如果未找到股票或获取失败,返回相应的错误提示。

该工具可被 LangChain 智能体调用,让 AI 能够回答用户关于股票价格的问题,我们也单独测试一下这个工具包,输出如下:

E:\pycharm_code\.venv\Scripts\python.exe E:\pycharm_code\langchain_test\utils\stock_util.py 
调用股票查询工具,查询比亚迪的股价。

==============================================
  比亚迪  [sz002594]
  ------------------------------------------
  当前价:100.76 元
  涨跌:  ↑ 涨 +0.01%(+0.01 元)
  今开:  101.05     昨收:100.75
  最高:  101.26     最低:100.52
  成交量:2,932,700 手
  数据时间:2026-05-07 09:32:48
==============================================

进程已结束,退出代码为 0

3. Agent实现

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Project :pythonProject
@File    :langchain_agent.py
@Author  :BillFang
@Date    :2026/5/2 13:58
@Description : LangGraph Agent 实现天气查询功能
"""

# 标准库导入

from langchain.agents import create_agent
# 第三方库导入
from langchain_core.messages import HumanMessage

# 本地模块导入
from init_llm import init_llm
from stock_util import query_stock
from weather_util import get_city_weather_data


# 初始化LLM实例
llm = init_llm()

def main():
    """主函数:执行天气查询示例"""
    try:
        # 构建工作流
        agent = create_agent(
            llm,
            tools=[get_city_weather_data, query_stock],
            system_prompt="你是一个非常厉害的AI助手,但不了解实时的一些知识"
        )

        # 执行查询
        query = "今天北京和扬州的天气比较,哪个城市的天气更适合出去踏青?"
        result = agent.invoke(
            {"messages": [HumanMessage(query)]}
        , debug=True)
        print(result["messages"][-1].content)
        print("============================================")
        print("=================分割线======================")
        print("============================================")
        # 执行查询
        query = "帮我查一下比亚迪和工商银行的实时股票信息,哪个涨幅更大?"
        result = agent.invoke(
            {"messages": [HumanMessage(query)]}
            , debug=True)
        print(result["messages"][-1].content)
    except Exception as e:
        print(f"程序执行失败: {e}")


if __name__ == '__main__':
    main()

这段代码实现了一个基于LangGraph的智能Agent,用于自动回答天气比较和股票查询问题。首先通过init_llm函数初始化大语言模型,然后使用create_agent创建一个智能体,配置了两个工具:get_city_weather_data用于查询城市天气,query_stock用于查询股票实时行情,并设置了系统提示让AI扮演一个不了解实时知识的助手。该Agent的核心能力在于能够根据用户问题的语义自动选择调用合适的工具,例如当用户询问天气相关问题时,Agent会识别出需要调用天气查询工具;当用户询问股票价格或涨幅时,Agent会自动选择股票查询工具,无需人工干预。在主函数中,Agent先后执行两个查询任务:第一个任务是比较北京和扬州的天气,判断哪个城市更适合踏青,Agent自动识别出需要天气数据,调用天气工具获取两个城市的信息并进行综合分析;第二个任务是查询比亚迪和工商银行的实时股票信息,比较哪个涨幅更大,Agent自动判断需要股票数据,调用股票工具获取两只股票的信息并计算涨跌幅。整个过程体现了Agent的智能路由和自动工具选择能力,使得AI能够突破知识截止日期的限制,根据问题类型灵活调用相应的实时数据接口来回答复杂问题,最终从result消息列表中提取最后一轮的内容作为回答输出。执行输出如下:

E:\pycharm_code\.venv\Scripts\python.exe E:\pycharm_code\langchain_test\langchain_create_agent.py 
[values] {'messages': [HumanMessage(content='今天北京和扬州的天气比较,哪个城市的天气更适合出去踏青?', additional_kwargs={}, response_metadata={}, id='8244100a-8069-4479-9b18-63ca221b75ad')]}
[updates] {'model': {'messages': [AIMessage(content='我来帮您查询北京和扬州的天气情况,然后为您分析哪个城市更适合踏青。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 68, 'prompt_tokens': 389, 'total_tokens': 457, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 0, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 4}}, 'model_provider': 'openai', 'model_name': 'mimo-v2-flash', 'system_fingerprint': None, 'id': '67eb016123d344c49f8f660af5773e60', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019e0017-4795-7250-9b3f-203e23f56270-0', tool_calls=[{'name': 'get_city_weather_data', 'args': {'city_name': '北京'}, 'id': 'call_15838b1a4ce54158988b8e02', 'type': 'tool_call'}, {'name': 'get_city_weather_data', 'args': {'city_name': '扬州'}, 'id': 'call_e607cd0e625d49729f4caab7', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 389, 'output_tokens': 68, 'total_tokens': 457, 'input_token_details': {'cache_read': 4}, 'output_token_details': {'reasoning': 0}})]}}
[values] {'messages': [HumanMessage(content='今天北京和扬州的天气比较,哪个城市的天气更适合出去踏青?', additional_kwargs={}, response_metadata={}, id='8244100a-8069-4479-9b18-63ca221b75ad'), AIMessage(content='我来帮您查询北京和扬州的天气情况,然后为您分析哪个城市更适合踏青。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 68, 'prompt_tokens': 389, 'total_tokens': 457, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 0, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 4}}, 'model_provider': 'openai', 'model_name': 'mimo-v2-flash', 'system_fingerprint': None, 'id': '67eb016123d344c49f8f660af5773e60', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019e0017-4795-7250-9b3f-203e23f56270-0', tool_calls=[{'name': 'get_city_weather_data', 'args': {'city_name': '北京'}, 'id': 'call_15838b1a4ce54158988b8e02', 'type': 'tool_call'}, {'name': 'get_city_weather_data', 'args': {'city_name': '扬州'}, 'id': 'call_e607cd0e625d49729f4caab7', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 389, 'output_tokens': 68, 'total_tokens': 457, 'input_token_details': {'cache_read': 4}, 'output_token_details': {'reasoning': 0}})]}
调用天气查询工具,查询城市:北京的天气。
{'app_id': 'ab203b794f7b27b777bb48f4eb30838c'}
ab203b794f7b27b777bb48f4eb30838c
调用天气查询工具,查询城市:扬州的天气。
{'app_id': 'ab203b794f7b27b777bb48f4eb30838c'}
ab203b794f7b27b777bb48f4eb30838c
定位成功:Yangzhou City, CN
[updates] {'tools': {'messages': [ToolMessage(content='{"coord": {"lon": 119.4078, "lat": 32.3969}, "weather": [{"id": 803, "main": "Clouds", "description": "多云", "icon": "04d"}], "base": "stations", "main": {"temp": 28.3, "feels_like": 28.7, "temp_min": 28.3, "temp_max": 28.3, "pressure": 1011, "humidity": 49, "sea_level": 1011, "grnd_level": 1009}, "visibility": 10000, "wind": {"speed": 3.6, "deg": 297, "gust": 4.62}, "clouds": {"all": 84}, "dt": 1778117996, "sys": {"country": "CN", "sunrise": 1778101850, "sunset": 1778150822}, "timezone": 28800, "id": 1787227, "name": "Yangzhou", "cod": 200}', name='get_city_weather_data', id='2ad45f1a-e724-4bf5-be96-7864ea497dc6', tool_call_id='call_e607cd0e625d49729f4caab7')]}}
定位成功:Beijing, CN
[updates] {'tools': {'messages': [ToolMessage(content='{"coord": {"lon": 116.3913, "lat": 39.9057}, "weather": [{"id": 800, "main": "Clear", "description": "晴", "icon": "01d"}], "base": "stations", "main": {"temp": 18.94, "feels_like": 17.23, "temp_min": 18.94, "temp_max": 18.94, "pressure": 1020, "humidity": 13, "sea_level": 1020, "grnd_level": 1015}, "visibility": 10000, "wind": {"speed": 6.11, "deg": 347, "gust": 9.25}, "clouds": {"all": 0}, "dt": 1778117424, "sys": {"type": 1, "id": 9609, "country": "CN", "sunrise": 1778101691, "sunset": 1778152429}, "timezone": 28800, "id": 1816670, "name": "Beijing", "cod": 200}', name='get_city_weather_data', id='d226a5d7-cb6f-4f68-ba7e-5220d4aba4ba', tool_call_id='call_15838b1a4ce54158988b8e02')]}}
[values] {'messages': [HumanMessage(content='今天北京和扬州的天气比较,哪个城市的天气更适合出去踏青?', additional_kwargs={}, response_metadata={}, id='8244100a-8069-4479-9b18-63ca221b75ad'), AIMessage(content='我来帮您查询北京和扬州的天气情况,然后为您分析哪个城市更适合踏青。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 68, 'prompt_tokens': 389, 'total_tokens': 457, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 0, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 4}}, 'model_provider': 'openai', 'model_name': 'mimo-v2-flash', 'system_fingerprint': None, 'id': '67eb016123d344c49f8f660af5773e60', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019e0017-4795-7250-9b3f-203e23f56270-0', tool_calls=[{'name': 'get_city_weather_data', 'args': {'city_name': '北京'}, 'id': 'call_15838b1a4ce54158988b8e02', 'type': 'tool_call'}, {'name': 'get_city_weather_data', 'args': {'city_name': '扬州'}, 'id': 'call_e607cd0e625d49729f4caab7', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 389, 'output_tokens': 68, 'total_tokens': 457, 'input_token_details': {'cache_read': 4}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='{"coord": {"lon": 116.3913, "lat": 39.9057}, "weather": [{"id": 800, "main": "Clear", "description": "晴", "icon": "01d"}], "base": "stations", "main": {"temp": 18.94, "feels_like": 17.23, "temp_min": 18.94, "temp_max": 18.94, "pressure": 1020, "humidity": 13, "sea_level": 1020, "grnd_level": 1015}, "visibility": 10000, "wind": {"speed": 6.11, "deg": 347, "gust": 9.25}, "clouds": {"all": 0}, "dt": 1778117424, "sys": {"type": 1, "id": 9609, "country": "CN", "sunrise": 1778101691, "sunset": 1778152429}, "timezone": 28800, "id": 1816670, "name": "Beijing", "cod": 200}', name='get_city_weather_data', id='d226a5d7-cb6f-4f68-ba7e-5220d4aba4ba', tool_call_id='call_15838b1a4ce54158988b8e02'), ToolMessage(content='{"coord": {"lon": 119.4078, "lat": 32.3969}, "weather": [{"id": 803, "main": "Clouds", "description": "多云", "icon": "04d"}], "base": "stations", "main": {"temp": 28.3, "feels_like": 28.7, "temp_min": 28.3, "temp_max": 28.3, "pressure": 1011, "humidity": 49, "sea_level": 1011, "grnd_level": 1009}, "visibility": 10000, "wind": {"speed": 3.6, "deg": 297, "gust": 4.62}, "clouds": {"all": 84}, "dt": 1778117996, "sys": {"country": "CN", "sunrise": 1778101850, "sunset": 1778150822}, "timezone": 28800, "id": 1787227, "name": "Yangzhou", "cod": 200}', name='get_city_weather_data', id='2ad45f1a-e724-4bf5-be96-7864ea497dc6', tool_call_id='call_e607cd0e625d49729f4caab7')]}
[updates] {'model': {'messages': [AIMessage(content='根据查询到的天气数据,我来为您分析一下北京和扬州的天气情况:\n\n## 北京天气\n- **天气状况**:晴天\n- **温度**:18.94°C\n- **湿度**:13%(非常干燥)\n- **风速**:6.11 m/s\n- **能见度**:10公里\n\n## 扬州天气\n- **天气状况**:多云\n- **温度**:28.3°C\n- **湿度**:49%(相对舒适)\n- **风速**:3.6 m/s\n- **能见度**:10公里\n\n## 踏青适宜度分析\n\n**扬州更适合踏青**,原因如下:\n\n1. **温度适宜**:扬州28.3°C的温度比北京的18.9°C更适合户外活动,既不会太冷也不会太热\n2. **湿度适中**:扬州49%的湿度比北京13%的干燥天气更舒适,对皮肤和呼吸道更友好\n3. **风力温和**:扬州风速3.6 m/s比北京的6.11 m/s更柔和,踏青时体感更舒适\n4. **多云天气**:扬州的多云天气可以提供自然遮阳,避免强烈阳光直射,而北京的晴天虽然阳光充足但可能过于强烈\n\n**温馨提示**:\n- 北京天气虽然晴朗,但湿度极低,如果选择在北京踏青,建议多补水并做好防晒\n- 扬州的天气条件整体更适合户外活动,尤其是春季踏青赏花\n\n如果您更喜欢晴朗的天气,北京也是不错的选择,但综合考虑舒适度,扬州的天气条件略胜一筹。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 382, 'prompt_tokens': 1073, 'total_tokens': 1455, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 0, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 2}}, 'model_provider': 'openai', 'model_name': 'mimo-v2-flash', 'system_fingerprint': None, 'id': '8df4143d95b54e3aacd9985580899b50', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--019e0017-8661-7183-9374-e1d78acf6aee-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 1073, 'output_tokens': 382, 'total_tokens': 1455, 'input_token_details': {'cache_read': 2}, 'output_token_details': {'reasoning': 0}})]}}
[values] {'messages': [HumanMessage(content='今天北京和扬州的天气比较,哪个城市的天气更适合出去踏青?', additional_kwargs={}, response_metadata={}, id='8244100a-8069-4479-9b18-63ca221b75ad'), AIMessage(content='我来帮您查询北京和扬州的天气情况,然后为您分析哪个城市更适合踏青。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 68, 'prompt_tokens': 389, 'total_tokens': 457, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 0, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 4}}, 'model_provider': 'openai', 'model_name': 'mimo-v2-flash', 'system_fingerprint': None, 'id': '67eb016123d344c49f8f660af5773e60', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019e0017-4795-7250-9b3f-203e23f56270-0', tool_calls=[{'name': 'get_city_weather_data', 'args': {'city_name': '北京'}, 'id': 'call_15838b1a4ce54158988b8e02', 'type': 'tool_call'}, {'name': 'get_city_weather_data', 'args': {'city_name': '扬州'}, 'id': 'call_e607cd0e625d49729f4caab7', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 389, 'output_tokens': 68, 'total_tokens': 457, 'input_token_details': {'cache_read': 4}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='{"coord": {"lon": 116.3913, "lat": 39.9057}, "weather": [{"id": 800, "main": "Clear", "description": "晴", "icon": "01d"}], "base": "stations", "main": {"temp": 18.94, "feels_like": 17.23, "temp_min": 18.94, "temp_max": 18.94, "pressure": 1020, "humidity": 13, "sea_level": 1020, "grnd_level": 1015}, "visibility": 10000, "wind": {"speed": 6.11, "deg": 347, "gust": 9.25}, "clouds": {"all": 0}, "dt": 1778117424, "sys": {"type": 1, "id": 9609, "country": "CN", "sunrise": 1778101691, "sunset": 1778152429}, "timezone": 28800, "id": 1816670, "name": "Beijing", "cod": 200}', name='get_city_weather_data', id='d226a5d7-cb6f-4f68-ba7e-5220d4aba4ba', tool_call_id='call_15838b1a4ce54158988b8e02'), ToolMessage(content='{"coord": {"lon": 119.4078, "lat": 32.3969}, "weather": [{"id": 803, "main": "Clouds", "description": "多云", "icon": "04d"}], "base": "stations", "main": {"temp": 28.3, "feels_like": 28.7, "temp_min": 28.3, "temp_max": 28.3, "pressure": 1011, "humidity": 49, "sea_level": 1011, "grnd_level": 1009}, "visibility": 10000, "wind": {"speed": 3.6, "deg": 297, "gust": 4.62}, "clouds": {"all": 84}, "dt": 1778117996, "sys": {"country": "CN", "sunrise": 1778101850, "sunset": 1778150822}, "timezone": 28800, "id": 1787227, "name": "Yangzhou", "cod": 200}', name='get_city_weather_data', id='2ad45f1a-e724-4bf5-be96-7864ea497dc6', tool_call_id='call_e607cd0e625d49729f4caab7'), AIMessage(content='根据查询到的天气数据,我来为您分析一下北京和扬州的天气情况:\n\n## 北京天气\n- **天气状况**:晴天\n- **温度**:18.94°C\n- **湿度**:13%(非常干燥)\n- **风速**:6.11 m/s\n- **能见度**:10公里\n\n## 扬州天气\n- **天气状况**:多云\n- **温度**:28.3°C\n- **湿度**:49%(相对舒适)\n- **风速**:3.6 m/s\n- **能见度**:10公里\n\n## 踏青适宜度分析\n\n**扬州更适合踏青**,原因如下:\n\n1. **温度适宜**:扬州28.3°C的温度比北京的18.9°C更适合户外活动,既不会太冷也不会太热\n2. **湿度适中**:扬州49%的湿度比北京13%的干燥天气更舒适,对皮肤和呼吸道更友好\n3. **风力温和**:扬州风速3.6 m/s比北京的6.11 m/s更柔和,踏青时体感更舒适\n4. **多云天气**:扬州的多云天气可以提供自然遮阳,避免强烈阳光直射,而北京的晴天虽然阳光充足但可能过于强烈\n\n**温馨提示**:\n- 北京天气虽然晴朗,但湿度极低,如果选择在北京踏青,建议多补水并做好防晒\n- 扬州的天气条件整体更适合户外活动,尤其是春季踏青赏花\n\n如果您更喜欢晴朗的天气,北京也是不错的选择,但综合考虑舒适度,扬州的天气条件略胜一筹。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 382, 'prompt_tokens': 1073, 'total_tokens': 1455, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 0, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 2}}, 'model_provider': 'openai', 'model_name': 'mimo-v2-flash', 'system_fingerprint': None, 'id': '8df4143d95b54e3aacd9985580899b50', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--019e0017-8661-7183-9374-e1d78acf6aee-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 1073, 'output_tokens': 382, 'total_tokens': 1455, 'input_token_details': {'cache_read': 2}, 'output_token_details': {'reasoning': 0}})]}
根据查询到的天气数据,我来为您分析一下北京和扬州的天气情况:

## 北京天气
- **天气状况**:晴天
- **温度**:18.94°C
- **湿度**:13%(非常干燥)
- **风速**:6.11 m/s
- **能见度**:10公里

## 扬州天气
- **天气状况**:多云
- **温度**:28.3°C
- **湿度**:49%(相对舒适)
- **风速**:3.6 m/s
- **能见度**:10公里

## 踏青适宜度分析

**扬州更适合踏青**,原因如下:

1. **温度适宜**:扬州28.3°C的温度比北京的18.9°C更适合户外活动,既不会太冷也不会太热
2. **湿度适中**:扬州49%的湿度比北京13%的干燥天气更舒适,对皮肤和呼吸道更友好
3. **风力温和**:扬州风速3.6 m/s比北京的6.11 m/s更柔和,踏青时体感更舒适
4. **多云天气**:扬州的多云天气可以提供自然遮阳,避免强烈阳光直射,而北京的晴天虽然阳光充足但可能过于强烈

**温馨提示**:
- 北京天气虽然晴朗,但湿度极低,如果选择在北京踏青,建议多补水并做好防晒
- 扬州的天气条件整体更适合户外活动,尤其是春季踏青赏花

如果您更喜欢晴朗的天气,北京也是不错的选择,但综合考虑舒适度,扬州的天气条件略胜一筹。
============================================
=================分割线======================
============================================
[values] {'messages': [HumanMessage(content='帮我查一下比亚迪和工商银行的实时股票信息,哪个涨幅更大?', additional_kwargs={}, response_metadata={}, id='08f0aed3-8bbd-4de7-8354-499ca767e28d')]}
[updates] {'model': {'messages': [AIMessage(content='我来帮您查询比亚迪和工商银行的实时股票信息。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 56, 'prompt_tokens': 388, 'total_tokens': 444, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 0, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 366}}, 'model_provider': 'openai', 'model_name': 'mimo-v2-flash', 'system_fingerprint': None, 'id': '106be9d3838c42429e512ece0ff6c1d9', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019e0017-94f9-7173-a811-418fc16d70fb-0', tool_calls=[{'name': 'query_stock', 'args': {'stock_name': '比亚迪'}, 'id': 'call_e88a2e0af236470aabc71063', 'type': 'tool_call'}, {'name': 'query_stock', 'args': {'stock_name': '工商银行'}, 'id': 'call_ce5635345e1c4af3857499a0', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 388, 'output_tokens': 56, 'total_tokens': 444, 'input_token_details': {'cache_read': 366}, 'output_token_details': {'reasoning': 0}})]}}
[values] {'messages': [HumanMessage(content='帮我查一下比亚迪和工商银行的实时股票信息,哪个涨幅更大?', additional_kwargs={}, response_metadata={}, id='08f0aed3-8bbd-4de7-8354-499ca767e28d'), AIMessage(content='我来帮您查询比亚迪和工商银行的实时股票信息。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 56, 'prompt_tokens': 388, 'total_tokens': 444, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 0, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 366}}, 'model_provider': 'openai', 'model_name': 'mimo-v2-flash', 'system_fingerprint': None, 'id': '106be9d3838c42429e512ece0ff6c1d9', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019e0017-94f9-7173-a811-418fc16d70fb-0', tool_calls=[{'name': 'query_stock', 'args': {'stock_name': '比亚迪'}, 'id': 'call_e88a2e0af236470aabc71063', 'type': 'tool_call'}, {'name': 'query_stock', 'args': {'stock_name': '工商银行'}, 'id': 'call_ce5635345e1c4af3857499a0', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 388, 'output_tokens': 56, 'total_tokens': 444, 'input_token_details': {'cache_read': 366}, 'output_token_details': {'reasoning': 0}})]}
调用股票查询工具,查询比亚迪的股价。
调用股票查询工具,查询工商银行的股价。
[updates] {'tools': {'messages': [ToolMessage(content='\n==============================================\n  工商银行  [sh601398]\n  ------------------------------------------\n  当前价:7.36 元\n  涨跌:  ↑ 涨 +0.00%(+0.00 元)\n  今开:  7.36     昨收:7.36\n  最高:  7.38     最低:7.32\n  成交量:41,861,400 手\n  数据时间:2026-05-07 09:40:07\n==============================================\n', name='query_stock', id='067a7572-3f99-44b6-a0ad-833ed5c3a3f9', tool_call_id='call_ce5635345e1c4af3857499a0')]}}
[updates] {'tools': {'messages': [ToolMessage(content='\n==============================================\n  比亚迪  [sz002594]\n  ------------------------------------------\n  当前价:100.41 元\n  涨跌:  ↓ 跌 -0.34%(-0.34 元)\n  今开:  101.05     昨收:100.75\n  最高:  101.26     最低:100.27\n  成交量:6,126,400 手\n  数据时间:2026-05-07 09:40:06\n==============================================\n', name='query_stock', id='97fff33d-7522-4db3-84a4-ed6e27b10542', tool_call_id='call_e88a2e0af236470aabc71063')]}}
[values] {'messages': [HumanMessage(content='帮我查一下比亚迪和工商银行的实时股票信息,哪个涨幅更大?', additional_kwargs={}, response_metadata={}, id='08f0aed3-8bbd-4de7-8354-499ca767e28d'), AIMessage(content='我来帮您查询比亚迪和工商银行的实时股票信息。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 56, 'prompt_tokens': 388, 'total_tokens': 444, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 0, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 366}}, 'model_provider': 'openai', 'model_name': 'mimo-v2-flash', 'system_fingerprint': None, 'id': '106be9d3838c42429e512ece0ff6c1d9', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019e0017-94f9-7173-a811-418fc16d70fb-0', tool_calls=[{'name': 'query_stock', 'args': {'stock_name': '比亚迪'}, 'id': 'call_e88a2e0af236470aabc71063', 'type': 'tool_call'}, {'name': 'query_stock', 'args': {'stock_name': '工商银行'}, 'id': 'call_ce5635345e1c4af3857499a0', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 388, 'output_tokens': 56, 'total_tokens': 444, 'input_token_details': {'cache_read': 366}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='\n==============================================\n  比亚迪  [sz002594]\n  ------------------------------------------\n  当前价:100.41 元\n  涨跌:  ↓ 跌 -0.34%(-0.34 元)\n  今开:  101.05     昨收:100.75\n  最高:  101.26     最低:100.27\n  成交量:6,126,400 手\n  数据时间:2026-05-07 09:40:06\n==============================================\n', name='query_stock', id='97fff33d-7522-4db3-84a4-ed6e27b10542', tool_call_id='call_e88a2e0af236470aabc71063'), ToolMessage(content='\n==============================================\n  工商银行  [sh601398]\n  ------------------------------------------\n  当前价:7.36 元\n  涨跌:  ↑ 涨 +0.00%(+0.00 元)\n  今开:  7.36     昨收:7.36\n  最高:  7.38     最低:7.32\n  成交量:41,861,400 手\n  数据时间:2026-05-07 09:40:07\n==============================================\n', name='query_stock', id='067a7572-3f99-44b6-a0ad-833ed5c3a3f9', tool_call_id='call_ce5635345e1c4af3857499a0')]}
[updates] {'model': {'messages': [AIMessage(content='根据实时股票数据:\n\n**比亚迪**(sz002594):\n- 当前价:100.41元\n- 涨跌:↓ 跌 -0.34%(-0.34元)\n\n**工商银行**(sh601398):\n- 当前价:7.36元\n- 涨跌:↑ 涨 +0.00%(+0.00元)\n\n**涨幅对比**:\n- 比亚迪:-0.34%(下跌)\n- 工商银行:+0.00%(持平)\n\n**结论**:工商银行的涨幅更大,因为比亚迪当前是下跌状态(-0.34%),而工商银行持平(+0.00%)。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 174, 'prompt_tokens': 757, 'total_tokens': 931, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 0, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 2}}, 'model_provider': 'openai', 'model_name': 'mimo-v2-flash', 'system_fingerprint': None, 'id': '4bffc701a85642ed915b95967662a5b3', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--019e0017-9855-7350-ba28-209a8be4641d-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 757, 'output_tokens': 174, 'total_tokens': 931, 'input_token_details': {'cache_read': 2}, 'output_token_details': {'reasoning': 0}})]}}
[values] {'messages': [HumanMessage(content='帮我查一下比亚迪和工商银行的实时股票信息,哪个涨幅更大?', additional_kwargs={}, response_metadata={}, id='08f0aed3-8bbd-4de7-8354-499ca767e28d'), AIMessage(content='我来帮您查询比亚迪和工商银行的实时股票信息。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 56, 'prompt_tokens': 388, 'total_tokens': 444, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 0, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 366}}, 'model_provider': 'openai', 'model_name': 'mimo-v2-flash', 'system_fingerprint': None, 'id': '106be9d3838c42429e512ece0ff6c1d9', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019e0017-94f9-7173-a811-418fc16d70fb-0', tool_calls=[{'name': 'query_stock', 'args': {'stock_name': '比亚迪'}, 'id': 'call_e88a2e0af236470aabc71063', 'type': 'tool_call'}, {'name': 'query_stock', 'args': {'stock_name': '工商银行'}, 'id': 'call_ce5635345e1c4af3857499a0', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 388, 'output_tokens': 56, 'total_tokens': 444, 'input_token_details': {'cache_read': 366}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='\n==============================================\n  比亚迪  [sz002594]\n  ------------------------------------------\n  当前价:100.41 元\n  涨跌:  ↓ 跌 -0.34%(-0.34 元)\n  今开:  101.05     昨收:100.75\n  最高:  101.26     最低:100.27\n  成交量:6,126,400 手\n  数据时间:2026-05-07 09:40:06\n==============================================\n', name='query_stock', id='97fff33d-7522-4db3-84a4-ed6e27b10542', tool_call_id='call_e88a2e0af236470aabc71063'), ToolMessage(content='\n==============================================\n  工商银行  [sh601398]\n  ------------------------------------------\n  当前价:7.36 元\n  涨跌:  ↑ 涨 +0.00%(+0.00 元)\n  今开:  7.36     昨收:7.36\n  最高:  7.38     最低:7.32\n  成交量:41,861,400 手\n  数据时间:2026-05-07 09:40:07\n==============================================\n', name='query_stock', id='067a7572-3f99-44b6-a0ad-833ed5c3a3f9', tool_call_id='call_ce5635345e1c4af3857499a0'), AIMessage(content='根据实时股票数据:\n\n**比亚迪**(sz002594):\n- 当前价:100.41元\n- 涨跌:↓ 跌 -0.34%(-0.34元)\n\n**工商银行**(sh601398):\n- 当前价:7.36元\n- 涨跌:↑ 涨 +0.00%(+0.00元)\n\n**涨幅对比**:\n- 比亚迪:-0.34%(下跌)\n- 工商银行:+0.00%(持平)\n\n**结论**:工商银行的涨幅更大,因为比亚迪当前是下跌状态(-0.34%),而工商银行持平(+0.00%)。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 174, 'prompt_tokens': 757, 'total_tokens': 931, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 0, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 2}}, 'model_provider': 'openai', 'model_name': 'mimo-v2-flash', 'system_fingerprint': None, 'id': '4bffc701a85642ed915b95967662a5b3', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--019e0017-9855-7350-ba28-209a8be4641d-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 757, 'output_tokens': 174, 'total_tokens': 931, 'input_token_details': {'cache_read': 2}, 'output_token_details': {'reasoning': 0}})]}
根据实时股票数据:

**比亚迪**(sz002594):
- 当前价:100.41元
- 涨跌:↓ 跌 -0.34%(-0.34元)

**工商银行**(sh601398):
- 当前价:7.36元
- 涨跌:↑ 涨 +0.00%(+0.00元)

**涨幅对比**:
- 比亚迪:-0.34%(下跌)
- 工商银行:+0.00%(持平)

**结论**:工商银行的涨幅更大,因为比亚迪当前是下跌状态(-0.34%),而工商银行持平(+0.00%)。

进程已结束,退出代码为 0

二、核心解析:Agent工具选择的底层逻辑

Agent之所以能自动选择工具,核心是“LLM语义判断 + 提示词规则 + 工具元信息”三者协同作用,并非复杂的硬编码匹配,而是通过大模型理解用户需求,结合工具信息做出选择。为了更直观理解这一过程,附上Agent工具选择流程图,再拆解核心细节:

流程图完整覆盖“输入→判断→执行→返回”全链路,与源码中工具选择逻辑(create_agent、ToolCallingAgent调用)高度贴合,下面拆解工具选择的3个核心要素及具体逻辑。

流程图清晰呈现了工具选择的全链路,结合该流程,下面拆解工具选择的3个核心要素及具体逻辑。

1. 工具选择的3个核心要素

  • 工具元信息(@tool装饰器的作用):@tool装饰器会自动为工具生成元数据(包括工具名称、描述、参数信息),这些元数据会传递给LLM,让LLM知道“这个工具能做什么、需要什么参数”。比如get_weather工具的注释,就是告诉LLM“该工具用于查询天气,需要传入城市参数”。

  • 提示词规则(引导LLM做选择):system提示词中明确了“什么场景调用什么工具”,相当于给LLM一个“决策手册”,避免LLM误判。提示词的清晰度直接决定工具选择的准确率,比如明确区分天气和股票的适用场景,避免混淆。

  • LLM的语义理解能力:LLM会先解析用户提问的核心需求(比如“北京天气”→ 天气需求,“AAPL股价”→ 股票需求),再结合工具元信息和提示词规则,判断是否需要调用工具、调用哪个工具。若需求不匹配任何工具,则直接返回原生回答。

2. 工具选择的完整流程(一步一步拆解)

  1. 用户输入提问,Agent接收输入(如“上海今天下雨吗?”);

  2. Agent将“用户提问 + 工具元信息 + 提示词规则”一起传递给LLM;

  3. LLM解析提问,判断核心需求是“查询上海天气”,匹配到get_weather工具;

  4. LLM生成工具调用指令(包含工具名称、传入参数:city=上海);

  5. Agent执行工具调用,获取工具返回结果;

  6. Agent将工具结果整理后,返回给用户;

  7. 若用户提问不匹配任何工具(如“什么是AI”),LLM直接生成回答,不执行任何工具调用。

三、重点:Agent工具选择源码拆解

这部分是核心,我们拆解LangChain源码中“工具选择”的关键逻辑,聚焦create_agent函数、工具元信息处理、LLM决策三个核心环节,不用深入全部源码,只看和工具选择相关的核心代码片段。

1. 源码核心入口:create_agent函数

create_agent是Agent创建的入口,也是工具选择逻辑的“绑定中心”,其核心作用是将LLM、工具、提示词整合,生成具备工具选择能力的Agent。我们拆解其核心源码:

def create_agent(
    llm: BaseChatModel,
    tools: List[BaseTool],
    prompt: ChatPromptTemplate,
    **kwargs: Any,
) -> AgentRunnable:
    """
    创建具备工具调用能力的Agent,核心是绑定LLM、工具和提示词
    """
    # 1. 处理工具元信息:将工具列表转换为LLM可识别的格式(包含工具名称、描述、参数)
    tool_config = ToolConfig(tools=tools)
    # 2. 创建工具调用处理器:负责解析LLM的工具调用指令,执行工具并返回结果
    tool_caller = ToolCallingAgent(
        llm=llm,
        prompt=prompt,
        tool_config=tool_config,
    )
    # 3. 返回Agent实例,具备invoke方法,可接收用户输入并执行工具选择
    return AgentRunnable(tool_caller=tool_caller)

# 关键说明:
# - ToolConfig:将工具列表转换为元数据(如[{"name":"get_weather", "description":"查询天气...", "parameters":{"city": "str"}}])
# - ToolCallingAgent:核心处理器,负责让LLM生成工具调用指令,执行工具
# - AgentRunnable:对外提供invoke方法,接收用户输入,触发工具选择流程

2. 工具元信息的处理(@tool装饰器源码)

@tool装饰器的核心作用是“给工具添加元信息”,让LLM能识别工具的用途和参数,其简化源码如下:

def tool(func: Callable) -> BaseTool:
    """
    工具装饰器,为函数添加元信息,转换为LangChain可识别的BaseTool实例
    """
    # 1. 提取函数的文档字符串(就是我们写的工具注释),作为工具描述
    description = func.__doc__.strip() if func.__doc__ else ""
    # 2. 提取函数的参数信息(如city: str),作为工具的参数说明
    params = inspect.signature(func).parameters
    param_schema = {
        name: {"type": param.annotation.__name__} 
        for name, param in params.items()
    }
    # 3. 创建BaseTool实例,封装函数、描述、参数信息
    return BaseTool(
        name=func.__name__,  # 工具名称(如get_weather)
        description=description,  # 工具描述(注释内容)
        func=func,  # 工具执行函数
        args_schema=param_schema,  # 参数 schema
    )

# 关键结论:
# 我们写的工具注释,会被@tool装饰器提取为工具描述,传递给LLM,是工具选择的关键依据
# 注释越清晰、场景越明确,LLM选择工具的准确率越高

3. LLM决策工具调用的核心逻辑

Agent让LLM判断是否调用工具、调用哪个工具,核心是“将用户提问、工具元信息、提示词拼接后,传给LLM,让LLM返回工具调用指令”,简化源码逻辑如下:

class ToolCallingAgent:
    def __init__(self, llm, prompt, tool_config):
        self.llm = llm
        self.prompt = prompt
        self.tools = tool_config.tools  # 工具列表(带元信息)
    
    def invoke(self, input_data: dict) -> dict:
        # 1. 提取用户输入和对话历史
        user_input = input_data["input"]
        chat_history = input_data.get("chat_history", [])
        
        # 2. 拼接提示词:将系统提示词、对话历史、用户输入、工具元信息整合
        messages = self.prompt.format_messages(
            input=user_input,
            chat_history=chat_history,
            # 将工具元信息传入提示词,让LLM知道有哪些工具可用
            tools=[tool.to_dict() for tool in self.tools]
        )
        
        # 3. 调用LLM,让LLM判断是否调用工具、调用哪个工具
        llm_response = self.llm.invoke(messages)
        
        # 4. 解析LLM返回结果:若有工具调用指令,则执行工具;否则直接返回回答
        if llm_response.tool_calls:
            # 解析工具调用指令(工具名称、参数)
            tool_calls = [
                {"name": call["name"], "parameters": call["parameters"]}
                for call in llm_response.tool_calls
            ]
            # 执行工具调用,获取结果
            tool_results = self._run_tools(tool_calls)
            # 整理工具结果,返回给用户
            return {"output": self._format_tool_results(tool_results)}
        else:
            # 不调用工具,直接返回LLM的原生回答
            return {"output": llm_response.content}
    
    def _run_tools(self, tool_calls):
        # 执行工具调用的核心方法:根据工具名称匹配工具,传入参数执行
        results = []
        for call in tool_calls:
            # 根据工具名称,从工具列表中找到对应的工具
            tool = next(t for t in self.tools if t.name == call["name"])
            # 执行工具,传入参数
            result = tool.func(**call["parameters"])
            results.append(result)
        return results

4. 结论

  • 工具选择的核心是LLM的语义判断,而非硬编码匹配,LangChain只是提供了“工具元信息封装”“工具调用执行”的框架;

  • @tool装饰器的核心作用是“提取工具元信息”,注释是工具元信息的关键,直接影响LLM的选择准确率;

  • 提示词是“引导LLM决策”的关键,通过明确规则,避免LLM误判(比如区分天气和股票场景);

  • Agent的工具选择流程:接收输入 → 拼接提示词+工具元信息 → LLM决策 → 执行工具/直接回答。

四、工具选择的优化技巧

在实际开发中,优化工具选择的准确率,可重点关注以下3点:

  1. 优化工具注释:明确工具的适用场景、参数要求,避免模糊描述。比如不要只写“查询天气”,要写“查询指定城市的天气、气温、晴雨等信息,参数为城市名称”。

  2. 优化提示词规则:在system提示词中,明确区分不同工具的适用场景,甚至可以添加“排除规则”(比如“若用户问的是历史天气,暂不调用工具,直接告知无法查询”)。

  3. 控制LLM温度:将temperature设为0~0.2,避免LLM产生随机回答,确保工具选择的稳定性(温度越高,工具选择的随机性越强,越容易误判)。

五、常见问题排查

  • 问题1:Agent不调用工具,即使提问命中场景?→ 排查提示词是否明确,工具注释是否清晰,LLM是否具备工具调用能力(部分基础模型不支持工具调用)。

  • 问题2:Agent调用错误的工具(比如天气问题调用股票工具)?→ 优化提示词,明确区分不同工具的适用场景,避免场景混淆;同时优化工具注释,突出工具的核心用途。

  • 问题3:工具调用时报参数错误?→ 检查@tool装饰器提取的参数是否正确,确保用户提问中包含工具所需的参数(比如查询天气时,用户需明确城市)。

总结

LangChain Agent的工具选择逻辑,本质是“LLM语义理解 + 工具元信息 + 提示词规则”的协同作用,核心不是复杂的源码逻辑,而是“让LLM清晰知道该用什么工具、什么时候用工具”。

本文通过实战案例铺垫,拆解了工具选择的核心流程和源码关键环节,重点说明:@tool装饰器的作用、提示词的重要性、LLM的决策逻辑,帮你从“会用Agent”提升到“理解Agent工具选择的底层原理”。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐