天气查询 Agent 零基础教程

🎯 你将构建的程序

运行后输入一个城市名,AI Agent 自动调用天气 API 查询并返回结果:

你:帮我查一下上海今天天气
AI:上海今天天气晴朗,温度 20°C,湿度 55%,风速 4.3 km/h

Agent 解决方案:从问题到回答

设计原则:零跳跃学习

传统路线的问题:知识点 A → 知识点 C(发现需要 B,回头补 B)→ 知识点 D(发现需要前置知识 X)

本教程的目标:知识点 A → 知识点 B(A 的自然延伸)→ 知识点 C(B 的必然发展)→ 知识点 D(C 的合理进阶)


目录

  1. 第一步:创建文件并编写模块文档
  2. 第二步:导入依赖库
  3. 第三步:定义工具函数
  4. 第四步:注册工具列表
  5. 第五步:定义状态类
  6. 第六步:初始化模型
  7. 第七步:定义节点函数
  8. 第八步:构建状态图
  9. 第九步:编写主程序
  10. 完整代码
  11. 附录:技术名词速查表

📂 先看全貌 —— 完整代码由以下 7 个部分组成:
weather.py 代码结构总览

程序执行流程:程序运行时发生了什么?

下面这张图展示了你运行 python weather.py 后,程序内部发生的完整过程。建议先看懂这个流程,再跟着后面的步骤写代码,这样每一步你都知道自己在构建哪个环节:
Agent 执行流程

关键数据流向(理解这个,就理解了整个 Agent):

  1. 用户消息 → 作为 HumanMessage 放入 messages 列表 → 传入 agent_node
  2. agent_node → LLM 读 messages,决定调用 get_weather → 返回带 tool_callsAIMessage,追加到 messages
  3. tool_node → 从 messages 中取出 tool_calls,执行 get_weather("上海") → 将结果作为 ToolMessage 追加到 messages
  4. agent_node(第二次)→ LLM 再读 messages(现在有了天气数据),生成最终回答 → 返回纯文本 AIMessage,追加到 messages
  5. 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 导入类型相关模块

TypedDictAnnotated 用于第五步定义状态类 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 图构建组件

StateGraphSTARTEND 是第八步构建状态图的核心组件。

# 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 装饰器做了什么:
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 工具函数流程图

get_weather 工具执行流程


第四步:注册工具列表

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.2tools 来自 第四步

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 图结构可视化

LangGraph 工作流程


第九步:编写主程序

最后一步:编写程序入口,创建用户输入、执行状态图、打印结果。这里用到了 第二步 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 执行流程时序图

Agent 执行时序图

天气API Tools节点 LLM Agent节点 Graph 用户 天气API Tools节点 LLM Agent节点 Graph 用户 should_continue 判断 有工具调用 → 去 tools tools → agent 回到 agent 继续推理 should_continue 判断 无工具调用 → 结束 输入: "上海天气怎么样?" 进入 agent 节点 调用 LLM 推理 返回: 决定调用 get_weather("上海") 返回 AIMessage(tool_calls=[...]) 进入 tools 节点 HTTP 请求获取天气数据 返回天气 JSON 返回 ToolMessage(content="上海:晴朗...") 再次进入 agent 节点 调用 LLM 整理回答 返回: "上海今天天气晴朗..." 返回 AIMessage(content="...") 输出最终回答

完整代码

💡 如果你不想跟着一步步敲,可以直接复制下面的完整代码。但建议先跟着步骤做一遍,这样才能真正理解每个部分的作用。

将以上所有步骤的代码按顺序组合,得到完整的 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)
Logo

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

更多推荐