目录一、支持搜索的智能代理系统案例介绍Tavily 搜索工具二、搜索流程(重要)三、代码实现步骤1准备工作定义聊天模型和搜索工具步骤2定义状态步骤3定义模型节点步骤4定义工具节点步骤5构建图设置节点与边步骤6可视化图步骤7执行非流式与流式普通信息测试实时信息测试三、补充相关问题:一、支持搜索的智能代理系统案例介绍案例一只是为了演示什么是 Graph。对于 LangGraph实际上是要调用 LLM 来完成智能应用系统。因此该案例将会使用 Graph API 来完成一个支持搜索、支持调用 LLM 的智能代理系统。核心功能如下智能对话与工具调用基于聊天模型能够理解用户问题并决定是否需要调用搜索工具自动搜索整合通过 Tavily 搜索工具获取实时信息并将搜索结果整合到回答中循环决策机制能够多次调用工具和模型直到获得满意答案流程设置如下例如我们可以用它来实时信息查询查询最新新闻、股价、天气等。示例今天特斯拉的最新股价是多少还有哪些重要的科技新闻事实核查验证信息的准确性。示例有人说阿波罗登月是伪造的请提供证据来验证登月的真实性深度研究多轮搜索获取全面信息。示例我需要关于 人工智能在医疗诊断中的应用 的最新研究论文、临床试验结果和专家观点请提供全面的综述知识扩展补充模型知识库之外的信息。示例GPT‑4o‑mini 的训练数据截止到什么时候之后有哪些重要的 AI 发展事件Tavily 搜索工具Tavily 是一个专门给 AI 大模型设计的联网搜索 API 服务它不是大模型也不是 LangChain 原生内置组件但是 LangChain 做了适配封装可以直接调用。它和 LLM 不是一类东西LLM (DeepSeek、ChatGPT) 负责理解文本、思考推理、生成回答。Tavily专门去互联网网页做搜索返回清洗干净的网页摘要、片段给大模型提供外部实时资料本身不会生成回答。和 LangChain 的关系:LangChain 社区提供了封装好的工具类 TavilySearchResults属于 langchain_community.tools 下面的组件。也就是说Tavily 本体是一个独立的云端 API 服务LangChain 只是写好了调用它的 SDK 封装方便直接在 Agent 里面使用。使用前提需要去 Tavily 官网注册账号拿到 api_key请求会走 Tavily 的远程接口联网获取网页搜索结果有免费额度超过额度会计费。在我们这个 Agent 案例里它扮演的角色:用户问实时问题大模型自己知识库没有最新数据不能瞎编。所以 Agent 就调用TavilySearchResults把用户问题传给 TavilyTavily 访问互联网把整理好的搜索文本拿回来再丢回给大模型大模型结合搜索素材再写答案。Tavily 不是普通网页爬虫输出做过优化过滤广告、杂乱标签输出简短摘要专门适配大模型输入比直接调用普通网页搜索更适合 Agent。二、搜索流程(重要)我们先看第一张查询流程图整个 Agent 会收到用户输入的问题。举图上的两个例子“今天特斯拉的最新股价是多少还有哪些重要的科技新闻” 这个例子属于需要联网获取实时数据而第二条 “11 等于几” 属于模型本身知识库就可以直接回答不需要调用外部搜索。用户提问进入之后交给 LLM 做判断会产生两条分支。第一条分支不需要搜索、不需要调用工具。大模型直接产出 AIMessage生成最终回答流程直接结束这里不会出现 ToolMessage。 而第二条分支需要联网搜索就要执行 Tavily 搜索工具。工具运行完成之后会生成 ToolMessageToolMessage 里面存放的就是搜索拿到的互联网原始结果。紧接着把 ToolMessage 追加进消息历史连同之前全部对话重新交给 LLM 做整合结果。整合完成之后得到新的 AIMessage再次回到判断节点继续判断是否还需要再次调用工具由此形成循环。再切换到第二张消息规范示意图对比两种消息会话结构。第一种不需要执行工具的会话结构。消息列表很简单只有 HumanMessage也就是用户提问紧接着就是 AIMessage也就是大模型给出的回答。消息链条到此结束没有其他类型消息普通聊天对话就是这个结构。第二种需要执行工具也就是 LLM 已经 bind_tools 绑定了工具的会话结构消息链条会变长。首先这个 HumanMessage 是用户的原始问题。接着返回 AIMessage但这条 AIMessage不携带普通文本答案里面存放的是工具调用的指令告诉程序我想要调用哪一个工具、传入什么参数。程序读到 AIMessage 里面的工具调用指令就去实际运行 Tavily 搜索工具。工具执行完毕产出 ToolMessageToolMessage 专门用来存放工具执行之后返回的结果这里就是网页搜索回来的内容。ToolMessage 是工具和大模型之间的桥梁大模型本身不能直接调用网络只能通过 ToolMessage 接收工具输出。现在把 HumanMessage、带工具调用的 AIMessage、ToolMessage 全部拼接完整消息列表重新送入 LLM。大模型读取用户问题、自己之前的调用指令、以及 ToolMessage 携带的搜索素材再生成一条新的 AIMessage这条 AIMessage 才是面向用户的最终结果。这里区分 LangChain 和 LangGraph 处理消息历史的差异。 原生 LangChain 使用 Agent 的时候需要我们手动维护这一整套消息列表手动完成消息追加。而 LangGraph 的推荐写法是把消息列表直接定义在 State 状态里面。每一个节点运行结束自动向 state 的消息列表追加 HumanMessage、AIMessage、ToolMessage不需要我们手动去操作列表变量。每一轮循环状态自动保存完整会话历史传给下一轮 LLM。红框圈出的部分就是一轮完整工具调用必不可少的三类消息HumanMessage、携带工具调用的 AIMessage、ToolMessage。三者缺一不可。缺少任意一种大模型就看不懂这一轮工具交互的完整上下文会出现逻辑错乱。这张图展示搜索 Agent 案例完整的图拓扑结构右侧列出开发这个 LangGraph 程序的 5 个标准步骤左侧是节点流转流程图。我们先看流程图的整体走向。程序从 __start__ 起点进入图直接流向 llm_call 节点。llm_call 就是调用大模型的节点在这里大模型完成思考做出决策之后产生两条分支路径。一条分支走向 __end__ 终点。这条分支对应的场景就是大模型判断不需要调用搜索工具直接产出最终回答整个 Agent 流程就此结束。另一条分支走向 tool_node 节点。这条分支代表大模型输出了工具调用指令需要执行 Tavily 搜索。tool_node 就是专门执行工具的节点在这个节点内部会真正运行搜索工具拿到搜索结果生成 ToolMessage。tool_node 执行完毕之后边会重新指回 llm_call 节点形成循环。工具拿到的搜索结果存放在 State 的消息列表里面再次交给大模型。让大模型把搜索到的真实素材交给大模型做内容整合、归纳改写生成回答文本。但与此同时这一轮送入 LLM 的时候工具绑定并没有解除大模型依然具备发起工具调用的能力。 所以大模型读完搜索返回的资料之后会同时做两件事。第一件 : 消化已有的搜索信息整理内容。第二件 : 再次做决策当前搜集到的信息够不够回答用户原始问题。如果信息已经足够LLM 直接输出普通文本的 AIMessage不生成工具调用指令条件边就走向__end__输出整合完成的答案流程结束。再看右侧罗列的开发五步流程这是写这个 Agent 代码的完整顺序。第一步定义状态。要自定义 Agent 的 State状态里面维护消息列表HumanMessage、AIMessage、ToolMessage 全部都会追加保存在这个状态中每一轮循环都依靠这份消息上下文给大模型。第二步定义 2 个节点。一共就两个业务节点llm_call 节点负责调用绑定工具的大模型tool_node 节点负责解析工具调用执行 Tavily 搜索封装 ToolMessage。没有别的额外节点。第三步定义图、添加节点和边。创建 StateGraph 图对象把两个节点注册进去。__start__ 连接到 llm_call。然后给 llm_call 配置条件边根据模型输出动态选择去往 __end__ 或者去往 tool_node。再添加普通边tool_node 执行结束固定回到 llm_call完成循环链路。第四步编译图。调用 compile 方法得到可执行的图实例。第五步执行图。调用 invoke传入用户的提问作为初始状态驱动整张图运行。整个架构非常简洁只有两个业务节点依靠条件边实现分支依靠普通边实现循环。所有的智能决策全部交给llm_call里面的大模型完成框架只负责状态保存和流程流转。三、代码实现步骤1准备工作定义聊天模型和搜索工具import os 导入 Python 内置操作系统模块。os 模块可以读写操作系统的环境变量。后面 os.environ 本质就是一个字典用来设置程序运行时的环境变量。Tavily、DeepSeek 的 SDK 底层会自动读取环境变量里面对应的 key 名字拿到接口密钥完成 API 身份校验。注意只对当前这个 Python 脚本生效不会修改电脑系统全局环境变量脚本运行结束这个环境变量就消失。os.environ[DEEPSEEK_API_KEY] sk‑9fbc00e9321944708169f7db8ae6e9f1 设置 DeepSeek 大模型的密钥环境变量。DEEPSEEK_API_KEY是 langchain‑deepseek 包约定读取的环境变量名名字不能修改。langchain 的 init_chat_model 在初始化 deepseek‑chat 模型时底层自动读取这个环境变量用来调用 DeepSeek 的 API 接口做身份认证。⚠️风险提示这种直接硬编码密钥写代码的方式只适合本地调试。一旦把这个 py 文件上传 Gitee/Git 代码仓库密钥会直接泄露别人会盗用你的 API 额度。正式项目建议使用.env文件。import operator Python 内置运算符模块。后面定义状态 MessagesState 的时候Annotated[list[AnyMessage], operator.add] 会用到。operator.add 代表列表相加合并LangGraph 识别这个注解之后节点返回的 messages 列表不会覆盖旧的消息而是做列表追加完整保存全部对话历史这是 LangGraph 状态更新非常关键的配置。from typing import TypedDict, Annotated 从 typing 类型注解库导入两个工具专门给 LangGraph 状态做类型定义。TypedDict字典类型的类型提示用来定义 Graph 的 State 状态对象我们后面会写class MessagesState(TypedDict)用来规定状态字典里面有哪些 key、对应什么数据类型。Annotated给类型附加额外元信息注解。这里除了声明字段是 list 列表额外带上operator.add告诉 LangGraph 该字段的合并策略。from langchain.chat_models import init_chat_model LangChain 提供的统一模型初始化函数。 它是一套兼容多厂商大模型的统一入口不用区分 OpenAI、DeepSeek 等不同厂商各自的类。只要写模型名字 deepseek‑chat底层自动加载对应厂商实现对外提供完全一致的调用接口。这也是我们可以简单把 OpenAI 换成 DeepSeek业务代码不用改动的根本原因。from langchain_core.messages import AnyMessage, AIMessage, SystemMessage, ToolMessage, HumanMessage 导入 LangChain 内核全部消息类型对象大模型交互全部靠这几类消息HumanMessage用户发给大模型的用户提问消息。AIMessage大模型输出返回的消息当大模型想要调用工具时这个对象内部会携带tool_calls 工具调用数组。SystemMessage系统提示词用来设定助手角色、行为规则。ToolMessage工具执行完成之后返回的结果消息把搜索结果包装成这个对象交给大模型继续处理。AnyMessage通用消息父类型可以代表上面任意一种消息用于状态里面保存消息列表。from langchain_tavily import TavilySearch 导入 Tavily 联网搜索工具这是 langchain 封装好的工具类。实例化之后就可以交给大模型绑定当大模型需要联网查实时信息就会调用这个工具执行网页搜索。from langgraph.constants import START, END LangGraph 内置常量。START图的起点标记代表整个工作流开始。END图的终点标记走到 END 代表整个 Agent 图流程执行结束。构建边的时候会使用这两个常量。from langgraph.graph import StateGraph 导入 LangGraph 最核心的类 StateGraph状态图。 我们写 Agent 工作流就是实例化这个对象然后往图里面添加节点、添加边、条件分支最后 compile 编译成可执行的图对象。常见踩坑环境变量名字写错字母SDK 就读取不到密钥报密钥缺失密钥字符串复制的时候多带空格换行接口鉴权失败导入顺序必须 os.environ 写在最前面在导入 TavilySearch、模型组件之前。如果把 import 写在 os.environ 前面SDK 初始化的时候还读不到环境变量依然会报错。search TavilySearch(max_results4) 创建搜索工具每次搜索最多返回 4 条结果只是创建对象还不联网。tools [search] 把搜索工具放进列表后续给模型使用。model init_chat_model(modeldeepseek-chat, temperature0) 初始化 deepseek 大模型temperature0 让输出更稳定减少随机瞎编。model_with_tools model.bind_tools(tools) 把搜索工具绑定给模型模型就知道可以调用搜索工具。整体只是做初始化配置不发网络请求得到带搜索能力的模型对象给后面图逻辑用。步骤2定义状态class MessagesState(TypedDict) 定义 LangGraph 图的状态类相当于规定整个图共用的数据容器有哪些字段。messages: Annotated[list[AnyMessage], operator.add] :messages保存全部对话消息的列表用户提问、AI 回复、工具返回结果全部放这里用来保存会话记忆、维护上下文。operator.add合并规则。节点返回新消息列表不是覆盖旧列表而是两个列表相加追加历史消息不会丢失。llm_calls: int 整型字段用来记录大模型被调用的次数做计数统计。这一段只做状态格式定义没有执行逻辑。后面所有节点拿到的 state就是这个 MessagesState 格式的字典。步骤3定义模型节点由于节点不需要返回整个状态模式只需一个更新。且 operator.add 表示新消息会追加到列表中而不是替换。因此模型节点代码如下llm_call 是 LangGraph 当中的一个节点函数参数 state 就是我们前面定义好的 MessagesS tate 状态字典整个节点的作用就是把对话交给大模型由大模型判断是直接输出答案还是需要调用搜索工具。messages state[messages] 这行代码就是从全局状态里取出完整的对话消息列表。这个节点的入口有两种来源一种是流程图刚启动此时列表里面只有用户的提问另一种是工具节点执行完毕之后流转回来消息列表里会同时存在用户问题、AI 的工具调用指令、工具返回的搜索结果。刚启动只有用户消息[H]工具执行完回来用户 AI 工具调用 工具返回[H,A,T]接下来调用 model_with_tools.invoke 执行大模型调用的时候手动拼接了一条系统提示词用来规定助手的角色行为再把取出的全部历史消息拼接在一起完整传给模型。模型返回的 result 会出现两种情况如果需要联网查资料返回的 AIMessage 就会携带 tool_calls 工具调用信息如果信息足够就直接返回普通的回答消息。函数最后返回一个更新字典不需要把完整的 state 全部返回只需要给出需要变更的字段。messages 字段把新生成的 AI 消息包装成列表返回依靠状态定义里的 operator.add这条消息会追加到原有消息列表末尾不会把历史对话覆盖掉。而 llm_calls 字段没有配置合并规则属于覆盖更新每执行一次该节点调用次数就加一如果字段还不存在就默认从 0 开始计数。当程序走到这个节点的时候会真实发起网络请求访问大模型接口执行完成之后 LangGraph 会自动把返回的更新内容合并到全局状态再继续往下走图的流程。步骤4定义工具节点tools_by_name 这一行是做工具名字和工具实例的映射字典把之前 tools 列表里的工具以工具名称作为 key工具对象作为 value 存进字典。后面大模型给出工具调用名字的时候可以直接通过名字快速拿到对应的工具对象不用循环遍历查找。tool_node 是图当中的工具执行节点函数入参同样是全局状态 state这个节点的职责就是真正执行大模型指定的工具在这里也就是执行 tavily 搜索。进入这个节点的前提是上一步 llm_call 节点返回的 AI 消息携带了 tool_calls 工具调用信息。代码先定义一个空的 result 列表用来存放工具执行完成之后生成的 ToolMessage 消息。state[messages][-1] 取消息列表的最后一条消息这条消息就是刚刚大模型输出的携带 tool_calls 的 AIMessage循环遍历它内部每一个 tool_call支持一次性调用多个工具。循环内部从 tool_call 取出工具名称去前面建好的 tools_by_name 字典拿到对应的工具实例再取出工具调用的参数 args调用工具的 invoke 方法真正发起搜索请求拿到搜索返回的结果 obs。拿到结果之后包装成 ToolMessage 对象同时带上对应的 tool_call_id这个 id 用来和之前 AI 的工具调用指令做配对把这条消息添加进 result 列表。函数最后返回更新字典把装好 ToolMessage 的 result 列表赋值给 messages。依靠状态定义中的 operator.add这些工具消息会追加到全局消息列表末尾。执行完工具节点之后图流程会再回到 llm_call 节点把刚刚拿到的搜索结果交给大模型让大模型结合搜索内容生成回答。步骤5构建图设置节点与边首先创建 StateGraph 实例 agent_builder传入我们前面写好的 MessagesState 状态类这就完成了图对象的初始化整个图都会共用这套状态结构。接着调用 add_node 把两个已经写好的函数注册成图里面的节点分别命名为 llm_call 和 tool_node注册之后图才知道有这两个可执行单元。add_edge(START, llm_call) 设置图的起始边代表整个图一启动就直接进入 llm_call 节点开始执行。should_continue 是条件分支判断函数接收全局状态作为入参。它取出消息列表里最后一条 AI 返回的消息检查这条消息是否存在 tool_calls。如果存在工具调用就返回字符串tool_node告诉图接下来去执行工具节点如果不存在工具调用直接返回 END代表整个 agent 流程结束。add_conditional_edges 用来添加条件边源节点是 llm_call使用上面的 should_continue 做判断逻辑path_map 声明了函数可能返回的两个去向。当 llm_call 节点执行完毕之后就会自动跑这个判断函数动态选择下一个节点。最后 add_edge(tool_node, llm_call) 添加一条普通固定边代表工具节点执行完成之后无条件回到大模型节点把工具拿到的搜索结果交给大模型再次处理由此形成循环。整个流程图的逻辑就全部搭建完毕。步骤6可视化图我们可以通过 Mermaid 图表来展示出 Graph 效果。Mermaid 是一种使用文本生成流程图、饼状图、甘特图等图表的描述语言它可以帮助用户以简单、直观的方式创建各种类型的图表包括流程图、时序图、甘特图等。Mermaid 在线绘图⼯具https://www.jyshare.com/front-end/9729/使用姿势LangGraph中编译后的图提供了获取 Graph 可绘制表示的方法get_graph()其参数 xrayTrue 或 xray1表示显示详细视图。再通过 draw_mermaid_png()将图形转换为 Mermaid 格式并生成 PNG图片的二进制数据。最后需要导入 matplotlib 库用于图像显示。效果如下:步骤7执行非流式与流式普通信息测试invoke 方法用来完整运行整个 LangGraph 图传入初始状态字典。这里我们只传入 messages 字段里面放一条 HumanMessage 用户消息内容是 “你好”这就是给 agent 的用户提问。invoke 会把图完整跑完直到走到 END 结束才会把最终完整状态返回给 result 变量属于非流式执行要等全部逻辑跑完才拿到全部结果。打印语句读取 result 里面的 llm_calls 字段输出大模型一共调用多少次。因为这句 “你好” 不需要搜索工具大模型直接回复只会执行一次 llm_call 节点所以打印结果就是一共调用了 1 次 LLM。接下来循环遍历最终状态里全部的 messages 消息列表调用 pretty_print() 方法这个是 LangGraph 封装好的格式化打印方法会把每一条消息按照角色清晰打印输出。看控制台运行输出最先打印调用次数之后先输出 Human Message也就是我们输入的用户消息 “你好”。再输出 AI Message就是大模型直接给出的完整回复。这个问题不需要调用搜索工具条件判断函数直接返回 END图直接结束全程不会进入 tool_node 工具节点没有工具消息产生。invoke 方式一次只能处理一轮用户输入跑完就结束。如果想要继续对话需要自己把历史消息拿出来再次调用 invoke 把完整消息传进去。实时信息测试这一次传入的用户提问是 “今天西安的天气如何”这类实时信息大模型自身知识库没有所以整个 agent 会走完完整的循环流程。调用 invoke 启动图之后首先进入 llm_call 节点大模型识别出需要查询天气返回携带 tool_calls 的 AIMessage流程走到条件判断转向 tool_node 工具节点。工具节点执行搜索拿到网页返回的原始搜索内容包装成 ToolMessage 追加进消息列表。工具节点执行完毕又无条件回到 llm_call 节点把用户问题、AI 工具调用指令、原始搜索结果全部交给大模型大模型基于搜索到的网页数据整理出可读的天气答案这一轮结束之后不再产生 tool_calls条件判断走到 END图运行结束。控制台打印的调用次数会大于 1这里经历了两轮大模型调用一次是生成工具调用指令一次是整理搜索结果输出回答。打印输出会依次看到Human Message 用户提问接着是携带 tool_calls 的 AI 消息里面记录了要调用 search 工具和查询参数。之后输出 ToolMessage也就是搜索工具返回的原始网页内容这部分内容很长是网页抓取下来的原始文本包含多个天气网站的链接、日期、温度、风力等原始数据没有做过滤整理。最后再输出一轮 AI Message就是大模型读完 ToolMessage 原始内容之后整理提炼出来的简洁易懂的天气回答给到用户。Tool Message 内容:{query: 西安今天天气, follow_up_questions: None, answer: None, images: [], results: [{url: https://qq.ip138.com/weather/shanxi/xian.htm, title: 西安天气预报,西安未来5天天气预报,陕西西安天气预报, content: | | | | | |\n --- --- \n| 星期一 2026-08-31 | | 晴 | 19℃ 32℃ | 微风 3级 |\n| 星期二 2026-09-01 | | 晴 | 20℃ 33℃ | 微风 3级 |\n| 星期三 2026-09-02 | | 晴 | 21℃ 32℃ | 微风 3级 |\n| 星期四 2026-09-03 | | 晴转多云 | 21℃ 31℃ | 微风 3级 |\n| 星期五 2026-09-04 | | 晴 | 22℃ 33℃ | 微风 3级 |\n\n \n\n天气更新时间2026-08-31 05:33:25\n\n陕西西安电话区号029\n\n陕西西安邮政编码...............................................n 减肥指数\n\n 春天不减肥夏天徒伤悲。天气较舒适快去运动吧。\n 易波动 健臻·血糖指数\n\n 气温多变血糖易波动请注意监测。\n \n\n 热 穿衣指数\n\n 适合穿T恤、短薄外套等夏季服装。\n 适宜 洗车指数 [...] 天气较好适合擦洗汽车。\n 中 空气污染扩散指数\n\n 易感人群应适当减少室外活动。\n\n 最弱 紫外线指数\n\n 辐射弱涂擦SPF8-12防晒护肤品。\n \n\n 减肥指数\n\n 天气有点热运动多补水。\n 易波动 健臻·血糖指数\n\n 血糖易波动注意监测。\n \n\n 炎热 穿衣指数\n\n 建议穿短衫、短裤等清凉夏季服装。\n 较适宜 洗车指数\n\n 无雨且风力较小易保持清洁度。\n 较差 空气污染扩散指数\n\n 气象条件较不利于空气污染物扩散。。\n\n# 天气资讯, score: 0.6999322, raw_content: None, id: b5ede7-02}, {url: https://www.qweather.com/weather30d/xian-101110101.html, title: 西安市未来30天天气预报 | 和风天气, content: 陕西省 - 中国 2026-08-28 星期五 七月十六 丙午年 马 .............................................\n\nQWeather\n\n22°~15°\n\nQWeather\n\n25°~13°\n\nQWeather\n\n25°~16°\n\n-\n\n-\n\n-\n\nQWeather\n\n-\n\n-\n\n-\n\n降水量\n\n紫外线\n\n-\n\n相对湿度\n\n-\n\n日出\n\n-\n\n日落\n\n### 2026-07 气温曲线\n\n### 附近城市\n\n咸阳市\n\nQWeather\n\n渭南市\n\nQWeather\n\n铜川市\n\nQWeather\n\n商洛市\n\nQWeather\n\n宝鸡市\n\nQWeather\n\n安康市\n\nQWeather\n\n庆阳市\n\nQWeather\n\n运城市\n\nQWeather\n\n三门峡市\n\nQWeather\n\n### 和风天气APP\n\n全球可视化天气应....................................6 和风天气版权所有, score: 0.60077465, raw_content: None, id: b4c65a-03}], response_time: 2.93, request_id: b5f69592-dbd6-4cae-8a8d-3cb7e6464420}ToolMessage 里面的内容是工具拿到的原始原始数据杂乱冗余不能直接返回给用户必须交给大模型做整理提炼这也是 agent 工作流的意义工具只负责拿数据大模型负责理解、加工、输出可读答案。整个过程是图内部自动循环完成上层只需要调用一次 invoke不需要手动处理工具调用逻辑。这次用户输入的问题是 “最近有什么热点新闻吗”属于需要联网获取最新资讯的问题大模型自身知识库没有实时新闻所以会触发一次性调用多个工具的逻辑。调用 invoke 启动整个图第一次进入 llm_call 节点大模型分析用户问题之后同时生成了两组 tool_calls也就是打算并行发起两次搜索一条查询最近一周热点新闻一条查询今日热点新闻头条。条件判断检测到存在 tool_calls流转进入 tool_node 工具节点。工具节点内部会循环遍历全部 tool_call依次执行两次搜索请求两次搜索分别拿到网页原始返回数据各自包装成独立的 ToolMessage 对象两条 ToolMessage 全部追加到消息列表当中。工具节点执行完毕之后固定边再次回到 llm_call 节点。此时消息列表里面已经包含用户提问、AI 的双工具调用指令、两条很长的 ToolMessage 搜索原始结果全部内容一起传给大模型大模型读取两份搜索返回的原始网页数据整合、过滤、整理之后输出整合完毕的新闻汇总回答本次不再生成 tool_calls条件分支走到 END图运行结束。控制台打印输出可以看到一共调用了 2 次 LLM第一次用来生成双工具调用指令第二次用来整合两份搜索结果生成最终回答。打印消息的时候会看到一条 HumanMessage 用户消息接着是携带两个 tool_calls 的 AIMessage之后接连输出两条长度很长的 ToolMessage分别对应两次搜索拿到的原始网页返回内容最后输出大模型整理完成的新闻汇总 AI 回答。这里也能体现 tool_node 的设计是支持多工具并行调用一次 AI 消息可以携带多个 tool_call循环批量执行生成多条 ToolMessage全部返回给大模型做统一处理。两份 ToolMessage 都是未加工的原始网页数据篇幅很长里面混杂网页链接、网页碎片文本不适合直接展示给用户必须交由大模型做摘要整合。三、补充我们回顾一下 LangChain 当中 AIMessage 的消息结构也是我们前面 LangGraph 工具节点底层依赖的数据格式。当大模型判断需要调用工具的时候返回的 AIMessage 里面 content 字段经常是空字符串真正的工具调用信息不会放在 content 文本当中而是存放在 tool_calls 这个列表属性里面。tool_calls 是一个列表也就意味着一次模型输出可以同时产生多条工具调用实现并行调用多个工具。列表里面每一个字典代表一次工具调用name 字段代表要调用的工具名字和我们代码里工具对象的 name 一一对应代码就是依靠这个 name 去找到对应的工具。args 字典存放调用工具所需要传入的全部参数比如这里的 query 查询词、search_depth 搜索深度都是大模型根据用户问题生成出来的参数内容。id 是这一次工具调用的唯一编号后面生成 ToolMessage 的时候必须带上完全一样的 tool_call_id框架才能够把工具返回结果和这次工具调用做配对。type 字段固定标记为 tool_call用来标识这是一条工具调用对象。additional_kwargs、response_metadata、usage_metadata 属于附加元数据refusal 用来记录模型拒绝回答的信息response_metadata 存放接口返回的原始响应信息usage_metadata 记录 token 消耗统计一般业务逻辑不会直接去操作这几个字段。放到我们前面写的 LangGraph 代码里tool_node 节点就是读取 state [messages][-1].tool_calls 拿到这份数据循环取出 name、args、id完成工具执行再把搜索结果包装成 ToolMessage 并且回填相同的 id。整套消息格式并不是 LangGraph 独创是 LangChain 就已经定义好的消息体系LangGraph 直接复用这套消息结构所以 AIMessage、HumanMessage、ToolMessage 这些消息对象可以在 LangChain 和 LangGraph 之间通用。ToolMessage 的构造逻辑也是我们 LangGraph 里面 tool_node 节点内部核心逻辑。仅仅调用完工具拿到返回数据还不够不能直接把原始结果随便丢给大模型必须封装成标准 ToolMessage 消息对象再放回消息列表传给聊天大模型大模型才能读懂这是工具的返回结果。完整的一轮工具调用上下文消息列表需要包含三类消息。首先是 HumanMessage代表用户原始提问。然后是 AIMessage也就是大模型输出的携带 tool_calls 的工具调用指令。最后跟上 ToolMessage也就是工具执行之后封装好的返回内容。把这一整套消息完整交给大模型大模型才知道哪次调用对应哪一份返回数据在此基础上生成最终回答 AIMessage。构造 ToolMessage 的时候有两个关键参数。content 字段填入工具执行完拿到的原始返回内容比如搜索接口返回的网页文本。tool_call_id 是非常关键的配对字段这个 id 必须和前面 AIMessage 里面 tool_calls 数组里面对应那一条的 id 完全一模一样。如果一次 AI 输出有多个 tool_call就需要循环每一个 tool_call每一个工具调用单独生成一条 ToolMessage每一条 ToolMessage 的 tool_call_id 分别填写对应工具调用的 id。大模型后端依靠这个 id 完成配对知道这条工具结果对应哪一次工具请求如果 id 写错或者缺失大模型就无法关联上下文会出现逻辑错乱。放到我们之前 LangGraph 的 tool_node 代码里面就是循环遍历 last_message.tool_calls执行工具拿到结果每循环一次就构造一个 ToolMessage把 content 和 tool_call_id 填好全部收集之后更新到 state 的 messages 列表再交给大模型做下一步处理。这套消息格式同样继承自 LangChain 消息体系LangGraph 直接复用这套消息对象。LangGraph 的 state 内部消息对象的序列化与反序列化规则。对外调用 invoke 的时候我们可以传入字典形式的消息用 type 标记消息类型content 填写消息内容这是普通 json 字典格式写起来比较方便。但是一旦存入 LangGraph 的 state 状态内部会自动完成反序列化把字典转换成 LangChain 完整的消息对象也就是 HumanMessage、AIMessage、ToolMessage 这类实例对象。所以在节点函数内部读取 state 里的 messages拿到的不是普通字典而是一个个消息实例不能用字典的方括号取值方式不能写state[messages][-1][tool_calls]。必须使用点表示法用.去访问对象属性示例state[messages][-1].tool_calls取 messages 列表最后一条消息对象读取它的 tool_calls 成员属性。结合我们之前写的 tool_node 节点代码就是依靠这个特性拿到上一轮 AI 输出的工具调用列表。如果误用字典方式去读取属性代码就会直接报错。输入的时候支持简易字典格式方便外部调用图内部节点运行时统一使用消息对象这就是 LangGraph 做的自动转换。相关问题:1. 这里我们同时问 3 个问题会报错为什么?Python 字典不允许重复写同一个 key代码里连续写三次 messages只会保留最后一条前面全部被覆盖属于语法层面问题不是 LangGraph 报错。invoke 调用代表 LangGraph 图完整执行一次运行到 END 节点就结束单次调用只能处理一组初始输入不能一次性批量处理多个独立问题。正确做法:单次测试注释切换每次只保留一组 messages 执行 invoke。多轮连续对话把上一轮 invoke 返回的完整 state 结果直接作为下一次 invoke 的入参消息历史会自动携带。多条消息放进同一次调用多条消息写在同一个 messages 列表内部不要重复 key但该方式是一次性全部交给模型并不会逐个问题走 Agent 循环一般不用于多轮问答。LangGraph 内部 state 中messages 存储的是消息对象 (HumanMessage/AIMessage/ToolMessage)节点读取属性要用.点语法不能用字典方括号取值。2. 这个案例和第一个智能快递配送案例有什么本质区别?快递配送案例图里面所有节点都是普通业务函数完全不依赖 LLM节点的走向、分支判断全部写死在代码逻辑里属于固定流程工作流。状态只是用来流转业务数据整个图的每一步怎么走都是开发者硬编码写死的没有大模型参与决策。Agent 搜索案例把LLM 作为图当中的一个节点。分支判断不再完全由代码写死由 LLM 输出的结果来决定下一步走向。LLM 会自主判断要不要调用工具、调用哪个工具、传什么参数。LangGraph 只负责流转状态、跳转节点、执行工具决策逻辑交给大模型完成。快递案例是 “人写死规则机器按流程跑”Agent 案例是 “交给 LLM 做思考决策框架负责执行流转”。这就是两套案例底层最关键的不同。3. 那这两个案例算简单的 agent 吗?不算只有搜索工具调用案例才是 Agent快递配送案例只是普通工作流不是 Agent。快递配送案例的节点全部是普通 Python 业务函数没有 LLM 参与。所有分支、流转逻辑全部由我们写代码硬编码确定系统没有自主思考、自主选择工具的能力。它只是利用 LangGraph 实现一套固定步骤的业务流程图属于确定性工作流。哪怕流程写的再复杂它也不会自主理解用户输入、自主决定下一步该干什么因此不属于 Agent。LLM 搜索 Agent 案例把 LLM 作为图中的节点。分支走向不再完全写死由 LLM 根据用户问题自主思考判断是否需要调用工具、调用哪个工具、生成调用参数。LangGraph 负责状态流转、节点调度、工具执行决策能力交给大模型具备感知‑思考‑行动的闭环这才是我们自己实现的简易 Agent。一句话区分快递案例LangGraph 工作流无大模型固定流程不是 Agent。搜索案例LangGraph LLM 做决策具备思考工具调用闭环属于自己实现的简易 Agent。补充LangGraph 本身只是图编排框架它本身不等于 Agent只有把 LLM 放进图让大模型承担决策才构成 Agent。4. 那 OpenAI、Anthropic、国内大厂 Agent 底层是什么真实能力到底多强?大厂的 Agent 和你用 LangGraph 手写实现的 Agent底层核心原理完全一致都遵循 ReAct 的闭环逻辑大模型思考决策、调用工具、接收工具返回结果再继续思考处理任务。我们自己基于 LangGraph 开发时状态维护、节点流转、循环终止条件、异常处理都需要自己编码实现自由度高可以任意更换不同厂商的大模型但搜索、代码沙盒这类工具也需要自己对接属于从零搭建整套 Agent 骨架。而 OpenAI、Anthropic 以及国内各家 AI 厂商并没有使用 LangGraph而是自研了内部 Agent 运行时。它们把循环调度、状态记忆、错误重试、安全校验、多智能体任务交接全部封装成 SDK 或者可视化平台同时自家大模型针对工具调用、任务拆解做过专项微调还内置好各类现成工具开箱即可使用。缺点是内部执行逻辑偏向黑盒可控性不如手写 LangGraph。不管是自研还是封装好的成品 Agent都不具备真正自主意识不会凭空给自己生成目标全部依赖用户给出的指令。 打个比方LangGraph 相当于拿到全部零件自己造车大厂 Agent 是直接购买组装完成的整车只需要输入任务不用关心内部细节。5. 它们的 agent 是由 langgraph 实现的吗? 和我们的有什么区别吗?OpenAI、Anthropic 以及国内大厂的 Agent 底层都不是基于 LangGraph 开发而是各自自研 Agent 调度运行时但核心都遵循 ReAct 循环的设计思想。LangGraph 属于第三方开源编排框架和厂商无关它把状态、节点、条件边这些基础能力提供给开发者所有流转逻辑对开发者完全透明模型可以自由切换不过工具、沙盒等能力都需要开发者自行接入相当于拿到零件自己组装汽车。各大厂商自研的 Agent 运行时把循环、状态管理、重试、安全校验全部封装大多优先绑定自家大模型部分还提供可视化拖拽平台开箱即用但内部流程偏黑盒自定义灵活度不如 LangGraph。实际开发中二者可以组合使用可以用 LangGraph 搭建业务流程骨架节点内部调用各家大模型做决策并不是非此即彼的关系。
阅读完成 · 觉得有帮助?