天气查询 Agent 零基础教程
天气查询 Agent 零基础教程
🎯 你将构建的程序
运行后输入一个城市名,AI Agent 自动调用天气 API 查询并返回结果:
你:帮我查一下上海今天天气 AI:上海今天天气晴朗,温度 20°C,湿度 55%,风速 4.3 km/h

设计原则:零跳跃学习
传统路线的问题:知识点 A → 知识点 C(发现需要 B,回头补 B)→ 知识点 D(发现需要前置知识 X)
本教程的目标:知识点 A → 知识点 B(A 的自然延伸)→ 知识点 C(B 的必然发展)→ 知识点 D(C 的合理进阶)
目录
- 第一步:创建文件并编写模块文档
- 第二步:导入依赖库
- 第三步:定义工具函数
- 第四步:注册工具列表
- 第五步:定义状态类
- 第六步:初始化模型
- 第七步:定义节点函数
- 第八步:构建状态图
- 第九步:编写主程序
- 完整代码
- 附录:技术名词速查表
📂 先看全貌 —— 完整代码由以下 7 个部分组成:
程序执行流程:程序运行时发生了什么?
下面这张图展示了你运行 python weather.py 后,程序内部发生的完整过程。建议先看懂这个流程,再跟着后面的步骤写代码,这样每一步你都知道自己在构建哪个环节:
关键数据流向(理解这个,就理解了整个 Agent):
- 用户消息 → 作为
HumanMessage放入messages列表 → 传入agent_node - agent_node → LLM 读
messages,决定调用get_weather→ 返回带tool_calls的AIMessage,追加到messages - tool_node → 从
messages中取出tool_calls,执行get_weather("上海")→ 将结果作为ToolMessage追加到messages - agent_node(第二次)→ LLM 再读
messages(现在有了天气数据),生成最终回答 → 返回纯文本AIMessage,追加到messages - should_continue → 每次 agent_node 执行后检查:
AIMessage里有tool_calls吗?有 → 去 tool_node;没有 → 结束
💡 把 Agent 理解为一个"有手脚的大脑":大脑(LLM)想"我需要查天气"→ 调动手脚(Tool)执行 → 手脚反馈结果 → 大脑再思考"现在我有了数据,可以回答了"。
环境准备
⚠️ 动手之前,先确保以下环境就绪。否则后面的代码无法运行。
1. 安装 Ollama(本地 AI 引擎)
Ollama 让你在本地运行大语言模型,无需联网、无需 API Key。
# 访问 https://ollama.com 下载并安装 Ollama
# 安装完成后,在终端执行:
ollama serve # 启动 Ollama 服务(保持运行)
ollama pull qwen3.5:2b # 下载通义千问 3.5 模型(约 1.2GB)
2. 安装 Python 依赖
pip install requests langchain-core langchain-ollama langgraph
| 包名 | 作用 | 哪一步用到 |
|---|---|---|
requests |
HTTP 请求(调用天气 API) | 第 2/3 步 |
langchain-core |
LangChain 核心模块(消息类型、工具定义) | 第 2/3/4/6 步 |
langchain-ollama |
Ollama 模型封装 | 第 2/6 步 |
langgraph |
状态图编排框架 | 第 2/7/8 步 |
3. 验证环境
python -c "import langgraph; print('OK')" # 应输出 OK
ollama list # 应显示 qwen3.5:2b
全部通过后,进入下一步。
第一步:创建文件并编写模块文档
Python 的模块文档(docstring)是写在文件开头的说明文字,它不会影响程序运行,但能让任何阅读代码的人快速理解项目。
💡 这一步写的 docstring 长达 60 行,可能看起来啰嗦,但它的价值在于:当你 3 个月后回头看这个文件时,不需要从头读代码就能理解整体设计。实际开发中,好的文档比好的代码更重要。
1.1 创建文件
在项目目录下创建 weather.py 文件。
1.2 编写模块文档
在文件开头,我们首先编写模块级文档字符串,说明整个模块的用途和设计思路:
"""
天气查询 Agent 模块
===================
【开发思路】
本模块基于 LangGraph 框架构建一个具备工具调用能力的 AI Agent,实现智能天气查询功能。
整体架构采用"状态机 + 工具调用"的设计模式,让大模型能够自主判断何时调用天气工具,
并将工具返回的结果整合到最终回答中。
核心设计理念:
1. 工具抽象:将天气查询功能封装为 LangChain Tool,使 LLM 能够通过函数调用方式使用
2. 状态管理:使用 TypedDict 定义状态结构,通过 Annotated 实现消息的累加式更新
3. 图编排:使用 StateGraph 构建可循环的状态机,支持条件分支和动态路由
4. 本地化:使用本地 Ollama 模型,避免依赖外部 API,保护数据隐私
【开发过程】
阶段一:工具定义
- 使用 @tool 装饰器将 Python 函数转换为 LangChain 工具
- 工具的 docstring 会自动转换为 LLM 可理解的工具描述
- 集成 Open-Meteo 免费 API,实现真实天气数据查询
阶段二:状态定义
- 使用 TypedDict 定义 AgentState 类型,明确状态结构
- messages 字段使用 Annotated[Sequence[BaseMessage], operator.add]
- operator.add 作为 reducer 函数,实现消息的追加而非覆盖
阶段三:模型配置
- 使用 ChatOllama 连接本地 Ollama 服务
- 通过 bind_tools() 方法将工具定义注入到模型中
- 模型会根据工具描述自动判断何时调用
阶段四:节点构建
- agent_node:LLM 推理节点,负责理解用户意图并决定是否调用工具
- tool_node:工具执行节点,使用 LangGraph 内置的 ToolNode
阶段五:流程编排
- 使用 StateGraph 创建状态图
- add_node() 注册节点
- add_edge() 添加确定性边
- add_conditional_edges() 添加条件分支边
阶段六:测试运行
- 创建 HumanMessage 作为用户输入
- 调用 graph.invoke() 执行状态图
- 从最终状态中提取 AI 回答
【技术栈】
- LangGraph:状态图编排框架,支持循环和条件分支
- LangChain:工具定义和模型抽象层
- Ollama:本地大模型运行时(qwen3.5:2b)
- Open-Meteo:免费开源天气 API,无需 API Key
【执行流程】
1. 用户输入"查上海天气"
2. Agent (LLM) 推理 → 判断需要调用工具
3. 调用 get_weather("上海") → 查询天气 API → 返回结果
4. Agent 再次推理 → 整合结果,生成最终回答
5. 输出 "上海今天晴朗,20°C"
循环机制:Agent 会在"推理→工具→推理"之间循环,
直到 LLM 判断信息充足,不再需要调用工具为止。
【关键概念说明】
- Agent:具备工具调用能力的 LLM,能自主决策并执行动作
- Tool:封装好的功能单元,LLM 可通过结构化参数调用
- State:在图节点间传递的数据结构,记录对话历史和中间结果
- Node:图中的处理单元,接收状态、执行逻辑、返回状态更新
- Edge:节点间的连接,分为确定性边和条件边
【扩展建议】
1. 可添加更多工具(如查询汇率、翻译等)实现多功能 Agent
2. 可添加记忆组件实现多轮对话
3. 可添加人工审核节点实现人机协作
"""
1.3 文档说明
为什么需要模块文档?
| 作用 | 说明 |
|---|---|
| 开发思路 | 让读者理解"为什么要这样设计" |
| 开发过程 | 提供清晰的开发路线图 |
| 技术栈 | 列出需要安装的依赖 |
| 执行流程 | 上述文字描述(详见教程开头的流程图) |
| 关键概念 | 解释专业术语 |
第二步:导入依赖库
现在给 weather.py 添加导入语句。以下 8 个代码块中的代码全部追加到同一个文件中,按 2.1 → 2.8 的顺序排列在模块文档下方。
📍 当前进度:① 创建文件 ✅ → ② 导入依赖 ← 📍 → ③ 工具定义 → ④ 注册工具 → ⑤ 状态类 → ⑥ 初始化模型 → ⑦ 节点函数 → ⑧ 构建图 → ⑨ 主程序
2.1 导入类型相关模块
TypedDict 和 Annotated 用于第五步定义状态类 AgentState。
# ==================== 导入依赖 ====================
# TypedDict:用于定义具有类型提示的字典类型
# 什么是 TypedDict?
# - Python 的普通字典:{"name": "张三", "age": 25},键和值可以是任意类型
# - TypedDict 字典:明确指定每个键的类型,IDE 会提供代码提示
# - 示例:class Person(TypedDict): name: str; age: int
from typing import TypedDict
# Annotated:用于为类型添加元数据(额外信息)
# 什么是 Annotated?
# - Annotated[类型, 元数据1, 元数据2, ...]
# - 可以在类型上附加说明、验证规则等
# - 本项目中用于指定 reducer 函数(消息如何合并)
from typing import Annotated
# Sequence:序列类型的泛型,用于类型注解
# 什么是 Sequence?
# - 序列是有序的数据集合,支持索引访问
# - 常见的序列:列表(list)、元组(tuple)、字符串(str)
# - Sequence[BaseMessage] 表示:一个包含 BaseMessage 对象的序列
from typing import Sequence
2.2 导入操作符模块
operator.add 作为状态 reducer,定义消息列表的"追加"更新方式(第五步用到)。
# operator 模块:提供标准操作符的函数形式
# 什么是 operator 模块?
# - Python 的运算符(+、-、*、/)都有对应的函数形式
# - operator.add 就是加法操作符 "+" 的函数形式
# - 为什么要用函数形式?因为可以当作参数传递
import operator
# operator.add:加法操作符的函数形式
# 用法示例:
# operator.add([1, 2], [3, 4]) # 结果:[1, 2, 3, 4]
# operator.add(1, 2) # 结果:3
# 在本项目中,operator.add 用作消息列表的 reducer
# reducer 的作用:定义如何将新消息合并到现有消息列表中
2.3 导入 HTTP 请求库
requests 用于第三步的工具函数中调用天气 API 获取数据。
# requests:HTTP 客户端库,用于调用 Open-Meteo API
# 什么是 HTTP 客户端?
# - HTTP 是互联网通信协议,浏览器和服务器通过它交换数据
# - requests 库让 Python 可以像浏览器一样发送 HTTP 请求
# - 示例:response = requests.get("https://api.example.com/weather")
import requests
# requests 的常用方法:
# - requests.get(url):发送 GET 请求(获取数据)
# - requests.post(url, data):发送 POST 请求(提交数据)
# - response.json():将响应内容解析为 JSON 格式
# - response.status_code:获取 HTTP 状态码(200=成功,404=未找到)
2.4 导入 LangChain 消息类型
HumanMessage(用户消息)、AIMessage(AI 消息)、ToolMessage(工具消息)是整个 Agent 的数据载体,第五步定义状态、第九步创建输入时都会用到。
# LangChain 核心消息类型
# 什么是消息?
# - 在对话系统中,每一条对话都是一条"消息"
# - 消息有不同的角色:用户、AI 助手、工具
# - LangChain 用不同的类来表示不同角色的消息
# BaseMessage:所有消息类型的基类(父类)
# - HumanMessage、AIMessage、ToolMessage 都继承自 BaseMessage
# - BaseMessage 定义了消息的通用属性:content(内容)、role(角色)
from langchain_core.messages import BaseMessage
# HumanMessage:用户消息
# - 表示用户发送给 AI 的消息
# - 示例:HumanMessage(content="上海天气怎么样?")
from langchain_core.messages import HumanMessage
# AIMessage:AI 助手消息
# - 表示 AI 返回的消息
# - 可能包含文本回答,也可能包含工具调用请求
# - 示例:AIMessage(content="上海今天天气晴朗...")
# - 示例:AIMessage(tool_calls=[{"name": "get_weather", ...}])
from langchain_core.messages import AIMessage
# ToolMessage:工具执行结果消息
# - 表示工具执行后返回的结果
# - 示例:ToolMessage(content="上海:晴朗,温度 20℃...")
from langchain_core.messages import ToolMessage
2.5 导入工具装饰器
@tool 装饰器是第三步的核心——它把普通 Python 函数变成 LLM 可调用的工具。
# tool 装饰器:将 Python 函数转换为 LangChain 工具
# 什么是装饰器?
# - 装饰器是一种特殊的函数,可以"装饰"其他函数
# - 被装饰的函数会获得额外的功能
# - @tool 装饰器会:
# 1. 提取函数名作为工具名
# 2. 提取 docstring 作为工具描述
# 3. 提取参数类型作为工具参数 schema
# 4. 让 LLM 能够理解并调用这个工具
from langchain_core.tools import tool
2.6 导入 Ollama 模型封装
ChatOllama 用于第六步创建本地 LLM 实例,连接你的 Ollama 服务。
# ChatOllama:Ollama 模型的 LangChain 封装
# 什么是 Ollama?
# - Ollama 是一个本地运行大模型的工具
# - 它可以下载和运行各种开源大模型(如 Qwen、Llama)
# - 安装命令:ollama pull qwen3.5:2b
# - 运行命令:ollama serve(启动服务)
# 什么是 ChatOllama?
# - LangChain 提供的 Ollama 接口封装
# - 让你可以用统一的方式调用 Ollama 模型
# - 支持对话、工具调用等功能
from langchain_ollama import ChatOllama
2.7 导入 LangGraph 图构建组件
StateGraph、START、END 是第八步构建状态图的核心组件。
# LangGraph 图构建组件
# 什么是 LangGraph?
# - LangGraph 是一个构建 AI Agent 工作流的框架
# - 它使用"状态图"的概念来编排 AI 的执行流程
# - 状态图 = 节点(处理单元)+ 边(流转路径)
# StateGraph:状态图构建器
# - 用于创建和管理状态图
# - 可以添加节点、添加边、设置入口点
# - 最终编译成可执行的 graph 对象
from langgraph.graph import StateGraph
# START:起始节点标记
# - 表示状态图的入口点
# - 所有流程都从 START 开始
from langgraph.graph import START
# END:结束节点标记
# - 表示状态图的终点
# - 到达 END 时流程结束
from langgraph.graph import END
2.8 导入工具节点
ToolNode 是 LangGraph 内置的工具执行器,第七步会用它创建工具节点。
# ToolNode:内置的工具执行节点
# 什么是 ToolNode?
# - LangGraph 提供的现成工具执行器
# - 它会自动:
# 1. 解析 AI 消息中的工具调用请求
# 2. 找到对应的工具函数
# 3. 执行工具并返回结果
# - 使用 ToolNode 可以省去自己写工具执行逻辑的工作
from langgraph.prebuilt import ToolNode
第三步:定义工具函数
3.1 工具函数概述
工具是 Agent 的"手脚",让 AI 能够执行实际操作。我们定义一个 get_weather 工具,用于查询城市天气。
💡 在写工具代码之前,先理解 @tool 装饰器做了什么:
3.2 工具函数完整代码
# ==================== 工具定义 ====================
@tool
def get_weather(city: str) -> str:
"""
查询指定城市的当前实时天气信息。
【开发思路】
本函数通过两步获取天气数据:
1. 调用 Open-Meteo 地理编码 API,将城市名称转换为经纬度坐标
2. 调用 Open-Meteo 天气预报 API,根据坐标获取实时天气数据
为什么要分两步?
- 天气 API 需要经纬度坐标,而用户通常只知道城市名称
- 地理编码 API 负责将人类可读的地名转换为机器可用的坐标
- 这种设计使得用户可以用自然语言(如"北京"、"Shanghai")查询天气
【设计考量】
- 使用 Open-Meteo 免费API,无需注册和 API Key,降低使用门槛
- 将天气代码(WMO标准)映射为中文描述,提升用户体验
- 异常处理确保网络问题不会导致程序崩溃
- 设置请求超时(10秒),防止长时间阻塞
【@tool 装饰器的作用】
- 自动提取函数签名和 docstring 生成工具描述
- LLM 会根据这些信息判断何时调用此工具
- Args 部分会转换为工具的参数 schema
- Returns 部分会告诉 LLM 工具的输出格式
Args:
city: 城市名称,支持中文或英文,如"北京"、"Shanghai"、"New York"
Returns:
str: 格式化的天气信息字符串,包含城市名、天气状况、温度、湿度、风速;
若查询失败则返回错误提示信息
Example:
>>> get_weather("上海")
'上海:晴朗,温度 19.8℃,湿度 57%,风速 4.1 km/h'
>>> get_weather("北京")
'北京:晴朗,温度 25.0℃,湿度 45%,风速 3.5 km/h'
>>> get_weather("不存在的城市")
'未找到城市:不存在的城市'
"""
# 使用 try-except 捕获所有可能的异常
# 包括:网络连接错误、超时错误、JSON 解析错误等
try:
# ==================== Step 1: 地理编码 ====================
# 构建地理编码 API 的请求 URL
# Open-Meteo 地理编码 API 用于将地名转换为经纬度坐标
geo_url = "https://geocoding-api.open-meteo.com/v1/search"
# 设置地理编码请求参数
geo_params = {
"name": city, # 要查询的城市名称(支持中英文)
"count": 1, # 只返回最匹配的一个结果(减少数据传输)
"language": "zh", # 使用中文返回地名(提升中文用户体验)
"format": "json" # 响应格式为 JSON(便于解析)
}
# 发送 HTTP GET 请求到地理编码 API
# params 参数会自动将字典转换为 URL 查询字符串
# timeout=10 设置 10 秒超时,防止网络问题导致程序无限等待
geo_response = requests.get(geo_url, params=geo_params, timeout=10)
# 将 HTTP 响应体(JSON 字符串)解析为 Python 字典
# .json() 方法会自动处理编码和解析
geo_data = geo_response.json()
# 检查响应中是否包含城市结果
# Open-Meteo 在找不到城市时返回 {"results": null} 或空 results 列表
if not geo_data.get("results"):
# 若未找到城市,返回友好的错误提示
# 这里不抛出异常,而是返回字符串,便于 LLM 理解和处理
return f"未找到城市:{city}"
# 从结果列表中提取第一个(最匹配的)位置信息
# results 是一个列表,按匹配度排序,第一个是最相关的
location = geo_data["results"][0]
# 提取纬度坐标(浮点数,范围 -90 到 90)
lat = location["latitude"]
# 提取经度坐标(浮点数,范围 -180 到 180)
lon = location["longitude"]
# 提取标准城市名称
# API 返回的名称可能比用户输入更规范
# 例如用户输入"shanghai",API 返回"Shanghai"或"上海"
city_name = location.get("name", city)
# ==================== Step 2: 获取天气数据 ====================
# 构建天气预报 API 的请求 URL
# Open-Meteo 天气预报 API 提供免费的天气数据
weather_url = "https://api.open-meteo.com/v1/forecast"
# 设置天气请求参数
weather_params = {
"latitude": lat, # 纬度坐标
"longitude": lon, # 经度坐标
# current 参数指定要获取的当前天气指标(多个指标用逗号分隔):
# - temperature_2m: 2米高度的气温(摄氏度)
# - relative_humidity_2m: 2米高度的相对湿度(百分比)
# - weather_code: WMO 天气代码(整数,表示天气状况)
# - wind_speed_10m: 10米高度的风速(km/h)
"current": "temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m",
"timezone": "auto" # 自动根据经纬度确定时区
}
# 发送 HTTP GET 请求到天气 API
weather_response = requests.get(weather_url, params=weather_params, timeout=10)
# 将响应解析为 Python 字典
weather_data = weather_response.json()
# ==================== Step 3: 解析天气数据 ====================
# 从响应中提取 "current" 字段
# current 包含当前时刻的天气数据
current = weather_data.get("current", {})
# 提取温度值(摄氏度)
# 若数据不存在,返回 "N/A"(Not Available)
temp = current.get("temperature_2m", "N/A")
# 提取相对湿度值(百分比,0-100)
humidity = current.get("relative_humidity_2m", "N/A")
# 提取风速值(km/h)
wind_speed = current.get("wind_speed_10m", "N/A")
# 提取 WMO 天气代码(整数)
# WMO 代码是国际标准化的天气状况编码
weather_code = current.get("weather_code", 0)
# ==================== Step 4: 天气代码映射 ====================
# 将 WMO 天气代码映射为中文描述
# WMO 代码分类:
# - 0-3: 晴天/多云
# - 45-48: 雾
# - 51-55: 毛毛雨
# - 61-65: 雨
# - 71-75: 雪
# - 80-82: 阵雨
# - 95: 雷暴
weather_desc = {
0: "晴朗", # 无云
1: "大部晴朗", # 少云
2: "局部多云", # 疏云
3: "阴天", # 阴
45: "雾", # 雾
48: "雾凇", # 冻雾
51: "小毛毛雨", # 轻毛毛雨
53: "中毛毛雨", # 中毛毛雨
55: "大毛毛雨", # 重毛毛雨
61: "小雨", # 小雨
63: "中雨", # 中雨
65: "大雨", # 大雨
71: "小雪", # 小雪
73: "中雪", # 中雪
75: "大雪", # 大雪
80: "小阵雨", # 小阵雨
81: "中阵雨", # 中阵雨
82: "大阵雨", # 大阵雨
95: "雷暴" # 雷暴
}.get(weather_code, f"天气代码{weather_code}") # 若代码不在映射中,显示原始代码
# ==================== Step 5: 格式化输出 ====================
# 格式化并返回完整的天气信息字符串
# 使用 f-string 进行字符串格式化
return f"{city_name}:{weather_desc},温度 {temp}℃,湿度 {humidity}%,风速 {wind_speed} km/h"
except Exception as e:
# 捕获所有异常(网络错误、超时、JSON解析错误等)
# 返回友好的错误信息,便于 LLM 理解和处理
return f"查询天气失败:{str(e)}"
3.3 工具函数流程图

第四步:注册工具列表
4.1 为什么要注册工具?
工具定义好后,需要放入一个列表中,这样 LLM 才知道有哪些工具可用。
4.2 注册代码
💡 工具从定义到调用的完整链路:
# ==================== 工具注册 ====================
# 将工具函数注册到工具列表中
# tools 列表会被传递给 LLM,使其知道有哪些工具可用
# LLM 会根据工具的名称、描述和参数定义决定何时调用
tools = [get_weather]
# 说明:
# - tools 是一个列表,可以包含多个工具
# - 每个工具都是被 @tool 装饰器装饰过的函数
# - 如果有更多工具,可以这样写:
# tools = [get_weather, translate, calculate]
第五步:定义状态类
5.1 什么是状态?
状态是 Agent 的"记忆",记录了对话过程中发生的所有事情。在 LangGraph 中,状态在节点之间传递,每个节点可以读取和更新状态。
5.2 状态类定义代码
只有 1 行有效代码,但涉及 4 个新概念。先解释:
| 概念 | 作用 | 类比 |
|---|---|---|
TypedDict |
给字典加类型约束 | 有格式的笔记本 |
Sequence[BaseMessage] |
消息的有序列表类型 | 排好队的消息队列 |
Annotated |
给类型附加元数据 | 给类型贴标签 |
operator.add |
reducer 函数,新消息追加而非覆盖 | 往聊天记录本末尾写,不擦掉旧内容 |
# ==================== 状态定义 ====================
class AgentState(TypedDict):
"""
Agent 状态定义类。
【开发思路】
LangGraph 使用状态机模式管理 Agent 的执行流程。
AgentState 定义了在图节点之间传递的状态结构。
为什么需要状态?
- 状态是节点间通信的载体
- 状态记录了对话历史和中间结果
- 状态的更新方式决定了信息的累积方式
本实现中,状态仅包含一个 messages 字段,用于存储对话历史。
这种设计遵循 LangChain 的消息模式,便于与各种 LLM 集成。
【类型注解说明】
- messages: 消息列表,包含 HumanMessage、AIMessage、ToolMessage 等
- Annotated[Sequence[BaseMessage], operator.add]:
* Sequence[BaseMessage]: 消息的序列类型
* Annotated: 为类型添加元数据
* operator.add: 作为 reducer 函数,定义状态更新方式
【reducer 函数的作用】
- 当节点返回 {"messages": [new_msg]} 时
- LangGraph 会调用 reducer(state["messages"], [new_msg])
- operator.add 执行列表加法:旧消息 + 新消息
- 结果:新消息追加到现有列表末尾
- 这实现了消息的累积,而非覆盖
【状态流转示意】
初始状态:
{"messages": [HumanMessage("查上海天气")]}
Agent 处理后(LLM 决定调用工具):
{"messages": [
HumanMessage("查上海天气"),
AIMessage(tool_calls=[{name: "get_weather", args: {city: "上海"}}])
]}
工具执行后:
{"messages": [
HumanMessage("查上海天气"),
AIMessage(tool_calls=[...]),
ToolMessage(content="上海:晴朗,温度 20℃...")
]}
Agent 再次处理后(LLM 生成最终回答):
{"messages": [
HumanMessage("查上海天气"),
AIMessage(tool_calls=[...]),
ToolMessage(content="..."),
AIMessage(content="上海今天天气晴朗...")
]}
【扩展建议】
可以添加更多状态字段,如:
- current_step: 当前执行步骤
- tool_results: 工具执行结果缓存
- user_context: 用户上下文信息
"""
messages: Annotated[Sequence[BaseMessage], operator.add]
5.3 状态流转图
💡 理解状态中的三种消息类型:

第六步:初始化模型
创建 LLM 实例并绑定工具。这里用到了 第二步 2.6 导入的 ChatOllama 和 第四步定义的 tools 列表。
6.1 创建 LLM 实例
# ==================== 模型初始化 ====================
# 初始化 ChatOllama 模型实例
# ChatOllama 是 LangChain 对 Ollama API 的封装
llm = ChatOllama(
# 模型配置
model="qwen3.5:2b", # Ollama 模型名称(通义千问 3.5 的 2B 参数版本)
# 安装命令: ollama pull qwen3.5:2b
# 服务配置
base_url="http://localhost:11434" # Ollama 服务地址(原生 API,无需 /v1 后缀)
# 默认端口: 11434
# 若 Ollama 运行在其他地址,需修改此处
)
6.2 绑定工具到模型
# 将工具绑定到 LLM,使其具备工具调用能力
llm_with_tools = llm.bind_tools(tools)
# bind_tools() 方法作用:
# 1. 将工具定义转换为 OpenAI 兼容的 function calling 格式
# 2. 将工具描述注入到模型的 system prompt 中
# 3. 使模型能够返回结构化的工具调用参数
# 绑定工具后,LLM 会:
# - 知道有哪些工具可用
# - 知道每个工具的功能和参数
# - 能够决定何时调用哪个工具
第七步:定义节点函数
定义 Agent 的三个核心组件:推理节点、工具执行节点、条件路由函数。代码中用到的 llm_with_tools 来自 第六步 6.2,tools 来自 第四步。
7.1 定义 agent_node(推理节点)
# ==================== 节点定义 ====================
def agent_node(state: AgentState) -> AgentState:
"""
LLM 推理节点函数。
【开发思路】
这是 Agent 的"大脑"节点,是整个系统的核心。
节点的职责:
1. 接收当前状态中的消息历史
2. 调用 LLM 进行推理,理解用户意图
3. 判断是否需要调用工具获取更多信息
4. 返回 AI 的响应(可能是工具调用请求或最终回答)
【执行流程】
1. 用户输入"查上海天气"
2. Agent (LLM) 推理 → 判断需要调用工具
3. 调用 get_weather("上海") → 查询天气 API → 返回结果
4. Agent 再次推理 → 整合结果,生成最终回答
5. 输出 "上海今天晴朗,20°C"
循环机制:Agent 会在"推理→工具→推理"之间循环,
直到 LLM 判断信息充足,不再需要调用工具为止。
情况一:LLM 决定调用工具
- 返回的 AIMessage 包含 tool_calls 属性
- tool_calls 是一个列表,每个元素包含工具名和参数
- 示例:tool_calls=[{name: "get_weather", args: {city: "上海"}}]
情况二:LLM 直接回答
- 返回的 AIMessage 仅包含 content 属性
- content 是字符串形式的回答
- 示例:content="上海今天天气晴朗..."
【节点函数的返回值】
- 必须返回一个字典
- 字典的键对应 AgentState 的字段名
- 字典的值是要更新的内容
- LangGraph 会自动调用 reducer 合并新旧状态
Args:
state: 当前 Agent 状态,包含历史消息列表
state["messages"] 是消息列表
state["messages"][-1] 是最新的消息
Returns:
AgentState: 更新后的状态字典
{"messages": [AIMessage]} 表示追加一条 AI 消息
"""
# 调用绑定了工具的 LLM,传入历史消息进行推理
# invoke() 方法会:
# 1. 将消息列表转换为模型可理解的格式
# 2. 发送到 Ollama API
# 3. 解析响应并返回 AIMessage 对象
res = llm_with_tools.invoke(state["messages"])
# 返回状态更新字典
# 这里只返回新消息,LangGraph 会自动将其追加到 messages 列表
# 等价于:state["messages"] = state["messages"] + [res]
return {"messages": [res]}
7.2 创建 tool_node(工具执行节点)
# 创建工具执行节点
# ToolNode 是 LangGraph 内置的工具执行器
# 它会自动:
# 1. 解析 AIMessage 中的 tool_calls
# 2. 根据工具名称找到对应的工具函数
# 3. 使用提供的参数调用工具函数
# 4. 将工具返回值包装为 ToolMessage
tool_node = ToolNode(tools=tools)
7.3 定义 should_continue(条件路由函数)
def should_continue(state: AgentState) -> str:
"""
条件路由函数,决定 Agent 的下一步行动。
【开发思路】
在 LangGraph 中,条件边允许根据状态动态选择下一个节点。
这类似于编程中的 if-else 分支,但基于状态而非变量。
本函数是 agent 节点的出口路由,检查 LLM 是否需要调用工具:
- 有工具调用 → 路由到 "tools" 节点执行工具
- 无工具调用 → 返回 END 结束流程
【判断逻辑】
LLM 返回的 AIMessage 对象中:
- tool_calls 属性:包含工具调用请求的列表
- 若 tool_calls 非空(有内容):说明 LLM 认为需要调用工具
- 若 tool_calls 为空或 None:说明 LLM 已经可以给出最终回答
【条件边的返回值】
- 返回值必须是 add_conditional_edges() 中指定的路径之一
- 本例中可能的返回值:"tools" 或 END
- "tools" 对应工具执行节点
- END 是 LangGraph 内置的结束标记
【为什么需要这个函数?】
- Agent 可能需要多次调用工具
- 每次工具调用后,Agent 需要再次推理
- 只有当 Agent 认为信息足够时才结束
- 这个函数实现了"循环直到完成"的逻辑
Args:
state: 当前 Agent 状态
state["messages"][-1] 是最新的 AIMessage
Returns:
str: 下一个节点名称,"tools" 或 END
"""
# 获取最新的一条消息
# state["messages"] 是消息列表
# [-1] 表示取最后一个元素(最新的消息)
last_msg = state["messages"][-1]
# 如果 LLM 决定调用工具,tool_calls 会是一个非空列表
if last_msg.tool_calls:
# 有工具调用,返回 "tools" 节点名称
# LangGraph 会将流程路由到 tools 节点
return "tools"
# 无工具调用,返回 END 表示流程结束
# LangGraph 会结束图的执行,返回最终状态
return END
第八步:构建状态图
用 LangGraph 的 StateGraph 把前面定义的所有组件组装起来。这一步用到了:
- 第五步 定义的
AgentState(状态类型) - 第七步 7.1 定义的
agent_node(推理节点) - 第七步 7.2 创建的
tool_node(工具执行节点) - 第七步 7.3 定义的
should_continue(条件路由函数)
8.1 创建状态图构建器
# ==================== 图构建 ====================
# 创建状态图构建器
# StateGraph 是 LangGraph 的核心类,用于构建状态机
# AgentState 是状态类型,定义了节点间传递的数据结构
builder = StateGraph(AgentState)
8.2 注册节点
# ------------------ 注册节点 ------------------
# 注册 "agent" 节点
# 第一个参数是节点名称(字符串),用于在边定义中引用
# 第二个参数是节点函数,接收状态并返回状态更新
builder.add_node("agent", agent_node)
# 注册 "tools" 节点
# tool_node 是 ToolNode 实例,它实现了 Runnable 接口
# 可以像函数一样作为节点使用
builder.add_node("tools", tool_node)
8.3 定义边
# ------------------ 定义边 ------------------
# 添加起始边:从 START 直接进入 "agent" 节点
# START 是 LangGraph 内置的起始标记
# 这条边表示:图开始执行时,首先进入 agent 节点
builder.add_edge(START, "agent")
# 添加条件边:从 "agent" 节点出发
# 条件边允许根据状态动态选择下一个节点
# 参数说明:
# - "agent": 边的起始节点
# - should_continue: 条件函数,决定走哪条路径
# - ["tools", END]: 可能的路径列表
builder.add_conditional_edges(
"agent", # 从 agent 节点出发
should_continue, # 使用 should_continue 函数决定路径
["tools", END] # 可能的路径:tools 节点或 END
)
# 添加普通边:工具执行完毕后,返回 "agent" 节点继续推理
# 这形成了 agent → tools → agent 的循环
# 循环会一直持续,直到 LLM 决定不再调用工具
# 这是 Agent 能够"多步推理"的关键设计
builder.add_edge("tools", "agent")
8.4 编译图
# ------------------ 编译图 ------------------
# 编译状态图,生成可执行的 graph 对象
# compile() 方法会:
# 1. 验证图的完整性(所有节点都有入边和出边)
# 2. 构建内部数据结构
# 3. 返回一个可调用的 Runnable 对象
graph = builder.compile()
8.5 图结构可视化

第九步:编写主程序
最后一步:编写程序入口,创建用户输入、执行状态图、打印结果。这里用到了 第二步 2.4 导入的 HumanMessage 和 第八步 8.4 编译的 graph。
9.1 主程序代码
# ==================== 主程序 ====================
# 主程序入口
# if __name__ == "__main__": 确保代码只在直接运行时执行
# 而在被导入为模块时不执行
if __name__ == "__main__":
# ------------------ 创建用户输入 ------------------
# 创建用户输入消息
# HumanMessage 表示用户发送的消息
# content 参数是消息内容
user_input = HumanMessage(content="帮我查一下上海今天天气")
# ------------------ 执行状态图 ------------------
# 执行状态图,传入初始状态
# invoke() 方法会:
# 1. 从 START 开始执行
# 2. 按照边的定义依次执行节点
# 3. 在条件边处根据状态选择路径
# 4. 到达 END 时停止并返回最终状态
# 参数:{"messages": [user_input]} 是初始状态
result = graph.invoke({"messages": [user_input]})
# ------------------ 提取结果 ------------------
# 从结果中提取最后一条消息的内容
# result 是最终状态,包含所有消息
# result["messages"][-1] 是最后一条消息(AI 的最终回答)
# .content 属性是消息的文本内容
final_answer = result["messages"][-1].content
# ------------------ 输出结果 ------------------
# 打印 AI 的最终回答
print("🤖 AI 最终回答:")
print(final_answer)
9.2 执行流程时序图

完整代码
💡 如果你不想跟着一步步敲,可以直接复制下面的完整代码。但建议先跟着步骤做一遍,这样才能真正理解每个部分的作用。
将以上所有步骤的代码按顺序组合,得到完整的 weather.py 文件:
"""
天气查询 Agent 模块
===================
【开发思路】
本模块基于 LangGraph 框架构建一个具备工具调用能力的 AI Agent,实现智能天气查询功能。
整体架构采用"状态机 + 工具调用"的设计模式,让大模型能够自主判断何时调用天气工具,
并将工具返回的结果整合到最终回答中。
核心设计理念:
1. 工具抽象:将天气查询功能封装为 LangChain Tool,使 LLM 能够通过函数调用方式使用
2. 状态管理:使用 TypedDict 定义状态结构,通过 Annotated 实现消息的累加式更新
3. 图编排:使用 StateGraph 构建可循环的状态机,支持条件分支和动态路由
4. 本地化:使用本地 Ollama 模型,避免依赖外部 API,保护数据隐私
【开发过程】
阶段一:工具定义
- 使用 @tool 装饰器将 Python 函数转换为 LangChain 工具
- 工具的 docstring 会自动转换为 LLM 可理解的工具描述
- 集成 Open-Meteo 免费 API,实现真实天气数据查询
阶段二:状态定义
- 使用 TypedDict 定义 AgentState 类型,明确状态结构
- messages 字段使用 Annotated[Sequence[BaseMessage], operator.add]
- operator.add 作为 reducer 函数,实现消息的追加而非覆盖
阶段三:模型配置
- 使用 ChatOllama 连接本地 Ollama 服务
- 通过 bind_tools() 方法将工具定义注入到模型中
- 模型会根据工具描述自动判断何时调用
阶段四:节点构建
- agent_node:LLM 推理节点,负责理解用户意图并决定是否调用工具
- tool_node:工具执行节点,使用 LangGraph 内置的 ToolNode
阶段五:流程编排
- 使用 StateGraph 创建状态图
- add_node() 注册节点
- add_edge() 添加确定性边
- add_conditional_edges() 添加条件分支边
阶段六:测试运行
- 创建 HumanMessage 作为用户输入
- 调用 graph.invoke() 执行状态图
- 从最终状态中提取 AI 回答
【技术栈】
- LangGraph:状态图编排框架,支持循环和条件分支
- LangChain:工具定义和模型抽象层
- Ollama:本地大模型运行时(qwen3.5:2b)
- Open-Meteo:免费开源天气 API,无需 API Key
【执行流程】
1. 用户输入"查上海天气"
2. Agent (LLM) 推理 → 判断需要调用工具
3. 调用 get_weather("上海") → 查询天气 API → 返回结果
4. Agent 再次推理 → 整合结果,生成最终回答
5. 输出 "上海今天晴朗,20°C"
循环机制:Agent 会在"推理→工具→推理"之间循环,
直到 LLM 判断信息充足,不再需要调用工具为止。
【关键概念说明】
- Agent:具备工具调用能力的 LLM,能自主决策并执行动作
- Tool:封装好的功能单元,LLM 可通过结构化参数调用
- State:在图节点间传递的数据结构,记录对话历史和中间结果
- Node:图中的处理单元,接收状态、执行逻辑、返回状态更新
- Edge:节点间的连接,分为确定性边和条件边
【扩展建议】
1. 可添加更多工具(如查询汇率、翻译等)实现多功能 Agent
2. 可添加记忆组件实现多轮对话
3. 可添加人工审核节点实现人机协作
"""
# ==================== 导入依赖 ====================
# TypedDict:用于定义具有类型提示的字典类型
# Annotated:用于为类型添加元数据(如 reducer 函数)
# Sequence:序列类型的泛型,用于类型注解
from typing import TypedDict, Annotated, Sequence
# operator 模块:提供标准操作符的函数形式
# operator.add:加法操作符的函数形式,用作消息列表的 reducer
import operator
# requests:HTTP 客户端库,用于调用 Open-Meteo API
import requests
# LangChain 核心消息类型
# BaseMessage:所有消息类型的基类
# HumanMessage:用户消息
# AIMessage:AI 助手消息
# ToolMessage:工具执行结果消息
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, ToolMessage
# tool 装饰器:将 Python 函数转换为 LangChain 工具
from langchain_core.tools import tool
# ChatOllama:Ollama 模型的 LangChain 封装
from langchain_ollama import ChatOllama
# LangGraph 图构建组件
# StateGraph:状态图构建器
# START:起始节点标记
# END:结束节点标记
from langgraph.graph import StateGraph, START, END
# ToolNode:内置的工具执行节点
from langgraph.prebuilt import ToolNode
# ==================== 工具定义 ====================
@tool
def get_weather(city: str) -> str:
"""
查询指定城市的当前实时天气信息。
【开发思路】
本函数通过两步获取天气数据:
1. 调用 Open-Meteo 地理编码 API,将城市名称转换为经纬度坐标
2. 调用 Open-Meteo 天气预报 API,根据坐标获取实时天气数据
为什么要分两步?
- 天气 API 需要经纬度坐标,而用户通常只知道城市名称
- 地理编码 API 负责将人类可读的地名转换为机器可用的坐标
- 这种设计使得用户可以用自然语言(如"北京"、"Shanghai")查询天气
【设计考量】
- 使用 Open-Meteo 免费API,无需注册和 API Key,降低使用门槛
- 将天气代码(WMO标准)映射为中文描述,提升用户体验
- 异常处理确保网络问题不会导致程序崩溃
- 设置请求超时(10秒),防止长时间阻塞
【@tool 装饰器的作用】
- 自动提取函数签名和 docstring 生成工具描述
- LLM 会根据这些信息判断何时调用此工具
- Args 部分会转换为工具的参数 schema
- Returns 部分会告诉 LLM 工具的输出格式
Args:
city: 城市名称,支持中文或英文,如"北京"、"Shanghai"、"New York"
Returns:
str: 格式化的天气信息字符串,包含城市名、天气状况、温度、湿度、风速;
若查询失败则返回错误提示信息
Example:
>>> get_weather("上海")
'上海:晴朗,温度 19.8℃,湿度 57%,风速 4.1 km/h'
>>> get_weather("北京")
'北京:晴朗,温度 25.0℃,湿度 45%,风速 3.5 km/h'
>>> get_weather("不存在的城市")
'未找到城市:不存在的城市'
"""
# 使用 try-except 捕获所有可能的异常
# 包括:网络连接错误、超时错误、JSON 解析错误等
try:
# ==================== Step 1: 地理编码 ====================
# 构建地理编码 API 的请求 URL
# Open-Meteo 地理编码 API 用于将地名转换为经纬度坐标
geo_url = "https://geocoding-api.open-meteo.com/v1/search"
# 设置地理编码请求参数
geo_params = {
"name": city, # 要查询的城市名称(支持中英文)
"count": 1, # 只返回最匹配的一个结果(减少数据传输)
"language": "zh", # 使用中文返回地名(提升中文用户体验)
"format": "json" # 响应格式为 JSON(便于解析)
}
# 发送 HTTP GET 请求到地理编码 API
# params 参数会自动将字典转换为 URL 查询字符串
# timeout=10 设置 10 秒超时,防止网络问题导致程序无限等待
geo_response = requests.get(geo_url, params=geo_params, timeout=10)
# 将 HTTP 响应体(JSON 字符串)解析为 Python 字典
# .json() 方法会自动处理编码和解析
geo_data = geo_response.json()
# 检查响应中是否包含城市结果
# Open-Meteo 在找不到城市时返回 {"results": null} 或空 results 列表
if not geo_data.get("results"):
# 若未找到城市,返回友好的错误提示
# 这里不抛出异常,而是返回字符串,便于 LLM 理解和处理
return f"未找到城市:{city}"
# 从结果列表中提取第一个(最匹配的)位置信息
# results 是一个列表,按匹配度排序,第一个是最相关的
location = geo_data["results"][0]
# 提取纬度坐标(浮点数,范围 -90 到 90)
lat = location["latitude"]
# 提取经度坐标(浮点数,范围 -180 到 180)
lon = location["longitude"]
# 提取标准城市名称
# API 返回的名称可能比用户输入更规范
# 例如用户输入"shanghai",API 返回"Shanghai"或"上海"
city_name = location.get("name", city)
# ==================== Step 2: 获取天气数据 ====================
# 构建天气预报 API 的请求 URL
# Open-Meteo 天气预报 API 提供免费的天气数据
weather_url = "https://api.open-meteo.com/v1/forecast"
# 设置天气请求参数
weather_params = {
"latitude": lat, # 纬度坐标
"longitude": lon, # 经度坐标
# current 参数指定要获取的当前天气指标(多个指标用逗号分隔):
# - temperature_2m: 2米高度的气温(摄氏度)
# - relative_humidity_2m: 2米高度的相对湿度(百分比)
# - weather_code: WMO 天气代码(整数,表示天气状况)
# - wind_speed_10m: 10米高度的风速(km/h)
"current": "temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m",
"timezone": "auto" # 自动根据经纬度确定时区
}
# 发送 HTTP GET 请求到天气 API
weather_response = requests.get(weather_url, params=weather_params, timeout=10)
# 将响应解析为 Python 字典
weather_data = weather_response.json()
# ==================== Step 3: 解析天气数据 ====================
# 从响应中提取 "current" 字段
# current 包含当前时刻的天气数据
current = weather_data.get("current", {})
# 提取温度值(摄氏度)
# 若数据不存在,返回 "N/A"(Not Available)
temp = current.get("temperature_2m", "N/A")
# 提取相对湿度值(百分比,0-100)
humidity = current.get("relative_humidity_2m", "N/A")
# 提取风速值(km/h)
wind_speed = current.get("wind_speed_10m", "N/A")
# 提取 WMO 天气代码(整数)
# WMO 代码是国际标准化的天气状况编码
weather_code = current.get("weather_code", 0)
# ==================== Step 4: 天气代码映射 ====================
# 将 WMO 天气代码映射为中文描述
# WMO 代码分类:
# - 0-3: 晴天/多云
# - 45-48: 雾
# - 51-55: 毛毛雨
# - 61-65: 雨
# - 71-75: 雪
# - 80-82: 阵雨
# - 95: 雷暴
weather_desc = {
0: "晴朗", # 无云
1: "大部晴朗", # 少云
2: "局部多云", # 疏云
3: "阴天", # 阴
45: "雾", # 雾
48: "雾凇", # 冻雾
51: "小毛毛雨", # 轻毛毛雨
53: "中毛毛雨", # 中毛毛雨
55: "大毛毛雨", # 重毛毛雨
61: "小雨", # 小雨
63: "中雨", # 中雨
65: "大雨", # 大雨
71: "小雪", # 小雪
73: "中雪", # 中雪
75: "大雪", # 大雪
80: "小阵雨", # 小阵雨
81: "中阵雨", # 中阵雨
82: "大阵雨", # 大阵雨
95: "雷暴" # 雷暴
}.get(weather_code, f"天气代码{weather_code}") # 若代码不在映射中,显示原始代码
# ==================== Step 5: 格式化输出 ====================
# 格式化并返回完整的天气信息字符串
# 使用 f-string 进行字符串格式化
return f"{city_name}:{weather_desc},温度 {temp}℃,湿度 {humidity}%,风速 {wind_speed} km/h"
except Exception as e:
# 捕获所有异常(网络错误、超时、JSON解析错误等)
# 返回友好的错误信息,便于 LLM 理解和处理
return f"查询天气失败:{str(e)}"
# ==================== 工具注册 ====================
# 将工具函数注册到工具列表中
# tools 列表会被传递给 LLM,使其知道有哪些工具可用
# LLM 会根据工具的名称、描述和参数定义决定何时调用
tools = [get_weather]
# ==================== 状态定义 ====================
class AgentState(TypedDict):
"""
Agent 状态定义类。
【开发思路】
LangGraph 使用状态机模式管理 Agent 的执行流程。
AgentState 定义了在图节点之间传递的状态结构。
为什么需要状态?
- 状态是节点间通信的载体
- 状态记录了对话历史和中间结果
- 状态的更新方式决定了信息的累积方式
本实现中,状态仅包含一个 messages 字段,用于存储对话历史。
这种设计遵循 LangChain 的消息模式,便于与各种 LLM 集成。
【类型注解说明】
- messages: 消息列表,包含 HumanMessage、AIMessage、ToolMessage 等
- Annotated[Sequence[BaseMessage], operator.add]:
* Sequence[BaseMessage]: 消息的序列类型
* Annotated: 为类型添加元数据
* operator.add: 作为 reducer 函数,定义状态更新方式
【reducer 函数的作用】
- 当节点返回 {"messages": [new_msg]} 时
- LangGraph 会调用 reducer(state["messages"], [new_msg])
- operator.add 执行列表加法:旧消息 + 新消息
- 结果:新消息追加到现有列表末尾
- 这实现了消息的累积,而非覆盖
【状态流转示意】
初始状态:
{"messages": [HumanMessage("查上海天气")]}
Agent 处理后(LLM 决定调用工具):
{"messages": [
HumanMessage("查上海天气"),
AIMessage(tool_calls=[{name: "get_weather", args: {city: "上海"}}])
]}
工具执行后:
{"messages": [
HumanMessage("查上海天气"),
AIMessage(tool_calls=[...]),
ToolMessage(content="上海:晴朗,温度 20℃...")
]}
Agent 再次处理后(LLM 生成最终回答):
{"messages": [
HumanMessage("查上海天气"),
AIMessage(tool_calls=[...]),
ToolMessage(content="..."),
AIMessage(content="上海今天天气晴朗...")
]}
【扩展建议】
可以添加更多状态字段,如:
- current_step: 当前执行步骤
- tool_results: 工具执行结果缓存
- user_context: 用户上下文信息
"""
messages: Annotated[Sequence[BaseMessage], operator.add]
# ==================== 模型初始化 ====================
# 初始化 ChatOllama 模型实例
# ChatOllama 是 LangChain 对 Ollama API 的封装
llm = ChatOllama(
# 模型配置
model="qwen3.5:2b", # Ollama 模型名称(通义千问 3.5 的 2B 参数版本)
# 安装命令: ollama pull qwen3.5:2b
# 服务配置
base_url="http://localhost:11434" # Ollama 服务地址(原生 API,无需 /v1 后缀)
# 默认端口: 11434
# 若 Ollama 运行在其他地址,需修改此处
)
# 将工具绑定到 LLM,使其具备工具调用能力
llm_with_tools = llm.bind_tools(tools)
# bind_tools() 方法作用:
# 1. 将工具定义转换为 OpenAI 兼容的 function calling 格式
# 2. 将工具描述注入到模型的 system prompt 中
# 3. 使模型能够返回结构化的工具调用参数
# ==================== 节点定义 ====================
def agent_node(state: AgentState) -> AgentState:
"""
LLM 推理节点函数。
【开发思路】
这是 Agent 的"大脑"节点,是整个系统的核心。
节点的职责:
1. 接收当前状态中的消息历史
2. 调用 LLM 进行推理,理解用户意图
3. 判断是否需要调用工具获取更多信息
4. 返回 AI 的响应(可能是工具调用请求或最终回答)
【执行流程】
1. 用户输入"查上海天气"
2. Agent (LLM) 推理 → 判断需要调用工具
3. 调用 get_weather("上海") → 查询天气 API → 返回结果
4. Agent 再次推理 → 整合结果,生成最终回答
5. 输出 "上海今天晴朗,20°C"
循环机制:Agent 会在"推理→工具→推理"之间循环,
直到 LLM 判断信息充足,不再需要调用工具为止。
情况一:LLM 决定调用工具
- 返回的 AIMessage 包含 tool_calls 属性
- tool_calls 是一个列表,每个元素包含工具名和参数
- 示例:tool_calls=[{name: "get_weather", args: {city: "上海"}}]
情况二:LLM 直接回答
- 返回的 AIMessage 仅包含 content 属性
- content 是字符串形式的回答
- 示例:content="上海今天天气晴朗..."
【节点函数的返回值】
- 必须返回一个字典
- 字典的键对应 AgentState 的字段名
- 字典的值是要更新的内容
- LangGraph 会自动调用 reducer 合并新旧状态
Args:
state: 当前 Agent 状态,包含历史消息列表
state["messages"] 是消息列表
state["messages"][-1] 是最新的消息
Returns:
AgentState: 更新后的状态字典
{"messages": [AIMessage]} 表示追加一条 AI 消息
"""
# 调用绑定了工具的 LLM,传入历史消息进行推理
# invoke() 方法会:
# 1. 将消息列表转换为模型可理解的格式
# 2. 发送到 Ollama API
# 3. 解析响应并返回 AIMessage 对象
res = llm_with_tools.invoke(state["messages"])
# 返回状态更新字典
# 这里只返回新消息,LangGraph 会自动将其追加到 messages 列表
# 等价于:state["messages"] = state["messages"] + [res]
return {"messages": [res]}
# 创建工具执行节点
# ToolNode 是 LangGraph 内置的工具执行器
# 它会自动:
# 1. 解析 AIMessage 中的 tool_calls
# 2. 根据工具名称找到对应的工具函数
# 3. 使用提供的参数调用工具函数
# 4. 将工具返回值包装为 ToolMessage
tool_node = ToolNode(tools=tools)
def should_continue(state: AgentState) -> str:
"""
条件路由函数,决定 Agent 的下一步行动。
【开发思路】
在 LangGraph 中,条件边允许根据状态动态选择下一个节点。
这类似于编程中的 if-else 分支,但基于状态而非变量。
本函数是 agent 节点的出口路由,检查 LLM 是否需要调用工具:
- 有工具调用 → 路由到 "tools" 节点执行工具
- 无工具调用 → 返回 END 结束流程
【判断逻辑】
LLM 返回的 AIMessage 对象中:
- tool_calls 属性:包含工具调用请求的列表
- 若 tool_calls 非空(有内容):说明 LLM 认为需要调用工具
- 若 tool_calls 为空或 None:说明 LLM 已经可以给出最终回答
【条件边的返回值】
- 返回值必须是 add_conditional_edges() 中指定的路径之一
- 本例中可能的返回值:"tools" 或 END
- "tools" 对应工具执行节点
- END 是 LangGraph 内置的结束标记
【为什么需要这个函数?】
- Agent 可能需要多次调用工具
- 每次工具调用后,Agent 需要再次推理
- 只有当 Agent 认为信息足够时才结束
- 这个函数实现了"循环直到完成"的逻辑
Args:
state: 当前 Agent 状态
state["messages"][-1] 是最新的 AIMessage
Returns:
str: 下一个节点名称,"tools" 或 END
"""
# 获取最新的一条消息
# state["messages"] 是消息列表
# [-1] 表示取最后一个元素(最新的消息)
last_msg = state["messages"][-1]
# 如果 LLM 决定调用工具,tool_calls 会是一个非空列表
if last_msg.tool_calls:
# 有工具调用,返回 "tools" 节点名称
# LangGraph 会将流程路由到 tools 节点
return "tools"
# 无工具调用,返回 END 表示流程结束
# LangGraph 会结束图的执行,返回最终状态
return END
# ==================== 图构建 ====================
# 创建状态图构建器
# StateGraph 是 LangGraph 的核心类,用于构建状态机
# AgentState 是状态类型,定义了节点间传递的数据结构
builder = StateGraph(AgentState)
# ------------------ 注册节点 ------------------
# 注册 "agent" 节点
# 第一个参数是节点名称(字符串),用于在边定义中引用
# 第二个参数是节点函数,接收状态并返回状态更新
builder.add_node("agent", agent_node)
# 注册 "tools" 节点
# tool_node 是 ToolNode 实例,它实现了 Runnable 接口
# 可以像函数一样作为节点使用
builder.add_node("tools", tool_node)
# ------------------ 定义边 ------------------
# 添加起始边:从 START 直接进入 "agent" 节点
# START 是 LangGraph 内置的起始标记
# 这条边表示:图开始执行时,首先进入 agent 节点
builder.add_edge(START, "agent")
# 添加条件边:从 "agent" 节点出发
# 条件边允许根据状态动态选择下一个节点
# 参数说明:
# - "agent": 边的起始节点
# - should_continue: 条件函数,决定走哪条路径
# - ["tools", END]: 可能的路径列表
builder.add_conditional_edges(
"agent", # 从 agent 节点出发
should_continue, # 使用 should_continue 函数决定路径
["tools", END] # 可能的路径:tools 节点或 END
)
# 添加普通边:工具执行完毕后,返回 "agent" 节点继续推理
# 这形成了 agent → tools → agent 的循环
# 循环会一直持续,直到 LLM 决定不再调用工具
# 这是 Agent 能够"多步推理"的关键设计
builder.add_edge("tools", "agent")
# ------------------ 编译图 ------------------
# 编译状态图,生成可执行的 graph 对象
# compile() 方法会:
# 1. 验证图的完整性(所有节点都有入边和出边)
# 2. 构建内部数据结构
# 3. 返回一个可调用的 Runnable 对象
graph = builder.compile()
# ==================== 主程序 ====================
# 主程序入口
# if __name__ == "__main__": 确保代码只在直接运行时执行
# 而在被导入为模块时不执行
if __name__ == "__main__":
# ------------------ 创建用户输入 ------------------
# 创建用户输入消息
# HumanMessage 表示用户发送的消息
# content 参数是消息内容
user_input = HumanMessage(content="帮我查一下上海今天天气")
# ------------------ 执行状态图 ------------------
# 执行状态图,传入初始状态
# invoke() 方法会:
# 1. 从 START 开始执行
# 2. 按照边的定义依次执行节点
# 3. 在条件边处根据状态选择路径
# 4. 到达 END 时停止并返回最终状态
# 参数:{"messages": [user_input]} 是初始状态
result = graph.invoke({"messages": [user_input]})
# ------------------ 提取结果 ------------------
# 从结果中提取最后一条消息的内容
# result 是最终状态,包含所有消息
# result["messages"][-1] 是最后一条消息(AI 的最终回答)
# .content 属性是消息的文本内容
final_answer = result["messages"][-1].content
# ------------------ 输出结果 ------------------
# 打印 AI 的最终回答
print("🤖 AI 最终回答:")
print(final_answer)
附录:技术名词速查表
| 名词 | 英文 | 通俗解释 | 类比 |
|---|---|---|---|
| LLM | Large Language Model | 大语言模型,能理解和生成文本的 AI | 博学但没有手脚的人 |
| Agent | Agent | 智能体,能自主决策和执行任务的 AI | 有大脑和手脚的完整的人 |
| Tool | Tool | 工具,Agent 可以调用的功能 | AI 的手脚 |
| State | State | 状态,记录对话过程中的信息 | 聊天记录本 |
| Node | Node | 节点,工作流程中的处理站 | 流水线上的工位 |
| Edge | Edge | 边,节点之间的连接 | 流水线上的传送带 |
| Graph | Graph | 图,完整的工作流程设计 | 工厂流水线设计图 |
| Message | Message | 消息,对话中的单条信息 | 聊天中的一句话 |
| HumanMessage | - | 用户消息 | 用户说的话 |
| AIMessage | - | AI 消息 | AI 回复的话 |
| ToolMessage | - | 工具消息 | 工具返回的结果 |
| TypedDict | - | 类型字典,有类型提示的字典 | 有格式的笔记本 |
| Annotated | - | 注解类型,为类型添加元数据 | 给类型加标签 |
| reducer | - | 归约函数,定义状态更新方式 | 定义如何合并新旧数据 |
| invoke | - | 调用,执行某个操作 | 按下启动按钮 |
| tool_calls | - | 工具调用请求,AI 想要调用的工具列表 | AI 的"待办事项" |
| decorator | - | 装饰器,修改函数行为的语法 | 给函数贴标签 |
| API | Application Programming Interface | 应用程序接口,程序之间通信的方式 | 餐厅菜单 |
| JSON | JavaScript Object Notation | 一种数据格式,易于人和机器阅读 | 标准化的数据表格 |
快速参考卡片
运行程序
# 1. 启动 Ollama 服务
ollama serve
# 2. 拉取模型
ollama pull qwen3.5:2b
# 3. 运行程序
python weather.py
核心代码模板
# 1. 定义工具
@tool
def my_tool(param: str) -> str:
"""工具描述"""
return "结果"
# 2. 定义状态
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
# 3. 创建模型
llm = ChatOllama(model="qwen3.5:2b")
llm_with_tools = llm.bind_tools([my_tool])
# 4. 定义节点
def agent_node(state):
res = llm_with_tools.invoke(state["messages"])
return {"messages": [res]}
# 5. 构建图
builder = StateGraph(AgentState)
builder.add_node("agent", agent_node)
builder.add_node("tools", ToolNode([my_tool]))
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue, ["tools", END])
builder.add_edge("tools", "agent")
graph = builder.compile()
# 6. 运行
result = graph.invoke({"messages": [HumanMessage(content="问题")]})
print(result["messages"][-1].content)
手脚 |
| State | State | 状态,记录对话过程中的信息 | 聊天记录本 |
| Node | Node | 节点,工作流程中的处理站 | 流水线上的工位 |
| Edge | Edge | 边,节点之间的连接 | 流水线上的传送带 |
| Graph | Graph | 图,完整的工作流程设计 | 工厂流水线设计图 |
| Message | Message | 消息,对话中的单条信息 | 聊天中的一句话 |
| HumanMessage | - | 用户消息 | 用户说的话 |
| AIMessage | - | AI 消息 | AI 回复的话 |
| ToolMessage | - | 工具消息 | 工具返回的结果 |
| TypedDict | - | 类型字典,有类型提示的字典 | 有格式的笔记本 |
| Annotated | - | 注解类型,为类型添加元数据 | 给类型加标签 |
| reducer | - | 归约函数,定义状态更新方式 | 定义如何合并新旧数据 |
| invoke | - | 调用,执行某个操作 | 按下启动按钮 |
| tool_calls | - | 工具调用请求,AI 想要调用的工具列表 | AI 的"待办事项" |
| decorator | - | 装饰器,修改函数行为的语法 | 给函数贴标签 |
| API | Application Programming Interface | 应用程序接口,程序之间通信的方式 | 餐厅菜单 |
| JSON | JavaScript Object Notation | 一种数据格式,易于人和机器阅读 | 标准化的数据表格 |
---
## 快速参考卡片
### 运行程序
```bash
# 1. 启动 Ollama 服务
ollama serve
# 2. 拉取模型
ollama pull qwen3.5:2b
# 3. 运行程序
python weather.py
核心代码模板
# 1. 定义工具
@tool
def my_tool(param: str) -> str:
"""工具描述"""
return "结果"
# 2. 定义状态
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
# 3. 创建模型
llm = ChatOllama(model="qwen3.5:2b")
llm_with_tools = llm.bind_tools([my_tool])
# 4. 定义节点
def agent_node(state):
res = llm_with_tools.invoke(state["messages"])
return {"messages": [res]}
# 5. 构建图
builder = StateGraph(AgentState)
builder.add_node("agent", agent_node)
builder.add_node("tools", ToolNode([my_tool]))
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue, ["tools", END])
builder.add_edge("tools", "agent")
graph = builder.compile()
# 6. 运行
result = graph.invoke({"messages": [HumanMessage(content="问题")]})
print(result["messages"][-1].content)
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐




所有评论(0)