AI Agents in LangGraph-2

从零构建一个基于状态图的循环智能体

上一篇文章已经介绍如何手写一个执行ReAct协议的AI agent,这一篇文章,我们重点讲述利用langGraph基于状态图构建循环智能体,让大模型具备下面所示的自主迭代能力:

思考 →\rightarrow→ 决定调用工具 →\rightarrow→ 执行工具 →\rightarrow→ 把结果喂回大模型

环境变量的加载

python 复制代码
from langchain_openai import ChatOpenAI
import os
import re
from load_dotenv import load_dotenv
load_dotenv()

if os.environ.get("OPENAI_API_KEY"):
    print("Bro API KEY Variable exists")
else :
    raise ValueError("OPENAI_API_KEY not found")

if os.environ.get("TAVILY_API_KEY"):
    print("Bro AVILY_API KEY Variable exists")
else :
    raise ValueError("OPENAI_API_KEY not found")

from langchain_core.prompts import PromptTemplate
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser, PydanticOutputParser
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
import operator
from langchain_core.messages import AnyMessage, SystemMessage, HumanMessage, ToolMessage
from langchain_openai import ChatOpenAI
from langchain_tavily import TavilySearch

llm_openai = ChatOpenAI(model = "gpt-5-mini-2025-08-07", temperature=0)

tool = TavilySearch(max_results=4) #increased number of results
print(type(tool))
print(tool.name)

调用 python-dotenv 库自动读取当前目录下的 .env 文件,将其中的环境变量(如 OPENAI_API_KEY, TAVILY_API_KEY)加载到系统的 os.environ 中,方便后续调用大模型和搜索工具时免去手动硬编码密钥的麻烦。

定义智能体的状态

python 复制代码
class AgentState(TypedDict):
    messages: Annotated[list[AnyMessage], operator.add]

这段代码是使用 LangGraph 开发 AI Agent(智能体)时的核心模式。它定义了 Agent 的状态(State)结构以及状态的更新规则(Reducer)。

  • LangGraph 默认使用一个 Python 字典(TypedDict)在图(Graph)的各个节点(Nodes)之间传递和共享数据。AgentState 就是这个字典的"说明书",声明了状态中包含哪些字段。
  • listAnyMessage声明了 messages 字段的类型是一个列表,列表中的元素是 AnyMessage(LangChain 中所有消息类型的基类,包括 HumanMessage, AIMessage, SystemMessage, ToolMessage 等)。
  • Annotated关键字允许你在声明类型的同时,附加一些自定义的元数据(Metadata)。在这里,Annotated 将实际类型 listAnyMessage 和元数据 operator.add 绑定在了一起。
  • operator.add利用了 LangGraph 的 Reducer(合并器)机制, 当指定了 operator.add(即 Python内置的加法运算,对于列表而言就是 list1 + list2 拼接)作为 Reducer 后: 当一个节点返回新消息 {"messages": new_msg} 时,LangGraph 不会覆盖原状态,而是会调用 operator.add(existing_messages, new_msg),将新消息追加到原有的消息列表末尾。

核心类Agent的设计

python 复制代码
class Agent:

    def __init__(self, model, tools, system=""):
        self.system = system
         # 1. 初始化状态图,指定状态结构为 AgentState
        graph = StateGraph(AgentState)
        
        # 2. 注册节点。节点名对应具体执行的类方法
        graph.add_node("llm", self.call_openai)
        graph.add_node("action", self.take_action)
        
        # 3. 注册条件边。从 "llm" 节点出来后,调用 self.exists_action 函数判断去向
        graph.add_conditional_edges(
            "llm",
            self.exists_action,
            {True: "action", False: END}  # 返回 True 去 "action" 节点,返回 False 则结束 (END)
        )

        # 4. 注册普通边。从 "action" 节点出来后,必定回到 "llm" 节点
        graph.add_edge("action", "llm")

        # 5. 设置图的起点
        graph.set_entry_point("llm")
        
        # 6. 编译图,使其可运行
        self.graph = graph.compile()
        
        # 7. 将工具列表转换成字典,方便通过名字快速查找工具,例如 {"tavily": TavilySearchResults(...)}
        self.tools = {t.name: t for t in tools}
    
        # 8. 将工具绑定到大语言模型中,让模型"知道"有哪些工具可用,并在需要时输出格式化的工具调用指令 (tool_calls)
        self.model = model.bind_tools(tools)

    # 路由条件:这个函数用来决定模型生成回复后,是继续调用工具还是直接回答用户。
    def exists_action(self, state: AgentState):
        # 获取当前消息列表中的最后一条消息(即 LLM 刚刚生成的回复)
        result = state['messages'][-1]
        # 检查大模型是否产生了工具调用指令(result.tool_calls 列表是否为空)
        return len(result.tool_calls) > 0

    # LLM 节点:call_openai 调用绑定的模型进行思考和决策。
    def call_openai(self, state: AgentState):
        messages = state['messages']
         # 如果初始化时传入了全局 System Prompt (系统设定),则临时将其拼接到消息列表的最前面
        if self.system:
            messages = [SystemMessage(content=self.system)] + messages
    
        # 调用大模型
        message = self.model.invoke(messages)
        # 返回更新的状态。由于在 AgentState 中配置了 operator.add,
        # 这里的 [message] 会被自动追加到已有的 messages 历史列表中
        return {'messages': [message]}
        
    #  工具执行节点->当 exists_action 确定需要执行工具时,程序会流转到这个节点。
    def take_action(self, state: AgentState):
        # 1. 获取大模型在最后一条消息中输出的所有工具调用指令
        tool_calls = state['messages'][-1].tool_calls
        results = []
        # 2. 遍历大模型要求调用的每一个工具(大模型支持单次并行调用多个工具)
        for t in tool_calls:
            print(f"Calling: {t}")
            # 容错处理:如果大模型胡诌了一个不存在的工具名
            if not t['name'] in self.tools:       
                print("\n ....bad tool name....")
                result = "bad tool name, retry"  # 反馈给大模型工具名错误,让它重试
            else:
                # 正常调用:传入大模型生成的参数 t['args'] 执行工具
                result = self.tools[t['name']].invoke(t['args'])
            # 3. 将工具执行结果封装为 ToolMessage 格式,必须携带 tool_call_id 以便大模型对齐
            results.append(ToolMessage(tool_call_id=t['id'], name=t['name'], content=str(result)))
        print("Back to the model!")
        # 4. 返回执行结果列表,这些 ToolMessage 会被自动追加到 messages 状态中
        return {'messages': results}

这段代码实现了一个基于LangGraph的ReAct架构的 AI Agent, 它通过构建一个有向环状图(Cyclic Graph),使大模型能够循环进行:"思考(LLM) -> 决定是否调用工具 -> 执行工具(Action) -> 将结果反馈给大模型 -> 再次思考" 的过程,直到任务完成。

该段代码的状态图如下所示:
#mermaid-svg-KYDkujrDfMQ03PWv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KYDkujrDfMQ03PWv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KYDkujrDfMQ03PWv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KYDkujrDfMQ03PWv .error-icon{fill:#552222;}#mermaid-svg-KYDkujrDfMQ03PWv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KYDkujrDfMQ03PWv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KYDkujrDfMQ03PWv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KYDkujrDfMQ03PWv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KYDkujrDfMQ03PWv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KYDkujrDfMQ03PWv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KYDkujrDfMQ03PWv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KYDkujrDfMQ03PWv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KYDkujrDfMQ03PWv .marker.cross{stroke:#333333;}#mermaid-svg-KYDkujrDfMQ03PWv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KYDkujrDfMQ03PWv p{margin:0;}#mermaid-svg-KYDkujrDfMQ03PWv .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-KYDkujrDfMQ03PWv .cluster-label text{fill:#333;}#mermaid-svg-KYDkujrDfMQ03PWv .cluster-label span{color:#333;}#mermaid-svg-KYDkujrDfMQ03PWv .cluster-label span p{background-color:transparent;}#mermaid-svg-KYDkujrDfMQ03PWv .label text,#mermaid-svg-KYDkujrDfMQ03PWv span{fill:#333;color:#333;}#mermaid-svg-KYDkujrDfMQ03PWv .node rect,#mermaid-svg-KYDkujrDfMQ03PWv .node circle,#mermaid-svg-KYDkujrDfMQ03PWv .node ellipse,#mermaid-svg-KYDkujrDfMQ03PWv .node polygon,#mermaid-svg-KYDkujrDfMQ03PWv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-KYDkujrDfMQ03PWv .rough-node .label text,#mermaid-svg-KYDkujrDfMQ03PWv .node .label text,#mermaid-svg-KYDkujrDfMQ03PWv .image-shape .label,#mermaid-svg-KYDkujrDfMQ03PWv .icon-shape .label{text-anchor:middle;}#mermaid-svg-KYDkujrDfMQ03PWv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-KYDkujrDfMQ03PWv .rough-node .label,#mermaid-svg-KYDkujrDfMQ03PWv .node .label,#mermaid-svg-KYDkujrDfMQ03PWv .image-shape .label,#mermaid-svg-KYDkujrDfMQ03PWv .icon-shape .label{text-align:center;}#mermaid-svg-KYDkujrDfMQ03PWv .node.clickable{cursor:pointer;}#mermaid-svg-KYDkujrDfMQ03PWv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-KYDkujrDfMQ03PWv .arrowheadPath{fill:#333333;}#mermaid-svg-KYDkujrDfMQ03PWv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-KYDkujrDfMQ03PWv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-KYDkujrDfMQ03PWv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KYDkujrDfMQ03PWv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-KYDkujrDfMQ03PWv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KYDkujrDfMQ03PWv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-KYDkujrDfMQ03PWv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-KYDkujrDfMQ03PWv .cluster text{fill:#333;}#mermaid-svg-KYDkujrDfMQ03PWv .cluster span{color:#333;}#mermaid-svg-KYDkujrDfMQ03PWv div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-KYDkujrDfMQ03PWv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-KYDkujrDfMQ03PWv rect.text{fill:none;stroke-width:0;}#mermaid-svg-KYDkujrDfMQ03PWv .icon-shape,#mermaid-svg-KYDkujrDfMQ03PWv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KYDkujrDfMQ03PWv .icon-shape p,#mermaid-svg-KYDkujrDfMQ03PWv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-KYDkujrDfMQ03PWv .icon-shape .label rect,#mermaid-svg-KYDkujrDfMQ03PWv .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KYDkujrDfMQ03PWv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-KYDkujrDfMQ03PWv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-KYDkujrDfMQ03PWv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} True
False
入口
节点: llm
条件: exists_action
节点: action
结束 END

演示示例

python 复制代码
prompt = """You are a smart research assistant. Use the search engine to look up information. \
You are allowed to make multiple calls (either together or in sequence). \
Only look up information when you are sure of what you want. \
If you need to look up some information before asking a follow up question, you are allowed to do that!
"""

model = llm_openai
abot = Agent(model, [tool], system=prompt)
from IPython.display import Image, display

display(Image(abot.graph.get_graph().draw_mermaid_png()))

使用上面的代码打印出当前agent的有向图结构,和上图的状态图基本一致

输入messages,测试agent是否正常工作

python 复制代码
messages = [HumanMessage(content="What is the weather in sf?")]
result = abot.graph.invoke({"messages": messages})

模型输出结果如下所示:

shell 复制代码
Calling: {'name': 'tavily_search', 'args': {'query': 'San Francisco weather now', 'search_depth': 'fast', 'include_images': False}, 'id': 'call_T37UDw9Aax3rTgzG4ej4XUmS', 'type': 'tool_call'}
Back to the model!
Calling: {'name': 'tavily_search', 'args': {'query': 'San Francisco CA weather current conditions', 'search_depth': 'fast'}, 'id': 'call_3HjyVeQPuU8Vbvbrhcy97wiw', 'type': 'tool_call'}
Back to the model!

更具体一点:

python 复制代码
result['messages'][-1].content

模型输出结果

'Do you mean San Francisco, CA? Do you want the current conditions right now or a forecast (today/next few days)? I can look it up --- OK to fetch current weather?'

相关推荐
Navicat中国10 分钟前
Navicat 18 内置 AI 助手让数据库开发从繁琐操作迈向智能协作
大数据·人工智能·数据库开发
4SAPI32 分钟前
多模型请求路由怎么设计?成本、速度和能力如何平衡
人工智能·python·gateway·php
魔乐社区43 分钟前
翼芯・智算测试与适配优化专区,让国产算力“选得准、跑得通、用得好“
人工智能·开源·大模型
XCDoris3 小时前
郴州logo设计机构
大数据·人工智能·python
Goboy3 小时前
一个人带三个Agent,我用全新TRAE从0到1搭了个资讯周刊站
人工智能·trae
俊哥V3 小时前
每日 AI 研究简报 · 2026-10-19
人工智能·ai
AI_AGENT_DEV_AI3 小时前
AI英语教培智能体的开发
人工智能
物联网软硬件开发-轨物科技3 小时前
【技术白皮书】光伏清检一体化机器人系统解决方案技术白皮书
大数据·运维·网络·人工智能·机器人
gCode Teacher 格码致知3 小时前
Pandas教学-17:Series与Dataframe的关系
开发语言·python·pandas
海宇数据3 小时前
零信任架构实战:基于海宇网上踪迹欺诈行为识别构建自动化信贷资信评估网关
运维·人工智能·架构·自动化