一、概念
- 路由代理
- 工具代理
- 自主循环代理
- 多代理
二、路由代理(Router Agent)
I 概念(有了条件边,就意味着有了选择)
在 LangGraph 中,我们可以利用 “条件边” 这一概念来指导或约束大模型在处理特定任务时的逻辑流程。这种机制允许大模型在达到某一环节并满足预设条件时,根据不同的条件输出或数据,选择性地执行不同的逻辑路径。
在 LangGraph 等框架中,条件边赋予了工作流在运行时动态决策的能力。静态边(add_edge)像固定的地铁线路,而条件边则像打车导航,能够根据当前状态(State)实时选择最优路线。
II 有向无环图(DAG)代码演示
只有静态边的智能体框架,通常被称为基于有向无环图(DAG)的框架。
在智能体架构中,如果执行路径的每一步都是提前确定好的,且节点之间仅通过静态边进行单向、无循环的连接,这种结构就被称为 DAG(Directed Acyclic Graph)。它对应的是一种静态编排。如代码所示:
from langgraph.graph import START, StateGraph, END
from langgraph.graph import StateGraph
import os
def node_a(state):
return {"x": state["x"] + 1}
def node_b(state):
return {"x": state["x"] - 2}
builder = StateGraph(dict)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
# 构建节点之间的边
builder.add_edge(START, "node_a")
builder.add_edge("node_a", "node_b")
builder.add_edge("node_b", END)
graph = builder.compile()
# 渲染流程图可视化
image_data = graph.get_graph().draw_mermaid_png()
# 写入文件
with open("Router_Agent_G.png", "wb") as f:
f.write(image_data)
print(f"✅ 图片已成功生成: {os.path.abspath("Router_Agent_G.png")}")
为了管理这样复杂的图结构,LangGraph 使用的是一个类似于 if-else 语句的结构组件,称为 Router(路由)。这个组件允许大模型从一组预设选项中选择合适的步骤来进行执行。这个设计思路并不难理解,同时由于 LangGraph 的底层封装,实现起来也非常简单,我们看下面的代码:
如果想选择性地路由到 1 个或多个边,则需要使用 add_conditional_edges 方法。该方法也在 Graph 的基类中进行了定义,如下所示:
Graph 基类源码片段
class Graph:
def __init__(self) -> None:
self.nodes: dict[str, NodeSpec] = {}
self.edges = set[tuple[str, str]]()
self.branches: defaultdict[str, dict[str, Branch]] = defaultdict(dict)
self.support_multiple_edges = False
self.compiled = False
def add_conditional_edges(
self,
source: str, # 起始节点
path: Union[ # 这是一个可调用对象,其返回值决定接下来执行的节点。这个函数可以是简单的 Python 函数,或者是任何可以被调用来决定分支路径的对象。
Callable[..., Union[Hashable, list[Hashable]]],
Callable[..., Awaitable[Union[Hashable, list[Hashable]]]],
Runnable[Any, Union[Hashable, list[Hashable]]],
],
path_map: Optional[Union[dict[Hashable, str], list[str]]] = None, # 路径到节点名称的可选映射。如果省略,path 返回的路径应该是节点名称。
then: Optional[str] = None, # 在path选择的节点之后执行的节点的名称。
) -> Self:
add_conditional_edges 源码地址:https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.graph.Graph
根据源码的定义我们可以非常明确的分析出其调用过程。这里我们要关注 path 参数,它指的是一个函数调用对象,与普通的节点类似,这个对象接受图的当前 state 并返回一个值,根据返回值的不同,来决定路由到哪个节点。比如想构建一个带有路由的图结构,这里我们定义一个 routing_function 作为路由函数,并添加一个新的节点node_c,代码如下所示:
II 简单的路由代理 代码演示
from langgraph.graph import START, StateGraph, END
from langgraph.graph import StateGraph
from IPython.display import Image, display
import os
from typing import TypedDict, Annotated
from operator import add
class State(TypedDict):
x: int
def node_a(state):
return {"x": state["x"] + 1}
def node_b(state):
return {"x": state["x"] - 2}
def node_c(state):
return {"x": state["x"] + 1}
def routing_function(state):
if state["x"] == 10:
return True
else:
return False
builder = StateGraph(State)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_node("node_c", node_c)
builder.set_entry_point("node_a")
# 构建节点之间的边
builder.add_conditional_edges("node_a", routing_function, {
True: "node_b",
False: "node_c",
})
builder.add_edge("node_b", END)
builder.add_edge("node_c", END)
graph = builder.compile()
# 2、渲染流程图可视化
image_data = graph.get_graph().draw_mermaid_png()
# 写入文件
with open("Router_Agent_G.png", "wb") as f:
f.write(image_data)
print(f"✅ 图片已成功生成: {os.path.abspath("Router_Agent_G.png")}")
II 复杂的路由代理 代码演示(由大模型判断路由【给添加提示词】)
这里的核心是 Router function,它根据输入数据的结构和内容,动态地决定下一步应该执行的节点。例如,对于具体的查询请求,Router 决定需要访问数据库 (Mysql 节点),而对于简单的问候(如 "Hello"),则直接返回一个响应 (Response 节点)。每个决策路径最终都指向一个结束节点 (End)。所以我们要明确的是,在构建实际的 Agent 时,Router fuction 的定义才是最关键且最重要的。我们需要在这个函数中,基于特定的一些格式或者标识来区分该执行哪一条分支的逻辑。而对于消息的传递,大模型往往是通过结构化输出,引导其在响应的过程中应遵循哪种模式来工作,就类似于工具调用过程。Router 就很好的利用到了这个特性,通过结构化输出的特性来控制接下来的分支路径。
这里我们先来了解一下什么是结构化输出。在 LangGraph 中,实现结构化输出可以通过以下三种有效方式完成:
- 提示工程:指示大模型以特定格式做出回应。
- 输出解析器:采用后处理的方法从大模型的响应中提取结构化数据。
- 工具调用:利用一些内置工具调用功能来生成结构化输出。
1、结构化输出-提示词工程
1.1 给大模型写提示词
# 1、配置模型
from dotenv import load_dotenv
load_dotenv()
import os
key = os.environ["QWEN_API_KEY"]
base_url = os.environ["QWEN_BASE_URL"]
model_name = os.environ["QWEN_MODEL_NAME"]
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model=model_name, api_key=key,base_url=base_url,temperature=0,)
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"Answer the user query. Wrap the output in ``<code>json</code>``",
),
("human", "{query}"),
]
)
chain = prompt | llm
ans = chain.invoke({"query": "我叫奥特曼,今年38岁,邮箱地址是aoteman#qq.com,电话是12111111111"})
print(f"{'*' * 80} \n {ans.content} \n {'*' * 80} ")
"""
打印的结果:
```json
{
"name": "奥特曼",
"age": 38,
"email": "aoteman#qq.com",
"phone": "12111111111",
"note": "信息已记录。温馨提示:标准邮箱地址中的“#”通常应为“@”,如需修正请提供正确格式。"
}</code></pre>
<p>"""</p>
<pre><code>
##### 1.2 优化方案,提示+输出解析器(extract_json)
直接通过提示工程让大模型生成特定格式的输出虽然是可行的,但这种方法在复杂的 Agent 构建流程中非常不稳定,
一个优选的优化方法是:通过输出解析器来格式化大模型生成的响应。这种做法可以提高输出的准确性和一致性,这种形式的实现方法如下所示:
```python
# 1、配置模型
from dotenv import load_dotenv
load_dotenv()
import os
key = os.environ["QWEN_API_KEY"]
base_url = os.environ["QWEN_BASE_URL"]
model_name = os.environ["QWEN_MODEL_NAME"]
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model=model_name, api_key=key,base_url=base_url,temperature=0,)
from langchain_core.messages import AIMessage
import json
import re
from typing import List
def extract_json(message: AIMessage) -> List[dict]:
"""Extracts JSON content from a string where JSON is embedded between ``<code>json and </code>`` tags.
Parameters:
text (str): The text containing the JSON content.
Returns:
list: A list of extracted JSON strings.
"""
text = message.content
# 定义正则表达式模式来匹配JSON块
pattern = r"``<code>json(.*?)</code>``"
# 在字符串中查找模式的所有非重叠匹配
matches = re.findall(pattern, text, re.DOTALL)
# 返回匹配的JSON字符串列表,去掉任何开头或结尾的空格
try:
return [json.loads(match.strip()) for match in matches]
except Exception:
raise ValueError(f"Failed to parse: {message}")
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"Answer the user query. Wrap the output in ``<code>json</code>``",
),
("human", "{query}"),
]
)
chain = prompt | llm | extract_json
ans = chain.invoke({"query": "我叫奥特曼,今年38岁,邮箱地址是aoteman#qq.com,电话是12111111111"})
print(f"{'*' * 80} \n {ans} \n {'*' * 80} ")
"""
输出的结果:
[{'name': '奥特曼', 'age': 38, 'email': 'aoteman#qq.com', 'phone': '12111111111', 'note': '信息已记录。温馨提示:标准邮箱地址中的“#”通常应为“@”,如需修正请提供正确格式。'}]
"""
2、langgraph-格式化输出
内置工具方法
从结果可以明显看出,通过定制化的输出解析器得到的结果会更加的符合预期,而在 LangGraph 中更常用的,且效果更好的是,直接使用其内置的工具方法:.with_structured_output()。
这个方法通过接受一个定义了所需输出属性的名称、类型和描述的模式作为输入,进而生成一个类似模型的 Runnable 。不同于常规模型输出字符串或消息,这个 Runnable 输出一个与输入模式相匹配的对象。可以通过几种方式指定这种架构,包括
- TypedDict
- JSON Schema
- Pydantic 类。
如果采用 TypedDict 或 JSON Schema ,Runnable 将输出一个字典;若使用 Pydantic 类,则输出一个 Pydantic 对象。
先尝试和实践使用 Pydantic 类做格式化输出,应用的场景是:从文本中提取格式化的数据。
具体内容转入具体文章的链接:https://altairnexus.top/2026/06/17/langgraph-structured-output/
2.1 代码演示
2.1.1 执行语句,将数据插入mysql数据库
掌握到这种程度,我们就可以利用这些判别条件作为 Router Function 来构建决策分支。通过使用 Pydantic 模型来提取结构化数据,能够在大语言模型 (LLM) 调用过程中将非结构化文本转换为结构化数据格式。这种转换使得我们构建的流程图能够根据用户的不同输入,智能判断是应当生成常规响应还是执行数据库操作。代码如下所示:
MySQL 完整安装配置步骤(Ubuntu 系统)
# 更新软件源索引
sudo apt-get update
# 安装 MySQL 服务端
sudo apt-get install mysql-server
# 启动 MySQL 服务
sudo service mysql start
进入 MySQL 客户端,执行账号与库创建 SQL
0. 登录数据库
mysql -u root
1. 创建专用业务账号 gpt
CREATE USER 'gpt'@'localhost' IDENTIFIED BY 'gpt';
2. 给账号授予全库全部权限
GRANT ALL PRIVILEGES ON *.* TO 'gpt'@'localhost';
3. 刷新权限使配置生效
FLUSH PRIVILEGES;
4. 创建项目专用数据库 langgraph
create database langgraph;
Python 依赖安装(对接 MySQL)
pip install sqlalchemy pymysql
MySQL 完整安装配置步骤(windows 系统)
https://altairnexus.top/2026/07/08/mysql_databases/
2.1.2 代码演示
from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData
from sqlalchemy.orm import declarative_base, sessionmaker
from typing import Optional, Literal
from pydantic import BaseModel, Field
# 1、配置模型
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
load_dotenv()
key = os.environ["OPENAI_API_KEY"]
base_url = os.environ["OPENAI_BASE_URL"]
model_name = os.environ["OPENAI_MODEL_NAME"]
llm = ChatOpenAI(model=model_name, api_key=key, base_url=base_url, temperature=0, request_timeout=10)
# 给字段加examples示例,让模型"认得"要提取什么。例子比描述更能锚定模型对字段的理解,尤其对中文输入。
class UserInfo(BaseModel):
"""用户个人信息。当路由判定为 user_info 时填写本字段,缺失字段留空。"""
name: Optional[str] = Field(default=None, description="用户姓名", examples=["奥特曼", "张三"])
age: Optional[int] = Field(default=None, description="用户年龄", examples=[38])
email: Optional[str] = Field(default=None, description="用户邮箱地址", examples=["aoteman@qq.com"])
phone: Optional[str] = Field(default=None, description="用户电话号码", examples=["12111111111"])
# 定义正常生成模型回复的模型
class ConversationalResponse(BaseModel):
"""对话回复。当路由判定为 chat 时填写本字段。"""
response: str = Field(description="A conversational response to the user's query")
# 定义最终响应模型:先做显式路由判定,再按路由填写对应字段。
# 用 route 字段做分类,比 Union 交给模型选工具更稳定(兼容接口模型对工具选择的遵循度参差不齐)。
class FinalResponse(BaseModel):
"""先判断用户消息属于哪一类,再填对应字段。
判定规则:
- 含姓名/年龄/邮箱/电话/手机号等任意个人信息信号词(我叫/我是/今年X岁/邮箱/电话/手机)→ route="user_info",并填 user_info。
- 纯寒暄/提问/闲聊,不含任何个人信息字段(你好/谢谢/解释一下XX/今天天气怎么样)→ route="chat",并填 chat。"""
route: Literal["user_info", "chat"] = Field(description="消息类型路由:含个人信息选 user_info,纯对话选 chat")
user_info: Optional[UserInfo] = Field(default=None, description="route=user_info 时填写")
chat: Optional[ConversationalResponse] = Field(default=None, description="route=chat 时填写")
# 创建基类
Base = declarative_base()
# 定义 UserInfo 映射模型,对应数据库 users 表
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(50))
age = Column(Integer)
email = Column(String(100))
phone = Column(String(15))
# 数据库连接 URI 说明
# gpt: MySQL 用户名
# gpt: MySQL 密码
# localhost: MySQL 服务地址
# langgraph: 目标数据库名
# charset=utf8mb4:支持完整Unicode、emoji字符
# 1、读取配置信息
passwrod = os.environ["MYSQL_PASSWORD"]
acc = os.environ["MYSQL_account"]
DATABASE_URI = f'mysql+pymysql://{acc}:{passwrod}@localhost/langgraph?charset=utf8mb4'
engine = create_engine(DATABASE_URI, echo=True)
# 自动创建数据表(不存在则新建,存在不会覆盖)
Base.metadata.create_all(engine)
# 创建数据库会话工厂
Session = sessionmaker(bind=engine)
session = Session()
from langchain_core.messages import SystemMessage
system = SystemMessage(content=(
"你是一个消息分类与信息抽取器,先判定 route,再按 route 填对应字段。\n"
"判定规则(必须严格遵守):\n"
"1) 若用户消息中出现姓名/年龄/邮箱/电话/手机号等任意个人信息信号词"
"(如 我叫/我是/姓名/今年X岁/邮箱/电话/手机),route 必须为 'user_info',"
"并在 user_info 中填入抽取到的字段,缺失字段留空;此时 chat 必须为空。\n"
"2) 若用户消息是纯寒暄/提问/闲聊,不含任何个人信息字段"
"(如 你好/谢谢/解释一下XX/今天天气怎么样),route 必须为 'chat',"
"并在 chat.response 中给出自然语言回复;此时 user_info 必须为空。\n"
"禁止在含个人信息的消息上选 'chat',也禁止在纯对话消息上选 'user_info'。"
))
# 接下来我们定义节点函数,其中 structured_output 作为路由节点将用户输入的文本转化成格式化输出,搭配 Router Function 构建分支。
def structured_output(state):
"""generate structured output"""
print(state)
print("----------------")
messages = state['messages']
structured_llm = llm.with_structured_output(FinalResponse)
# 给大模型写的提示词很重要,直接决定了路由是否正确,以及后续是否可以把结构性数据存入数据库
response = structured_llm.invoke([system] + messages)
return {"messages": [response]}
# 然后分别定义两个分支节点,其中 final_answer 用于直接生成响应,而 insert_db 用于执行数据库插入操作。
def final_answer(state):
"""generate natural language responses"""
print(state)
print("----------------")
result = state['messages'][-1]
# 兼容两种返回:结构化对象 或 纯字符串
if isinstance(result, FinalResponse):
response = result.chat.response if result.chat else ""
else:
response = result
return {"messages": [response]}
def insert_db(state):
"""Insert user information into the database"""
session = Session() # 确保为每次操作创建新的会话
try:
result = state['messages'][-1]
info = result.user_info
# 创建用户实例
user = User(name=info.name, age=info.age, email=info.email, phone=info.phone)
# 添加到会话
session.add(user)
# 提交事务
session.commit()
return {"messages": [f"数据已成功存储至Mysql数据库。"]}
except Exception as e:
session.rollback() # 出错时回滚
return {"messages": [f"数据存储失败,错误原因:{e}"]}
finally:
session.close() # 关闭会话
# 定义好了所有节点函数后,开始构建图。
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
import operator
from langchain_core.messages import AnyMessage, SystemMessage, HumanMessage, ToolMessage
class AgentState(TypedDict):
messages: Annotated[list[AnyMessage], operator.add]
# 定义 generate_branch 函数作为 Router Function,根据经过 structured_output 节点后产生的 route 字段,选择连接不同的节点,即 final_answer 或 insert_db。
def generate_branch(state: AgentState):
result = state['messages'][-1]
# 兜底:结构化失败退化成字符串时退回对话分支
if not isinstance(result, FinalResponse):
return False
return result.route == "user_info"
# 构建图并使用条件边来生成 Router。代码如下:
graph = StateGraph(AgentState)
# 添加三个节点
graph.add_node("structured_output", structured_output)
graph.add_node("final_answer", final_answer)
graph.add_node("insert_db", insert_db)
# 设置图的启动节点
graph.set_entry_point("structured_output")
# 设置条件边
graph.add_conditional_edges(
"structured_output",
generate_branch,
{True: "insert_db", False: "final_answer"}
)
# 设置终止节点
graph.set_finish_point("final_answer")
graph.set_finish_point("insert_db")
# 编译图
graph = graph.compile()
# 3、渲染流程图可视化
image_data = graph.get_graph().draw_mermaid_png()
with open("Router_Mysql_G.png", "wb") as f:
f.write(image_data)
# 接下来进行测试,首先测试执行插入数据库的条件分支。
query="我叫杰克奥特曼,今年30岁,邮箱地址是jake@qq.com,电话是01010101010101"
input = {"messages": [HumanMessage(content=query)]}
result = graph.invoke(input)
print(f"{"测试执行插入数据库的条件分支:"}\n{'*'*80}\n{result}\n{'*'*80}")
# 读取最终输出文本
result = result["messages"][-1]
print(f"{"读取最终输出文本:"}\n{'*'*80}\n{result}\n{'*'*80}")
# 返回结果:'数据已成功存储至Mysql数据库。'
# 而如果正常的问答,则会经过 final_answer 直接生成响应。
# query="你好,请你介绍一下你自己"
# 普通问答分支测试1
query = "你好,请你介绍一下你自己"
input_message = {"messages": [HumanMessage(content=query)]}
result = graph.invoke(input_message)
print(f"{"普通问答分支测试1:"}\n{'*'*80}\n{result}\n{'*'*80}")
result=result["messages"][-1]
print(f"{"读取最终输出文本:"}\n{'*'*80}\n{result}\n{'*'*80}")
# 返回输出:
# '你好!我是一个由人工智能驱动的助手,旨在为您提供信息、回答问题和提供帮助支持。请随时告诉我您需要什么帮助!'
# 普通问答分支测试2
query = "请问什么是机器学习"
input_message = {"messages": [HumanMessage(content=query)]}
result = graph.invoke(input_message)
print(f"{"普通问答分支测试2:"}\n{'*'*80}\n{result}\n{'*'*80}")
result = result["messages"][-1]
print(f"{"读取最终输出文本:"}\n{'*'*80}\n{result}\n{'*'*80}")
query="我叫孙悟空,今年500岁,邮箱地址是wukong@gmial.com"
input_message = {"messages": [HumanMessage(content=query)]}
result = graph.invoke(input_message)
print(f"{"测试执行插入数据库的条件分支:"}\n{'*'*80}\n{result}\n{'*'*80}")
如上示例就是 LangGraph 中 Router 的常用使用形式,通过预定义的分支结构,可以根据用户的输入请求灵活适配不同的场景,在这个过程中,结构化输出对于路由至关重要,因为它们确保系统可以可靠地解释大模型的决定并采取行动。这种 Router Agent(路由代理)的优势就是可以精准的控制程序链路中的每一个细节,但同时也表现出来了这是一种相对有限的控制级别的代理架构,因为大模型通常只能控制单个决策。想象一下上面的场景中,如果我们希望定义的 insert_db 不仅仅只是包含插入数据库,而是有一堆各式各样的工具,比如网络搜索,RAG 等等,应该如何进一步的扩展呢?难道要做对每一个工具在 insert_db 节点下再通过 Router Function 做分支判断吗?虽然可行,但总归并不是高效的做法。
由此,我们接下来进一步给大家介绍 Tool Calling Agent(工具调用代理)来高效的解决这一问题。
三、工具代理(Tool Agent)
I 前言
Tool Calling Agent(工具调用代理)是 LangGraph 支持的第二种 AI Agent 代理架构。这个代理架构是在 Router Agent 的基础上,大模型可以自主选择并使用多种工具来完成某个条件分支中的任务。工具调用大家应该非常熟悉了,当我们希望代理与外部系统交互时,工具就非常有用。外部系统(例如 API)通常需要特定输入模式,而不是自然语言。例如,当我们绑定 API 作为工具时,我们赋予大模型对所需输入模式的感知,大模型就能根据用户的自然语言输入选择调用工具,并返回符合该工具架构的输出。
在 LangGraph 框架中,可以直接使用预构建 ToolNode 进行工具调用,其内部实现原理和我们之前介绍的手动实现的 Function Calling 流程思路基本一致,即:
LangGraph ToolNode 源码: https://langchain-ai.github.io/langgraph/reference/prebuilt/#langgraph.prebuilt.tool_node.ToolNode
1、tool_node源码
tools_by_name = {tool.name: tool for tool in tools}
def tool_node(state: dict):
result = []
for tool_call in state["messages"][-1].tool_calls:
tool = tools_by_name[tool_call["name"]]
observation = tool.invoke(tool_call["args"])
result.append(ToolMessage(content=observation, tool_call_id=tool_call["id"]))
return {"messages": result}
II 正文
1 使用 ToolNode 接入工具
经过 ToolNode 工具后,其返回的是一个 LangChain Runnable 对象,会将图形状态(带有消息列表)作为输入并输出状态更新以及工具调用的结果,通过这种设计去适配 LangGraph 中其他的功能组件。比如我们后续要介绍的 LangGraph 预构建的更高级 AI Agent 架构 – ReAct,两者搭配起来可以开箱即用,同时通过 ToolNode 构建的工具对象也能与任何 StateGraph 一起使用,只要其状态中具有带有适当 Reducer 的 messages 键。由此,对于 ToolNode 的使用,有三个必要的点需要满足,即:
- 状态必须包含消息列表。
- 最后一条消息必须是 AIMessage。
- AIMessage 必须填充 tool_calls。
我们尝试进行一下实践。首先,既然是工具调用代理,我们就准备一下需要用的外部工具 / 函数。这里我们使用 Serper API 去构建实时联网检索的功能。
测试google 浏览器api方法的连通性
import requests
import json
from dotenv import load_dotenv
load_dotenv()
import os
weather_key = os.environ["OPEN_WEATHER_KEY"]
GOOGLE_SEARCH_KEY = os.environ["GOOGLE_SEARCH_KEY"]
def fetch_real_time_info(query):
"""Get real-time Internet information"""
url = "https://google.serper.dev/search"
payload = json.dumps({
"q": query,
"num": 1,
})
headers = {
'X-API-KEY': GOOGLE_SEARCH_KEY,
'Content-Type': 'application/json'
}
response = requests.post(url, headers=headers, data=payload)
data = json.loads(response.text) # 将返回的JSON字符串转换为字典
if 'organic' in data:
return json.dumps(data['organic'], ensure_ascii=False) # 返回'organic'部分的JSON字符串
else:
return json.dumps({"error": "No organic results found"}, ensure_ascii=False) # 如果没有'organic'键,返回错误信息
# 使用示例
query = "小米汽车"
result = fetch_real_time_info(query)
print(f"{"连通性测试:"}\n{'*'*80}\n{result}\n{'*'*80}")
ToolNode
如果功能正常,该函数将根据用户的输入,返回实时的网页检索信息,这包括标题、链接、摘要等等有效的信息。而如果想要将普通的函数变成 ToolNode 可以应用的外部函数,只需要在函数定义时添加 @tool 装饰器。
单工具测试
from langchain_core.tools import tool
import requests
import json
# 如果功能正常,该函数将根据用户的输入,返回实时的网页检索信息,这包括标题、链接、摘要等等有效的信息。而如果想要将普通的函数变成 ToolNode 可以应用的外部函数,只需要在函数定义时添加 @tool 装饰器。
from dotenv import load_dotenv
load_dotenv()
import os
weather_key = os.environ["OPEN_WEATHER_KEY"]
GOOGLE_SEARCH_KEY = os.environ["GOOGLE_SEARCH_KEY"]
@tool
def fetch_real_time_info(query):
"""Get real-time Internet information"""
url = "https://google.serper.dev/search"
payload = json.dumps({
"q": query,
"num": 1,
})
headers = {
'X-API-KEY': GOOGLE_SEARCH_KEY,
'Content-Type': 'application/json'
}
response = requests.post(url, headers=headers, data=payload)
data = json.loads(response.text) # 将返回的JSON字符串转换为字典
if 'organic' in data:
return json.dumps(data['organic'], ensure_ascii=False) # 返回'organic'部分的JSON字符串
else:
return json.dumps({"error": "No organic results found"}, ensure_ascii=False) # 如果没有'organic'键,返回错误信息
# 导入预构建工具执行节点
from langgraph.prebuilt import ToolNode
# 存放所有已用@tool装饰的工具列表
tools = [fetch_real_time_info]
# 实例化ToolNode工具执行节点
tool_node = ToolNode(tools)
# ToolNode 使用消息列表对图状态进行操作。所以它要求消息列表中的最后一条消息是带有 tool_calls 参数的 AIMessage,
# 比如我们可以手动调用工具节点(模拟调用工具):
from langchain_core.messages import AIMessage
message_with_single_tool_call = AIMessage(
content="",
tool_calls=[
{
"name": "fetch_real_time_info",
"args": {"query": "小米汽车"},
"id": "tool_call_id",
"type": "tool_call",
}
],
)
# tool_node.invoke({"messages": [message_with_single_tool_call]})
from langgraph.runtime import Runtime
from langgraph._internal._constants import CONF, CONFIG_KEY_RUNTIME
result = tool_node.invoke(
{"messages": [message_with_single_tool_call]},
config={CONF: {CONFIG_KEY_RUNTIME: Runtime()}},)
print(f"{"模拟调用结果:"}\n{'*'*80}\n{result}\n{'*'*80}")
再增加一个工具,模拟将多个工具调用同时传递给 AIMessage 的 tool_calls 参数,仍然可以使用 ToolNode 进行并行工具调用:
模拟多工具调用:
from langchain_core.tools import tool
import requests
import json
# 如果功能正常,该函数将根据用户的输入,返回实时的网页检索信息,这包括标题、链接、摘要等等有效的信息。而如果想要将普通的函数变成 ToolNode 可以应用的外部函数,只需要在函数定义时添加 @tool 装饰器。
from dotenv import load_dotenv
load_dotenv()
import os
weather_key = os.environ["OPEN_WEATHER_KEY"]
GOOGLE_SEARCH_KEY = os.environ["GOOGLE_SEARCH_KEY"]
@tool
def fetch_real_time_info(query):
"""Get real-time Internet information"""
url = "https://google.serper.dev/search"
payload = json.dumps({
"q": query,
"num": 1,
})
headers = {
'X-API-KEY': GOOGLE_SEARCH_KEY,
'Content-Type': 'application/json'
}
response = requests.post(url, headers=headers, data=payload)
data = json.loads(response.text) # 将返回的JSON字符串转换为字典
if 'organic' in data:
return json.dumps(data['organic'], ensure_ascii=False) # 返回'organic'部分的JSON字符串
else:
return json.dumps({"error": "No organic results found"}, ensure_ascii=False) # 如果没有'organic'键,返回错误信息
@tool
def get_weather(location):
"""Call to get the current weather."""
if location.lower() in ["beijing"]:
return "北京的温度是16度,天气晴朗。"
elif location.lower() in ["shanghai"]:
return "上海的温度是20度,部分多云。"
else:
return "不好意思,并未查询到具体的天气信息。"
# 导入预构建工具执行节点
from langgraph.prebuilt import ToolNode
# 存放所有已用@tool装饰的工具列表
tools = [fetch_real_time_info, get_weather]
# 实例化ToolNode工具执行节点
tool_node = ToolNode(tools)
from langchain_core.messages import AIMessage
message_with_multiple_tool_calls = AIMessage(
content="",
tool_calls=[
{
"name": "fetch_real_time_info",
"args": {"query": "小米汽车"},
"id": "tool_call_id",
"type": "tool_call",
},
{
"name": "get_weather",
"args": {"location": "beijing"},
"id": "tool_call_id_2",
"type": "tool_call",
},
],
)
from langgraph.runtime import Runtime
from langgraph._internal._constants import CONF, CONFIG_KEY_RUNTIME
result = tool_node.invoke(
{"messages": [message_with_multiple_tool_calls]},
config={CONF: {CONFIG_KEY_RUNTIME: Runtime()}},)
print(f"{"模拟将多个工具调用同时传递给 AIMessage:"}\n{'*'*80}\n{result}\n{'*'*80}")
# 而 Tool Calling Agent 的本质原理是:让大模型根据用户的输入,自动的去判断应该使用哪个函数,并实际的执行,最后结合工具的响应结果,用户的原始问题作为完整的 Prompt 生成最终的问题。
# 用大模型实现工具路由调用
# 1、配置模型
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
load_dotenv()
key = os.environ["OPENAI_API_KEY"]
base_url = os.environ["OPENAI_BASE_URL"]
model_name = os.environ["OPENAI_MODEL_NAME"]
llm = ChatOpenAI(model=model_name, api_key=key, base_url=base_url, temperature=0, request_timeout=10)
from langchain_core.prompts import ChatPromptTemplate
result = llm.bind_tools(tools)
print(f"{"绑定大模型之后的工具:"}\n{'*'*80}\n{result}\n{'*'*80}")
print(f"{"工具方面的详细信息:"}\n{'*'*80}\n{result.kwargs}\n{'*'*80}")
print(f"{"测试调用工具,直接看tool_call里面是不是正确的工具:"}\n{'*'*80}\n{result.invoke("小米汽车").tool_calls}\n{'*'*80}")
print(f"{"测试调用工具,直接看tool_call里面是不是正确的工具:"}\n{'*'*80}\n{result.invoke("北京现在天气怎么样?").tool_calls}\n{'*'*80}")
# 由此可见,借助大模型可以正确的填充 tool_calls 信息,因此我们可以将其直接传递给 ToolNode,从而完成完整的 Tool Calling Agent 链路。代码如下:
result_validation = tool_node.invoke({"messages": [result.invoke("小米汽车")]},
config={CONF: {CONFIG_KEY_RUNTIME: Runtime()}},)
print(f"{"Tool Calling Agent 链路测试1:"}\n{'*'*80}\n{result_validation}\n{'*'*80}")
result = tool_node.invoke({"messages": [result.invoke("beijing现在天气怎么样?")]},
config={CONF: {CONFIG_KEY_RUNTIME: Runtime()}},)
print(f"{"Tool Calling Agent 链路测试2:"}\n{'*'*80}\n{result}\n{'*'*80}")
III Tool Calling Agent 的完整实现案例
接下来,我们来构建完整的 Tool Calling Agent。这里我们对 Router Agent 实现的图做进一步的升级,即用户输入问题后,如果不需要外部工具的信息,则直接生成回复,否则,则进入一个工具库中,选择最合适的工具执行,并返回最终的响应。因此,我们首先来定义工具库:
from pydantic import BaseModel, Field
class SearchQuery(BaseModel):
query: str = Field(description="Questions for networking queries")
# 5、工具定义:
from langchain_core.tools import tool
import requests
import json
from dotenv import load_dotenv
load_dotenv()
import os
weather_key = os.environ["OPEN_WEATHER_KEY"]
GOOGLE_SEARCH_KEY = os.environ["GOOGLE_SEARCH_KEY"]
@tool(args_schema = SearchQuery)
def fetch_real_time_info(query):
"""Get real-time Internet information"""
url = "https://google.serper.dev/search"
payload = json.dumps({
"q": query,
"num": 1,
})
headers = {
'X-API-KEY': GOOGLE_SEARCH_KEY,
'Content-Type': 'application/json'
}
response = requests.post(url, headers=headers, data=payload)
data = json.loads(response.text) # 将返回的JSON字符串转换为字典
if 'organic' in data:
return json.dumps(data['organic'], ensure_ascii=False) # 返回'organic'部分的JSON字符串
else:
return json.dumps({"error": "No organic results found"}, ensure_ascii=False) # 如果没有'organic'键,返回错误信息
# 工具1:天气查询工具
class WeatherLoc(BaseModel):
location: str = Field(description="The location name of the city")
@tool(args_schema = WeatherLoc)
def get_weather(location):
"""Call to get the current weather."""
if location.lower() in ["beijing"]:
return "北京的温度是16度,天气晴朗。"
elif location.lower() in ["shanghai"]:
return "上海的温度是20度,部分多云。"
else:
return "不好意思,并未查询到具体的天气信息。"
from typing import Union
# 工具2:数据库插入用户信息工具
from typing import Optional, Literal
from pydantic import BaseModel, Field
class UserInfo(BaseModel):
"""Extracted user information, such as name, age, email, and phone number, if relevant."""
name: str = Field(description="The name of the user")
age: Optional[int] = Field(description="The age of the user")
email: str = Field(description="The email address of the user")
# phone: Optional[str] = Field(description="The phone number of the user")
phone: Optional[Union[str, int]] = Field(description="The phone number of the user")
from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData
from sqlalchemy.orm import declarative_base, sessionmaker
passwrod = os.environ["MYSQL_PASSWORD"]
acc = os.environ["MYSQL_account"]
DATABASE_URI = f'mysql+pymysql://{acc}:{passwrod}@localhost/langgraph?charset=utf8mb4'
engine = create_engine(DATABASE_URI, echo=True)
# 创建基类
Base = declarative_base()
# 自动创建数据表(不存在则新建,存在不会覆盖)
Base.metadata.create_all(engine)
# 创建数据库会话工厂
Session = sessionmaker(bind=engine)
session = Session()
# 定义 UserInfo 映射模型,对应数据库 users 表
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(50))
age = Column(Integer)
email = Column(String(100))
phone = Column(String(15))
@tool(args_schema = UserInfo)
def insert_db(name, age, email, phone):
"""Insert user information into the database, The required parameters are name, age, email, phone"""
session = Session() # 确保为每次操作创建新的会话
try:
# 创建用户实例
user = User(name=name, age=age, email=email, phone=phone)
# 添加到会话
session.add(user)
# 提交事务
session.commit()
return {"messages": ["数据已成功存储至Mysql数据库。"]}
except Exception as e:
session.rollback() # 出错时回滚
return {"messages": [f"数据存储失败,错误原因:{e}"]}
finally:
session.close() # 关闭会话
# 三个工具定义完毕后,我们将其传递给 ToolNode,并进行大模型的实例化。
# 导入预构建工具执行节点
from langgraph.prebuilt import ToolNode
tools = [insert_db, fetch_real_time_info, get_weather]
tool_node = ToolNode(tools)
print(f"{"ToolNode:"}\n{'*'*80}\n{tool_node}\n{'*'*80}")
# 1、配置模型
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
load_dotenv()
key = os.environ["OPENAI_API_KEY"]
base_url = os.environ["OPENAI_BASE_URL"]
model_name = os.environ["OPENAI_MODEL_NAME"]
llm = ChatOpenAI(model=model_name, api_key=key, base_url=base_url, temperature=0, request_timeout=20)
# 这里仍然使用 Pydantic 来做结构化输出,帮助 Router Function 来选择路径分支。
# 定义正常生成模型回复的模型
class ConversationalResponse(BaseModel):
"""Respond to the user's query in a conversational manner. Be kind and helpful."""
response: str = Field(description="A conversational response to the user's query")
# from typing import Union
# # 定义最终响应模型,可以是用户信息或一般响应
# class FinalResponse(BaseModel):
# final_output: Union[ConversationalResponse, SearchQuery, WeatherLoc, UserInfo]
class FinalResponse(BaseModel):
"""先判断用户消息属于哪一类,再填对应字段。
判定规则:
- 含姓名/年龄/邮箱/电话/手机号等个人信息(我叫/我是/今年X岁/邮箱/手机)→ route="insert_db",并填 insert_db
- 询问某地天气(今天天气/北京天气)→ route="get_weather",并填 get_weather
- 需要联网获取实时信息(小米汽车最新款/某新闻)→ route="fetch_real_time_info",并填 fetch_real_time_info
- 纯寒暄/闲聊/解释类提问(你好/谢谢/解释一下XX)→ route="chat",并填 chat"""
route: Literal["insert_db", "get_weather", "fetch_real_time_info", "chat"] = Field(
description="消息类型路由:个人信息→insert_db,天气→get_weather,联网搜索→fetch_real_time_info,纯对话→chat"
)
insert_db: Optional[UserInfo] = Field(default=None, description="route=insert_db 时填写")
get_weather: Optional[WeatherLoc] = Field(default=None, description="route=get_weather 时填写")
fetch_real_time_info: Optional[SearchQuery] = Field(default=None, description="route=fetch_real_time_info 时填写")
chat: Optional[ConversationalResponse] = Field(default=None, description="route=chat 时填写")
@property
def final_output(self):
"""下游通过 .final_output 访问;按 route 返回当前选中的子对象,
使其与原 Union 设计兼容(isinstance 判断、.response 访问、str() 喂模型都不受影响)。"""
return getattr(self, self.route)
# 依次定义三个节点函数:
# def chat_with_model(state):
# """generate structured output"""
# # print(state)
# # print("------------------")
# messages = state['messages']
# structured_llm = llm.with_structured_output(FinalResponse)
# response = structured_llm.invoke(messages)
# return {"messages": [response]}
ROUTER_SYSTEM_PROMPT = """
你是一个严格的消息路由分类器。你的唯一任务是:判断用户当前消息属于下面哪一类,并只填写对应字段,其余字段必须为 null。不要回答用户问题,不要解释,只做分类。
【四条路由的精确定义与边界】
1. route="insert_db" —— 用户在主动提供自己的个人信息,目的是"登记/录入/保存"。
触发信号(满足任一即可,但必须是"在告知自己的信息"):
- 含自指词:"我叫/我是/我的名字是/本人/我姓"
- 含年龄自述:"今年X岁/我X岁了"
- 含联系方式:"邮箱是/电话是/手机是/我的邮箱/我的电话"
典型:"我叫张三,今年20,邮箱z@163.com,电话138xxx"
边界:用户只是"问别人的信息"("张三的邮箱是多少")不算,归 chat。
填写:insert_db 字段,把识别到的 name/age/email/phone 填进去,缺的填 null。
2. route="get_weather" —— 用户在询问某地的天气状况。
触发信号:含"天气/气温/多少度/下雨吗/冷不冷/热不热"且指向一个地点。
典型:"北京天气怎么样/上海今天多少度/beijing的天气"
边界:"今天天气真好啊"(陈述感受,非查询)不算,归 chat。
填写:get_weather 字段,location 填城市名(中文或英文统一转小写英文,如"北京"→"beijing")。
3. route="fetch_real_time_info" —— 用户在询问需要联网才能回答的实时/外部信息。
触发信号(满足任一):
- 具体品牌/产品/新闻/人物/事件的最新情况:"小米汽车/苹果发布会/某某公司最新"
- 需要"当前/最新/现在/最近"等时效词的事实查询
- 任何你训练数据里没有、必须上网查的内容
典型:"小米汽车最新款是什么/最近有什么新闻/OpenAI最新模型"
边界:常识性问题("Python是什么")不算,归 chat;天气不算(走 get_weather)。
填写:fetch_real_time_info 字段,query 填一个适合搜索引擎的简洁查询词。
4. route="chat" —— 以上三类都不满足时的兜底。
包括:寒暄(你好/谢谢/再见)、闲聊、知识问答、解释概念、对上一轮的追问、无法判断意图。
典型:"你好/解释一下什么是RAG/谢谢你/今天心情不好"
填写:chat 字段,response 填你对用户的自然语言回复。
【优先级与冲突处理】
当多条规则似乎都命中时,按此优先级判定:
insert_db(主动给个人信息)> get_weather(明确问天气)> fetch_real_time_info(明确要联网)> chat
即:用户"在提供自己的信息"优先于一切;其次明确问天气;其次明确要联网;都不明确才 chat。
判断不准时,宁可选 chat,不要把闲聊误判成工具调用。
【输出要求】
- route 必须是上述四个字面量之一,不得自造。
- 只填 route 对应的那一个字段,另外三个字段必须为 null。
- 不要输出任何额外文字,不要在字段里塞 JSON 字符串。"""
def chat_with_model(state):
messages = state['messages']
structured_llm = llm.with_structured_output(FinalResponse)
# 在用户消息前注入路由系统提示
response = structured_llm.invoke([SystemMessage(content=ROUTER_SYSTEM_PROMPT)] + messages)
return {"messages": [response]}
def final_answer(state):
"""generate natural language responses"""
# print(state)
# print("------------------")
messages = state['messages'][-1]
response = messages.final_output.response
return {"messages": [response]}
model_with_tools = llm.bind_tools(tools)
def execute_function(state):
"""generate natural language responses"""
messages = state['messages'][-1].final_output
response = tool_node.invoke({"messages": [model_with_tools.invoke(str(messages))]})
print(f"response:{response}")
response = response["messages"][0].content
return {"messages": [response]}
# 定义好了所有节点函数后,开始构建图。
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
import operator
from langchain_core.messages import AnyMessage, SystemMessage, HumanMessage, ToolMessage
# 定义图的状态模式。
class AgentState(TypedDict):
messages: Annotated[list[AnyMessage], operator.add]
# 定义 generate_branch 函数作为 Router Function,根据经过 chat_with_model 节点后产生的不同 Pydantic 对象,选择连接不同的节点,即 final_answer 或 execute_function。
def generate_branch(state: AgentState):
result = state['messages'][-1]
output = result.final_output
if isinstance(output, ConversationalResponse):
return False
else:
return True
# 构建图并使用条件边来生成 Router。代码如下:
graph = StateGraph(AgentState)
# 添加三个节点
graph.add_node("chat_with_model", chat_with_model)
graph.add_node("final_answer", final_answer)
graph.add_node("execute_function", execute_function)
# 设置图的启动节点
graph.set_entry_point("chat_with_model")
# 设置条件边
graph.add_conditional_edges(
"chat_with_model",
generate_branch,
{True: "execute_function", False: "final_answer"}
)
# 设置终止节点
graph.set_finish_point("final_answer")
graph.set_finish_point("execute_function")
# 编译图
graph = graph.compile()
image_data = graph.get_graph().draw_mermaid_png()
with open("Router_Agent_G.png", "wb") as f:
f.write(image_data)
# 完成图的编译后,便可以开始进行功能测试,这里我们选择不同的输入场景,以测试是否可以正确的进入到指定的功能函数中完成需求。
# 场景 1:调用对话工具
query1="你好,请你介绍一下你自己"
input_message1 = {"messages": [HumanMessage(content=query1)]}
result1 = graph.invoke(input_message1)
print(f"{"纯对话测试,结构化输出:"}\n{'*'*80}\n{result1}\n{'*'*80}\n")
# 查看最终输出
result1 = result1["messages"][-1]
print(f"{"纯对话测试,只看AI响应的原话:"}\n{'*'*80}\n{result1}\n{'*'*80}\n")
# 场景 2:调用谷歌浏览器查询工具
query2="小米汽车"
input_message2 = {"messages": [HumanMessage(content=query2)]}
result2 = graph.invoke(input_message2)
print(f"{"需要联网搜索工具,结构化输出:"}\n{'*'*80}\n{result2}\n{'*'*80}\n")
# 查看最终输出
result2 = result2["messages"][-1]
print(f"{"需要联网搜索工具,只看AI响应的原话:"}\n{'*'*80}\n{result2}\n{'*'*80}\n")
# 场景 3:调用天气查询工具
query3="shanghai的天气怎么样?"
input_message3 = {"messages": [HumanMessage(content=query3)]}
result3 = graph.invoke(input_message3)
print(f"{"调用天气查询工具,结构化输出:"}\n{'*'*80}\n{result3}\n{'*'*80}\n")
# 查看最终输出
result3 = result3["messages"][-1]
print(f"{"调用天气查询工具,只看AI响应的原话:"}\n{'*'*80}\n{result3}\n{'*'*80}\n")
# 场景 4:调用数据库插入用户信息工具
query4="我叫朱迪,大明成祖皇帝,今年278岁,邮箱是judy@163.com,电话是110110110110110"
input_message4 = {"messages": [HumanMessage(content=query4)]}
result4 = graph.invoke(input_message4)
print(f"{"调用数据库插入用户信息工具,结构化输出:"}\n{'*'*80}\n{result4}\n{'*'*80}\n")
IV 手动构建 Tool Calling Agent 的方法
当然,工具调用的过程也可以手动进行实现,我们只需要进行适当的逻辑修改即可,如下示例所示:
# 1、获取google浏览器工具定义:
from pydantic import BaseModel, Field
class SearchQuery(BaseModel):
query: str = Field(description="Questions for networking queries")
from langchain_core.tools import tool
import requests
import json
from dotenv import load_dotenv
load_dotenv()
import os
weather_key = os.environ["OPEN_WEATHER_KEY"]
GOOGLE_SEARCH_KEY = os.environ["GOOGLE_SEARCH_KEY"]
@tool(args_schema = SearchQuery)
def fetch_real_time_info(query):
"""Get real-time Internet information"""
url = "https://google.serper.dev/search"
payload = json.dumps({
"q": query,
"num": 1,
})
headers = {
'X-API-KEY': GOOGLE_SEARCH_KEY,
'Content-Type': 'application/json'
}
response = requests.post(url, headers=headers, data=payload)
data = json.loads(response.text) # 将返回的JSON字符串转换为字典
if 'organic' in data:
return json.dumps(data['organic'], ensure_ascii=False) # 返回'organic'部分的JSON字符串
else:
return json.dumps({"error": "No organic results found"}, ensure_ascii=False) # 如果没有'organic'键,返回错误信息
# 2、插入mysql数据库
from typing import Union
from typing import Optional, Literal
from pydantic import BaseModel, Field
class UserInfo(BaseModel):
"""Extracted user information, such as name, age, email, and phone number, if relevant."""
name: str = Field(description="The name of the user")
age: Optional[int] = Field(description="The age of the user")
email: str = Field(description="The email address of the user")
phone: Optional[Union[str, int]] = Field(description="The phone number of the user")
from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData
from sqlalchemy.orm import declarative_base, sessionmaker
passwrod = os.environ["MYSQL_PASSWORD"]
acc = os.environ["MYSQL_account"]
DATABASE_URI = f'mysql+pymysql://{acc}:{passwrod}@localhost/langgraph?charset=utf8mb4'
engine = create_engine(DATABASE_URI)
# 创建基类
Base = declarative_base()
# 自动创建数据表(不存在则新建,存在不会覆盖)
Base.metadata.create_all(engine)
# 创建数据库会话工厂
Session = sessionmaker(bind=engine)
session = Session()
# 定义 UserInfo 映射模型,对应数据库 users 表
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(50))
age = Column(Integer)
email = Column(String(100))
phone = Column(String(15))
@tool(args_schema = UserInfo)
def insert_db(name, age, email, phone):
"""Insert user information into the database, The required parameters are name, age, email, phone"""
session = Session() # 确保为每次操作创建新的会话
try:
# 创建用户实例
user = User(name=name, age=age, email=email, phone=phone)
# 添加到会话
session.add(user)
# 提交事务
session.commit()
return {"messages": ["数据已成功存储至Mysql数据库。"]}
except Exception as e:
session.rollback() # 出错时回滚
return {"messages": [f"数据存储失败,错误原因:{e}"]}
finally:
session.close() # 关闭会话
# 工具3:天气查询工具
class WeatherLoc(BaseModel):
location: str = Field(description="The location name of the city")
@tool(args_schema = WeatherLoc)
def get_weather(location):
"""Call to get the current weather."""
if location.lower() in ["beijing"]:
return "北京的温度是16度,天气晴朗。"
elif location.lower() in ["shanghai"]:
return "上海的温度是20度,部分多云。"
else:
return "不好意思,并未查询到具体的天气信息。"
# 测试连通性
print(f'''
name: {fetch_real_time_info.name}
description: {fetch_real_time_info.description}
arguments: {fetch_real_time_info.args}
''')
# 1、配置模型
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
load_dotenv()
key = os.environ["OPENAI_API_KEY"]
base_url = os.environ["OPENAI_BASE_URL"]
model_name = os.environ["OPENAI_MODEL_NAME"]
llm = ChatOpenAI(model=model_name, api_key=key, base_url=base_url, temperature=0, request_timeout=10)
def chat_with_model(state):
"""generate structured output"""
messages = state['messages']
response = llm.invoke(messages) # 这里可以不使用格式化输出
return {"messages": [response]}
# 2、执行工具节点。
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
import operator
from langchain_core.messages import AnyMessage, SystemMessage, HumanMessage, ToolMessage
# 定义图的状态模式。
class AgentState(TypedDict):
messages: Annotated[list[AnyMessage], operator.add]
def execute_function(state: AgentState):
tool_calls = state['messages'][-1].tool_calls
results = []
tools = [insert_db, fetch_real_time_info, get_weather]
tools = {t.name: t for t in tools}
for t in tool_calls:
if not t['name'] in tools:
result = "bad tool name, retry"
else:
result = tools[t['name']].invoke(t['args'])
results.append(ToolMessage(tool_call_id=t['id'], name=t['name'], content=str(result)))
return {'messages': results}
def final_answer(state):
"""generate natural language responses"""
messages = state['messages'][-1]
return {"messages": [messages]}
# 请你基于现在得到的信息,进行总结,生成专业的回复,注意,请用中文回复
SYSTEM_PROMPT = """
Please summarize the information obtained so far and generate a professional response. Note, please reply in Chinese.
"""
# 生成最终自然语言回复节点
def natural_response(state):
"""generate final language responses"""
messages = state['messages'][-1]
messages = [SystemMessage(content=SYSTEM_PROMPT)] + [HumanMessage(content=messages.content)]
response = llm.invoke(messages)
return {"messages": [response]}
# 路由判断:是否存在工具调用
def exists_function_calling(state: AgentState):
result = state['messages'][-1]
return len(result.tool_calls) > 0
# 图状态定义
class AgentState(TypedDict):
messages: Annotated[list[AnyMessage], operator.add]
from IPython.display import Image, display
from langgraph.graph import StateGraph
# 初始化图
graph = StateGraph(AgentState)
# 注册全部节点
graph.add_node("chat_with_model", chat_with_model)
graph.add_node("execute_function", execute_function)
graph.add_node("final_answer", final_answer)
graph.add_node("natural_response", natural_response)
# 入口节点
graph.set_entry_point("chat_with_model")
# 条件分支:模型输出是否带工具调用
graph.add_conditional_edges(
"chat_with_model",
exists_function_calling,
{True: "execute_function", False: "final_answer"}
)
# 固定边:工具执行/直接回答后统一汇总生成自然语言
graph.add_edge("execute_function", "natural_response")
graph.add_edge("final_answer", "natural_response")
# 终止节点
graph.set_finish_point("natural_response")
# 编译流程图
graph = graph.compile()
# 可视化流程图
# image_data = graph.get_graph().draw_mermaid_png()
# with open("man-made_Tool_Calling_Agent_G.png", "wb") as f:
# f.write(image_data)
# 一、绑定工具到 LLM
# 工具列表
tools = [insert_db, fetch_real_time_info, get_weather]
# 将工具绑定到大模型,模型输出会携带tool_calls
llm = llm.bind_tools(tools)
# 二、测试用例 1:纯闲聊,无需调用工具
query = "你好,请你介绍一下你自己"
input_message = {"messages": [HumanMessage(content=query)]}
result = graph.invoke(input_message)
# 打印最终AI自然语言回复
result=result["messages"][-1].content
print(f"{"测试用例 1:纯闲聊,无需调用工具:"}\n{'*'*80}\n{result}\n{'*'*80}\n\n")
# 测试用例 2:联网搜索工具(fetch_real_time_info)
query = "小米汽车"
input_message = {"messages": [HumanMessage(content=query)]}
result = graph.invoke(input_message)
result=result["messages"][-1].content
print(f"{"测试用例 2:联网搜索工具:"}\n{'*'*80}\n{result}\n{'*'*80}\n\n")
# 四、测试用例 3:用户信息入库工具(insert_db)
query = "我叫艾斯奥特曼,今年38岁,邮箱地址是aoteman#qq.com,电话是12111111111111"
input_message = {"messages": [HumanMessage(content=query)]}
result = graph.invoke(input_message)
result=result["messages"][-1].content
print(f"{"测试用例 3:用户信息入库工具:"}\n{'*'*80}\n{result}\n{'*'*80}\n\n")
以上就是两个基于 LangGraph 实现的 Tool Calling Agent 代理架构,从整体上看,无论是对于图结构,还是工具的接入,可扩展性都非常高,使用这个代理架构需要注意的是 Router Function 的分支判断,除此之外,对于工具调用而言,因为调用工具的参数是由大模型根据用户输入的自然语言生成的,所以一定会存在尝试调用不存在的工具,或者无法返回与请求的架构匹配的参数等边缘情况,会直接导致整个图的运行中断。这一部分进阶优化的内容,我们在下一小节再进行详细的介绍和实践。
四、自主循环代理(React Agent)
LangGraph 中 ReAct 的构建原理
LangGraph 内置预构建组件介绍
LangGraph 提供开箱即用的预制组件:
ToolNode:上一节讲解过,专门处理外部工具调用,底层封装图结构,接收 JSON Schema 格式参数,执行工具并返回结果。
ReAct 代理架构:官方封装好的循环智能代理,和手动实现 ReAct 的核心思路完全一致,但做了图结构适配
LangGraph ReAct 循环执行逻辑
本质是while 自治循环:
大模型执行推理,自主判断是否需要调用工具、选择工具、生成工具入参;
执行选中的工具,拿到工具返回观测结果;
将工具输出放回上下文,再次送入大模型;
模型判断无需再调用工具时,终止循环,输出最终自然语言答案
底层核心构成
LangGraph 内置 ReAct 不是黑盒,底层由三部分组合成一张可循环图:
LLM(绑定工具的 Tool Calling 大模型)
Tool Calling 工具执行节点(ToolNode)
Router 路由判断节点(区分「继续调用工具」/「结束输出」)
整体是自治循环代理图,开箱即用,无需手动写节点、分支、循环逻辑。
基于 LangGraph 内置 ReAct 组件,搭建天气查询智能助手。
I 简单实现代码
# 1、配置模型
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
load_dotenv()
key = os.environ["OPENAI_API_KEY"]
base_url = os.environ["OPENAI_BASE_URL"]
model_name = os.environ["OPENAI_MODEL_NAME"]
llm = ChatOpenAI(model=model_name, api_key=key, base_url=base_url, temperature=0, request_timeout=10)
from langchain_core.tools import tool
from typing import Union, Optional
from pydantic import BaseModel, Field
import requests
import json
from dotenv import load_dotenv
load_dotenv()
import os
weather_key = os.environ["OPEN_WEATHER_KEY"]
class WeatherLoc(BaseModel):
location: str = Field(description="The location name of the city")
# 工具3:天气查询工具
@tool(args_schema=WeatherLoc)
def get_weather(location):
"""
Function to query current weather.
:param loc: Required parameter, of type string, representing the specific city name for the weather query. \
Note that for cities in China, the corresponding English city name should be used. For example, to query the weather for Beijing, \
the loc parameter should be input as 'Beijing'.
:return: The result of the OpenWeather API query for current weather, with the specific URL request address being: https://api.openweathermap.org/data/2.5/weather
The return type is a JSON-formatted object after parsing, represented as a string, containing all important weather information.
"""
# Step 1. 构建请求
url = "https://api.openweathermap.org/data/2.5/weather"
# Step 2. 设置查询参数
params = {
"q": location,
"appid": weather_key, # 输入API key
"units": "metric", # 使用摄氏度而不是华氏度
"lang":"zh_cn" # 输出语言为简体中文
}
# Step 3. 发送GET请求
response = requests.get(url, params=params)
# Step 4. 解析响应
data = response.json()
return json.dumps(data)
result = get_weather.invoke({"location": "TOKYO"})
# print(f"{"测试调用天气工具:"}\n{'*'*80}\n{result}\n{'*'*80}\n\n")
tools = [get_weather]
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage
# 系统提示词:指导 Agent 的行为
# system_prompt = "你是一个有用的天气助手。请根据用户的问题,使用工具查询天气信息,并用中文给出简洁友好的回答。"
prompt = """Answer the following questions as best you can. You have access to the following tools:
{tools}
Use the following format:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question
Begin!
Question: {input}
Thought:{agent_scratchpad}
"""
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage
# 创建 Agent(新版 API)
agent = create_agent(llm, tools, system_prompt=prompt)
# ── 示例 1:单城市 ──
# print("=" * 70)
# for chunk in agent.stream(
# {"messages": [HumanMessage(content="大理今天的天气是多少度?")]},
# stream_mode="values"
# ):
# print(f"\n{"换行"}\n{"换行"}")
# chunk["messages"][-1].pretty_print() # 框架自带,一行搞定 ReAct 展示
result = agent.invoke({"messages": [HumanMessage(content="大理今天的天气是多少度?")]},)
# print(result)
# 只打印messages字段中的AIMessage字段里的回答内容
result["messages"][-1].pretty_print()
# ── 示例 2:多城市对比 ──
# print("\n" + "=" * 70)
# for chunk in agent.stream(
# {"messages": [HumanMessage(content="现在昆明和大理今天的天气差多少度?")]},
# stream_mode="values"
# ):
# print("\n换行\n换行")
# chunk["messages"][-1].pretty_print()
result = agent.invoke({"messages": [HumanMessage(content="现在昆明和大理今天的天气差多少度?")]},)
# print(result)
# 只打印messages字段中的AIMessage字段里的回答内容
result["messages"][-1].pretty_print()
2. 案例实操:构建复杂工具应用的 ReAct 自治代理
在这个案例中,我们将通过一个多工具场景需求来测试 LangGraph 中 ReAct 代理的构建方法和效果。我们设计了几个工具,以实现实时数据的查询和管理。
首先,用户可以通过一个工具根据城市名称实时获取当前天气信息。接着,如果用户希望将查询到的天气数据保存到本地数据库中,可以使用另一个工具完成数据的插入操作。
此外,我们还提供了一个工具,允许用户基于本地数据库中的天气数据进行提问数据进行提问。
通过这些工具的组合,我们能够快速验证如何在复杂的应用场景中有效地整合不同功能,并实际的感知 LangGraph 框架下 ReAct 代理模式带来的开发便捷性和可扩展性。
首先,我们接入实时天气数据查询的在线 API,代码定义如下:
OpenWeather API 的注册与使用,注册地址:https://openweathermap.org/
# 工具3:天气查询工具
from langchain_core.tools import tool
from typing import Union, Optional
from pydantic import BaseModel, Field
import requests
import json
from dotenv import load_dotenv
load_dotenv()
import os
weather_key = os.environ["OPEN_WEATHER_KEY"]
class WeatherLoc(BaseModel):
location: str = Field(description="The location name of the city")
class WeatherInfo(BaseModel):
"""Extracted weather information for a specific city."""
city_id: int = Field(default=0, description="可选,城市ID,不传则自动忽略")
city_name: str = Field(..., description="The name of the city")
main_weather: str = Field(..., description="The main weather condition")
description: str = Field(..., description="A detailed description of the weather")
temperature: float = Field(..., description="Current temperature in Celsius")
feels_like: float = Field(..., description="Feels-like temperature in Celsius")
temp_min: float = Field(..., description="Minimum temperature in Celsius")
temp_max: float = Field(..., description="Maximum temperature in Celsius")
# 工具1:天气查询工具
@tool(args_schema=WeatherLoc)
def get_weather(location):
"""
Function to query current weather.
:param loc: Required parameter, of type string, representing the specific city name for the weather query. \
Note that for cities in China, the corresponding English city name should be used. For example, to query the weather for Beijing, \
the loc parameter should be input as 'Beijing'.
:return: The result of the OpenWeather API query for current weather, with the specific URL request address being: https://api.openweathermap.org/data/2.5/weather
The return type is a JSON-formatted object after parsing, represented as a string, containing all important weather information.
"""
# Step 1. 构建请求
url = "https://api.openweathermap.org/data/2.5/weather"
# Step 2. 设置查询参数
params = {
"q": location,
"appid": weather_key, # 输入API key
"units": "metric", # 使用摄氏度而不是华氏度
"lang":"zh_cn" # 输出语言为简体中文
}
# Step 3. 发送GET请求
response = requests.get(url, params=params)
# Step 4. 解析响应
data = response.json()
return json.dumps(data)
# 测试一下 get_weather 函数的有效性,正常情况下可以得到输入城市名的实时天气信息,测试代码如下:
# 接下来,设计一个用于存储实时天气信息的表。这里我们定义一个新的模型 Weather,并包括上述所提取出来的字段。连接 Mysql 数据库及创建表的代码如下所示:
from sqlalchemy import create_engine, Column, Integer, String, Float
from sqlalchemy.orm import sessionmaker, declarative_base
# 创建基类
Base = declarative_base()
# 定义 WeatherInfo 模型
class Weather(Base):
__tablename__ = 'weather'
id = Column(Integer, primary_key=True, autoincrement=True) # 新增自增主键
city_id = Column(Integer, primary_key=True) # 城市ID
city_name = Column(String(50)) # 城市名称
main_weather = Column(String(50)) # 主要天气状况
description = Column(String(100)) # 描述
temperature = Column(Float) # 温度
feels_like = Column(Float) # 体感温度
temp_min = Column(Float) # 最低温度
temp_max = Column(Float) # 最高温度
"""
接下来,使用 LangChain 的 tool 装饰器将普通的函数注册为 LangGraph 中支持的工具服务,根据需求的设计,我们要依次创建三个外部函数,分别是:
get_weather 工具:用于根据城市名称实时查询该城市的当前天气数据。
insert_weather_to_db 工具:如果用户想要把查询到的天气数据插入到数据库的表中,则使用此函数完成数据库的插入操作。
query_weather_from_db 工具:如果用户想基于本地数据库的天气数据直接进行提问,则使用此函数完成数据库的查询操作。
如上节课实践的流程一样,我们依然使用 pydantic 来做工具的参数校验和结构化输出。三个工具函数的定义代码依次如下所示:
"""
from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData
from sqlalchemy.orm import declarative_base, sessionmaker
passwrod = os.environ["MYSQL_PASSWORD"]
acc = os.environ["MYSQL_account"]
DATABASE_URI = f'mysql+pymysql://{acc}:{passwrod}@localhost/langgraph?charset=utf8mb4'
engine = create_engine(DATABASE_URI)
# 自动创建数据表(不存在则新建,存在不会覆盖)
Base.metadata.create_all(engine)
# 创建数据库会话工厂
Session = sessionmaker(bind=engine)
@tool(args_schema=WeatherInfo)
def insert_weather_to_db(city_id, city_name, main_weather, description, temperature, feels_like, temp_min, temp_max):
"""Insert weather information into the database."""
session = Session() # 确保为每次操作创建新的会话
try:
# 创建天气实例
weather = Weather(
city_id=city_id,
city_name=city_name,
main_weather=main_weather,
description=description,
temperature=temperature,
feels_like=feels_like,
temp_min=temp_min,
temp_max=temp_max
)
# 使用 merge 方法来插入或更新(如果已有记录则更新)
session.merge(weather)
# 提交事务
session.commit()
return {"messages": [f"天气数据已成功存储至Mysql数据库。"]}
except Exception as e:
session.rollback() # 出错时回滚
return {"messages": [f"数据存储失败,错误原因:{e}"]}
finally:
session.close() # 关闭会话
class QueryWeatherSchema(BaseModel):
"""Schema for querying weather information by city name."""
city_name: str = Field(..., description="The name of the city to query weather information")
@tool(args_schema=QueryWeatherSchema)
def query_weather_from_db(city_name: str):
"""Query weather information from the database by city name."""
session = Session()
try:
# 查询天气数据
weather_data = session.query(Weather).filter(Weather.city_name == city_name).first()
if weather_data:
return {
"city_id": weather_data.city_id,
"city_name": weather_data.city_name,
"main_weather": weather_data.main_weather,
"description": weather_data.description,
"temperature": weather_data.temperature,
"feels_like": weather_data.feels_like,
"temp_min": weather_data.temp_min,
"temp_max": weather_data.temp_max
}
else:
return {"messages": [f"未找到城市 '{city_name}' 的天气信息。"]}
except Exception as e:
return {"messages": [f"查询失败,错误原因:{e}"]}
finally:
session.close() # 关闭会话
class SearchQuery(BaseModel):
query: str = Field(description="Questions for networking queries")
from langchain_core.tools import tool
import requests
import json
from dotenv import load_dotenv
load_dotenv()
import os
weather_key = os.environ["OPEN_WEATHER_KEY"]
GOOGLE_SEARCH_KEY = os.environ["GOOGLE_SEARCH_KEY"]
@tool(args_schema = SearchQuery)
def fetch_real_time_info(query):
"""Get real-time Internet information"""
url = "https://google.serper.dev/search"
payload = json.dumps({
"q": query,
"num": 1,
})
headers = {
'X-API-KEY': GOOGLE_SEARCH_KEY,
'Content-Type': 'application/json'
}
response = requests.post(url, headers=headers, data=payload)
data = json.loads(response.text) # 将返回的JSON字符串转换为字典
if 'organic' in data:
return json.dumps(data['organic'], ensure_ascii=False) # 返回'organic'部分的JSON字符串
else:
return json.dumps({"error": "No organic results found"}, ensure_ascii=False) # 如果没有'organic'键,返回错误信息
tools = [fetch_real_time_info, get_weather, insert_weather_to_db, query_weather_from_db]
object = [i.name for i in tools]
print(f"{"工具列表:"}\n{'*'*80}\n{object}\n{'*'*80}\n\n")
print(f"\n{'-'*80}\n{"以上将工具定义完毕"}\n{'-'*80}\n\n")
# 1、配置模型
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
load_dotenv()
key = os.environ["OPENAI_API_KEY"]
base_url = os.environ["OPENAI_BASE_URL"]
model_name = os.environ["OPENAI_MODEL_NAME"]
llm = ChatOpenAI(model=model_name, api_key=key, base_url=base_url, temperature=0, request_timeout=120)
"""
当有了工具列表和模型后,就可以通过 create_react_agent 这个 LangGraph 框架中预构建的方法来创建自治循环代理(ReAct)的工作流,其必要的参数如下:
model:支持工具调用的 LangChain 聊天模型。
tools:工具列表、ToolExecutor 或 ToolNode 实例。
state_schema:图的状态模式。必须有 messages 和 is_last_step 键。默认为定义这两个键的 Agent State。
上述三点我们均在前面的课程中详细且作为重点介绍过,大家应该是比较容易理解的。所以,创建 ReAct 代理的代码就如下所示:
"""
from langgraph.prebuilt import create_react_agent
graph = create_react_agent(llm, tools=tools)
image_data = graph.get_graph().draw_mermaid_png()
with open("React_Agent_G.png", "wb") as f:
f.write(image_data)
# 返回的是编译好的 LangGraph 可运行程序,可直接用于聊天交互。调用方式则和之前使用的方法一样,我们可以依次针对不同复杂程度的需求依次进行提问。首先是测试是否可以不使用工具,直接调用大模型生成响应。
# query="你好,请你介绍一下你自己"
# input_message = {"messages": [HumanMessage(content=query)]}
# 可以自动处理成 HumanMessage 的消息格式
finan_response = graph.invoke({"messages": ["你好,请你介绍一下你自己"]})
print(f"{"进行一般对话响应:"}\n{'*'*80}\n{finan_response}\n{'*'*80}\n\n")
# finan_response = finan_response["messages"][-1].content
# print(f"{"进行一般对话响应:"}\n{'*'*80}\n{finan_response}\n{'*'*80}\n\n")
# 加大输入问题的复杂度,接下来我们提问的问题希望它能够自动找到正确的工具函数,基于工具的执行结果作为既定的事实,引导生成最终的回复。
# finan_response = graph.invoke({"messages": ["吉林今天的天气怎么样?把查询的结果存到数据库"]})
# print(f"{"查询天气:"}\n{'*'*80}\n{finan_response}\n{'*'*80}\n\n")
# print(f"{"查询天气调用了哪些工具1:"}\n{'*'*80}\n{finan_response['messages'][1].tool_calls}\n{'*'*80}\n\n")
# # 遍历整个消息列表,提取所有包含 tool_calls 的消息
# all_tool_calls = [
# msg.tool_calls
# for msg in finan_response['messages']
# if hasattr(msg, 'tool_calls') and msg.tool_calls
# ]
# print(f"{"查询天气调用了哪些工具2:"}\n{'*'*80}\n{all_tool_calls}\n{'*'*80}\n\n")
# finan_response = graph.invoke({"messages": ["去数据库查询一下吉林今天的天气怎么样?"]}) # 模拟用户输入:
# all_tool_calls = [
# msg.tool_calls
# for msg in finan_response['messages']
# if hasattr(msg, 'tool_calls') and msg.tool_calls
# ]
# print(f"{"查询天气:"}\n{'*'*80}\n{finan_response}\n{'*'*80}\n\n")
# print(f"{"从数据库中查询天气调用了哪些工具:"}\n{'*'*80}\n{all_tool_calls}\n{'*'*80}\n\n")
# finan_response = graph.invoke({"messages": ["你知道关于小米的销售情况吗?请用中文回复我"]})
# all_tool_calls = [
# msg.tool_calls
# for msg in finan_response['messages']
# if hasattr(msg, 'tool_calls') and msg.tool_calls
# ]
# print(f"{"查询天气:"}\n{'*'*80}\n{finan_response}\n{'*'*80}\n\n")
# print(f"{"从数据库中查询天气调用了哪些工具:"}\n{'*'*80}\n{all_tool_calls}\n{'*'*80}\n\n")
# 继续加大问题的难度,我们要在一个问题中涉及多个工具的使用,比如:
# finan_response = graph.invoke({"messages": ["帮我查一下北京、上海、哈尔滨三个城市的天气,告诉我哪个城市最适合出游。同时,把查询到的数据存储到数据库中"]})
# all_tool_calls = [
# msg.tool_calls
# for msg in finan_response['messages']
# if hasattr(msg, 'tool_calls') and msg.tool_calls
# ]
# print(f"{"查询天气:"}\n{'*'*80}\n{finan_response}\n{'*'*80}\n\n")
# print(f"{"从数据库中查询天气调用了哪些工具:"}\n{'*'*80}\n{all_tool_calls}\n{'*'*80}\n\n")
# 同时,可以在数据库中查看数据的插入情况:
finan_response = graph.invoke({"messages": ["帮我分析一下数据库中北京和哈尔滨城市天气的信息,做一个详细的对比,并生成出行建议"]})
all_tool_calls = [
msg.tool_calls
for msg in finan_response['messages']
if hasattr(msg, 'tool_calls') and msg.tool_calls
]
print(f"{"查询天气:"}\n{'*'*80}\n{finan_response}\n{'*'*80}\n\n")
print(f"{"从数据库中查询天气调用了哪些工具:"}\n{'*'*80}\n{all_tool_calls}\n{'*'*80}\n\n")