[langgraph] Why Is It

必要性 

Agent ≠ prompt
Agent = 系统

而系统一定需要:

    • execution(LangGraph)
    • observability(LangSmith)
    • deployment(LangSmith)
    • iteration loop(LangSmith)

 

----> 该上 LangGraph,如果你开始出现这些需求:

    • 我需要明确状态 State

    • 我需要多个节点和分支

    • 我需要失败后恢复

    • 我需要长时间运行

    • 我需要人工审批

    • 我需要把多个 agent 组装成系统

    • 我不想被高层默认逻辑束缚

    • 我需要可控的 orchestration

这些正是 LangGraph 官方主打能力。

 

----> 给你两个最清晰的对比例子

场景 1:客服 FAQ + 查订单

用户问:“我的订单到哪了?如果延迟了就帮我解释一下。”

这类通常是:

    • 查订单 API

    • 拼接解释

    • 可能再调一个退款政策 tool

LangChain 更合适。
因为核心目标是快速构建,流程不复杂。

 

场景 2:企业级故障排查 agent

任务:

    1. 先读取告警

    2. 再读日志

    3. 判断是网络/数据库/应用层问题

    4. 如果证据不足,走额外诊断分支

    5. 如果风险高,暂停并通知值班工程师

    6. 等人工确认后继续

    7. 最后生成 RCA 摘要

    8. 全流程需要 trace、state、checkpoint

LangGraph 更合适。
因为这已经不是“一个 agent 调几个 tool”的问题了,而是一个真正的状态化 workflow。

 

----> 最后帮你压成一句最容易记住的话

LangChain 不是“不适合 agent”,而是更适合“先把 agent 做出来”。
LangGraph 不是“替代 LangChain”,而是更适合“把 agent 做成可控、可恢复、可上线的系统”。

 


实践出真知

一天可以快速过一遍的课程:LangGraph大模型全套教程,从入门到企业级实战

其他参考:2026吃透LangGraph全套实战教程,15天学完,让你少走99%弯路【通俗易懂,附课件代码资料】

LangChain的写法

import datetime
from langchain.tools import tool
from config.load_key import load_key
from langchain_community.chat_models import ChatTongyi

# 构建阿里云百炼大模型客户端
llm = ChatTongyi(
    model="qwen-plus",
    api_key=load_key("BAILIAN_API_KEY"),
)

# 定义工具 注意要添加注释
@tool
def get_current_date():
    """获取今天日期"""
    return datetime.datetime.today().strftime("%Y-%m-%d")

# 大模型绑定工具
llm_with_tools = llm.bind_tools([get_current_date])

# 工具容器
all_tools = {
    "get_current_date": get_current_date
}

# 把所有消息存到一起
query = "今天是几月几号"
messages = [query]

# 询问大模型。大模型会判断需要调用工具,并返回一个工具调用请求
ai_msg = llm_with_tools.invoke(messages)
# 无法给出直接的答案,但会选择调用工具
print(ai_msg) # this is a dict messages.append(ai_msg) # 打印需要调用的工具 print(ai_msg.tool_calls) if ai_msg.tool_calls: for tool_call in ai_msg.tool_calls: # 遍历 llm 认为需要的工具集 selected_tool = all_tools[tool_call["name"].lower()] tool_msg = selected_tool.invoke(tool_call) ----> 理解如下 messages.append(tool_msg) ----> 这本身就是ReAct的过程!! # 再次调用大模型 result = llm_with_tools.invoke(messages).content print(result)

 

StructuredTool.invoke(tool_call)??

可以把它理解成:

class StructuredTool:
    def invoke(self, tool_call):
        args = tool_call["args"]
        result = self.func(**args)
        return ToolMessage(
            content=str(result),
            tool_call_id=tool_call["id"]
        )

结合下面,也就理解了。

content=''
additional_kwargs={
    'tool_calls': [
        {
            'index': 0,
            'id': 'call_45f2c0e22294476bb7783d',
            'type': 'function',
            'function': {
                'name': 'get_current_date',
                'arguments': '{}'
            }
        }
    ]
}

tool_calls=[
    {
        'name': 'get_current_date',
        'args': {},
        'id': 'call_45f2c0e22294476bb7783d',
        'type': 'tool_call'
    }
]

 

LangGraph的写法

# 用 LangGraph 创建 ReAct Agent
agent = create_react_agent(
    model=llm,
    tools=[get_current_date],
    prompt="You are a helpful assistant",
)


# 调用 Agent
result = agent.invoke(
    {
        "messages": [
            {
                "role": "user",
                "content": "今天是几月几号",
            }
        ]
    }
)

 

好吧。这里像极了之前的MCP学习过程中的实现方式。

建议正式项目:MCP Server + LangGraph。

 

"""
mcp_server.py

这是 MCP Server,也就是“工具服务层”。

它的职责:
1. 定义工具
2. 通过 MCP 协议暴露工具
3. 让外部 Agent client 可以发现和调用这些工具

注意:
- 这里没有 LLM
- 这里没有 LangGraph
- 这里没有 Agent loop
- 这里不决定什么时候调用工具

这就是“工具层标准化”的核心。
"""

import datetime
from pathlib import Path

from mcp.server.fastmcp import FastMCP


mcp = FastMCP("date-demo-server")


@mcp.tool()
def get_current_date() -> str:
    """
    获取今天日期。

    Returns:
        str: 今天日期,格式为 YYYY-MM-DD。
    """
    return datetime.datetime.today().strftime("%Y-%m-%d")


@mcp.tool()
def save_user_note(note: str) -> str:
    """
    把一条 note 保存到本地 demo_notes.txt 文件。

    Args:
        note: 要保存的文本内容。

    Returns:
        str: 保存结果。

    这个工具是故意放进来的。
    因为它有“写入副作用”,非常适合演示第 4 层里的 human approval。
    """
    output_path = Path("demo_notes.txt")
    with output_path.open("a", encoding="utf-8") as f:
        f.write(note + "\n")

    return f"Saved note to {output_path.resolve()}"


if __name__ == "__main__":
    # stdio 模式:
    # client 会把这个 server.py 当成子进程启动。
    mcp.run(transport="stdio")

 

"""
client.py

这个文件是 Agent Client。

它的职责是:
1. 创建大模型 llm
2. 通过 MCP Client 连接 server.py
3. 从 MCP Server 加载 tools
4. 把 tools 交给 LangChain / LangGraph Agent
5. 让 Agent 自动完成:
   LLM 判断 -> tool call -> 执行工具 -> observation -> LLM 最终回答

和你之前手写版本最大的区别:

之前:
    你自己维护 all_tools
    你自己判断 ai_msg.tool_calls
    你自己 selected_tool.invoke(...)
    你自己 messages.append(tool_msg)
    你自己再次 llm.invoke(...)

现在:
    MCP Server 管工具暴露
    MultiServerMCPClient 管工具加载
    create_agent / LangGraph runtime 管 ReAct 循环
"""

import asyncio
import sys
from pathlib import Path

from config.load_key import load_key
from langchain_community.chat_models import ChatTongyi
from langchain_mcp_adapters.client importMultiServerMCPClient
from langchain.agents import create_agent


async def main() -> None:
    """
    主流程:

    1. 创建 LLM
    2. 创建 MCP client
    3. 加载 MCP tools
    4. 创建 Agent
    5. invoke Agent
    """

    # ------------------------------------------------------------
    # 1. 构建阿里云百炼大模型客户端
    # ------------------------------------------------------------
    llm = ChatTongyi(
        model="qwen-plus",
        api_key=load_key("BAILIAN_API_KEY"),
    )

    # ------------------------------------------------------------
    # 2. 找到 server.py 的绝对路径
    # ------------------------------------------------------------
    current_dir = Path(__file__).resolve().parent
    server_path = current_dir / "server.py"

    # ------------------------------------------------------------
    # 3. 创建 MCP Client
    # ------------------------------------------------------------
    # 这里的 date_server 是你给这个 MCP server 起的名字。
    #
    # transport="stdio":
    #   表示 client.py 会自动启动 server.py 子进程。
    #
    # command=sys.executable:
    #   表示使用当前 Python 环境运行 server.py。
    #   这样比直接写 "python" 更稳,因为它能确保使用同一个 venv。
    #
    # args=[str(server_path)]:
    #   表示要启动的 MCP Server 文件。
    # ------------------------------------------------------------
    mcp_client = MultiServerMCPClient(
        {
            "date_server": {
                "transport": "stdio",
                "command": sys.executable,
                "args": [str(server_path)],
            }
        }
    )

    # ------------------------------------------------------------
    # 4. 从 MCP Server 加载工具
    # ------------------------------------------------------------
    # 这一步之后,server.py 里的 get_current_date 会变成
    # LangChain / LangGraph Agent 可以调用的 tool。
    # ------------------------------------------------------------
    tools = await mcp_client.get_tools()

    print("Loaded MCP tools:")
    for tool in tools:
        print(f"- {tool.name}: {tool.description}")

    # ------------------------------------------------------------
    # 5. 创建 Agent
    # ------------------------------------------------------------
    # 这里的 create_agent 是当前 LangChain 官方推荐的高级 agent 创建方式。
    # 它内部会处理类似 ReAct 的循环:
    #
    # User Message
    #   -> LLM
    #   -> tool_calls
    #   -> MCP tool execution
    #   -> ToolMessage / Observation
    #   -> LLM
    #   -> Final Answer
    #
    # 所以你不需要再手写:
    #   if ai_msg.tool_calls:
    #       selected_tool.invoke(...)
    #       messages.append(tool_msg)
    # ------------------------------------------------------------
    agent = create_agent(
        model=llm,
        tools=tools,
        system_prompt=(
            "You are a helpful assistant. "
            "When the user asks about today's date, use the available date tool."
        ),
    )

    # ------------------------------------------------------------
    # 6. 调用 Agent
    # ------------------------------------------------------------
    result = await agent.ainvoke(
        {
            "messages": [
                {
                    "role": "user",
                    "content": "今天是几月几号?",
                }
            ]
        }
    )

    # ------------------------------------------------------------
    # 7. 打印完整 message 流,方便你观察 ReAct 过程
    # ------------------------------------------------------------
    print("\n========== Full Agent Messages ==========")
    for message in result["messages"]:
        print(message)
        print("----------------------------------------")

    # ------------------------------------------------------------
    # 8. 打印最终答案
    # ------------------------------------------------------------
    final_message = result["messages"][-1]

    print("\n========== Final Answer ==========")
    print(final_message.content)


if __name__ == "__main__":
    asyncio.run(main())

 

 

 

版本一:第 3 层 MCP Server + LangGraph ReAct Agent


 

"""
layer3_client_react_agent.py

第 3 层示范:

MCP Server + LangGraph prebuilt ReAct Agent

这个版本的目标:
1. 工具不再写在 client 里,而是来自 MCP Server
2. LangGraph 的 create_react_agent 负责 ReAct loop
3. 你不再手写 if ai_msg.tool_calls
4. 你不再手写 selected_tool.invoke(...)
5. 你不再手写 messages.append(tool_msg)

这就是:
    工具层标准化 + agent runtime 工程化
"""

import asyncio
import sys
from pathlib import Path

from config.load_key import load_key
from langchain_community.chat_models import ChatTongyi
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent


async def main() -> None:
    # ------------------------------------------------------------
    # 1. 创建 LLM
    # ------------------------------------------------------------
    llm = ChatTongyi(
        model="qwen-plus",
        api_key=load_key("BAILIAN_API_KEY"),
    )

    # ------------------------------------------------------------
    # 2. 定位 MCP Server
    # ------------------------------------------------------------
    current_dir = Path(__file__).resolve().parent
    server_path = current_dir / "mcp_server.py"

    # ------------------------------------------------------------
    # 3. 创建 MCP Client
    # ------------------------------------------------------------
    # MultiServerMCPClient 可以连接多个 MCP Server。
    #
    # 这里我们只连接一个:
    #   date_demo_server
    #
    # transport="stdio":
    #   client 会自动启动 mcp_server.py 子进程。
    # ------------------------------------------------------------
    mcp_client = MultiServerMCPClient(
        {
            "date_demo_server": {
                "transport": "stdio",
                "command": sys.executable,
                "args": [str(server_path)],
            }
        }
    )

    # ------------------------------------------------------------
    # 4. 从 MCP Server 加载 tools
    # ------------------------------------------------------------
    # 这一步是第 3 层的关键:
    #
    # 原来:
    #   tools = [get_current_date]
    #
    # 现在:
    #   tools = await mcp_client.get_tools()
    #
    # 工具来源变了。
    # 工具从“本地函数”变成了“标准化 MCP 工具服务”。
    # ------------------------------------------------------------
    tools = await mcp_client.get_tools()

    print("Loaded MCP tools:")
    for tool in tools:
        print(f"- {tool.name}: {tool.description}")

    # ------------------------------------------------------------
    # 5. 创建 LangGraph ReAct Agent
    # ------------------------------------------------------------
    # 这个 agent 内部会自动执行:
    #
    #   LLM -> tool_calls -> Tool execution -> ToolMessage -> LLM
    #
    # 这正是你原来手写的 ReAct loop。
    # ------------------------------------------------------------
    agent = create_react_agent(
        model=llm,
        tools=tools,
        prompt=(
            "You are a helpful assistant. "
            "When the user asks today's date, call get_current_date. "
            "Answer in Chinese."
        ),
    )

    # ------------------------------------------------------------
    # 6. 调用 Agent
    # ------------------------------------------------------------
    result = await agent.ainvoke(
        {
            "messages": [
                {
                    "role": "user",
                    "content": "今天是几月几号?",
                }
            ]
        }
    )

    # ------------------------------------------------------------
    # 7. 打印完整消息流,方便你观察 ReAct 过程
    # ------------------------------------------------------------
    print("\n========== Full Messages ==========")
    for msg in result["messages"]:
        print(msg)
        print("-----------------------------------")

    print("\n========== Final Answer ==========")
    print(result["messages"][-1].content)


if __name__ == "__main__":
    asyncio.run(main())

 

 

 

版本二:第 4 层 MCP Server + 自定义 StateGraph


 

自定义: 

  • State
  • Node
  • Edge
  • Conditional Edge
  • Checkpoint
  • Human Approval
  • Memory

创造一个“节点通过共享 state 通信”的图;每个 node 读入当前 state,并返回一部分 state update。

 

image

 

注意的知识点:

graph.ainvoke(...) 启动的是整个 graph。
它默认不会在每个 node 结束后返回。
它只会在两种情况下返回:第一,整个 graph 跑到 END;第二,中途遇到 interrupt()

所以 first_result 之所以“偏偏”在 approval_node 返回,不是因为 approval_node 是特殊节点,而是因为:approval_node 里面调用了 interrupt(...)

而前面的 date_agent_nodewriter_agent_node 都只是普通 return,没有 interrupt。

 

"""
layer4_client_custom_stategraph.py

第 4 层示范:

MCP Server
+
自定义 LangGraph StateGraph
+
多节点 workflow
+
checkpoint / short-term memory
+
human approval

这个版本的重点不是“代码更短”,而是“控制力更强”。

你不再把所有事情交给一个 prebuilt ReAct Agent。
你开始显式设计业务流程:

    date_agent_node
        ↓
    writer_agent_node
        ↓
    是否需要人工审批?
        ↓ yes
    approval_node
        ↓
    save_node
        ↓
    final_node

这就是生产级复杂 agent 系统的雏形。
"""

import asyncio
import sys
from pathlib import Path
from typing import Annotated, Any, Optional, TypedDict

from config.load_key import load_key
from langchain_core.messages import AIMessage, AnyMessage, HumanMessage
from langchain_community.chat_models import ChatTongyi
from langchain_mcp_adapters.client import MultiServerMCPClient

from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command, interrupt


# ============================================================
# 1. 定义 Graph State
# ============================================================
# 这是第 4 层最重要的思想之一:
#
# 你不再只维护 messages。
# 你开始维护“业务状态”。
#
# messages:
#   对话历史
#
# current_date:
#   MCP 工具返回的日期
#
# draft_answer:
#   writer_agent_node 生成的草稿
#
# requires_approval:
#   是否需要人工审批
#
# approved:
#   人工是否批准
#
# final_answer:
#   最终输出
# ============================================================

class DemoState(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]
    current_date: Optional[str]
    draft_answer: Optional[str]
    requires_approval: bool
    approved: Optional[bool]
    final_answer: Optional[str]


def get_latest_user_text(state: DemoState) -> str:
    """
    从 state["messages"] 中取出最新用户输入。
    """
    for msg in reversed(state["messages"]):
        if isinstance(msg, HumanMessage):
            return msg.content

        # 有些情况下 message 可能是 dict-like。
        if getattr(msg, "type", None) == "human":
            return msg.content

    return ""


def find_tool_by_name(tools: list[Any], expected_name: str):
    """
    从 MCP 加载出来的 tools 里找指定工具。

    有些工具适配层可能会对 tool name 做轻微包装,
    所以这里写得稍微稳一点。
    """
    for tool in tools:
        if tool.name == expected_name:
            return tool

    for tool in tools:
        if tool.name.endswith(expected_name):
            return tool

    available = [tool.name for tool in tools]
    raise RuntimeError(
        f"Cannot find tool {expected_name}. Available tools: {available}"
    )


async def build_graph():
    """
    创建并编译自定义 StateGraph。
    """

    # ------------------------------------------------------------
    # 1. 创建 LLM
    # ------------------------------------------------------------
    llm = ChatTongyi(
        model="qwen-plus",
        api_key=load_key("BAILIAN_API_KEY"),
    )

    # ------------------------------------------------------------
    # 2. 连接 MCP Server
    # ------------------------------------------------------------
    current_dir = Path(__file__).resolve().parent
    server_path = current_dir / "mcp_server.py"

    mcp_client = MultiServerMCPClient(
        {
            "date_demo_server": {
                "transport": "stdio",
                "command": sys.executable,
                "args": [str(server_path)],
            }
        }
    )

    tools = await mcp_client.get_tools()

    get_current_date_tool = find_tool_by_name(tools, "get_current_date")
    save_user_note_tool = find_tool_by_name(tools, "save_user_note")

    # ============================================================
    # 2. 定义 Nodes
    # ============================================================

    async def date_agent_node(state: DemoState) -> dict:
        """
        Agent 1:日期工具代理。

        这个节点只负责一件事:
        调用 MCP Server 里的 get_current_date 工具。

        注意:
        它不是直接调用本地 Python 函数。
        它是通过 MCP adapter 调用 MCP 工具。
        """
        current_date = await get_current_date_tool.ainvoke({})

        return {
            "current_date": current_date,
        }

    async def writer_agent_node(state: DemoState) -> dict:
        """
        Agent 2:回答生成代理。

        这个节点负责把工具结果转成自然语言回答。

        这里为了演示“多 agent workflow”,我们把:
            获取日期
            生成回答
        拆成两个不同 node。

        正式项目里,你可以继续拆:
            planner_agent
            data_agent
            report_agent
            reviewer_agent
            executor_agent
        """
        user_text = get_latest_user_text(state)

        requires_approval = any(
            keyword in user_text
            for keyword in ["保存", "记录", "写入", "save", "store"]
        )

        response = await llm.ainvoke(
            [
                {
                    "role": "system",
                    "content": (
                        "你是一个中文助手。"
                        "你需要基于工具返回的日期,给用户一个简洁自然的回答。"
                    ),
                },
                {
                    "role": "user",
                    "content": (
                        f"用户原始问题:{user_text}\n"
                        f"工具返回的日期:{state['current_date']}\n\n"
                        "请生成回答草稿。"
                    ),
                },
            ]
        )

        return {
            "draft_answer": response.content,
            "requires_approval": requires_approval,
        }

    def approval_node(state: DemoState) -> dict:
        """
        人工审批节点。

        这个节点体现第 4 层的核心生产能力:
        在执行有副作用的动作之前,暂停 graph,等待人工确认。

        注意:
        - interrupt 之前不要做不可逆副作用
        - 真正的 save_user_note 在 save_node 里执行
        """
        human_decision = interrupt(
            {
                "question": "检测到用户请求保存/写入内容,是否批准调用 save_user_note 工具?",
                "draft_answer": state["draft_answer"],
                "current_date": state["current_date"],
                "expected_tool": "save_user_note",
            }
        )

        # resume 之后,human_decision 会变成 Command(resume=...) 传入的值。
        if isinstance(human_decision, dict):
            approved = bool(human_decision.get("approved", False))  # <----
        else:
            approved = bool(human_decision)

        return {
            "approved": approved,
        }

    async def save_node(state: DemoState) -> dict:
        """
        写入节点。

        只有人工批准后,才允许调用 save_user_note。
        """
        if not state.get("approved"):
            return {
                "final_answer": (
                    f"{state['draft_answer']}\n\n"
                    "保存操作未执行,因为人工审批未通过。"
                )
            }

        save_result = await save_user_note_tool.ainvoke(
            {
                "note": f"Date answer: {state['draft_answer']}"
            }
        )

        return {
            "final_answer": (
                f"{state['draft_answer']}\n\n"
                f"保存操作已执行:{save_result}"
            )
        }

    def final_node(state: DemoState) -> dict:
        """
        最终输出节点。

        把 final_answer 或 draft_answer 放回 messages。
        """
        answer = state.get("final_answer") or state.get("draft_answer") or "没有生成答案。"

        return {
            "messages": [
                AIMessage(content=answer)
            ]
        }

    # ============================================================
    # 3. 定义条件路由
    # ============================================================

    def route_after_writer(state: DemoState) -> str:
        """
        writer_agent_node 之后决定走哪里。

        如果需要保存/写入,就进入 approval_node。
        如果只是普通查询日期,就直接 final_node。
        """
        if state.get("requires_approval"):
            return "approval"

        return "final"

    # ============================================================
    # 4. 创建 StateGraph
    # ============================================================

    builder = StateGraph(DemoState)

    builder.add_node("date_agent", date_agent_node)
    builder.add_node("writer_agent", writer_agent_node)
    builder.add_node("approval", approval_node)
    builder.add_node("save", save_node)
    builder.add_node("final", final_node)

    builder.add_edge(START, "date_agent")
    builder.add_edge("date_agent", "writer_agent")

    builder.add_conditional_edges(
        "writer_agent",
        route_after_writer,
        {
            "approval": "approval",
            "final": "final",
        },
    )

    builder.add_edge("approval", "save")
    builder.add_edge("save", "final")
    builder.add_edge("final", END)

    # ------------------------------------------------------------
    # 5. Checkpointer
    # ------------------------------------------------------------
    # InMemorySaver 是 demo 用。
    #
    # 生产环境不要只用内存。
    # 生产环境应该使用数据库型 checkpointer,比如 PostgresSaver。
    # ------------------------------------------------------------
    checkpointer = InMemorySaver()
    graph = builder.compile(checkpointer=checkpointer)
    return graph


async def main() -> None:
    graph = await build_graph()

    # thread_id 是 checkpoint / memory 的关键。
    #
    # 同一个 thread_id:
    #   可以恢复同一个 graph 执行状态
    #   可以保留 thread-level short-term memory
    #
    # 换一个 thread_id:
    #   就是一个新的会话 / 新的流程实例
    config = {
        "configurable": {
            "thread_id": "date-demo-thread-001"
        }
    }

    # 这个 query 故意包含“保存”,所以会触发 human approval。
    user_query = "今天是几月几号?请保存这个结果。"

    print("\n========== First Run ==========")

    first_result = await graph.ainvoke(
        {
            "messages": [
                HumanMessage(content=user_query)
            ],
            "current_date": None,
            "draft_answer": None,
            "requires_approval": False,
            "approved": None,
            "final_answer": None,
        },
        config=config,
    )

    # 如果图在 approval_node 暂停,会返回 __interrupt__。
    if "__interrupt__" in first_result:
        print("\n========== Human Approval Required ==========")
        print(first_result["__interrupt__"])

        user_input = input("\nApprove save_user_note? Type y/n: ").strip().lower()
        approved = user_input in {"y", "yes"}

        print("\n========== Resume Graph ==========")

        final_result = await graph.ainvoke(
            Command(
                resume={
                    "approved": approved
                }
            ),
            config=config,
        )
    else:
        final_result = first_result

    print("\n========== Final Messages ==========")
    for msg in final_result["messages"]:
        print(msg)
        print("-----------------------------------")

    print("\n========== Final Answer ==========")
    print(final_result["messages"][-1].content)


if __name__ == "__main__":
    asyncio.run(main())

 

posted @ 2026-04-20 22:20  郝壹贰叁  阅读(23)  评论(0)    收藏  举报