langgraph四大代理架构

内容纲要

一、概念

  • 路由代理
  • 工具代理
  • 自主循环代理
  • 多代理

二、路由代理(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 去构建实时联网检索的功能。

四、自主循环代理(React Agent)

五、多代理(Multi-Agent)

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部