从2025年10月18日LangChain正式发布了V1.0.0版本,这个版本可谓改动较大,但是对于开发人员受益颇多。
LangChain v1.0对命名空间进行了大刀阔斧的精简,将核心功能聚焦于Agent开发所需的基础组件,而将 legacy 功能迁移至langchain-classic包。这种精简带来了三个显著好处:

  • 一是减少认知负担,新开发者不再需要面对数十个模块的选择困难;
  • 二是降低安装体积,核心包大小减少60%;
  • 三是提升运行效率,避免了不必要的依赖加载。

对于需要升级的项目,官方提供了平滑迁移路径,只需将旧代码中from langchain.legacy_xxx的导入替换为from langchain_classic.xxx即可。

下面我们用LangChain v1.0 快速搭建一个天气查询的智能体(ReAct Agent):

采用Function Calling方式

1. 模型准备

使用的模型采用之前文章中搭建的Ollama运行的本地qwen3:1.7b模型,具体模型的本地搭建可以参考这里

2. 数据准备

要查询天气,就需要调用天气查询接口,我们选择百度地图的天气查询接口(每天5000次免费单次调用,每秒最多3次并发),但是这个接口需要传递district_id也就是行政区划码adcode,可以在这里下载。

但是下载的数据为csv格式,如果想提高查询效率,最好存储到数据库中,我们采用方便的sqlite数据库,用python完成数据入库:

## rw_csv_sql.py
from sqlalchemy import create_engine, func, select, or_
from sqlalchemy.orm import Session
from model import District, Base

engine = create_engine("sqlite:///app.db", echo=True)
Base.metadata.create_all(engine)


def getSession():
    return Session(engine)


# 从CSV文件导入数据,跳过第一行
def import_csv():
    with open("weather_district_id.csv", "r", encoding="utf-8") as f:
        next(f)  # 跳过第一行
        for line in f:
            (
                district_id,
                province,
                city,
                city_geocode,
                district,
                district_geocode,
                lon,
                lat,
            ) = line.strip().split(",")
            district = District(
                district_id=int(district_id),
                province=province,
                city=city,
                city_geocode=city_geocode,
                district=district,
                district_geocode=district_geocode,
                lon=float(lon),
                lat=float(lat),
            )

            session = getSession()
            session.add(district)
            session.commit()
            session.close()


def init_db():
    with getSession() as sess:
        stmt = select(func.count()).select_from(District)
        result = sess.execute(stmt).scalar()
        print(f"districts表中总行数为:{result}")
        if result == 0:
            import_csv()


# 从sqlite数据库查询数据
def test_query():
    with getSession() as session:
        stmt = select(District).where(District.district_id == 110100)
        result = session.execute(stmt)
        for row in result:
            print(row)


def query_by_district_name(district_name):
    with getSession() as session:
        stmt = select(District).where(
            or_(
                District.district.like(f"%{district_name}%"),
                District.district.like(f"%{district_name}区%"),
                District.city.like(f"%{district_name}%"),
                District.province.like(f"%{district_name}%"),
            )
        )
        result = session.execute(
            stmt, execution_options={"prebuffer_rows": True}
        ).scalars()
        return result

然后就可以调用query_by_district_name(district_name)来根据区域名称获取对应的行政区划码。

由于百度天气接口有并发请求限制,所以我们需要进行限速处理:

## web_request.py
import httpx as requests
import orjson
from dotenv import dotenv_values, load_dotenv
import asyncio
from aiolimiter import AsyncLimiter


load_dotenv()
my_baidu_ak = dotenv_values()["BAIDU_AK"]


def get_weather(district_id):
    try:
        res = requests.get(
            f"https://api.map.baidu.com/weather/v1/",
            params={
                "district_id": district_id,
                "data_type": "all",
                "ak": my_baidu_ak,
            },
            timeout=10,
        )
        if res:
            # print(res.json())
            data = res.json()
            location = data["result"]["location"]
            now = data["result"]["now"]
            province = location["province"]
            city = location["city"]
            district = location["name"]
            temp = now["temp"]
            feels_like = now["feels_like"]
            wind_dir = now["wind_dir"]
            wind_class = now["wind_class"]
            text = now["text"]
            # print(
            #     f"{province} {city} {district} 温度为 {temp}℃, 体感温度为 {feels_like}℃, 风向为 {wind_dir}, 风力为 {wind_class}, 天气为 {text}"
            # )
            return f"{province} {city} {district} 温度为 {temp}℃, 体感温度为 {feels_like}℃, 风向为 {wind_dir}, 风力为 {wind_class}, 天气为 {text}"

            # with open("baidu_weather.json", "w", encoding="utf-8") as f:
            #     f.write(
            #         orjson.dumps(res.json(), option=orjson.OPT_INDENT_2).decode("utf-8")
            #     )
    except Exception as e:
        print(f"请求百度天气API时出错: {e}")


# 最大并发数(例如限制同时最多 3 个请求,百度地图和高德地图给的天气查询默认最大并发就是3)
MAX_CONCURRENT = 3
# 信号量用于限流,但是不能限速(每 1 秒最多 3 个请求)
semaphore = asyncio.Semaphore(MAX_CONCURRENT)
# 创建限流器:并限速(每 1 秒最多 3 个请求,用1.1秒保证不会超速)
limiter = AsyncLimiter(max_rate=MAX_CONCURRENT, time_period=3.5)


# 异步发起请求
async def get_weather_async(client: requests.AsyncClient, district_id):
    async with limiter:  # 限制并发
        try:
            print(f"[{asyncio.get_event_loop().time():.2f}] 发起请求: {district_id}")
            res = await client.get(
                f"https://api.map.baidu.com/weather/v1/",
                params={
                    "district_id": district_id,
                    "data_type": "all",
                    "ak": my_baidu_ak,
                },
            )
            if res:
                # print(res.json())
                data = res.json()
                location = data["result"]["location"]
                now = data["result"]["now"]
                province = location["province"]
                city = location["city"]
                district = location["name"]
                temp = now["temp"]
                feels_like = now["feels_like"]
                wind_dir = now["wind_dir"]
                wind_class = now["wind_class"]
                text = now["text"]
                print(
                    f"{province} {city} {district} 温度为 {temp}℃, 体感温度为 {feels_like}℃, 风向为 {wind_dir}, 风力为 {wind_class}, 天气为 {text}"
                )
                return f"{province} {city} {district} 温度为 {temp}℃, 体感温度为 {feels_like}℃, 风向为 {wind_dir}, 风力为 {wind_class}, 天气为 {text}"
                # with open("baidu_weather.json", "w", encoding="utf-8") as f:
                #     f.write(
                #         orjson.dumps(res.json(), option=orjson.OPT_INDENT_2).decode("utf-8")
                #     )
        except Exception as e:
            print(f"请求百度天气API时出错: {e}")


async def get_weather_list_async(district_ids):
    async with requests.AsyncClient() as client:
        tasks = [get_weather_async(client, district_id) for district_id in district_ids]
        result = await asyncio.gather(*tasks)
        return result


if __name__ == "__main__":
    results = asyncio.run(get_weather_list_async([370103, 370402]))
    for i, res in enumerate(results):
        if isinstance(res, Exception):
            print(f"第{i+1}个请求出错: {res}")
        else:
            print(res)

通过封装的方法get_weather_list_async(district_ids)就可以进行优雅的并发调用了。

3. 编写智能体

下面就可以用LangChain v1.0 的API来编写ReAct类Agent了:

## main.py
"""
查询天气的Agent示例
"""
 from langchain.agents import create_agent
 from langchain.tools import tool
 from rw_csv_sql import query_by_district_name
 from web_request import get_weather_list_async
 import asyncio

 @tool
 def query_weather(district_name: str) -> str:
     """查询指定地区的天气信息."""
     name = get_district_from_input(district_name)
     res = query_by_district_name(name)
     district_ids = [row.district_id for row in res]

     if len(district_ids) > 0:
         results = asyncio.run(get_weather_list_async(district_ids))
         store = []  # 存储结果
         for i, reslt in enumerate(results):
             if isinstance(reslt, Exception):
                 print(f"第{i+1}个请求出错: {reslt}")
             else:
                 # print(reslt)
                 store.append(reslt)
         return "\n".join(store)

 agent = create_agent(
     model=ChatOllama(
         model="qwen3:1.7b",
         base_url="http://127.0.0.1:18080",
         temperature=0.7,
         top_p=0.9,
         max_tokens=1024,
     ),
     tools=[query_weather],
     system_prompt="你是一个贴心助手,当需要查询天气时,请调用query_weather工具.",
 )
 result = agent.invoke(
     {
         "messages": [
             # {"role": "user", "content": "吉林长春朝阳区的天气如何?"},
             # {"role": "user", "content": "朝阳位于长春市,取长春朝阳区天气"},
             # {"role": "user", "content": "杭州西湖区天气如何?"},
             {"role": "user", "content": "济南市中区天气如何?"},
         ]
     }
 )
 print("天气智能体结果:", result)

结果显示:

天气智能体结果: {'messages': [HumanMessage(content='济南市中区天气如何?', additional_kwargs={}, response_metadata={}, id='1b268a33-2748-4b07-983e-b1dda44e0d0a'), AIMessage(content='', additional_kwargs={}, response_metadata={'model': 'qwen3:1.7b', 'created_at': '2025-11-18T02:11:45.164919911Z', 'done': True, 'done_reason': 'stop', 'total_duration': 15255926426, 'load_duration': 53532119, 'prompt_eval_count': 160, 'prompt_eval_duration': 74933566, 'eval_count': 246, 'eval_duration': 15034951707, 'model_name': 'qwen3:1.7b', 'model_provider': 'ollama'}, id='lc_run--3c3dfabc-97ac-45bf-bb5f-096714d05f18-0', tool_calls=[{'name': 'query_weather', 'args': {'district_name': ' 
济南市'}, 'id': '19bce1b6-5de7-486b-8ffa-1d007adf49fa', 'type': 'tool_call'}], usage_metadata={'input_tokens': 160, 'output_tokens': 246, 'total_tokens': 406}), ToolMessage(content='山东省 济南市 济南市 温度为 2℃, 体感温度为 0℃, 风向为 西南风, 风力为 2级, 天气为 晴\n山东省 济南市 历下区 温度为 2℃, 体感温度为 0℃, 风向为 西南风, 风力为 2 
级, 天气为 晴\n山东省 济南市 市中区 温度为 2℃, 体感温度为 0℃, 风向为 西南风, 风力为 2级, 天气为 晴\n山东省 济南市 槐荫区 温度为 2℃, 体感温度为 0℃, 风向为 西南风, 风力为 
2级, 天气为 晴\n山东省 济南市 天桥区 温度为 2℃, 体感温度为 0℃, 风向为 西南风, 风力为 2级, 天气为 晴\n山东省 济南市 历城区 温度为 2℃, 体感温度为 0℃, 风向为 西南风, 风力为
 2级, 天气为 晴\n山东省 济南市 长清区 温度为 2℃, 体感温度为 0℃, 风向为 东南风, 风力为 2级, 天气为 晴\n山东省 济南市 章丘区 温度为 1℃, 体感温度为 -1℃, 风向为 西风, 风力为
 3级, 天气为 晴\n山东省 济南市 济阳区 温度为 2℃, 体感温度为 0℃, 风向为 西风, 风力为 2级, 天气为 晴\n山东省 济南市 莱芜区 温度为 0℃, 体感温度为 -2℃, 风向为 西风, 风力为 1级, 天气为 晴\n山东省 济南市 钢城区 温度为 0℃, 体感温度为 -1℃, 风向为 西北风, 风力为 2级, 天气为 晴\n山东省 济南市 平阴县 温度为 2℃, 体感温度为 0℃, 风向为 西风, 风力为 2级, 天气为 晴\n山东省 济南市 商河县 温度为 0℃, 体感温度为 -1℃, 风向为 西风, 风力为 2级, 天气为 晴', name='query_weather', id='3e27e63d-3aa3-4a59-89cf-764183e79ee4', tool_call_id='19bce1b6-5de7-486b-8ffa-1d007adf49fa'), AIMessage(content='济南市中区当前天气情况如下:\n- 温度:2℃,体感温度0℃\n- 风向:西南风,风力2级\n- 天气:晴\n\n请注意 
,济南市中区属于济南市辖区,天气情况与全市一致。', additional_kwargs={}, response_metadata={'model': 'qwen3:1.7b', 'created_at': '2025-11-18T02:12:24.778127314Z', 'done': True, 'done_reason': 'stop', 'total_duration': 26410779795, 'load_duration': 62636259, 'prompt_eval_count': 879, 'prompt_eval_duration': 4674770637, 'eval_count': 310, 'eval_duration': 21565468048, 'model_name': 'qwen3:1.7b', 'model_provider': 'ollama'}, id='lc_run--0f4ee469-8797-4f3a-b39f-48c349aecc87-0', usage_metadata={'input_tokens': 879, 'output_tokens': 310, 'total_tokens': 1189})]}

从结果中,可以看到最终的AIMessage正是我们需要的:

AIMessage(content='济南市中区当前天气情况如下:\n- 温度:2℃,体感温度0℃\n- 风向:西南风,风力2级\n- 天气:晴\n\n请注意 
,济南市中区属于济南市辖区,天气情况与全市一致。'

4. 优化空间

在这个简单的天气查询智能体中,让大模型根据我们的问题,进行思考推理并调用天气查询工具,返回我们想要的结果,但是其实还可以采用MCP的方式,把这里的调用天气查询工具,改成调用百度MCP服务,也是可以的。这个就是后面可以优化的空间。

采用MCP方式

1. 选择MCP服务

之前我们用的是百度的天气查询,所以我们很自然会选择百度地图的MCP服务,我们需要仔细查看百度地图MCP服务的API说明

在百度地图MCP服务中天气查询接口的入参是location(经纬度,经度在前),district_id(行政区划码adcode),is_china(是否在国内,默认为True),但是我们通常在对话中的输入一般都是城市名、地名、具体地址,这个需要大模型进行分析转换。由于我们使用的模型是qwen3,所以和百度地图MCP搭配使用会有些Bug,经常会查询出错(API response error: 查询的经纬度值范围无效)。这也是模型大战的后遗症,各家大模型对别家的MCP服务兼容性都不是很好。因为只有自家大模型最懂自己的MCP服务。鉴于此,我们使用高德地图的MCP服务,高德地图MCP服务的API说明也需要仔细查看。

在高德地图MCP服务中天气查询接口的入参是city(城市名称或城市adcode),可以接受城市名称输入,对于对话比较友好。

2. 编写MCP Client

MCP 客户端就是让大模型自动调用MCP服务

from langchain.agents import create_agent
from langchain.tools import tool
from rw_csv_sql import query_by_district_name
from web_request import get_weather_list_async, my_baidu_ak, my_amap_ak
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.tools import load_mcp_tools

async def init_mcp_client():
    client = MultiServerMCPClient(
        {
            "weather": {
                # 高德地图MCP服务
                "url": "https://mcp.amap.com/mcp?key=" + my_amap_ak,
                "transport": "streamable_http",
            },
        }
    )
    tools = await client.get_tools()
    return tools

3. 编写agent

async def create_mcp_agent(tools):
   agent = create_agent(
       model=ChatOllama(
           model="qwen3:1.7b",
           base_url="http://127.0.0.1:18080",
           temperature=0,
           top_p=0.9,
           max_tokens=1024,
       ),
       tools=tools,
       system_prompt="你是一个贴心助手,当需要查询天气时,请调用weather工具.",
   )
   return agent

4. 封装异步函数,执行

async def my_agent_with_mcp():
    """
    使用MCP的Agent示例
    """
    from langchain.agents import create_agent
    from langchain.tools import tool
    from rw_csv_sql import query_by_district_name
    from web_request import get_weather_list_async, my_baidu_ak, my_amap_ak
    import asyncio
    from langchain_mcp_adapters.client import MultiServerMCPClient
    from langchain_mcp_adapters.tools import load_mcp_tools

    async def init_mcp_client():
        client = MultiServerMCPClient(
            {
                "weather": {
                    # 高德地图MCP服务
                    "url": "https://mcp.amap.com/mcp?key=" + my_amap_ak,
                    "transport": "streamable_http",
                },
            }
        )
        tools = await client.get_tools()
        return tools

    async def create_mcp_agent(tools):
        agent = create_agent(
            model=ChatOllama(
                model="qwen3:1.7b",
                base_url="http://127.0.0.1:18080",
                temperature=0,
                top_p=0.9,
                max_tokens=1024,
            ),
            tools=tools,
            system_prompt="你是一个贴心助手,当需要查询天气时,请调用weather工具.",
        )
        return agent

    tools = await init_mcp_client()
    agent = await create_mcp_agent(tools)

    result = await agent.ainvoke(
        {
            "messages": [
                # {"role": "user", "content": "吉林长春朝阳区的天气如何?"},
                # {"role": "user", "content": "朝阳位于长春市,取长春朝阳区天气"},
                # {"role": "user", "content": "杭州西湖区天气如何?"},
                # {"role": "user", "content": "山东济南市中区天气如何?"},
                {"role": "user", "content": "济南市天气如何?"}
            ]
        }
    )
    print("天气智能体结果:", result)


if __name__ == "__main__":
    import asyncio

    asyncio.run(my_agent_with_mcp())

5. 结果

天气智能体结果: {'messages': [HumanMessage(content='济南市天气如何?', additional_kwargs={}, response_metadata={}, id='0d8c580e-1e3c-4550-9e29-1e08af14f9df'), AIMessage(content='', additional_kwargs={}, response_metadata={'model': 'qwen3:1.7b', 'created_at': '2025-11-19T05:52:32.12522016Z', 'done': True, 'done_reason': 'stop', 'total_duration': 36236211543, 'load_duration': 8930837269, 'prompt_eval_count': 2046, 'prompt_eval_duration': 18084869663, 'eval_count': 112, 'eval_duration': 8747191274, 'model_name': 'qwen3:1.7b', 'model_provider': 'ollama'}, id='lc_run--1c64618a-71d7-4b77-bbda-98d20a99b9fb-0', tool_calls=[{'name': 'maps_weather', 'args': {'city': '济南市'}, 'id': '1d51eb23-90f3-4799-98df-624432817eb8', 'type': 'tool_call'}], usage_metadata={'input_tokens': 2046, 'output_tokens': 112, 'total_tokens': 2158}), ToolMessage(content='{"city":"济南市","forecasts":[{"date":"2025-11-19","week":"3","dayweather":"晴","nightweather":"晴","daytemp":"13","nighttemp":"3","daywind":"北","nightwind":"北","daypower":"1-3","nightpower":"1-3","daytemp_float":"13.0","nighttemp_float":"3.0"},{"date":"2025-11-20","week":"4","dayweather":"多云","nightweather":"晴","daytemp":"11","nighttemp":"0","daywind":"北","nightwind":"北","daypower":"1-3","nightpower":"1-3","daytemp_float":"11.0","nighttemp_float":"0.0"},{"date":"2025-11-21","week":"5","dayweather":"晴","nightweather":"晴","daytemp":"14","nighttemp":"3","daywind":"北","nightwind":"北","daypower":"1-3","nightpower":"1-3","daytemp_float":"14.0","nighttemp_float":"3.0"},{"date":"2025-11-22","week":"6","dayweather":"晴","nightweather":"多云","daytemp":"15","nighttemp":"6","daywind":"北","nightwind":"北","daypower":"1-3","nightpower":"1-3","daytemp_float":"15.0","nighttemp_float":"6.0"}]}', name='maps_weather', id='ffa8d7b2-06f0-4f91-ba07-5e3250db6f6b', tool_call_id='1d51eb23-90f3-4799-98df-624432817eb8'), AIMessage(content='济南市未来四天的天气预报如下:\n11月19日:晴,气温13℃~3℃,北风\n11月20日:多云,气温11℃~0℃,北风\n11月21日:晴,气温14℃~3℃,北风\n11月22日:
晴,气温15℃~6℃,北风\n\n当前为晴天模式,气温在13℃至15℃之间,北风风力1-3级。', additional_kwargs={}, response_metadata={'model': 'qwen3:1.7b', 'created_at': '2025-11-19T05:53:07.155066618Z', 'done': True, 'done_reason': 'stop', 'total_duration': 33558027592, 'load_duration': 85445729, 'prompt_eval_count': 2404, 'prompt_eval_duration': 4345767854, 'eval_count': 332, 'eval_duration': 28921470098, 'model_name': 'qwen3:1.7b', 'model_provider': 'ollama'}, id='lc_run--f8342116-fca6-4266-80f7-1434890ca92e-0', 
usage_metadata={'input_tokens': 2404, 'output_tokens': 332, 'total_tokens': 2736})]}

其中可以看到我们需要的结果:

 AIMessage(content='济南市未来四天的天气预报如下:\n11月19日:晴,气温13℃~3℃,北风\n11月20日:多云,气温11℃~0℃,北风\n11月21日:晴,气温14℃~3℃,北风\n11月22日:
晴,气温15℃~6℃,北风\n\n当前为晴天模式,气温在13℃至15℃之间,北风风力1-3级。', additional_kwargs={}, response_metadata={'model': 'qwen3:1.7b', 'created_at': '2025-11-19T05:53:07.155066618Z', 'done': True, 'done_reason': 'stop', 'total_duration': 33558027592, 'load_duration': 85445729, 'prompt_eval_count': 2404, 'prompt_eval_duration': 4345767854, 'eval_count': 332, 'eval_duration': 28921470098, 'model_name': 'qwen3:1.7b', 'model_provider': 'ollama'}, id='lc_run--f8342116-fca6-4266-80f7-1434890ca92e-0', 
usage_metadata={'input_tokens': 2404, 'output_tokens': 332, 'total_tokens': 2736})
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐