第五章: LangGraph的路由代理-Router Agent介绍

LangGraph 的路由代理(Router Agent)学习笔记


1. LangGraph 四大类代理介绍

掌握了 LangGraph 的 State 状态定义模式后,就可以进入代理架构的学习阶段。我们希望 AI Agent 带来的是"只输入一个需求,不用关心处理细节"的智能化体验;但随着应用越来越复杂,系统需要被赋予更多控制权才能解决更复杂的问题。

按照大模型在流程中被赋予的控制权大小,LangGraph 将代理架构划分为四大类(控制权由弱到强):

代理架构 英文名 大模型的控制权 一句话理解
路由代理 Router Agent 只做一个分支选择 像"前台分诊员",判断你的问题该由哪个部门处理
工具调用代理 Tool Calling Agent 在一个分支里自主挑选工具 像"会查资料的助理",需要什么工具自己拿
自主循环代理 Autonomous Agent(ReAct) 循环地"思考→行动→观察"直到完成 像"自主办事的员工",不断试错直到把事办完
多代理 Multi-Agent 多个 Agent 分工协作 像"项目组",多个成员各司其职互相配合

控制权从弱到强的具体表现:

  1. 最基本:在两个潜在路径之间做出选择(路由);
  2. 进一步:在每条路径上存在多个可用工具时,自主决定调用哪一个(工具调用);
  3. 更复杂:评估生成的答案是否满足需求,如有必要就继续执行,直到得到满意的答案(自主循环);
  4. 最高级:由多个 Agent 分工协作完成复杂任务(多代理)。

本文的重点是第一大类------路由代理(Router Agent)。


2. LangGraph 的路由代理(Router Agent)

2.1 什么是 Router?为什么需要它?

在 LangGraph 中,可以用**条件边(Conditional Edges)**来指导或约束大模型处理任务的逻辑流程:当流程到达某个环节时,根据条件输出或数据的不同,选择性地执行不同的逻辑路径。

用一个生活化的比喻来理解:

  • add_edge(A, B) 就像一条单行道------从 A 出发只能去 B,没有选择余地;
  • add_conditional_edges 就像一个路口的路牌------到达路口后,根据当前情况决定走哪条路。

这个"做决定的函数"就叫做 Router(路由函数) 。它本质上类似一个 if-else 结构组件,允许大模型从一组预设选项中选择合适的下一步去执行。

典型使用场景:Agent 可以接收各种形式的输入,通过预设的路由逻辑决定执行什么操作。例如:

  • 输入是 "name: muyu, age: 18, phone: 123" 这类用户信息 → 路由到数据库写入节点;
  • 输入是 "Hello" 这类普通问候 → 直接路由到回复节点。

核心要点 :构建实际 Agent 时,Router Function 的定义是最关键的部分 。由于大模型输出的是自然语言,要让程序代码可靠地"读懂"大模型的决定,就需要借助结构化输出------让大模型按照约定格式响应,程序再据此选择分支。

2.2 实现步骤一:普通边的图结构(回顾)

在引入路由前,先回顾固定连线的图:节点 A → 节点 B,用 add_edge 直接串联。

python 复制代码
from langgraph.graph import START, StateGraph, END

# 定义两个节点函数:node_a 对状态加 1,node_b 对状态减 2
def node_a(state):
    return {"x": state["x"] + 1}

def node_b(state):
    return {"x": state["x"] - 2}

# 以 dict 作为状态类型创建图构建器
builder = StateGraph(dict)

# 注册节点,第一个参数是节点名称,第二个参数是节点函数
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)

# 构建节点之间的固定边:START -> node_a -> node_b -> END
builder.add_edge(START, "node_a")
builder.add_edge("node_a", "node_b")
builder.add_edge("node_b", END)

# 编译成可执行的图
graph = builder.compile()

这条链路是"写死"的:node_a 执行完必定执行 node_b,没有任何灵活性。

2.3 实现步骤二:用 add_conditional_edges 添加路由

想要从节点 A 选择性地 路由到 1 个或多个节点,需要使用 add_conditional_edges 方法,其核心参数如下:

python 复制代码
def add_conditional_edges(
    self,
    source: str,       # 起始节点(路由的出发地)
    path: ...          # 路由函数:接收当前 state,返回值决定接下来执行的节点
    path_map: ...      # 可选:把路由函数的返回值映射为节点名的字典
    then: ...          # 可选:路由选中的节点执行完之后,固定再执行的节点
)

方式一:路由函数直接返回节点名称。

python 复制代码
from langgraph.graph import START, StateGraph, END

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}

# 路由函数:接收当前 state,返回"下一个要执行的节点名称"
def routing_function(state):
    if state["x"] == 10:
        return "node_b"   # x 等于 10 时走 node_b 分支
    else:
        return "node_c"   # 否则走 node_c 分支

builder = StateGraph(dict)

builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_node("node_c", node_c)

# 设置入口节点为 node_a
builder.set_entry_point("node_a")

# 条件边:node_a 执行完后,由 routing_function 决定去 node_b 还是 node_c
builder.add_conditional_edges("node_a", routing_function)

graph = builder.compile()

默认情况下,routing_function 的返回值会被直接当作下一个节点的名称。

方式二:路由函数返回布尔值/枚举值,用 path_map 映射成节点名。

python 复制代码
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, END

# 用 TypedDict 定义状态:只包含一个整数字段 x
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}

# 路由函数:返回 True / False,而不是节点名
def routing_function(state):
    if state["x"] == 10:
        return True
    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")

# 第三个参数是 path_map:把路由函数的返回值映射为节点名
#   True  -> 去 node_b
#   False -> 去 node_c
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()

注意一个容易踩的坑:如果直接用裸 dict 作为 State 并在多分支中并发更新同一个键,会触发 InvalidUpdateError: Can receive only one value per step。建议像上面这样用 TypedDict 定义明确的状态结构,这是 LangGraph 推荐的做法。

用流程图表示上面的路由结构:

text 复制代码
                          ┌──────────────────────┐
                          │  routing_function    │
                          │  判断 state["x"]     │
                          └──────┬────────┬──────┘
                                 │        │
                     True(x==10) │        │ False(其他情况)
                                 ▼        ▼
   [START] ──► [node_a] ──► [node_b]  [node_c]
                                 │        │
                                 ▼        ▼
                              [END]     [END]

2.4 小结

  • 路由代理的本质 = 条件边 + 路由函数 ,就是一个图上的 if-else;
  • 路由函数决定"去哪",path_map 负责把返回值翻译成节点名;
  • 路由代理的优势是可以精准控制程序链路中的每一个细节 ;局限是控制级别相对有限------大模型通常只能控制单个决策,分支一多就需要手动堆叠判断,可扩展性差。这个局限会在后续的工具调用代理(Tool Calling Agent)中被解决。

3. 如何稳定地实现结构化输出?

3.1 为什么结构化输出对 Router 至关重要?

消息的传递过程中,大模型往往通过结构化输出来约束自己的响应模式(类似工具调用的过程)。Router 正是利用这个特性来控制接下来的分支路径:

  • 如果大模型只是输出一段自然语言,程序代码很难稳定地判断"该走哪个分支";
  • 如果大模型按照约定格式输出(如 JSON),程序就可以可靠地解释大模型的决定并采取行动。

在 LangGraph 中,实现结构化输出有三种有效方式:

  1. 提示工程:在提示词中指示大模型以特定格式做出回应;
  2. 输出解析器:用后处理的方法从大模型的响应中提取结构化数据;
  3. 工具调用:利用大模型内置的工具调用(Function Calling)功能生成结构化输出。

三种方式的稳定性依次增强,推荐程度依次提高。下面依次展开。

3.2 准备工作:初始化大模型(deepseek-chat)

本文所有示例统一使用 deepseek-chat 模型,通过 OpenAI 兼容接口接入:

python 复制代码
import os
from langchain_openai import ChatOpenAI

# 从环境变量中读取 DeepSeek 的 API Key,避免把密钥硬编码在代码里
api_key = os.getenv("DEEPSEEK_API_KEY")
# DeepSeek 提供的 OpenAI 兼容接口地址
base_url = "https://api.deepseek.com/v1"

# 初始化大模型:模型名指定为 deepseek-chat,temperature=0 让输出更稳定
llm = ChatOpenAI(
    model="deepseek-chat",
    api_key=api_key,
    base_url=base_url,
    temperature=0,
)

4. Router Agent - 结构化输出 - 提示词工程

4.1 方式一:纯提示工程

最直接的做法:在 system 提示词中要求大模型"用 JSON 包裹输出"。

python 复制代码
from langchain_core.prompts import ChatPromptTemplate

# 创建聊天提示词模板:
#   system 消息告诉大模型要用 json 格式输出
#   human 消息中的 {query} 是用户输入的占位符
prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "Answer the user query. Wrap the output in `json`"),
        ("human", "{query}"),
    ]
)

# 用 LangChain 的 LCEL 管道语法,把提示词和大模型串成一条链
chain = prompt | llm

# 调用链:输入一段自然语言,让大模型提取信息并按 json 格式返回
ans = chain.invoke({"query": "我叫奥特曼,今年38岁,邮箱地址是aoteman#qq.com,电话是1211111111"})

# 查看大模型的原始输出
print(ans.content)

输出:

json 复制代码
{
  "name": "奥特曼",
  "age": 38,
  "email": "aoteman@qq.com",
  "phone": "1211111111"
}

可以看到输出确实"长得像 JSON",但有明显问题:

  • 大模型的完整返回 其实被「三个反引号 + json」的代码块标记包裹着(ans.content 里既有 JSON 本体,也有这层标记),所以它不是可以直接 json.loads() 的纯 JSON 字符串;
  • 完全依赖大模型"自觉"遵守格式,在复杂 Agent 流程中非常不稳定,随时可能多一句少一句导致解析失败。

4.2 方式二:提示工程 + 输出解析器

进阶的优化方法:引入后处理步骤,通过输出解析器从大模型响应中提取结构化数据,提高输出的准确性和一致性。

python 复制代码
from langchain_core.messages import AIMessage
import json
import re
from typing import List

# 自定义输出解析函数:从大模型回复中提取「json 代码块」里的内容并解析成 dict
def extract_json(message: AIMessage) -> List[dict]:
    """
    从字符串中提取被「三个反引号 + json」与「三个反引号」包裹的 JSON 内容。

    参数:
        message (AIMessage): 大模型返回的消息对象

    返回:
        list: 解析出来的 JSON 对象列表
    """
    text = message.content

    # 定义正则表达式模式,匹配「三个反引号 + json」与「三个反引号」之间的所有内容
    # re.DOTALL 让 . 可以匹配换行符,从而支持多行 JSON
    pattern = r"```json(.*?)```"

    # 在字符串中查找所有非重叠匹配
    matches = re.findall(pattern, text, re.DOTALL)

    # 把每个匹配到的 JSON 字符串去掉首尾空格后解析成 Python 对象
    try:
        return [json.loads(match.strip()) for match in matches]
    except Exception:
        # 解析失败时抛出带原始消息的异常,方便排查
        raise ValueError(f"Failed to parse: {message}")

# 提示词保持不变:仍然要求大模型用 json 输出
prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "Answer the user query. Wrap the output in `json`"),
        ("human", "{query}"),
    ]
)

# 在链的末尾追加自定义解析器:提示词 -> 大模型 -> 提取 JSON
chain = prompt | llm | extract_json

# 调用链,得到的是干净的 Python 字典列表
ans = chain.invoke({"query": "我叫奥特曼,今年38岁,邮箱地址是aoteman#qq.com,电话是1211111111"})

print(ans)
# 输出: [{'name': '奥特曼', 'age': 38, 'email': 'aoteman@qq.com', 'phone': '1211111111'}]

对比可见,通过定制化的输出解析器得到的结果更加符合预期------已经是可直接使用的 Python 字典了。

4.3 小结与局限

方式 稳定性 说明
纯提示工程 低 依赖大模型自觉遵守格式,容易输出多余内容
提示工程 + 解析器 中 后处理兜底,但正则和格式约定仍可能失效

这两种方式都靠"事后补救",还不够优雅。LangGraph 中更常用、效果更好 的是直接使用内置的方法 .with_structured_output(),见下一节。


5. Router Agent - 结构化输出 - 基于内置工具

5.1 .with_structured_output() 是什么?

.with_structured_output() 接受一个定义了所需输出属性的名称、类型和描述的模式 作为输入,返回一个类似模型的 Runnable。它不再输出字符串或消息,而是直接输出一个与模式匹配的对象。

支持三种模式指定方式,返回值类型不同:

模式类型 说明 返回值
Pydantic 类 功能最强,带运行时校验 Pydantic 对象
TypedDict 类 轻量,配合 Annotated 语法 字典
JSON Schema 无需导入类,直接写字典 字典

应用场景:从非结构化文本中提取格式化数据。

5.2 方式一:Pydantic 结构化输出(推荐)

使用 Pydantic 限定输出格式,可以确保所有数据都符合指定的结构和数据类型,减少数据处理错误并增加代码健壮性。Pydantic 的验证系统会自动确保字段符合预定义格式,输入数据不符合预期时会抛出错误。

python 复制代码
from typing import Optional
from pydantic import BaseModel, Field

# 定义 Pydantic 模型:描述要提取的用户信息的结构
class UserInfo(BaseModel):
    """Extracted user information, such as name, age, email, and phone number, if relevant."""
    # Field 的 description 参数会作为字段说明传给大模型,帮助它理解要提取什么
    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")  # 可选:电话

在这个 UserInfo 模型中:

  • name(必需):存储用户的名字;
  • age(可选):存储用户的年龄;
  • email(必需):存储用户的电子邮件地址;
  • phone(可选):存储用户的电话号码。

接下来把模型交给 .with_structured_output():

python 复制代码
# 传入 Pydantic 类 UserInfo,得到一个"会输出 UserInfo 对象"的新 Runnable
structured_llm = llm.with_structured_output(UserInfo)

# 从非结构化文本中提取用户信息,直接得到 UserInfo 实例
extracted_user_info = structured_llm.invoke(
    "我叫奥特曼,今年38岁,邮箱地址是aoteman#qq.com,电话是1211111111"
)

print(extracted_user_info)
# UserInfo(name='奥特曼', age=38, email='aoteman#qq.com', phone='1211111111')

它返回的是一个 UserInfo 的 Pydantic 对象,每个字段都填充了从原始文本中提取出来的信息。底层原理:LangChain 会把 Pydantic 模型转换成一个"虚拟工具"传给大模型(走 Function Calling),大模型按工具的参数模式返回数据,再由 PydanticToolsParser 解析并校验成对象。

有了结构化对象,路由分支的判断就有了可靠依据,例如:

python 复制代码
# isinstance 函数用于判断一个对象是否是某个已知类型(或其子类)的实例
if isinstance(extracted_user_info, UserInfo):
    print("执行节点A的逻辑")   # 是 UserInfo 对象 -> 走"入库"分支
else:
    print("执行节点B的逻辑")   # 不是 -> 走"普通对话"分支
python 复制代码
# 如果输入只是"你好"这类普通问候,大模型无法提取出用户信息,
# 我们可以约定返回字符串,此时 isinstance 判断为 False
extracted_user_info = "你好"

if isinstance(extracted_user_info, UserInfo):
    print("执行节点A的逻辑")
else:
    print("执行节点B的逻辑")
# 输出: 执行节点B的逻辑

这就是结构化输出对路由函数逻辑判断的意义所在。

5.3 方式二:TypedDict 结构化输出

如果不想使用 Pydantic 做运行时校验,可以用 TypedDict 定义模式,并使用 Annotated 语法为字段添加默认值和描述:

python 复制代码
from typing import Optional
from typing_extensions import Annotated, TypedDict

# 定义 TypedDict 模型
class UserInfo(TypedDict):
    """Extracted user information from text"""
    # Annotated 语法:Annotated[类型, 默认值, 字段描述]
    #   ... 表示必填,None 表示可选
    name: Annotated[str, ..., "The user's name"]
    age: Annotated[Optional[int], None, "The user's age"]
    email: Annotated[str, ..., "The user's email address"]
    phone: Annotated[Optional[str], None, "The user's phone number"]

structured_llm = llm.with_structured_output(UserInfo)

# 从非结构化文本中提取用户信息
extracted_user_info = structured_llm.invoke(
    "我叫奥特曼,今年38岁,邮箱地址是aoteman#qq.com,电话是1211111111"
)

print(extracted_user_info)
# {'age': 38, 'email': 'aoteman#qq.com', 'name': '奥特曼', 'phone': '1211111111'}

使用 TypedDict 创建的"对象"实际上是一个字典,没有 Pydantic 模型那样的方法和属性,功能相对简单。它主要用于静态类型检查,运行时不会做类型校验,但搭配 LangGraph 已实现的基本验证机制,也是一种不错的轻量选择。

5.4 方式三:JSON Schema 结构化输出

JSON Schema 是大家最熟悉的格式:不需要导入任何类,直接用字典就能准确描述每个参数,代价是代码更冗长:

python 复制代码
# 定义 JSON Schema:描述用户信息的结构
json_schema = {
    "title": "user_info",
    "description": "Extracted user information",
    "type": "object",                      # 这是一个对象类型
    "properties": {                        # 对象包含哪些属性
        "name": {
            "type": "string",              # 字符串类型
            "description": "The user's name",
        },
        "age": {
            "type": "integer",             # 整数类型
            "description": "The user's age",
            "default": None,               # 可选字段
        },
        "email": {
            "type": "string",
            "description": "The user's email address",
        },
        "phone": {
            "type": "string",
            "description": "The user's phone number",
            "default": None,
        },
    },
    "required": ["name", "email"],         # 必填字段:name 和 email
}

structured_llm = llm.with_structured_output(json_schema)

# 从非结构化文本中提取用户信息,返回字典
extracted_user_info = structured_llm.invoke(
    "我叫奥特曼,今年38岁,邮箱地址是aoteman#qq.com,电话是1211111111"
)

print(extracted_user_info)
# {'age': 38, 'email': 'aoteman#qq.com', 'name': '奥特曼', 'phone': '1211111111'}

5.5 三种方式对比

方式 返回类型 运行时校验 适用场景
Pydantic Pydantic 对象 有(不合规抛错) 生产环境、需要严格校验的 Router
TypedDict 字典 无 轻量场景、与 LangGraph State 风格一致
JSON Schema 字典 无 动态构建模式、跨语言场景

6. Router Agent - 基于结构化输出构建路由图

6.1 核心思路:Union 类型封装多种输出

三种结构化输出方法中,Pydantic 是处理路由决策最常用的方式 。在这种策略下,可以通过定义一个包含 Union 类型属性的父模型,灵活地从多种模式中选择适当的路由分支。

设想这个场景:用户输入可能是

  • 一段个人信息("我叫奥特曼,今年38岁......")→ 应该走数据库写入分支;
  • 一句普通问候("你好")→ 应该走普通对话回复分支。

我们创建一个统一的模型 FinalResponse 来封装可能的输出类型:

python 复制代码
from typing import Union, Optional
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")


# 模型二:定义"普通对话回复"场景下的模型
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")


# 父模型:final_output 字段可以是 UserInfo,也可以是 ConversationalResponse
class FinalResponse(BaseModel):
    final_output: Union[UserInfo, ConversationalResponse]

这个设计的关键在于:FinalResponse.final_output 用 Union 类型支持灵活的输出选项------系统可以根据不同的业务逻辑和用户输入,决定输出的具体形式。例如,用户请求提交个人数据时返回 UserInfo,普通查询时提供 ConversationalResponse。

先单独验证一下这个父模型的路由能力:

python 复制代码
# 让大模型按 FinalResponse 的结构输出
structured_llm = llm.with_structured_output(FinalResponse)

# 测试一:输入普通问候 -> 大模型自动选择 ConversationalResponse
result = structured_llm.invoke("你好")
print(result)
# FinalResponse(final_output=ConversationalResponse(response='你好!有什么我可以帮助您的吗?'))

# 可以直接取出对话回复内容
print(result.final_output.response)
# '你好!有什么我可以帮助您的吗?'

# 测试二:输入个人信息 -> 大模型自动选择 UserInfo
result = structured_llm.invoke("我叫奥特曼,今年38岁,邮箱地址是aoteman#qq.com,电话是1211111111")
print(result)
# FinalResponse(final_output=UserInfo(name='奥特曼', age=38, email='aoteman#qq.com', phone='1211111111'))

# 可以直接取出用户信息对象
print(result.final_output)
# UserInfo(name='奥特曼', age=38, email='aoteman#qq.com', phone='1211111111')

大模型会根据输入内容,自动在 Union 中的多个类型之间做选择------这正是路由的雏形。

6.2 完整案例目标

利用 Pydantic 结构化输出 + 条件边,构建一个完整路由图:

text 复制代码
                ┌──────────────────────────────┐
                │      chat_with_model         │
                │  结构化输出 FinalResponse    │
                └───────┬──────────────┬───────┘
                        │              │
        True:是UserInfo │              │ False:是ConversationalResponse
        (用户提供了信息)│              │ (普通对话)
                        ▼              ▼
                 ┌────────────┐  ┌──────────────┐
 [START] ──────► │ insert_db  │  │ final_answer │
                 │ 写入 MySQL │  │ 生成对话回复 │
                 └─────┬──────┘  └──────┬───────┘
                       │                │
                       ▼                ▼
                     [END]            [END]

即:图能把非结构化文本转换为结构化数据,并根据用户的不同输入智能判断是应当生成常规响应,还是执行数据库操作。

6.3 实现步骤

步骤一:定义 Pydantic 模型与大模型实例
python 复制代码
import os
from typing import Union, Optional
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI

# 定义数据库插入的用户信息模型
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")


# 定义正常生成模型回复的模型
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")


# 定义最终响应模型:可以是用户信息,也可以是一般对话响应
class FinalResponse(BaseModel):
    final_output: Union[UserInfo, ConversationalResponse]


# 生成模型实例:使用 deepseek-chat
llm = ChatOpenAI(
    model="deepseek-chat",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com/v1",
    temperature=0,
)
步骤二:准备 MySQL 数据库环境

由于设计中用户可能触发数据库操作,需要先实现数据库连接,并基于 UserInfo 模型构建对应的表结构。首先安装依赖:

bash 复制代码
pip install sqlalchemy pymysql

然后初始化数据库(假设 MySQL 已启动,并已创建名为 langgraph 的数据库):

python 复制代码
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import declarative_base, sessionmaker

# 创建 SQLAlchemy 基类
Base = declarative_base()

# 定义与 UserInfo 对应的数据库表模型
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:gpt      -> 用户名:密码
#   localhost    -> MySQL 服务器地址
#   langgraph    -> 要连接的数据库名
#   charset=utf8mb4 -> 字符集,支持更广泛的 Unicode 字符
DATABASE_URI = 'mysql+pymysql://gpt:gpt@localhost/langgraph?charset=utf8mb4'
engine = create_engine(DATABASE_URI, echo=True)

# 如果表不存在则创建表
Base.metadata.create_all(engine)

# 创建会话工厂
Session = sessionmaker(bind=engine)

连接成功后,langgraph 数据库中会自动创建出 users 表(字段:id、name、age、email、phone)。

步骤三:定义节点函数

节点一:chat_with_model------路由前置节点,把用户输入的文本转化为结构化输出,供 Router Function 判断分支:

python 复制代码
def chat_with_model(state):
    """generate structured output"""
    # 取出状态中的消息列表
    messages = state['messages']
    # 让大模型按 FinalResponse 的结构输出(内含 Union 的两种类型)
    structured_llm = llm.with_structured_output(FinalResponse)
    response = structured_llm.invoke(messages)
    # 把结构化结果追加进消息列表,返回状态更新
    return {"messages": [response]}

节点二:final_answer------普通对话分支,直接生成自然语言响应:

python 复制代码
def final_answer(state):
    """generate natural language responses"""
    # 取最后一条消息(是 chat_with_model 产生的 FinalResponse 对象)
    messages = state['messages'][-1]
    # 从 ConversationalResponse 中取出对话文本
    response = messages.final_output.response
    return {"messages": [response]}

节点三:insert_db------数据库分支,把提取出的用户信息写入 MySQL:

python 复制代码
def insert_db(state):
    """Insert user information into the database"""
    session = Session()  # 确保为每次操作创建新的会话
    try:
        # 取最后一条消息中的 UserInfo 对象
        result = state['messages'][-1]
        output = result.final_output
        # 用提取出的字段创建数据库 User 实例
        user = User(name=output.name, age=output.age, email=output.email, phone=output.phone)
        # 添加到会话并提交事务
        session.add(user)
        session.commit()
        return {"messages": ["数据已成功存储至Mysql数据库。"]}
    except Exception as e:
        # 出错时回滚事务,并返回失败信息
        session.rollback()
        return {"messages": [f"数据存储失败,错误原因:{e}"]}
    finally:
        # 无论成功失败,最终都关闭会话
        session.close()
步骤四:定义图状态与 Router Function

先定义图的状态模式------一个带 operator.add reducer 的消息列表(每轮更新会把新消息追加到列表末尾而不是覆盖):

python 复制代码
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
import operator
from langchain_core.messages import AnyMessage, HumanMessage

# 图的状态定义:messages 键使用 operator.add,表示新消息累加进列表
class AgentState(TypedDict):
    messages: Annotated[list[AnyMessage], operator.add]

再定义 Router Function------根据 chat_with_model 产生的不同 Pydantic 对象选择分支:

python 复制代码
def generate_branch(state: AgentState):
    # 取最后一条消息(FinalResponse 对象)
    result = state['messages'][-1]
    output = result.final_output

    # 判断 final_output 的具体类型来决定走向:
    if isinstance(output, UserInfo):
        return True    # 是用户信息 -> 走数据库插入分支
    elif isinstance(output, ConversationalResponse):
        return False   # 是对话回复 -> 走普通回答分支
步骤五:构建图并编译
python 复制代码
graph = StateGraph(AgentState)

# 添加三个节点
graph.add_node("chat_with_model", chat_with_model)
graph.add_node("final_answer", final_answer)
graph.add_node("insert_db", insert_db)

# 设置图的启动节点
graph.set_entry_point("chat_with_model")

# 设置条件边(这里就是 Router):
#   generate_branch 返回 True  -> 执行 insert_db
#   generate_branch 返回 False -> 执行 final_answer
graph.add_conditional_edges(
    "chat_with_model",
    generate_branch,
    {True: "insert_db", False: "final_answer"}
)

# 设置终止节点:两个分支执行完都结束
graph.set_finish_point("final_answer")
graph.set_finish_point("insert_db")

# 编译图
graph = graph.compile()
步骤六:运行测试

测试一:输入个人信息,应走 insert_db 分支

python 复制代码
query = "我叫奥特曼,今年38岁,邮箱地址是aoteman#qq.com,电话是1211111111"
input_message = {"messages": [HumanMessage(content=query)]}

result = graph.invoke(input_message)

print(result["messages"][-1])
# '数据已成功存储至Mysql数据库。'

此时状态中 messages 列表依次包含:用户提问的 HumanMessage → FinalResponse(final_output=UserInfo(...)) → 数据库插入结果文本,SQL 日志中可以看到 INSERT INTO users ... 已成功执行。

测试二:输入普通对话,应走 final_answer 分支

python 复制代码
query = "你好,请你介绍一下你自己"
input_message = {"messages": [HumanMessage(content=query)]}

result = graph.invoke(input_message)

print(result["messages"][-1])
# '你好!我是一个智能助手,设计用来帮助回答问题、提供信息......'

普通问答会经过 final_answer 直接生成响应,全程不触碰数据库。

python 复制代码
query = "请问什么是机器学习"
input_message = {"messages": [HumanMessage(content=query)]}

result = graph.invoke(input_message)

print(result["messages"][-1])
# '机器学习是一种人工智能(AI)的分支......'(一段完整的知识性回答)

测试三:字段缺失也不影响

python 复制代码
query = "我叫孙悟空,今年500岁,邮箱地址是wukong@gmial.com"  # 没有电话
input_message = {"messages": [HumanMessage(content=query)]}

result = graph.invoke(input_message)
# FinalResponse(final_output=UserInfo(name='孙悟空', age=500,
#              email='wukong@gmial.com', phone=None))
# phone 为可选字段,缺失时填 None,依然成功入库

6.4 Router Agent 的优势与局限

以上就是 LangGraph 中 Router 的常用使用形式:通过预定义的分支结构,根据用户输入灵活适配不同场景。其中结构化输出对路由至关重要------它确保系统可以可靠地解释大模型的决定并采取行动。

  • 优势:可以精准地控制程序链路中的每一个细节,逻辑清晰、可预测。
  • 局限 :这是一种控制级别相对有限 的代理架构,大模型通常只能控制单个决策。想象一下,如果希望 insert_db 分支里不止是插入数据库,而是有一堆各式各样的工具(网络搜索、RAG 等),难道要在每个分支下再嵌套一层 Router Function 做判断吗?虽然可行,但并不高效。

要高效解决"分支内多工具自主选择"的问题,就需要进一步引入 LangGraph 支持的第二种代理架构------Tool Calling Agent(工具调用代理) :它在 Router Agent 的基础上,让大模型可以自主选择并使用多种工具来完成某个条件分支中的任务(通过 @tool 定义工具、bind_tools 绑定给大模型、ToolNode 统一执行)。这部分内容将在下一篇笔记中展开。


附:全文要点速记

  1. 四大代理架构按控制权从弱到强:Router → Tool Calling → 自主循环(ReAct) → Multi-Agent;
  2. Router = 条件边 + 路由函数 ,add_conditional_edges(source, path, path_map) 是核心 API,path_map 把返回值映射为节点名;
  3. 结构化输出三种方式 :提示工程(不稳定)→ 提示工程 + 解析器(中)→ .with_structured_output()(推荐);
  4. .with_structured_output() 支持 Pydantic / TypedDict / JSON Schema 三种模式,Pydantic 最适合 Router;
  5. Union 父模型 (FinalResponse)+ isinstance 类型判断是构建多分支路由图的标准套路;
  6. Router 适合"少数固定分支"的场景;分支内需要多工具选择时,应升级为 Tool Calling Agent。
相关推荐
泯泷1 小时前
让知识"长骨架"——RAPTOR、GraphRAG 与 OpenViking(四)
人工智能·llm
Eloudy2 小时前
从 DeepSeek-V1 到 DeepSeek-V2:一次从“堆稠密参数”到“稀疏计算+压缩缓存”的架构转向
gpt·llm·transformer
泯泷3 小时前
从"检索管道"到"主动助理"——Agentic RAG 与双层记忆(五)
人工智能·llm·agent
泯泷5 小时前
AI Agent 的下一站:让世界主动来找它
人工智能·llm·agent
Robert_Terisa6 小时前
从Markdown到Typst:大一新生vibe coding转译器实录
llm·开源项目·独立开发·vibe coding·大一新生
泯泷7 小时前
AI 社交的真问题:助理拥有“我”,平台拥有“别人”
人工智能·llm·agent
Flynn Liang7 小时前
Agentic 工作流:子代理、Supervisor 与人工介入
java·langchain·多智能体·supervisor·agentic·langchain4j
泯泷19 小时前
让 Agent 分清“马上办、等一等、顺手办”
人工智能·llm·agent
泯泷21 小时前
语音,正在成为 AI Agent 最自然的接口
人工智能·llm·agent