用LangChain1.0搭建第一个天气查询智能体
LangChain v1.0发布重大更新:精简架构提升开发效率 2025年10月发布的LangChain v1.0对框架进行了重大重构,将核心功能聚焦于Agent开发,迁移旧功能至langchain-classic包。新版带来三大优势:降低认知负担(模块数量减少)、安装体积缩减60%、运行效率提升。迁移只需简单替换导入路径即可。 文章以天气查询Agent为例,演示如何使用新版本: 基于本地qwen
从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})
更多推荐


所有评论(0)