LangGraph 从入门到实战

模块一:LangGraph 核心概念与生态系统

  1. 开发环境搭建与工具链配置

3.1 环境准备

虚拟环境管理

# 使用虚拟环境隔离项目依赖,避免与系统 Python 环境冲突

# 方案 1:pycharm
# pycharm创建项目后会自带虚拟环境,如果没有可以按下面方法创建

# 方案 2:venv(Python 内置)
# 创建虚拟环境
python -m venv venv

# 激活虚拟环境
# Windows:
venv\Scripts\activate

# macOS/Linux:
source venv/bin/activate

# 方案 3:uv
pip install uv
uv venv
uv init
source .venv/bin/activate

3.2 依赖安装策略

在项目根目录创建 requirements.txt 文件:

# ============== 核心框架 ==============
langchain==1.2.0
langchain-community==0.4.1
langchain-core~=1.2.7
langgraph==1.0.5                     # 核心框架
langgraph-checkpoint==3.0.1          # 内存持久化后端
langgraph-checkpoint-postgres==3.0.2 # 数据库持久化后端
langgraph-cli[inmem]>=0.4.0          # CLI 工具 
langsmith==0.4.59                    # 监控工具

# ============== LLM 集成 ==============
dashscope==1.25.4                 # 千问 SDK

# ============== MCP 相关 ==============
langchain-mcp-adapters==0.2.1     # MCP 客户端
fastmcp==2.13.1                   # MCP 服务端(自建服务用)

# ============== 工具库 ==============
pydantic==2.12.5                  # 数据验证
pydantic-settings==2.12.0         # 配置管理
python-dotenv==1.2.1              # 环境变量
tenacity==9.1.2                   # 重试机制
httpx==0.28.1                     # HTTP 客户端
aiohttp==3.13.2                   # 异步 HTTP

安装依赖

pip install -r requirements.txt

3.3 必要配置

  1. 环境变量配置

创建 .env 文件:

# LangSmith 配置(用于追踪和监控)
LANGSMITH_TRACING=true
LANGSMITH_ENDPOINT=https://api.smith.langchain.com
LANGSMITH_API_KEY=你的秘钥
LANGSMITH_PROJECT=Langgraph_test

# 千问 API 配置
DASHSCOPE_API_KEY=your_qwen_api_key

# 可选:OpenAI(用于对比测试)
OPENAI_API_KEY=your_openai_api_key
  1. Python 代码加载环境变量

from dotenv import load_dotenv
import os

# 加载 .env 文件
load_dotenv()


# 验证配置
def check_environment():
    required_keys = [
        "DASHSCOPE_API_KEY",
        "LANGSMITH_API_KEY"
    ]

    for key in required_keys:
        if not os.getenv(key):
            print(f"缺少环境变量: {key}")
        else:
            print(f"{key}: 已配置")


if __name__ == '__main__':
    check_environment()
  1. 千问模型配置

import os

from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi

load_dotenv()

# 初始化千问模型
llm = ChatTongyi(
    model="qwen-turbo",  # 或 qwen-plus, qwen-max
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    temperature=0.7,
    top_p=0.9,
)

# 测试调用
if __name__ == '__main__':
    response = llm.invoke("你好,请介绍一下 LangGraph")
    print(response.content)
  1. Hello World 与核心概念初探

4.1 最简单的图结构

完整代码示例

import os

from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
from langgraph.graph import StateGraph, MessagesState, START, END

load_dotenv()

# 初始化千问模型
llm = ChatTongyi(
    model="qwen-turbo",  # 或 qwen-plus, qwen-max
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    temperature=0.7,
    top_p=0.9,
)

# 定义节点函数:调用 LLM
def chat_node(state: MessagesState):
    # 调用千问模型
    response = llm.invoke(state["messages"])
    # 返回新消息(LangGraph 会自动添加到 messages 列表)
    return {"messages": [response]}

# 创建图:以 MessagesState 为状态模式
builder = StateGraph(MessagesState)

# 添加节点
builder.add_node("chat", chat_node)

# 定义流程:START → chat → END
builder.add_edge(START, "chat")
builder.add_edge("chat", END)

# 编译并执行
graph = builder.compile()
result = graph.invoke({"messages": [{"role": "user", "content": "你好,请介绍一下 LangGraph"}]})

# 打印 AI 的回复
print(result["messages"][-1].content)

4.5 实践练习:扩展 Hello World

import os
from typing import TypedDict
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
from langgraph.graph import StateGraph, START, END
from langchain_core.messages import HumanMessage, SystemMessage

# ==========================================
# 0. 配置 LLM (请学生填入自己的 Key)
# ==========================================
load_dotenv()


llm = ChatTongyi(
        model="qwen-max",
        api_key=os.getenv("DASHSCOPE_API_KEY"),
        temperature=0.7
    )


# ==========================================
# 1. 定义状态 (State)
# ==========================================
class AgentState(TypedDict):
    topic: str  # 原始输入的话题
    draft: str  # 节点1生成的初稿
    final_post: str  # 节点2润色后的最终文案


# ==========================================
# 2. 定义节点 (Nodes) - 这里引入了 LLM
# ==========================================

def write_draft(state: AgentState) -> AgentState:
    """
    节点 1: 初稿生成员
    任务: 根据话题写一段朴实的话
    """
    topic = state["topic"]
    print(f"\n--- [Node 1] 正在为 '{topic}' 撰写初稿 ---")

    # 调用 LLM
    messages = [
        SystemMessage(content="你是一个社交媒体助手。请根据用户的话题,写一段简单的描述,不要加表情包,50字以内。"),
        HumanMessage(content=topic)
    ]
    response = llm.invoke(messages)

    # 返回更新的状态
    return {"draft": response.content}


def polish_post(state: AgentState) -> AgentState:
    """
    节点 2: 润色编辑员
    任务: 读取上一步的 draft,进行美化
    """
    draft_text = state["draft"]
    print(f"--- [Node 2] 正在润色初稿: {draft_text[:20]}... ---")

    # 调用 LLM (注意:输入是上一个节点的产出)
    messages = [
        SystemMessage(content="你是一个爆款文案专家。请将用户的文字改写得更有吸引力,添加3个Emoji,并加上2个Hashtag。"),
        HumanMessage(content=draft_text)
    ]
    response = llm.invoke(messages)

    # 返回更新的状态
    return {"final_post": response.content}


# ==========================================
# 3. 构建图 (Build Graph)
# ==========================================

builder = StateGraph(AgentState)

# 添加节点
builder.add_node("writer", write_draft)
builder.add_node("editor", polish_post)

# 添加边 (串行逻辑)
# START -> writer -> editor -> END
builder.add_edge(START, "writer")
builder.add_edge("writer", "editor")
builder.add_edge("editor", END)

# 编译
graph = builder.compile()

# ==========================================
# 4. 运行作业
# ==========================================

# 可以在这里修改话题
user_topic = "周五下班去吃火锅"

print(f"=== 开始生成文案: {user_topic} ===")
result = graph.invoke({"topic": user_topic})

print("\n" + "=" * 30)
print("FINAL OUTPUT (最终文案)")
print("=" * 30)
print(result["final_post"])

模块二:图结构设计与状态管理

实践练习:Agent 反思循环

目标:构建一个自我反思改进的 Agent

代码示例(学生课后完成)

import os
from typing import TypedDict, Literal
from dotenv import load_dotenv
from langchain_community.chat_models.tongyi import ChatTongyi
from langgraph.graph import StateGraph, START, END

# 加载环境变量
load_dotenv()

# 初始化 LLM
llm = ChatTongyi(
    model="qwen-max",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    temperature=0.7
)


# ============================================================
# 1. 定义 State
# ============================================================
class ReflectionState(TypedDict):
    task: str  # 任务描述
    draft: str  # 当前草稿
    quality_score: float  # 质量分数 (0-1)
    reflection: str  # 反思内容
    iteration: int  # 当前迭代次数
    max_iterations: int  # 最大迭代次数


# ============================================================
# 2. 定义 Node
# ============================================================

def generate_draft(state: ReflectionState) -> dict:
    """
    Node 1: 生成初稿
    使用 LLM 生成或改进内容
    """
    print(f"\n{'=' * 60}")
    print(f"Node 1: 生成初稿 (第 {state['iteration'] + 1} 次)")
    print(f"{'=' * 60}")

    task = state["task"]
    iteration = state["iteration"]

    if iteration == 0:
        # 第一次:生成初稿
        prompt = f"请写一篇关于'{task}'的文章,大约150字左右。"
        print(f"提示词: {prompt}")

    else:
        # 后续:根据反思改进
        previous_draft = state["draft"]
        reflection = state["reflection"]

        prompt = f"""
请改进以下文章:

原文:
{previous_draft}

改进建议:
{reflection}

请根据改进建议重写文章,保持150字左右。
"""
        print(f"提示词: 根据反思改进第 {iteration} 次")
        print(f"反思建议: {reflection}")

    # 调用 LLM
    response = llm.invoke(prompt)
    draft = response.content

    print(f"\n生成结果:")
    print(f"{draft}")

    return {
        "draft": draft,
        "iteration": iteration + 1
    }


def evaluate_quality(state: ReflectionState) -> dict:
    """
    Node 2: 评估质量
    使用 LLM 评估文章质量
    """
    print(f"\n{'=' * 60}")
    print(f"Node 2: 评估质量")
    print(f"{'=' * 60}")

    draft = state["draft"]
    task = state["task"]

    prompt = f"""
请评估以下文章的质量。

任务要求: 写一篇关于'{task}'的文章
文章内容:
{draft}

评估标准:
1. 内容完整性 (是否涵盖主题)
2. 逻辑清晰度 (结构是否合理)
3. 语言表达 (是否流畅准确)

请给出0-1的分数(保留两位小数),只输出数字,不要其他内容。
例如:0.75
"""

    # 调用 LLM 评估
    response = llm.invoke(prompt)
    score_text = response.content.strip()

    try:
        # 提取分数
        score = float(score_text)
        score = max(0.0, min(1.0, score))  # 确保在 0-1 范围内
    except ValueError:
        print(f"⚠️  无法解析分数,使用默认值 0.5")
        score = 0.5

    print(f"\n质量分数: {score:.2f}")

    return {"quality_score": score}


def reflect_and_improve(state: ReflectionState) -> dict:
    """
    Node 3: 反思改进
    使用 LLM 分析问题并给出改进建议
    """
    print(f"\n{'=' * 60}")
    print(f"Node 3: 反思改进")
    print(f"{'=' * 60}")

    draft = state["draft"]
    task = state["task"]
    score = state["quality_score"]

    prompt = f"""
请分析以下文章存在的问题,并给出具体的改进建议。

任务要求: 写一篇关于'{task}'的文章
当前文章:
{draft}

当前评分: {score:.2f}

请简要指出1-2个主要问题,并给出改进方向(50字以内)。
"""

    # 调用 LLM 反思
    response = llm.invoke(prompt)
    reflection = response.content

    print(f"\n改进建议:")
    print(f"{reflection}")

    return {"reflection": reflection}


# ============================================================
# 3. 定义路由函数(条件边)
# ============================================================

def should_continue(state: ReflectionState) -> Literal["reflect", "end"]:
    """
    决定是否继续改进

    终止条件:
    1. 质量分数 >= 0.75
    2. 达到最大迭代次数
    """
    score = state["quality_score"]
    iteration = state["iteration"]
    max_iter = state["max_iterations"]

    print(f"\n{'=' * 60}")
    print(f"条件判断: 是否继续改进")
    print(f"{'=' * 60}")
    print(f"当前分数: {score:.2f}")
    print(f"迭代次数: {iteration}/{max_iter}")

    # 终止条件1:质量达标
    if score >= 0.9:
        print(f"✅ 决策: 质量达标 (>= 0.9),结束改进")
        return "end"

    # 终止条件2:达到最大迭代次数
    if iteration >= max_iter:
        print(f"⚠️  决策: 达到最大迭代次数,停止改进")
        return "end"

    # 继续改进
    print(f"❌ 决策: 质量不足,继续改进")
    return "reflect"


# ============================================================
# 4. 构建图
# ============================================================

def build_graph():
    """构建反思循环图"""

    # 创建图
    builder = StateGraph(ReflectionState)

    # 添加节点
    builder.add_node("generate_draft", generate_draft)
    builder.add_node("evaluate_quality", evaluate_quality)
    builder.add_node("reflect", reflect_and_improve)

    # 添加边
    # Edge 1: START → generate_draft (入口)
    builder.add_edge(START, "generate_draft")

    # Edge 2: generate_draft → evaluate_quality (普通边)
    builder.add_edge("generate_draft", "evaluate_quality")

    # Edge 3: evaluate_quality → [条件判断] (条件边)
    builder.add_conditional_edges(
        source="evaluate_quality",
        path=should_continue,
        path_map={
            "reflect": "reflect",  # 不合格 → 反思
            "end": END  # 合格 → 结束
        }
    )

    # Edge 4: reflect → generate_draft (循环边)
    builder.add_edge("reflect", "generate_draft")

    # 编译
    graph = builder.compile()

    return graph


# ============================================================
# 5. 运行示例
# ============================================================

def run_reflection_loop(task: str, max_iterations: int = 3):
    """
    运行反思循环

    Args:
        task: 任务描述
        max_iterations: 最大迭代次数
    """
    print("\n" + "=" * 60)
    print("反思循环 Agent 启动")
    print("=" * 60)
    print(f"任务: {task}")
    print(f"最大迭代次数: {max_iterations}")
    print(f"质量阈值: 0.9")

    # 构建图
    graph = build_graph()

    # 初始状态
    initial_state = {
        "task": task,
        "draft": "",
        "quality_score": 0.0,
        "reflection": "",
        "iteration": 0,
        "max_iterations": max_iterations
    }

    # 执行图
    final_state = graph.invoke(initial_state)

    # 显示最终结果
    print("\n" + "=" * 60)
    print("最终结果")
    print("=" * 60)
    print(f"总迭代次数: {final_state['iteration']}")
    print(f"最终质量分数: {final_state['quality_score']:.2f}")
    print(f"\n最终文章:")
    print("-" * 60)
    print(final_state['draft'])
    print("-" * 60)

    return final_state


if __name__ == "__main__":
    # 运行示例
    result = run_reflection_loop(
        task="人工智能的发展历程",
        max_iterations=3
    )

4.1 并行执行(Fan-out/Fan-in)

代码示例:

import os
from typing import Annotated, TypedDict                                                                            
from operator import add

from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
from langgraph.graph import StateGraph, START, END
from langgraph.types import Send

load_dotenv()

llm = ChatTongyi(
        model="qwen-max",
        api_key=os.getenv("DASHSCOPE_API_KEY"),
        temperature=0.7
    )

class State(TypedDict):
    query: str
    results: Annotated[list[str], add]  # results 字段使用了 Annotated[list[str], add],三个并行节点返回的列表会被自动拼接,而不是相互覆盖。
    final_answer: str

def search_web(state: State) -> dict:
    return {"results": [f"Web 结果: {state['query']}"]}

def search_kb(state: State) -> dict:
    return {"results": [f"知识库结果: {state['query']}"]}

def search_db(state: State) -> dict:
    return {"results": [f"数据库结果: {state['query']}"]}

def aggregate(state: State) -> dict:
    combined = "\n".join(state["results"])
    answer = llm.invoke(f"综合以下信息回答:\n{combined}")
    return {"final_answer": answer.content}

# 扇出路由:同时发送到多个节点
def fan_out(state: State):
    return [
        Send("search_web", state),
        Send("search_kb", state),
        Send("search_db", state),
    ]

builder = StateGraph(State)
builder.add_node("search_web", search_web)
builder.add_node("search_kb", search_kb)
builder.add_node("search_db", search_db)
builder.add_node("aggregate", aggregate)

builder.add_conditional_edges(START, fan_out)  # 并行扇出
builder.add_edge("search_web", "aggregate")     # 扇入汇聚
builder.add_edge("search_kb", "aggregate")
builder.add_edge("search_db", "aggregate")
builder.add_edge("aggregate", END)

graph = builder.compile()

if __name__ == '__main__':
    print(graph.invoke({"query": "如何使用 LangGraph?"}))

4.3 分层结构(Hierarchical)

代码示例:

import os
from typing import Annotated, TypedDict
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
from langgraph.graph import StateGraph, START, END

load_dotenv()

llm = ChatTongyi(
        model="qwen-max",
        api_key=os.getenv("DASHSCOPE_API_KEY"),
        temperature=0.7
    )

# ============ 子图:深度研究 ============
class ResearchState(TypedDict):
    topic: str
    search_results: list[str]
    summary: str

def search_node(state: ResearchState) -> dict:
    return {"search_results": [f"搜索结果: {state['topic']}"]}

def summarize_node(state: ResearchState) -> dict:
    return {"summary": f"总结: {state['search_results']}"}

# 构建子图
research_builder = StateGraph(ResearchState)
research_builder.add_node("search", search_node)
research_builder.add_node("summarize", summarize_node)
research_builder.add_edge(START, "search")
research_builder.add_edge("search", "summarize")
research_builder.add_edge("summarize", END)
research_subgraph = research_builder.compile()

# ============ 主图 ============
class MainState(TypedDict):
    topic: str       # 与子图同名,自动传递
    summary: str     # 与子图同名,自动回传
    final_report: str

def generate_report(state: MainState) -> dict:
    report = llm.invoke(f"根据以下总结生成报告:\n{state['summary']}")
    return {"final_report": report.content}

# 把子图当作一个节点使用
main_builder = StateGraph(MainState)
main_builder.add_node("research", research_subgraph)  # 子图作为节点
main_builder.add_node("report", generate_report)
main_builder.add_edge(START, "research")
main_builder.add_edge("research", "report")
main_builder.add_edge("report", END)

main_graph = main_builder.compile()

if __name__ == '__main__':
    print(main_graph.invoke({"topic": "如何使用 LangGraph?"}))

综合实践项目:多轮对话客服系统

项目目标:构建一个完整的客服 Agent,整合本模块所有知识点

完整实现示例(学生课后完成):

import os
import uuid
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.checkpoint.memory import MemorySaver

load_dotenv()

# ============================================================
# 初始化 LLM
# ============================================================
llm = ChatTongyi(
    model="qwen-max",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    temperature=0.7
)


# ============================================================
# State 定义(使用 MessagesState)
# ============================================================
class State(MessagesState):
    """使用内置的 MessagesState,自动管理消息历史"""
    pass


# ============================================================
# 模拟数据库
# ============================================================
ORDERS = {
    "ORD001": {"商品": "iPhone 15", "状态": "已发货", "价格": 5999},
    "ORD002": {"商品": "AirPods", "状态": "配送中", "价格": 1299},
}


# ============================================================
# Node: 客服助手(一个节点搞定所有逻辑)
# ============================================================
def customer_service_node(state: State) -> dict:
    """
    客服助手节点
    处理所有用户请求:意图识别 + 订单查询 + 产品咨询
    """

    # 构建系统提示
    system_prompt = """你是智能客服助手。

你的能力:
1. 查询订单:当用户提供订单号(格式:ORD001、ORD002等),从数据库查询订单信息
2. 产品咨询:回答产品相关问题

订单数据库:
- ORD001: iPhone 15, 已发货, ¥5999
- ORD002: AirPods, 配送中, ¥1299

产品信息:
- iPhone 15: ¥5999
- AirPods: ¥1299

处理流程:
1. 如果用户提到"订单"或"查询",引导用户提供订单号
2. 如果用户提供了订单号(ORD开头),查询并返回订单详情
3. 如果是产品咨询,直接回答
4. 回答要简洁、友好
"""

    # 获取最新用户消息
    messages = state["messages"]
    user_message = messages[-1].content

    # 检查是否包含订单号
    order_id = None
    for word in user_message.split():
        if word.startswith("ORD"):
            order_id = word
            break

    # 如果有订单号,直接查询数据库
    if order_id and order_id in ORDERS:
        order = ORDERS[order_id]
        response = f"""📦 订单详情
━━━━━━━━━━━━━━━
订单号:{order_id}
商品:{order['商品']}
状态:{order['状态']}
价格:¥{order['价格']}
━━━━━━━━━━━━━━━

还需要其他帮助吗?"""

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

    # 否则调用 LLM 处理
    full_messages = [SystemMessage(content=system_prompt)] + messages

    response = llm.invoke(full_messages)

    return {"messages": [response]}


# ============================================================
# 构建图
# ============================================================
def build_graph():
    """构建客服图"""

    # 创建内存检查点
    memory = MemorySaver()

    # 创建图
    graph = StateGraph(State)

    # 只需要一个节点
    graph.add_node("customer_service", customer_service_node)

    # 简单的线性流程
    graph.add_edge(START, "customer_service")
    graph.add_edge("customer_service", END)

    # 编译时添加 checkpointer
    return graph.compile(checkpointer=memory)


# ============================================================
# 交互式运行
# ============================================================
def run_interactive():
    """交互式运行"""

    graph = build_graph()

    # 生成唯一会话 ID
    thread_id = str(uuid.uuid4())
    config = {"configurable": {"thread_id": thread_id}}

    print("\n" + "=" * 50)
    print("💬 智能客服系统")
    print("=" * 50)
    print(f"会话 ID: {thread_id[:8]}...")
    print("输入 'quit' 退出")
    print("-" * 50)

    # 欢迎语
    print("\n🤖 您好!我是智能客服助手")
    print("我可以帮您:")
    print("  1. 查询订单(请提供订单号)")
    print("  2. 产品咨询")

    while True:
        try:
            # 获取用户输入
            user_input = input("\n👤 您: ").strip()

            if not user_input:
                continue

            if user_input.lower() in ["quit", "exit", "q"]:
                print("\n👋 感谢使用,再见!")
                break

            # 构造输入
            inputs = {"messages": [HumanMessage(content=user_input)]}

            # 执行图并获取响应
            result = graph.invoke(inputs, config=config)

            # 获取最新的 AI 回复
            messages = result.get("messages", [])
            if messages:
                last_message = messages[-1]
                if isinstance(last_message, AIMessage):
                    print(f"\n🤖 {last_message.content}")

        except Exception as e:
            print(f"\n❌ 错误: {e}")
            import traceback
            traceback.print_exc()


# ============================================================
# 自动测试
# ============================================================
def run_auto_test():
    """自动测试"""

    graph = build_graph()

    thread_id = str(uuid.uuid4())
    config = {"configurable": {"thread_id": thread_id}}

    print("\n" + "=" * 50)
    print(" 智能客服系统 - 自动测试")
    print("=" * 50)

    test_cases = [
        "我要查询订单",
        "ORD001",
        "iPhone多少钱",
        "谢谢"
    ]

    for user_input in test_cases:
        print(f"\n👤 用户: {user_input}")

        inputs = {"messages": [HumanMessage(content=user_input)]}

        try:
            result = graph.invoke(inputs, config=config)

            messages = result.get("messages", [])
            if messages:
                last_message = messages[-1]
                if isinstance(last_message, AIMessage):
                    print(f"\n🤖 {last_message.content}")

        except Exception as e:
            print(f"❌ 错误: {e}")
            break

    print("\n" + "=" * 50)
    print("✅ 测试完成")
    print("=" * 50)


# ============================================================
# 主函数
# ============================================================
if __name__ == "__main__":
    run_auto_test()
    #run_interactive()

模块三:工具系统与 Agent 增强

  1. 工具系统核心

1.1 工具定义与最佳实践

1.1.2 使用 @tool 装饰器定义工具

基础示例

from langchain.tools import tool

@tool
def get_weather(city: str) -> str:
    """获取指定城市的天气信息。
    
    Args:
        city: 城市名称,例如"北京"、"上海"
        
    Returns:
        天气信息描述字符串
    """
    weather_data = {
        "北京": "晴天,25°C",
        "上海": "多云,28°C",
        "深圳": "雷阵雨,30°C"
    }
    return weather_data.get(city, f"{city}的天气信息暂时无法获取")

# 测试工具
print(get_weather.name)         # get_weather
print(get_weather.description)  # 获取指定城市的天气信息...
result = get_weather.invoke({"city": "北京"})
print(result)  # 晴天,25°C
1.1.3 DocString 最佳实践

关键原则:DocString 是 LLM 理解工具用途的唯一依据!

@tool
def search_product(
    keyword: str,
    category: str = "all",
    max_price: float = None,
    min_rating: float = 4.0
) -> str:
    """在电商平台搜索商品。
    
    使用场景:
    - 用户想要查找特定商品时调用
    - 用户询问"有什么XX产品"、"帮我找XX"时使用
    
    Args:
        keyword: 搜索关键词,例如"手机"、"笔记本电脑"
        category: 商品分类,可选值:"electronics"(电子产品)、"clothing"(服装)、"all"(全部)
        max_price: 最高价格限制,单位为元,不设置则不限制
        min_rating: 最低评分要求,范围 1.0-5.0,默认 4.0
        
    Returns:
        JSON 格式的商品列表,包含名称、价格、评分等信息
        
    注意事项:
    - 关键词尽量具体,避免过于宽泛
    - 价格限制时请考虑用户预算
    - 评分过滤会影响结果数量
    """
    # 实际实现...
    pass
1.1.4 复杂参数:Pydantic 模型

对于复杂参数,使用 Pydantic 提供强类型验证:

from pydantic import BaseModel, Field
from typing import Optional
from datetime import date

class FlightSearchParams(BaseModel):
    """航班搜索参数"""
    departure_city: str = Field(description="出发城市,例如'北京'")
    arrival_city: str = Field(description="到达城市,例如'上海'")
    departure_date: date = Field(description="出发日期,格式 YYYY-MM-DD")
    return_date: Optional[date] = Field(None, description="返程日期(往返票时必填)")
    passengers: int = Field(default=1, ge=1, le=9, description="乘客人数,1-9人")
    cabin_class: str = Field(default="economy", description="舱位等级:economy/business/first")

@tool(args_schema=FlightSearchParams)
def search_flights(
    departure_city: str,
    arrival_city: str,
    departure_date: date,
    return_date: Optional[date] = None,
    passengers: int = 1,
    cabin_class: str = "economy"
) -> str:
    """搜索航班信息。
    
    Returns:
        JSON 格式的航班列表
    """
    flight_info = {
        "route": f"{departure_city} -> {arrival_city}",
        "date": str(departure_date),
        "passengers": passengers,
        "class": cabin_class
    }
    return f"找到 3 班符合条件的航班: {flight_info}"

1.2 ToolRuntime 统一上下文访问

1.2.2 访问 State(会话状态)
from langchain.tools import tool, ToolRuntime

# ✅ 访问当前会话状态
@tool
def summarize_conversation(runtime: ToolRuntime) -> str:
    """总结当前对话内容"""
    messages = runtime.state["messages"]
    
    human_msgs = sum(1 for m in messages if m.__class__.__name__ == "HumanMessage")
    ai_msgs = sum(1 for m in messages if m.__class__.__name__ == "AIMessage")
    
    return f"对话包含 {human_msgs} 条用户消息,{ai_msgs} 条 AI 回复"

# ✅ 访问自定义状态字段
@tool
def get_user_preference(pref_name: str, runtime: ToolRuntime) -> str:
    """获取用户偏好设置"""
    preferences = runtime.state.get("user_preferences", {})
    return preferences.get(pref_name, "未设置")
1.2.3 更新 State(使用 Command)
from langgraph.types import Command
from langchain.messages import RemoveMessage
from langgraph.graph.message import REMOVE_ALL_MESSAGES

@tool
def clear_conversation() -> Command:
    """清空对话历史"""
    return Command(
        update={
            "messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES)],
        }
    )

@tool
def update_user_name(new_name: str, runtime: ToolRuntime) -> Command:
    """更新用户姓名"""
    return Command(update={"user_name": new_name})
1.2.4 访问 Context(运行时配置)
from dataclasses import dataclass
from langchain.agents import create_agent

# 1. 定义 Context 结构
@dataclass
class UserContext:
    user_id: str
    api_key: str

# 2. 在工具中访问 Context
@tool
def get_account_info(runtime: ToolRuntime[UserContext]) -> str:
    """获取账户信息"""
    user_id = runtime.context.user_id
    api_key = runtime.context.api_key
    
    # 使用 user_id 查询数据库...
    return f"用户 {user_id} 的账户余额:5000 元"

# 3. 创建 Agent 时指定 context_schema
agent = create_agent(
    model=model,
    tools=[get_account_info],
    context_schema=UserContext
)

# 4. 调用时传入 context
result = agent.invoke(
    {"messages": [{"role": "user", "content": "查询我的余额"}]},
    context=UserContext(user_id="user_123", api_key="sk-xxx")
)
1.2.5 访问 Store(长期记忆)
from langgraph.store.memory import InMemoryStore

@tool
def get_user_info(user_id: str, runtime: ToolRuntime) -> str:
    """查询用户信息"""
    store = runtime.store
    user_info = store.get(("users",), user_id)
    return str(user_info.value) if user_info else "用户不存在"

@tool
def save_user_info(user_id: str, name: str, age: int, runtime: ToolRuntime) -> str:
    """保存用户信息"""
    store = runtime.store
    store.put(("users",), user_id, {"name": name, "age": age})
    return "保存成功"

# 创建带 Store 的 Agent
store = InMemoryStore()
agent = create_agent(
    model=model,
    tools=[get_user_info, save_user_info],
    store=store
)
1.2.6 Stream Writer(流式输出)
@tool
def process_large_file(filename: str, runtime: ToolRuntime) -> str:
    """处理大文件"""
    writer = runtime.stream_writer
    
    # 分步流式输出进度
    writer(f"开始读取文件:{filename}")
    # 模拟处理...
    writer("文件读取完成,开始解析...")
    # 模拟解析...
    writer("解析完成,正在生成报告...")
    
    return f"文件 {filename} 处理完成"
  1. Agent 与工具集成

2.1 create_agent 标准实现

2.1.3 完整示例
import os
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi

load_dotenv()

model = ChatTongyi(
    model="qwen-max",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
)
from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain.tools import tool
from datetime import datetime

load_dotenv()

# 定义工具
@tool
def get_weather(city: str) -> str:
    """获取城市天气"""
    return f"{city}的天气是晴天,25°C"

@tool
def get_time() -> str:
    """获取当前时间"""
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

# 创建 Agent
agent = create_agent(
    model=model,
    tools=[get_weather, get_time],
    system_prompt="你是一个友好的助手,可以查询天气和时间。"
)

# 调用
result = agent.invoke({
    "messages": [{"role": "user", "content": "北京今天天气怎么样?"}]
})

print(result["messages"][-1].content)

2.2 ToolNode 自动化工具调用

2.2.2 使用 ToolNode 构建 Agent
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.graph import StateGraph, MessagesState, START, END
from langchain_openai import ChatOpenAI

# 定义工具
tools = [get_weather, get_time]

# 绑定工具到模型
model = ChatTongyi(
    model="qwen-max",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
).bind_tools(tools)

# 定义 Agent 节点
def call_model(state: MessagesState):
    response = model.invoke(state["messages"])
    return {"messages": [response]}

# 构建图
workflow = StateGraph(MessagesState)
workflow.add_node("agent", call_model)
workflow.add_node("tools", ToolNode(tools))  # 自动处理工具调用

# 添加边
workflow.add_edge(START, "agent")
workflow.add_conditional_edges("agent", tools_condition)
workflow.add_edge("tools", "agent")

# 编译
app = workflow.compile()

# 调用
result = app.invoke({
    "messages": [{"role": "user", "content": "北京和上海的天气?"}]
})
2.2.3 ToolNode 并行执行
from langchain_core.messages import AIMessage

# 模拟 LLM 返回多个工具调用
ai_message = AIMessage(
    content="",
    tool_calls=[
        {"name": "get_weather", "args": {"city": "北京"}, "id": "call_1"},
        {"name": "get_weather", "args": {"city": "上海"}, "id": "call_2"},
        {"name": "get_time", "args": {}, "id": "call_3"}
    ]
)

# ToolNode 会并行执行这 3 个工具
workflow = StateGraph(MessagesState)
workflow.add_node("tools", ToolNode(tools))
workflow.add_edge(START, "tools")
workflow.add_edge("tools", END)

app = workflow.compile()
result = app.invoke({"messages": [ai_message]})

# 输出包含 3 个 ToolMessage
for msg in result["messages"]:
    print(f"[{msg.type}]: {msg.content}")

2.3 多轮对话与状态管理

2.3.1 使用 Checkpointer(短期记忆)
from langgraph.checkpoint.memory import MemorySaver

# 创建 Checkpointer
memory = MemorySaver()

# 创建带记忆的 Agent
agent = create_agent(
    model=modle,
    tools=tools,
    checkpointer=memory
)

# 使用线程 ID 隔离会话
config = {"configurable": {"thread_id": "user_123"}}

# 第一轮对话
response1 = agent.invoke(
    {"messages": [{"role": "user", "content": "我叫张三"}]},
    config=config
)

# 第二轮对话(Agent 能记住上下文)
response2 = agent.invoke(
    {"messages": [{"role": "user", "content": "我叫什么名字?"}]},
    config=config
)
# 输出: "您叫张三。"
2.3.2 自定义状态扩展
from langchain.agents import AgentState
from typing_extensions import NotRequired

# 定义自定义状态
class CustomState(AgentState):
    user_preferences: NotRequired[dict]
    task_count: NotRequired[int]

# 方式1:通过 state_schema
agent = create_agent(
    model=model,
    tools=tools,
    state_schema=CustomState
)

# 方式2:通过 Middleware(推荐)
from langchain.agents.middleware import AgentMiddleware

class CustomMiddleware(AgentMiddleware):
    state_schema = CustomState
    
    def before_model(self, state: CustomState, runtime):
        # 可以访问 state["user_preferences"]
        return None

agent = create_agent(
    model=model,
    tools=tools,
    middleware=[CustomMiddleware()]
)
  1. Middleware 系统

3.3 自定义中间件开发

3.3.1 装饰器方式(简单场景)
from langchain.agents.middleware import before_model, after_model
from langchain.agents import AgentState
from langgraph.runtime import Runtime

@before_model(can_jump_to=["end"])
def safety_check(state: AgentState, runtime: Runtime) -> dict | None:
    """安全检查:对话长度限制"""
    if len(state["messages"]) >= 20:
        return {
            "messages": [{"role": "assistant", "content": "对话已达上限"}],
            "jump_to": "end"
        }
    return None

@after_model
def log_response(state: AgentState, runtime: Runtime) -> dict | None:
    """记录模型响应"""
    print(f"模型返回: {state['messages'][-1].content}")
    return None

# 使用
agent = create_agent(
    model=model,
    tools=tools,
    middleware=[safety_check, log_response]
)
3.3.2 类方式(复杂场景)
from typing import Any
from langchain.agents.middleware import AgentMiddleware, AgentState
from langgraph.runtime import Runtime

class LoggingMiddleware(AgentMiddleware):
    """统一日志中间件:可复用、可扩展(推荐工程化写法)"""

    def before_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
        print(f"[before_model] messages={len(state['messages'])}")
        return None

    def after_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
        print(f"[after_model] assistant={state['messages'][-1].content}")
        return None

agent = create_agent(
    model=model,
    tools=tools,
    middleware=[LoggingMiddleware()],
)
3.3.3 Wrap-style Hook(重试机制)
from typing import Callable
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse

@wrap_model_call
def retry_model(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
    for attempt in range(3):
        try:
            return handler(request)
        except Exception as e:
            if attempt == 2:
                raise
            print(f"重试 {attempt + 1}/3: {e}")

agent = create_agent(
    model=model,
    tools=tools,
    middleware=[retry_model],
)
3.3.6 Agent jumps(提前退出/跳转)
from typing import Any
from langchain.agents.middleware import after_model, hook_config, AgentState
from langchain.messages import AIMessage
from langgraph.runtime import Runtime

@after_model
@hook_config(can_jump_to=["end"])
def check_for_blocked(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
    last = state["messages"][-1]
    if "BLOCKED" in last.content:
        return {
            "messages": [AIMessage("该请求无法处理,我需要在这里停止。")],
            "jump_to": "end",
        }
    return None
3.3.7 自定义 State schema
from typing import Any
from typing_extensions import NotRequired
from langchain.agents.middleware import AgentState, before_model, after_model
from langgraph.runtime import Runtime

class CustomState(AgentState):
    model_call_count: NotRequired[int]
    user_id: NotRequired[str]

@before_model(state_schema=CustomState, can_jump_to=["end"])
def check_call_limit(state: CustomState, runtime: Runtime) -> dict[str, Any] | None:
    if state.get("model_call_count", 0) > 10:
        return {"jump_to": "end"}
    return None

@after_model(state_schema=CustomState)
def increment_counter(state: CustomState, runtime: Runtime) -> dict[str, Any] | None:
    return {"model_call_count": state.get("model_call_count", 0) + 1}
  1. 模型上下文协议(MCP)完整指南

4.2 客户端集成与工具调用

4.2.3 Python 示例:连接多个 MCP Server

基础用法(无状态):

pip install langchain-mcp-adapters
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent

async def main():
    # 连接多个 MCP Server
    client = MultiServerMCPClient(
        {
            "math": {
                "transport": "stdio",  # 本地子进程通信
                "command": "python",
                "args": ["/path/to/math_server.py"],
            },
            "weather": {
                "transport": "http",  # HTTP 远程服务
                "url": "http://localhost:8000/mcp",
            },
        }
    )

    # 获取所有工具
    tools = await client.get_tools()

    # 创建 Agent
    agent = create_agent(
        model,  # 最新模型
        tools
    )

    # 调用数学工具
    result1 = await agent.ainvoke({
        "messages": [{"role": "user", "content": "计算 3 × 12"}]
    })

    # 调用天气工具
    result2 = await agent.ainvoke({
        "messages": [{"role": "user", "content": "纽约天气如何?"}]
    })

    print(result1)
    print(result2)

asyncio.run(main())

进阶用法:加载 Resources 和 Prompts :

from langchain_mcp_adapters.client import MultiServerMCPClient

client = MultiServerMCPClient({...})  # 只是保存配置,不连接

# 1. 加载工具(这里才真正连接)
tools = await client.get_tools()
print("\n=== TOOLS ===")
for t in tools:
    print(f"- {t.name}: {t.description}")
    print(f"  args_schema: {t.args_schema}")

# 2. 加载资源(文件、数据等)
print("\n=== RESOURCES (templates/listing) ===")
math_blobs = await client.get_resources("math")
print(f"resources count: {len(math_blobs)}")
for blob in math_blobs:
    print(f"- URI: {blob.metadata.get('uri')} | MIME: {blob.mimetype}")
    print(blob.as_string())

print("\n=== RESOURCES (templates/listing) ===")
blobs = await client.get_resources("weather",uris=["weather://北京/history"])
print(f"resources count: {len(blobs)}")
for blob in blobs:
    print(f"- URI: {blob.metadata.get('uri')} | MIME: {blob.mimetype}")
    print(blob.as_string())

# 3. 加载提示模板
messages = await client.get_prompt(
    server_name="math",
    prompt_name="explain_math",
    arguments={
        "topic": "计算 3 × 12"
    }
)
print(messages)

4.3 自定义 MCP Server(FastMCP)

4.3.1 本地服务器(stdio)示例
pip install fastmcp
# math_server.py
from fastmcp import FastMCP

mcp = FastMCP("Math")

@mcp.tool()
def add(a: int, b: int) -> int:
    """加法运算"""
    return a + b

@mcp.tool()
def multiply(a: int, b: int) -> int:
    """乘法运算"""
    return a * b

@mcp.resource("formula://basic")
async def get_formulas() -> str:
    """返回基础数学公式"""
    return "面积 = 长 × 宽\n体积 = 长 × 宽 × 高"

@mcp.prompt()
def explain_math(topic: str) -> str:
    """生成数学解释提示"""
    return f"请用简单的语言解释 {topic} 的概念,并举例说明。"

if __name__ == "__main__":
    mcp.run(transport="stdio")
4.3.2 远程服务器(Streamable HTTP)示例
# weather_server.py
from fastmcp import FastMCP
from pydantic import BaseModel

mcp = FastMCP("Weather")

class WeatherData(BaseModel):
    temperature: float
    condition: str

@mcp.tool()
async def get_weather(location: str) -> str:
    """获取天气信息"""
    return f"{location} 的天气是晴天,温度 25°C"

@mcp.tool()
async def get_detailed_weather(location: str) -> WeatherData:
    """获取详细天气数据(结构化输出)"""
    return WeatherData(temperature=25.0, condition="sunny")

@mcp.resource("weather://{location}/history")
async def get_weather_history(location: str) -> str:
    """返回历史天气数据"""
    return f"{location} 过去7天天气记录..."

if __name__ == "__main__":
    mcp.run(transport="streamable-http")
4.3.5 有状态 Session 示例
from langchain_mcp_adapters.tools import load_mcp_tools
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent

client = MultiServerMCPClient({...})

# 创建持久会话
async with client.session("math") as session:
    # 在会话中加载工具
    tools = await load_mcp_tools(session)

    agent = create_agent(
        "claude-sonnet-4-5-20250929",
        tools
    )

    # 多次调用共享会话状态
    result = await agent.ainvoke({
        "messages": [{"role": "user", "content": "设置变量 x=10,然后计算 x+5"}]
    })

4.4 核心特性详解

4.4.1 Tools(工具)

from fastmcp import FastMCP
from pydantic import BaseModel

mcp = FastMCP("Data")

class UserProfile(BaseModel):
    name: str
    age: int
    email: str

@mcp.tool()
async def get_user_profile(user_id: str) -> dict:
    """获取用户资料(返回结构化数据)"""
    return {
        "text": f"用户 {user_id} 的资料已找到",
        "structured_content": {
            "name": "张三",
            "age": 30,
            "email": "zhangsan@example.com"
        }
    }

# 客户端访问结构化内容
from langchain.messages import ToolMessage

result = await agent.ainvoke({"messages": [...]})
for message in result["messages"]:
    if isinstance(message, ToolMessage) and message.artifact:
        data = message.artifact["structured_content"]
        print(f"姓名: {data['name']}, 年龄: {data['age']}")
        
多模态内容(Multimodal Content)

客户端示例:

from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent

client = MultiServerMCPClient({...})
tools = await client.get_tools()
agent = create_agent(model, tools)

result = await agent.ainvoke(
    {"messages": [{"role": "user", "content": "截取当前页面的屏幕截图"}]}
)

# 访问多模态内容
for message in result["messages"]:
    if message.type == "tool":
        # 以提供方本地格式提供的原始内容
        print(f"Raw content: {message.content}")

        # 标准化的内容块
        for block in message.content_blocks:  
            if block["type"] == "text":  
                print(f"Text: {block['text']}")  
            elif block["type"] == "image":  
                print(f"Image URL: {block.get('url')}")  
                print(f"Image base64: {block.get('base64', '')[:50]}...")

4.4.2 Resources(资源)

客户端示例

from langchain_mcp_adapters.client import MultiServerMCPClient

client = MultiServerMCPClient({...})

# 加载所有资源
blobs = await client.get_resources("server_name")

# 或加载特定资源
blobs = await client.get_resources(
    "server_name",
    uris=["file:///path/to/config.json"]
)

for blob in blobs:
    print(f"URI: {blob.metadata['uri']}")
    print(f"MIME: {blob.mimetype}")
    content = blob.as_string()  # 文本内容
    # 或 blob.as_bytes() 获取二进制数据

服务端示例

from fastmcp import FastMCP

mcp = FastMCP("Files")

@mcp.resource("config://app.json")
async def get_config() -> str:
    """返回配置文件"""
    return '{"version": "1.0", "debug": true}'

@mcp.resource("docs://{category}/{filename}")
async def get_document(category: str, filename: str) -> str:
    """参数化资源路径"""
    return f"文档内容:{category}/{filename}"

4.4.3 Prompts(提示模板)

客户端示例:

from langchain_mcp_adapters.client import MultiServerMCPClient

client = MultiServerMCPClient({...})

# 加载提示模板
messages = await client.get_prompt(
    "server_name",
    "code_review",
    arguments={"language": "python", "focus": "security"}
)

# 使用提示
for message in messages:
    print(f"{message.type}: {message.content}")

服务端示例:

from fastmcp import FastMCP

mcp = FastMCP("Prompts")

@mcp.prompt()
def code_review(language: str, focus: str = "general") -> str:
    """生成代码审查提示"""
    return f"""请审查以下 {language} 代码,重点关注{focus}方面:
    1. 代码质量
    2. 潜在bug
    3. 最佳实践
    """

@mcp.prompt()
def summarize(style: str = "concise") -> list:
    """生成摘要提示(返回消息列表)"""
    return [
        {"role": "system", "content": f"你是一个{style}的摘要助手"},
        {"role": "user", "content": "请总结以下内容"}
    ]

4.5 高级特性 (扩展 可跳过)

4.5.1 工具拦截器(Tool Interceptors)

示例 1:注入用户上下文

from dataclasses import dataclass
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.interceptors import MCPToolCallRequest
from langchain.agents import create_agent

@dataclass
class Context:
    user_id: str
    api_key: str

async def inject_user_context(
    request: MCPToolCallRequest,
    handler,
):
    """将用户凭证注入到工具调用中"""
    runtime = request.runtime
    user_id = runtime.context.user_id  # 访问运行时上下文
    api_key = runtime.context.api_key

    # 修改工具参数
    modified_request = request.override(
        args={**request.args, "user_id": user_id}
    )
    return await handler(modified_request)

client = MultiServerMCPClient(
    {...},
    tool_interceptors=[inject_user_context],
)
tools = await client.get_tools()
agent = create_agent(model, tools, context_schema=Context)

# 调用时传入上下文
result = await agent.ainvoke(
    {"messages": [{"role": "user", "content": "查询我的订单"}]},
    context={"user_id": "user_123", "api_key": "sk-..."}
)

示例 2:访问 Store(长期记忆)

from dataclasses import dataclass
from langchain_mcp_adapters.interceptors import MCPToolCallRequest
from langgraph.store.memory import InMemoryStore

@dataclass
class Context:
    user_id: str

async def personalize_search(
    request: MCPToolCallRequest,
    handler,
):
    """基于用户偏好个性化搜索"""
    runtime = request.runtime
    user_id = runtime.context.user_id
    store = runtime.store  # 访问存储

    # 读取用户偏好
    prefs = store.get(("preferences",), user_id)

    if prefs and request.name == "search":
        # 应用用户的语言和结果限制偏好
        modified_args = {
            **request.args,
            "language": prefs.value.get("language", "zh"),
            "limit": prefs.value.get("result_limit", 10),
        }
        request = request.override(args=modified_args)

    return await handler(request)

client = MultiServerMCPClient(
    {...},
    tool_interceptors=[personalize_search],
)
agent = create_agent(
    model,
    tools,
    context_schema=Context,
    store=InMemoryStore()
)

示例 3:基于状态的权限控制

from langchain.messages import ToolMessage

async def require_authentication(
    request: MCPToolCallRequest,
    handler,
):
    """阻止未认证用户调用敏感工具"""
    runtime = request.runtime
    state = runtime.state  # 访问对话状态
    is_authenticated = state.get("authenticated", False)

    sensitive_tools = ["delete_file", "update_settings", "export_data"]

    if request.name in sensitive_tools and not is_authenticated:
        # 返回错误而不是执行工具
        return ToolMessage(
            content="需要认证。请先登录。",
            tool_call_id=runtime.tool_call_id,# 访问工具调用 ID
        )

    return await handler(request)

client = MultiServerMCPClient(
    {...},
    tool_interceptors=[require_authentication],
)

拦截器组合(洋葱模式)

async def outer_interceptor(request, handler):
    print("外层: 执行前")
    result = await handler(request)
    print("外层: 执行后")
    return result

async def inner_interceptor(request, handler):
    print("内层: 执行前")
    result = await handler(request)
    print("内层: 执行后")
    return result

client = MultiServerMCPClient(
    {...},
    tool_interceptors=[outer_interceptor, inner_interceptor],
)

# 执行顺序:
# 外层: 执行前 → 内层: 执行前 → 工具执行 → 内层: 执行后 → 外层: 执行后

状态更新与命令(Command)

from langgraph.types import Command

async def handle_task_completion(
    request: MCPToolCallRequest,
    handler,
):
    """标记任务完成并切换到summary agent"""
    result = await handler(request)

    if request.name == "submit_order":
        return Command(
            update={
                "messages": [result] if isinstance(result, ToolMessage) else [],
                "task_status": "completed",  # 更新状态
            },
            goto="summary_agent",  # 切换到下一个节点
        )

    return result

async def end_on_success(
    request: MCPToolCallRequest,
    handler,
):
    """任务完成时提前结束"""
    result = await handler(request)

    if request.name == "mark_complete":
        return Command(
            update={"messages": [result], "status": "done"},
            goto="__end__",  # 结束执行
        )

    return result

4.5.2 进度通知(Progress Notifications)

from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.callbacks import Callbacks, CallbackContext

async def on_progress(
    progress: float,
    total: float | None,
    message: str | None,
    context: CallbackContext,
):
    """处理进度更新"""
    percent = (progress / total * 100) if total else progress
    tool_info = f" ({context.tool_name})" if context.tool_name else ""
    print(f"[{context.server_name}{tool_info}] 进度: {percent:.1f}% - {message}")

client = MultiServerMCPClient(
    {...},
    callbacks=Callbacks(on_progress=on_progress),
)

4.5.3 日志记录(Logging)

from langchain_mcp_adapters.callbacks import Callbacks, CallbackContext
from mcp.types import LoggingMessageNotificationParams

async def on_logging_message(
    params: LoggingMessageNotificationParams,
    context: CallbackContext,
):
    """处理服务器日志"""
    print(f"[{context.server_name}] {params.level}: {params.data}")

client = MultiServerMCPClient(
    {...},
    callbacks=Callbacks(on_logging_message=on_logging_message),
)

4.5.4 交互式输入(Elicitation)

服务端示例:

from pydantic import BaseModel
from mcp.server.fastmcp import Context, FastMCP

server = FastMCP("Profile")

class UserDetails(BaseModel):
    email: str
    age: int

@server.tool()
async def create_profile(name: str, ctx: Context) -> str:
    """创建用户资料,通过 elicitation 请求详细信息"""
    result = await ctx.elicit(
        message=f"请提供 {name} 的详细信息:",
        schema=UserDetails,
    )
    if result.action == "accept" and result.data:
        return f"为 {name} 创建资料: email={result.data.email}, age={result.data.age}"
    if result.action == "decline":
        return f"用户拒绝。为 {name} 创建了最小资料。"
    return "资料创建已取消。"

if __name__ == "__main__":
    server.run(transport="http")

客户端处理:

from mcp.shared.context import RequestContext
from mcp.types import ElicitRequestParams, ElicitResult

async def on_elicitation(
    mcp_context: RequestContext,
    params: ElicitRequestParams,
    context: CallbackContext,
) -> ElicitResult:
    """处理 elicitation 请求"""
    # 实际应用中,这里会提示用户输入
    # 基于 params.message 和 params.requestedSchema
    return ElicitResult(
        action="accept",
        content={"email": "user@example.com", "age": 25},
    )

client = MultiServerMCPClient(
    {...},
    callbacks=Callbacks(on_elicitation=on_elicitation),
)

模块四:持久化与内存管理

  1. 短期内存:Checkpointer(线程级持久化)

2.2 开发环境:InMemorySaver

2.2.1 基本使用
import os

from dotenv import load_dotenv
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.checkpoint.memory import MemorySaver
from langchain_openai import ChatOpenAI

load_dotenv()

# 步骤 1️:初始化检查点
checkpointer = MemorySaver()

# 步骤 2️:定义简单的对话节点
model = ChatOpenAI(
        model=os.getenv("QWEN_MODEL_NLP", "qwen-max"),
        base_url=os.getenv("QWEN_BASE_URL"),
        api_key=os.getenv("QWEN_API_KEY"),
        temperature=0.3,
    )

def chatbot(state: MessagesState):
    return {"messages": [model.invoke(state["messages"])]}

# 步骤 3️:构建图并编译(关键!)
builder = StateGraph(MessagesState)
builder.add_node("chat", chatbot)
builder.add_edge(START, "chat")
builder.add_edge("chat", END)

#  编译时传入 checkpointer
graph = builder.compile(checkpointer=checkpointer)
2.2.2 多轮对话示例
# 定义配置,区分不同会话
thread_config = {"configurable": {"thread_id": "session_user_123"}}

# 第一次对话
result1 = graph.invoke(
    {"messages": [HumanMessage(content="你好,我叫张三")]},
    config=thread_config
)
print(result1["messages"][-1].content)

# 第二次对话(Agent 会自动读取 thread_id 对应的历史状态)
result2 = graph.invoke(
    {"messages": [HumanMessage(content="我刚才说我叫什么?")]},
    config=thread_config
)
print(result2["messages"][-1].content)  # 应该回答:你说你叫张三

# 其他会话(不同的 thread_id)
result3 = graph.invoke(
    {"messages": [HumanMessage(content="我刚才说了什么?")]},
    config={"configurable": {"thread_id": "session_user_456"}}
)
print(result3["messages"][-1].content)  # 无法回答,因为这是新会话

2.3 生产环境:数据库持久化

2.3.2 PostgreSQL 实现

依赖安装

pip install langgraph langgraph-checkpoint-postgres psycopg[binary,pool]

同步版本完整代码 (可以直接跑)

import os
from typing import Annotated

from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.postgres import PostgresSaver
from psycopg_pool import ConnectionPool

load_dotenv()

model = ChatOpenAI(
        model=os.getenv("QWEN_MODEL_NLP", "qwen-max"),
        base_url=os.getenv("QWEN_BASE_URL"),
        api_key=os.getenv("QWEN_API_KEY"),
        temperature=0.3,
    )

# ================= 1. 定义一个简单的 Graph (为了让代码能跑) =================
class State(TypedDict):
    messages: Annotated[list, add_messages]


def chatbot(state: State):
    return {"messages": [model.invoke(state.get("messages"))]}


builder = StateGraph(State)
builder.add_node("chatbot", chatbot)
builder.add_edge(START, "chatbot")
builder.add_edge("chatbot", END)

# ================= 2. PostgreSQL 配置与运行 =================

# 请确保数据库已存在,替换你的连接字符串
DB_URI = os.getenv("DB_URI")

# 使用上下文管理器确保连接池正确开启和关闭
# 如果你不使用 context manager (with 语句),需要手动调用 pool.open()
with ConnectionPool(conninfo=DB_URI, min_size=1, max_size=10) as pool:
    # 初始化 Checkpointer
    checkpointer = PostgresSaver(pool)

    # 重点: 首次运行时必须执行 setup() 创建表结构
    # checkpointer.setup()

    # 编译图
    graph = builder.compile(checkpointer=checkpointer)

    # ================= 3. 执行测试 =================
    thread_id = "postgres_user_001"
    config = {"configurable": {"thread_id": thread_id}}

    print(f"--- 开始对话 (Thread: {thread_id}) ---")
    # 第一轮对话
    input_data = {"messages": [("user", "你好,Postgres!")]}
    result = graph.invoke(input_data, config)
    print(f"回复: {result['messages'][-1].content}")

    # 验证记忆(再次发送,查看是否延续之前的 thread_id)
    result2 = graph.invoke({"messages": [("user", "我刚才说了什么?")]}, config)
    print(f"回复2: {result2['messages'][-1].content}")

异步版本 (FastAPI 常用)

import os
import sys  # 新增
import asyncio
from typing import Annotated

from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver  # 修正引用位置

load_dotenv()

# 确保读取到了环境变量,否则后面会报错
if not os.getenv("DB_URI"):
    raise ValueError("请在 .env 文件中配置 DB_URI")

model = ChatOpenAI(
    model=os.getenv("QWEN_MODEL_NLP", "qwen-max"),
    base_url=os.getenv("QWEN_BASE_URL"),
    api_key=os.getenv("QWEN_API_KEY"),
    temperature=0.3,
)


# ================= 1. 定义 Graph =================
class State(TypedDict):
    messages: Annotated[list, add_messages]


def chatbot(state: State):
    return {"messages": [model.invoke(state.get("messages"))]}


builder = StateGraph(State)
builder.add_node("chatbot", chatbot)
builder.add_edge(START, "chatbot")
builder.add_edge("chatbot", END)


# ================= 2. 异步执行主逻辑 =================
async def main():
    DB_URI = os.getenv("DB_URI")

    # 异步连接必须使用 context manager
    async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
        # 第一次运行时必须取消注释,用于创建数据库表结构
        # 运行一次后可以再次注释掉,或者保留着(它是幂等的,不会重复创建)
        await checkpointer.setup()

        graph = builder.compile(checkpointer=checkpointer)

        config = {"configurable": {"thread_id": "async_pg_002"}}

        print("--- 异步 PostgreSQL 测试 ---")

        # 第一轮对话
        print("User: Hello Async DB")
        response1 = await graph.ainvoke({"messages": [("user", "Hello Async DB")]}, config)
        print(f"Bot: {response1['messages'][-1].content}")

        # 第二轮对话(测试记忆)
        print("\nUser: 我刚才说了什么")
        response2 = await graph.ainvoke({"messages": [("user", "我刚才说了什么")]}, config)
        print(f"Bot: {response2['messages'][-1].content}")

        print("执行完成")


if __name__ == "__main__":
    # 解决 Windows 下 psycopg "ProactorEventLoop" 错误
    if sys.platform == 'win32':
        asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

    asyncio.run(main())
2.3.3 Redis 实现

依赖安装

pip install langgraph langgraph-checkpoint-redis redis

同步版本完整代码

import os
import redis
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.redis import RedisSaver

load_dotenv()

# 确保读取到了环境变量,否则后面会报错
if not os.getenv("DB_URI"):
    raise ValueError("请在 .env 文件中配置 DB_URI")

model = ChatOpenAI(
    model=os.getenv("QWEN_MODEL_NLP", "qwen-max"),
    base_url=os.getenv("QWEN_BASE_URL"),
    api_key=os.getenv("QWEN_API_KEY"),
    temperature=0.3,
)


# ================= 1. 定义 Graph =================
class State(TypedDict):
    messages: Annotated[list, add_messages]


def chatbot(state: State):
    return {"messages": [model.invoke(state.get("messages"))]}


builder = StateGraph(State)
builder.add_node("chatbot", chatbot)
builder.add_edge(START, "chatbot")
builder.add_edge("chatbot", END)


# ================= 2. Redis 配置与运行 =================
# 建立连接
redis_client = redis.Redis(
    host=os.getenv("REDIS_HOST"),
    port=os.getenv("REDIS_PORT"),
    db=12,
    password=os.getenv("REDIS_PASSWORD"), # 如果有密码请取消注释
    #decode_responses=False # 注意:LangGraph 序列化通常处理二进制,这里建议保持 False 或根据具体库版本调整
)

# 初始化 Checkpointer
checkpointer = RedisSaver(redis_client=redis_client)

# 编译
graph = builder.compile(checkpointer=checkpointer)

# ================= 3. 执行测试 =================
config = {"configurable": {"thread_id": "redis_user_999"}}

print(f"--- Redis 对话 ---")
result = graph.invoke({"messages": [("user", "Redis 准备好了吗?")]}, config)
print(f"Bot: {result['messages'][-1].content}")

# 验证 Key 是否存在
print(f"Key 数量: {len(redis_client.keys('checkpoint:*'))}")
  1. 长期内存:Store(跨会话持久化)

3.2 Store 基础用法

3.2.1 基本操作
from langgraph.store.memory import InMemoryStore

# 1. 初始化 Store
store = InMemoryStore()

# 2. 存储数据(使用命名空间组织)
store.put(
    namespace=('user_preferences', 'user_123'),
    key='language',
    value={'lang': 'zh-CN', 'tone': 'professional'}
)

# 3. 检索数据
preference = store.get(('user_preferences', 'user_123'), 'language')
print(preference)  # {'lang': 'zh-CN', 'tone': 'professional'}

# 4. 列出所有 key
keys = store.search(('user_preferences', 'user_123'))

3.3 语义搜索

3.3.1 配置语义搜索
from langchain_community.embeddings import DashScopeEmbeddings
from langgraph.store.memory import InMemoryStore

# 1. 配置 Embedding 模型
embeddings = DashScopeEmbeddings(
    model='text-embedding-v2',
    dashscope_api_key=os.getenv('DASHSCOPE_API_KEY')
)

# 2. 创建支持语义搜索的 Store
store = InMemoryStore(
    index={'dims': 1536, 'embed': embeddings}
)
3.3.2 语义搜索示例
# 存入多条记忆
memories = [
    '我喜欢吃意大利菜,尤其是披萨和意面',
    '我最近在学习 Python 和机器学习',
    '我养了一只叫 Luna 的猫',
    '我每周末都去健身房锻炼'
]

for i, memory in enumerate(memories):
    store.put(
        namespace=('user_memories', 'user_123'),
        key=str(i),
        value={'text': memory}
    )

# 语义搜索(根据问题找相关记忆)
results = store.search(
    ('user_memories', 'user_123'),
    query='用户的饮食习惯是什么?',
    limit=2
)

for item in results:
    print(item.value['text'])
# 输出:我喜欢吃意大利菜,尤其是披萨和意面

3.4 生产环境 Store 配置

3.4.1 PostgreSQL Store(推荐)

安装依赖

pip install -U "psycopg[binary,pool]" langgraph langgraph-checkpoint-postgres

基本使用

from langgraph.store.postgres import PostgresStore

DB_URI = 'postgresql://user:pass@localhost:5432/langgraph_store'

# 创建 Store
with PostgresStore.from_conn_string(DB_URI) as store:
    # 启用 PGVector 扩展进行向量搜索
    store.setup()  # 自动创建表和索引
    
    # 配置 Embedding
    from langchain_openai import OpenAIEmbeddings
    embeddings = OpenAIEmbeddings()
    
    # 编译图
    graph = builder.compile(
        store=store,
        checkpointer=checkpointer
    )

4.2 策略一:修剪消息(Trim Messages)

代码示例

from typing import Literal
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_core.messages.utils import trim_messages, count_tokens_approximately
from langgraph.graph import StateGraph, MessagesState, START, END

# 初始化模型
model = ChatOpenAI(model="gpt-4o-mini")

# 定义处理节点的逻辑
def call_model_with_trimming(state: MessagesState):
    # --- 核心逻辑开始 ---
    # 在调用模型前,先修剪消息
    # 策略:保留最后 max_tokens 个 token,且保证从 Human 消息开始(避免切断对话流)
    trimmed_messages = trim_messages(
        state["messages"],
        strategy="last",             # 保留最新的
        token_counter=count_tokens_approximately, # 估算 Token 数
        max_tokens=100,              # 设定非常严格的限制用于演示
        start_on="human",            # 确保第一条是用户发的消息(避免只有 AI 回复)
        end_on=("human", "tool"),    # 确保以有效消息结束
        include_system=True          # 始终保留 SystemMessage (如果有)
    )
    # --- 核心逻辑结束 ---
    
    print(f"Debug: 实际发送给模型的消息数: {len(trimmed_messages)}")
    response = model.invoke(trimmed_messages)
    return {"messages": [response]}

# 构建图
builder = StateGraph(MessagesState)
builder.add_node("bot", call_model_with_trimming)
builder.add_edge(START, "bot")
builder.add_edge("bot", END)

# 编译
graph = builder.compile()

4.3 策略二:删除消息(Remove Messages)

代码示例

from langchain_core.messages import RemoveMessage
from langgraph.graph import StateGraph, MessagesState, START, END

# 定义清理逻辑
def manage_memory(state: MessagesState):
    messages = state["messages"]
    # 假设我们只想在数据库中保留最新的 4 条消息(2轮对话)
    if len(messages) > 4:
        # 计算需要删除的消息:除了最后 4 条之外的所有消息
        num_to_remove = len(messages) - 4
        # 取出最早的若干条消息
        messages_to_remove = messages[:num_to_remove]
        
        print(f"Debug: 正在物理删除 {len(messages_to_remove)} 条旧消息...")
        
        # 返回 RemoveMessage 对象列表,LangGraph 收到后会执行删除操作
        return {"messages": [RemoveMessage(id=m.id) for m in messages_to_remove]}
    return {}

def simple_bot(state: MessagesState):
    # 这里是一个简单的回声机器人模拟
    last_msg = state["messages"][-1]
    return {"messages": [("ai", f"收到:{last_msg.content}")]}

# 构建图
builder = StateGraph(MessagesState)
builder.add_node("bot", simple_bot)
builder.add_node("cleaner", manage_memory) # 专门的清理节点

# 流程:开始 -> 机器人回复 -> 清理旧消息 -> 结束
builder.add_edge(START, "bot")
builder.add_edge("bot", "cleaner")
builder.add_edge("cleaner", END)

app = builder.compile()

4.4 策略三:总结消息(Summarize Messages)

代码示例

import os

from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage, RemoveMessage
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.checkpoint.memory import MemorySaver  # 必须引入检查点
load_dotenv()
# 1. 初始化 Qwen 模型
model = ChatOpenAI(
    model=os.getenv("QWEN_MODEL_NLP", "qwen-max"),
    base_url=os.getenv("QWEN_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1"),
    api_key=os.getenv("QWEN_API_KEY"),
    temperature=0.3,
)


# 定义状态
class State(MessagesState):
    summary: str


# 2. 核心对话节点 (自动注入摘要)
def call_model(state: State):
    msgs = state["messages"]
    # 如果存在摘要,将其作为 SystemMessage 插入到队首
    if state.get("summary"):
        msgs = [SystemMessage(content=f"前情摘要: {state['summary']}")] + msgs
    return {"messages": [model.invoke(msgs)]}


# 3. 摘要生成节点 (生成 + 清理)
def summarize_node(state: State):
    summary = state.get("summary", "")
    # 构造更简洁的总结 Prompt
    prompt = f"当前摘要:{summary}\n请结合上述新消息更新摘要,保留关键信息(如姓名、喜好等):" if summary else "请总结上述对话的关键信息:"

    # 生成新摘要 (只基于最近消息和 Prompt)
    new_summary = model.invoke(state["messages"] + [HumanMessage(content=prompt)]).content
    print(f"\n[系统日志] 触发记忆压缩 -> 更新摘要: {new_summary[:30]}...")

    # 删除旧消息:保留最后 2 条消息用于上下文衔接,其余删除
    delete_msgs = [RemoveMessage(id=m.id) for m in state["messages"][:-2]]
    return {"summary": new_summary, "messages": delete_msgs}


# 4. 判断逻辑 (每 6 条消息触发一次总结)
def should_summarize(state: State):
    return "summarize" if len(state["messages"]) > 6 else END


# 5. 构建图
workflow = StateGraph(State)
workflow.add_node("chat", call_model)
workflow.add_node("summarize", summarize_node)

workflow.add_edge(START, "chat")
workflow.add_conditional_edges("chat", should_summarize, {"summarize": "summarize", END: END})
workflow.add_edge("summarize", END)

# 编译 (必须加上 checkpointer 才能在多轮对话中记住状态)
app = workflow.compile(checkpointer=MemorySaver())

# 6. 运行测试
if __name__ == '__main__':
    config = {"configurable": {"thread_id": "qwen_memory_test"}}

    questions = [
        "我叫 Alice,我有一只叫 Luna 的猫。",
        "Luna 喜欢吃金枪鱼罐头。",
        "它今年三岁了。",
        "它最讨厌洗澡。",  # 第4条
        "我的猫叫什么名字?它喜欢吃什么?"  # 第5条,交互后总数变多,将触发总结
    ]

    for q in questions:
        print(f"\nUser: {q}")
        # 只需要传入新消息,历史记录由 checkpointer 自动管理
        res = app.invoke({"messages": [HumanMessage(content=q)]}, config)
        print(f"Qwen: {res['messages'][-1].content}")
  1. 管理检查点(Checkpoint Management)

5.1 查看线程状态

查看当前或特定检查点的状态:

config = {
    "configurable": {
        "thread_id": "session_user_123",
        # 可选:指定特定检查点 ID
        # "checkpoint_id": "1f029ca3-1f5b-6704-8004-820c16b69a5a"
    }
}

# 获取当前状态
state = graph.get_state(config)
print(state)

返回结果示例

StateSnapshot(
    values= {'messages': [
        HumanMessage(content="你好,我叫张三"), 
        AIMessage(content='你好张三!有什么可以帮你的吗?'),
        HumanMessage(content="我叫什么名字?"), 
        AIMessage(content='你叫张三。')
    ]}, 
    next=(),
    config={'configurable': {'thread_id': 'session_user_123', ...}},
    metadata={'source': 'loop', 'step': 4, ...},
    created_at='2025-05-05T16:01:24.680462+00:00',
    ...
)

5.2 查看线程历史

查看一个线程的所有检查点历史(版本控制):

config = {"configurable": {"thread_id": "session_user_123"}}

# 获取所有历史快照
history = list(graph.get_state_history(config))

for snapshot in history:
    print(f"步骤 {snapshot.metadata['step']}: {snapshot.values['messages'][-1].content}")
  1. 完整实战案例

7.3 核心功能实现

import os
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
from langchain_core.messages import SystemMessage
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.checkpoint.postgres import PostgresSaver
from langgraph.store.postgres import PostgresStore
from psycopg_pool import ConnectionPool

load_dotenv()

DB_URI = os.getenv('DATABASE_URL')
model = ChatTongyi(
    model="qwen-max",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    temperature=0.7
)

# 初始化存储(共用同一个连接池,不需要 with 上下文管理器)
pool = ConnectionPool(conninfo=DB_URI, min_size=5, max_size=20,kwargs={"autocommit": True})
checkpointer = PostgresSaver(pool)
store = PostgresStore(pool)

# 首次运行时创建表
checkpointer.setup()
store.setup()
print("数据库表初始化成功!")


# ==================== 节点定义 ====================

def customer_service_bot(state: MessagesState, config: RunnableConfig):
    """智能客服主节点"""

    # 1. 获取用户偏好
    user_id = config['configurable']['user_id']
    item = store.get(('user_preferences', user_id), 'settings')

    # 2. 构建系统提示词
    system_prompt = '你是一个专业的客服助手。'
    if item:
        preferences = item.value
        lang = preferences.get('language', 'zh-CN')
        tone = preferences.get('tone', 'professional')
        system_prompt += f'\n请使用{lang}语言,保持{tone}风格。'

    # 3. 调用模型
    messages = [SystemMessage(content=system_prompt)] + state['messages']
    response = model.invoke(messages)

    return {'messages': [response]}


# ==================== 构建图 ====================

builder = StateGraph(MessagesState)
builder.add_node('customer_service', customer_service_bot)
builder.add_edge(START, 'customer_service')
builder.add_edge('customer_service', END)

app = builder.compile(
    checkpointer=checkpointer,  # 短期记忆
    store=store  # 长期记忆
)


# ==================== 使用示例 ====================

config = {
    'configurable': {
        'thread_id': 'customer_001',
        'user_id': 'user_123'
    }
}

if __name__ == '__main__':
    try:
        # 第一次对话
        response1 = app.invoke(
            {'messages': [('user', '你好,我想咨询订单问题')]},
            config=config
        )
        print(response1['messages'][-1].content)

        # 第二次对话(自动读取历史)
        response2 = app.invoke(
            {'messages': [('user', '我的订单号是 12345')]},
            config=config
        )
        print(response2['messages'][-1].content)

        # 第三次对话
        response3 = app.invoke(
            {'messages': [('user', '我刚才说的订单号是多少?')]},
            config=config
        )
        print(response3['messages'][-1].content)
    finally:
        pool.close()

模块五: 人机协作与流式输出

人机协作

  1. 实战 :关键步骤需人工确认的助手

"""
研报助手 V4 - 人工审核闭环(单文件完整版)
=============================================
运行前请设置环境变量:
  export DASHSCOPE_API_KEY=sk-xxx
  export TAVILY_API_KEY=tvly-xxx

或者直接在下方【配置区域】中填写。

依赖安装:
  pip install langgraph langchain langchain-community tavily-python langchain-community[tongyi]

流程:
  用户输入主题 → AI 搜索撰写 → 暂停等人工审核
    → 批准 → 发布
    → 修改意见 → AI 重写 → 再次审核 → ...
"""

import os
import uuid
from typing import Annotated, Literal

from dotenv import load_dotenv
from typing_extensions import TypedDict
from langchain_core.messages import AIMessage, HumanMessage
from langchain_community.chat_models.tongyi import ChatTongyi
from langchain_community.tools.tavily_search import TavilySearchResults
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import ToolNode


load_dotenv()


# ============================================================
# 1. 状态定义
# ============================================================

class State(TypedDict):
    messages: Annotated[list, add_messages]


# ============================================================
# 2. LLM 和工具初始化
# ============================================================

model = ChatTongyi(
    model="qwen-max",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    temperature=0.7,
)

search_tool = TavilySearchResults(max_results=3)
tools = [search_tool]
model_with_tools = model.bind_tools(tools)


# ============================================================
# 3. 节点定义
# ============================================================

def writer_node(state):
    """写手节点:调用带搜索工具的 LLM 进行撰写或重写"""
    messages = state["messages"]
    response = model_with_tools.invoke(messages)
    return {"messages": [response]}


# 工具执行节点(自动处理 LLM 发出的 tool_calls)
tool_node = ToolNode(tools=tools)


def human_review_node(state):
    """
    人工审核占位节点。
    本身不执行逻辑,作为 interrupt_before 的挂载点,
    让程序在此处暂停,等待人工输入。
    """
    return state


def publisher_node(state):
    """发布节点:审核通过后执行(可扩展为存数据库、发邮件等)"""
    print("\n" + "=" * 50)
    print(">>> [系统] 研报已正式发布!已归档并发送邮件。 <<<")
    print("=" * 50 + "\n")
    return {"messages": [AIMessage(content="✅ 研报已正式发布,流程结束。")]}


# ============================================================
# 4. 路由函数
# ============================================================

def should_use_tools(state) -> Literal["tools", "human_review"]:
    """
    检查 writer 的输出:
    - 如果 LLM 请求了工具调用 → 去 tools 节点执行
    - 如果没有工具调用(已完成撰写)→ 去人工审核
    """
    messages = state["messages"]
    last_message = messages[-1]
    if hasattr(last_message, "tool_calls") and last_message.tool_calls:
        return "tools"
    return "human_review"


def review_router(state) -> Literal["publisher", "writer"]:
    """
    根据人工审核后追加的消息,决定下一步:
    - 包含"批准/通过/approve" → 发布
    - 其他内容 → 视为修改意见,回退给 writer 重写
    """
    messages = state["messages"]
    last_message = messages[-1]
    content = last_message.content.lower() if hasattr(last_message, "content") else ""

    approve_keywords = ["批准", "通过", "approve", "ok", "确认", "发布"]
    if any(kw in content for kw in approve_keywords):
        return "publisher"
    return "writer"


# ============================================================
# 5. 构建并编译图
# ============================================================

workflow = StateGraph(State)

# 添加节点
workflow.add_node("writer", writer_node)
workflow.add_node("tools", tool_node)
workflow.add_node("human_review", human_review_node)
workflow.add_node("publisher", publisher_node)

# 连线
workflow.add_edge(START, "writer")

workflow.add_conditional_edges(
    "writer",
    should_use_tools,
    {"tools": "tools", "human_review": "human_review"},
)

workflow.add_edge("tools", "writer")  # 工具执行完回到写手继续撰写

workflow.add_conditional_edges(
    "human_review",
    review_router,
    {"publisher": "publisher", "writer": "writer"},
)

workflow.add_edge("publisher", END)

# 编译(MemorySaver 支持暂停和恢复)
memory = MemorySaver()
app_v4 = workflow.compile(
    checkpointer=memory,
    interrupt_before=["human_review"],  # 关键:在人工审核节点前暂停
)


# ============================================================
# 6. 辅助函数(复制即可)
# ============================================================

def print_separator(char="─", length=60):
    print(char * length)


def print_ai_response(messages):
    """打印 AI 最新的回复"""
    for msg in reversed(messages):
        if hasattr(msg, "type") and msg.type == "ai" and msg.content:
            print("\n[AI 草稿]:")
            print_separator()
            print(msg.content)
            print_separator()
            return


# ============================================================
# 7. 主程序入口(复制即可)
# ============================================================

def run():
    thread_id = str(uuid.uuid4())
    config = {"configurable": {"thread_id": thread_id}}

    print("\n" + "=" * 60)
    print("  📊 研报助手 V4 - 人工审核版")
    print("=" * 60)

    # ----------------------------------------------------------
    # 第一步:用户输入研报主题
    # ----------------------------------------------------------
    topic = input("\n请输入研报主题(例:2026年新能源汽车市场分析): ").strip()
    if not topic:
        topic = "2024年全球AI芯片市场分析"
        print(f"使用默认主题:{topic}")

    initial_prompt = f"""你是一位专业的研究分析师。请针对以下主题撰写一份研究报告草稿。

主题:{topic}

要求:
1. 先使用搜索工具查找最新的相关资料
2. 基于搜索结果,撰写一份结构清晰的研报草稿
3. 研报需包含:摘要、行业背景、市场现状、关键趋势、风险分析、结论与建议
4. 引用数据需标明来源
5. 使用中文撰写

请开始。"""

    # ----------------------------------------------------------
    # 第二步:AI 搜索 + 撰写(会在 human_review 前自动暂停)
    # ----------------------------------------------------------
    print("\n⏳ AI 正在搜索资料并撰写草稿,请稍候...\n")

    for event in app_v4.stream(
        {"messages": [HumanMessage(content=initial_prompt)]},
        config=config,
        stream_mode="values",
    ):
        if "messages" in event:
            last_msg = event["messages"][-1]
            if hasattr(last_msg, "type"):
                if last_msg.type == "ai" and hasattr(last_msg, "tool_calls") and last_msg.tool_calls:
                    for tc in last_msg.tool_calls:
                        print(f"  🔍 正在调用工具: {tc['name']}")
                elif last_msg.type == "tool":
                    print(f"  ✅ 工具返回结果({len(last_msg.content)} 字符)")

    # 图已在 human_review 前暂停
    current_state = app_v4.get_state(config)
    print_ai_response(current_state.values["messages"])

    # ----------------------------------------------------------
    # 第三步:人工审核循环
    # ----------------------------------------------------------
    while True:
        print("\n" + "=" * 60)
        print("  👤 人工审核环节")
        print("=" * 60)
        print("  输入 '批准' / '通过'  → 正式发布")
        print("  输入修改意见           → AI 将根据意见重写")
        print("  输入 'quit'           → 退出程序")
        print_separator()

        user_input = input("\n您的审核意见: ").strip()

        if not user_input:
            print("⚠️  请输入审核意见。")
            continue

        if user_input.lower() == "quit":
            print("\n 已退出程序。")
            return

        # 将审核意见注入状态(human-in-the-loop 的核心)
        app_v4.update_state(
            config,
            {"messages": [HumanMessage(content=user_input)]},
        )

        # 恢复执行
        print("\n处理中...\n")

        for event in app_v4.stream(
            None,  # None = 从中断点继续
            config=config,
            stream_mode="values",
        ):
            if "messages" in event:
                last_msg = event["messages"][-1]
                if hasattr(last_msg, "type"):
                    if last_msg.type == "ai" and hasattr(last_msg, "tool_calls") and last_msg.tool_calls:
                        for tc in last_msg.tool_calls:
                            print(f"  🔍 正在调用工具: {tc['name']}")
                    elif last_msg.type == "tool":
                        print(f"  ✅ 工具返回结果({len(last_msg.content)} 字符)")

        # 检查图是否结束
        current_state = app_v4.get_state(config)

        if current_state.next:
            # 又暂停在 human_review 前 → 打印新草稿
            print_ai_response(current_state.values["messages"])
        else:
            # 图已结束 → 发布完成
            print_ai_response(current_state.values["messages"])
            print("\n流程已完成!")
            return


# ============================================================
# 启动
# ============================================================
if __name__ == "__main__":
    run()

Langgraph.json配置:

{
  "dependencies": ["./src"],
  "graphs": {
    "destination_agent": "./package1/test3.py:app_v4"
  },
  "env": ".env",
  "cors": {
    "enabled": true,
    "allow_origins": [
      "https://smith.langchain.com",
      "http://localhost:3000",
      "http://192.168.25.1:3000"
    ],
    "allow_credentials": true
  }
}

模块六: LangGraph 多 Agent 系统(实战版)

二、Subagents 模式:电商运营助手实战

2.2 实战案例:智能电商运营助手

Step 1: 定义底层工具
from langchain.tools import tool

@tool
def create_product(name: str, category: str, price: float, inventory: int) -> str:
    """创建新商品并上架。"""
    return f"商品已上架 - 名称: {name}, 类目: {category}, 价格: ¥{price}, 库存: {inventory}件"

@tool
def update_inventory(sku: str, quantity: int) -> str:
    """更新商品库存数量。"""
    return f"库存已更新 - SKU: {sku}, 新库存: {quantity}件"

@tool
def get_product_analytics(sku: str) -> str:
    """获取商品销售数据分析。"""
    return f"商品 {sku} 数据 - 浏览量: 1234, 销量: 45, 转化率: 3.6%"

@tool
def create_promotion(name: str, promotion_type: str, rules: str) -> str:
    """创建促销活动。"""
    return f"促销已创建 - 名称: {name}, 类型: {promotion_type}, 规则: {rules}"

@tool
def send_marketing_push(title: str, target_audience: str, channel: str) -> str:
    """发送营销推送消息。"""
    return f"推送已发送 - 标题: {title}, 渠道: {channel}, 目标: {target_audience}"
Step 2: 创建专业子 Agent
import os
from langchain.agents import create_agent
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi


load_dotenv()

model = ChatTongyi(
    model="qwen-max",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
)

# 商品管理专家
product_agent = create_agent(
    model,
    tools=[create_product, update_inventory, get_product_analytics],
    system_prompt="""你是电商商品管理专家。

    职责:
    - 创建和上架新商品
    - 管理商品库存
    - 分析商品销售数据
    
    工作原则:
    ✓ 确保商品信息完整准确
    ✓ 在最终消息中包含所有操作结果"""
)

# 营销推广专家
marketing_agent = create_agent(
    model,
    tools=[create_promotion, send_marketing_push],
    system_prompt="""你是电商营销推广专家。

    职责:
    - 创建各类促销活动
    - 制定营销推送策略
    
    工作原则:
    ✓ 活动规则清晰易懂
    ✓ 在最终消息中包含所有操作结果"""
)
Step 3: 将子 Agent 包装为工具
from langchain.tools import InjectedToolCallId
from langchain_core.messages import ToolMessage
from langgraph.types import Command
from typing import Annotated

@tool(
    "manage_product",  #工具名称, 工具的唯一标识符
    description="""商品管理专家。
t
    何时调用:
    - 用户需要上架、修改或下架商品
    - 涉及库存管理和调整
    - 需要商品销售数据分析
    
    输入:自然语言描述的商品管理需求
    示例:"上架一款夏季新款连衣裙,价格299元"""
)
def manage_product(
    request: str,
    tool_call_id: Annotated[str, InjectedToolCallId]  #自动获取当前这次工具调用的唯一身份证(快递单号)
    #InjectedToolCallId:这个值不要让用户或 LLM 传,请框架自动把当前这次工具调用的 ID 塞进来。
) -> Command:
    """调用商品管理专家 Agent"""
    result = product_agent.invoke({
        "messages": [{"role": "user", "content": request}]
    })
    
    final_message = result["messages"][-1].content
    
    return Command(update={
        "messages": [
            ToolMessage(
                content=final_message,
                tool_call_id=tool_call_id
            )
        ]
    })

@tool(
    "create_campaign",
    description="""营销推广专家。

    何时调用:
    - 用户需要创建促销活动
    - 需要发送营销推送
    
    输入:自然语言描述的营销需求
    示例:"创建618大促满减活动"""
)
def create_campaign(
    request: str,
    tool_call_id: Annotated[str, InjectedToolCallId]
) -> Command:
    """调用营销推广专家 Agent"""
    result = marketing_agent.invoke({
        "messages": [{"role": "user", "content": request}]
    })
    
    final_message = result["messages"][-1].content
    
    return Command(update={
        "messages": [
            ToolMessage(
                content=final_message,
                tool_call_id=tool_call_id
            )
        ]
    })
Step 4: 创建 Supervisor
supervisor = create_agent(
    model,
    tools=[manage_product, create_campaign],
    system_prompt="""你是智能电商运营助手的协调者(Supervisor)。

核心能力:
1. 理解用户的复杂运营请求
2. 将任务分解为子任务
3. 选择合适的专家处理各个子任务
4. 整合结果形成完整回复

可用专家:
- manage_product: 处理所有商品管理任务
- create_campaign: 处理所有营销推广任务

工作流程:
1. 分析请求,识别需要哪些专家
2. 可以并行调用多个专家(工具)
3. 整合所有专家结果,提供连贯回复

注意:
- 多领域任务需要调用多个专家
- 给用户完整且易懂的结果"""
)
Step 5: 使用示例
from langchain_core.messages import HumanMessage

# 示例 1:简单单领域请求
print("=== 简单请求 ===")
result = supervisor.invoke({
    "messages": [
        HumanMessage(content="上架一款新的蓝牙耳机,价格199元,库存500件")
    ]
})
print(result["messages"][-1].content)

# 示例 2:复杂多领域请求
print("\n=== 复杂请求 ===")
result = supervisor.invoke({
    "messages": [
        HumanMessage(content="""
        上架夏季新款防晒衣(价格159元,库存1000件),
        同时创建一个"清凉一夏"满199减20的促销活动
        """)
    ]
})
print(result["messages"][-1].content)

2.4 添加人机协同

from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver

# 为子Agent添加审批中间件
product_agent = create_agent(
    model,
    tools=[create_product, update_inventory, get_product_analytics],
    system_prompt=PRODUCT_PROMPT,
    middleware=[
        HumanInTheLoopMiddleware(
            interrupt_on={"create_product": True},
            description_prefix="📦 商品上架待审批",
        )
    ]
)

# Supervisor需要checkpointer来暂停/恢复
supervisor = create_agent(
    model,
    tools=[manage_product, create_campaign],
    system_prompt=SUPERVISOR_PROMPT,
    checkpointer=InMemorySaver(),
)

三、Handoffs 模式:旅行规划助手实战

Step 1: 定义自定义状态
from langchain.agents import AgentState
from typing_extensions import NotRequired
from typing import Literal

PlanningStep = Literal["preference_collector", "destination_recommender", "itinerary_planner"]

class TravelPlanningState(AgentState):
    """旅行规划工作流状态"""
    current_step: NotRequired[PlanningStep]
    budget_level: NotRequired[Literal["economy", "comfort", "luxury"]]
    travel_style: NotRequired[Literal["relaxation", "culture", "adventure"]]
    destination: NotRequired[str]
    travel_dates: NotRequired[str]
Step 2: 创建状态转换工具
from langchain.tools import tool, ToolRuntime
from langchain.messages import ToolMessage
from langgraph.types import Command

@tool
def record_preferences(
    budget_level: Literal["economy", "comfort", "luxury"],
    travel_style: Literal["relaxation", "culture", "adventure"],
    travel_dates: str,
    runtime: ToolRuntime[None, TravelPlanningState],
) -> Command:
    """记录用户旅行偏好并转换到目的地推荐步骤"""
    return Command(
        update={
            "messages": [
                ToolMessage(
                    content=f"偏好已记录 - 预算: {budget_level}, 风格: {travel_style}, 日期: {travel_dates}",
                    tool_call_id=runtime.tool_call_id,
                )
            ],
            "budget_level": budget_level,
            "travel_style": travel_style,
            "travel_dates": travel_dates,
            "current_step": "destination_recommender",
        }
    )

@tool
def select_destination(
    destination: str,
    runtime: ToolRuntime[None, TravelPlanningState],
) -> Command:
    """确认目的地选择并转换到行程制定步骤"""
    return Command(
        update={
            "messages": [
                ToolMessage(
                    content=f"目的地已选择: {destination}",
                    tool_call_id=runtime.tool_call_id,
                )
            ],
            "destination": destination,
            "current_step": "itinerary_planner",
        }
    )

@tool
def generate_itinerary(itinerary: str) -> str:
    """生成最终行程安排"""
    return f"行程已生成:\n{itinerary}"

@tool
def search_flights(destination: str) -> str:
    """搜索航班信息"""
    return f"找到飞往{destination}的航班 - 直飞 ¥2580, 转机 ¥1890"

@tool
def search_hotels(destination: str) -> str:
    """搜索酒店信息"""
    return f"{destination}推荐酒店 - 五星 ¥800/晚, 四星 ¥450/晚, 民宿 ¥200/晚"

@tool
def search_attractions(destination: str) -> str:
    """搜索景点信息"""
    return f"{destination}热门景点 - 景点A, 景点B, 景点C"
Step 3: 定义步骤配置
PREFERENCE_COLLECTOR_PROMPT = """你是旅行规划助手的第一线。

当前阶段:偏好收集

任务:
1. 热情问候用户
2. 依次询问:预算范围、旅行风格、出行日期
3. 收集完成后使用 record_preferences 记录并进入下一步

注意:友好对话,不要一次问多个问题"""

DESTINATION_RECOMMENDER_PROMPT = """你是旅行目的地推荐专家。

当前阶段:目的地推荐
用户偏好:预算 {budget_level}, 风格 {travel_style}, 日期 {travel_dates}

任务:
1. 根据偏好推荐2-3个目的地
2. 用户确认后使用 select_destination 进入下一步

可用工具:search_flights, search_hotels, search_attractions"""

ITINERARY_PLANNER_PROMPT = """你是行程规划专家。

当前阶段:行程制定
用户信息:预算 {budget_level}, 风格 {travel_style}, 日期 {travel_dates}, 目的地 {destination}

任务:
1. 使用搜索工具获取航班、酒店、景点信息
2. 制定详细行程
3. 使用 generate_itinerary 输出最终行程"""

STEP_CONFIG = {
    "preference_collector": {
        "prompt": PREFERENCE_COLLECTOR_PROMPT,
        "tools": [record_preferences],
        "requires": [],
    },
    "destination_recommender": {
        "prompt": DESTINATION_RECOMMENDER_PROMPT,
        "tools": [select_destination, search_flights, search_hotels, search_attractions],
        "requires": ["budget_level", "travel_style", "travel_dates"],
    },
    "itinerary_planner": {
        "prompt": ITINERARY_PLANNER_PROMPT,
        "tools": [generate_itinerary, search_flights, search_hotels, search_attractions],
        "requires": ["budget_level", "travel_style", "travel_dates", "destination"],
    },
}
Step 4: 创建步骤中间件
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from langchain.messages import SystemMessage
from typing import Callable

@wrap_model_call
def apply_step_config(
        request: ModelRequest,
        handler: Callable[[ModelRequest], ModelResponse], 
        #handler是一个接收ModelRequest返回ModelResponse的可调用对象
) -> ModelResponse:
    """
    动态配置Agent执行流程的核心中间件
    - 根据当前对话阶段自动注入定制化系统提示
    - 强制校验前置状态完整性
    - 按阶段切换可用工具集

    通过装饰器模式实现无侵入式流程控制
    """

    # 获取当前对话阶段(默认首阶段为偏好收集器)
    current_step = request.state.get("current_step", "preference_collector")
    #这里的state实际上就是指Agent 的“全局共享记忆库”,等于定义的业务变量字典 + 按时间排序的 Message 列表

    # 从全局配置表加载阶段专属参数
    step_config = STEP_CONFIG[current_step]

    # === 状态校验 ===
    # 确保所有前置依赖状态已填充(如:用户意图、设备型号等)
    for key in step_config["requires"]:
        if request.state.get(key) is None:
            raise ValueError(
                f"阶段 {current_step} 需要完整状态: {key} 未设置"
                f"(当前状态: {list(request.state.keys())})"
            )

    # === 动态提示词生成 ===
    # 使用状态变量填充系统提示(示例: "{device_model}保修状态: {warranty_status}")
    system_prompt = step_config["prompt"].format(**request.state)

    # === 执行配置注入 ===
    # 1. 替换系统提示词(实现上下文感知对话)
    # 2. 切换当前阶段可用工具集(如: 诊断工具/维修工具)
    request = request.override(
        system_prompt=system_prompt,
        tools=step_config["tools"],
    )

    return handler(request)  # 传递配置后的请求到下游处理
Step 5: 创建 Agent
import os
from langchain.agents import create_agent
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
from langgraph.checkpoint.memory import InMemorySaver


load_dotenv()

model = ChatTongyi(
    model="qwen-max",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
)

all_tools = [
    record_preferences,
    select_destination,
    generate_itinerary,
    search_flights,
    search_hotels,
    search_attractions,
]

travel_agent = create_agent(
    model,
    tools=all_tools,
    state_schema=TravelPlanningState,
    middleware=[apply_step_config],
    checkpointer=InMemorySaver(),
)
Step 6: 测试完整工作流
import uuid

thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}

# 第1轮:初始问题
print("=== 第1轮 ===")
result = travel_agent.invoke(
    {"messages": [{"role": "user", "content": "我想规划一次旅行"}]},
    config
)
print(result["messages"][-1].content)

# 第2轮:回答偏好
print("\n=== 第2轮 ===")
result = travel_agent.invoke(
    {"messages": [{"role": "user", "content": "预算中等舒适型,想体验文化,7月初出发玩5天"}]},
    config
)
print(result["messages"][-1].content)

# 第3轮:选择目的地
print("\n=== 第3轮 ===")
result = travel_agent.invoke(
    {"messages": [{"role": "user", "content": "我选择去西安"}]},
    config
)
print(result["messages"][-1].content)

# 第4轮:生成行程
print("\n=== 第4轮 ===")
result = travel_agent.invoke(
    {"messages": [{"role": "user", "content": "帮我制定详细行程"}]},
    config
)
print(result["messages"][-1].content)

3.5 添加回退功能

@tool
def go_back_to_preferences(
    runtime: ToolRuntime[None, TravelPlanningState]
) -> Command:
    """返回到偏好收集步骤"""
    return Command(
        update={
            # 必须包含 ToolMessage 来响应 LLM 的调用
            "messages": [
                ToolMessage(
                    content="已重置状态,请重新提供偏好信息。",
                    tool_call_id=runtime.tool_call_id
                )
            ],
            "current_step": "preference_collector"
        }
    )

@tool
def go_back_to_destination(
    runtime: ToolRuntime[None, TravelPlanningState]
) -> Command:
    """返回到目的地推荐步骤"""
    return Command(
        update={
            # 必须包含 ToolMessage 来响应 LLM 的调用
            "messages": [
                ToolMessage(
                    content="已返回目的地推荐阶段。",
                    tool_call_id=runtime.tool_call_id
                )
            ],
            "current_step": "destination_recommender"
        }
    )

# 更新配置
STEP_CONFIG["itinerary_planner"]["tools"].extend([
    go_back_to_preferences,
    go_back_to_destination
])

all_tools = [
    record_preferences,
    select_destination,
    generate_itinerary,
    search_flights,
    search_hotels,
    search_attractions,
    go_back_to_preferences,  # <--- Add this
    go_back_to_destination,  # <--- Add this
]

ITINERARY_PLANNER_PROMPT = """你是行程规划专家。

当前阶段:行程制定
用户信息:预算 {budget_level}, 风格 {travel_style}, 日期 {travel_dates}, 目的地 {destination}

任务:
1. 使用搜索工具获取航班、酒店、景点信息
2. 制定详细行程
3. 使用 generate_itinerary 输出最终行程
4. 当用户有新的偏好更改使用go_back_to_preferences重新进行修改
5. 当用户需要重新推荐目的地使用go_back_to_destination重新推荐
"""

四、Skills 模式:SQL 助手实战

4.2 实战案例:企业 SQL 助手

Step 1: 定义技能结构
from typing import TypedDict

class Skill(TypedDict):
    """可以渐进式披露的技能"""
    name: str          # 唯一标识符
    description: str   # 1-2句简短描述(显示在系统提示词中)
    content: str       # 完整内容(按需加载)
Step 2: 定义业务技能
SKILLS: list[Skill] = [
    {
        "name": "sales_analytics",
        "description": "销售数据分析的数据库schema和业务逻辑,包括客户、订单和收入。",
        "content": """# 销售分析 Schema

## 表结构

### customers (客户表)
- customer_id (主键)
- name (客户名称)
- email
- signup_date (注册日期)
- status (active/inactive)
- customer_tier (客户等级: bronze/silver/gold/platinum)

### orders (订单表)
- order_id (主键)
- customer_id (外键 -> customers)
- order_date (订单日期)
- status (订单状态: pending/completed/cancelled/refunded)
- total_amount (订单总金额)
- sales_region (销售区域: north/south/east/west)

### order_items (订单明细表)
- item_id (主键)
- order_id (外键 -> orders)
- product_id (产品ID)
- quantity (数量)
- unit_price (单价)
- discount_percent (折扣百分比)

## 业务规则

**活跃客户定义**:
status = 'active' AND signup_date <= CURRENT_DATE - INTERVAL '90 days'

**收入计算规则**:
只计算 status = 'completed' 的订单。使用 orders 表的 total_amount 字段(已包含折扣)。

**客户生命周期价值 (CLV)**:
客户所有已完成订单的 total_amount 总和。

**高价值订单定义**:
total_amount > 1000 的订单。

## 示例查询
```sql
-- 查询最近一季度收入前10的客户
SELECT 
    c.customer_id,
    c.name,
    c.customer_tier,
    SUM(o.total_amount) as total_revenue
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE o.status = 'completed'
  AND o.order_date >= CURRENT_DATE - INTERVAL '3 months'
GROUP BY c.customer_id, c.name, c.customer_tier
ORDER BY total_revenue DESC
LIMIT 10;
```

**重要注意事项**:
- 始终在 WHERE 子句中包含 status = 'completed'
- 使用 INTERVAL 而不是固定日期
- 金额字段已包含税费和折扣
""",
    },
    {
        "name": "inventory_management",
        "description": "库存追踪的数据库schema和业务逻辑,包括产品、仓库和库存水平。",
        "content": """# 库存管理 Schema

## 表结构

### products (产品表)
- product_id (主键)
- product_name (产品名称)
- sku (库存单位)
- category (类别)
- unit_cost (单位成本)
- reorder_point (补货点:最低库存警戒线)
- discontinued (是否停产: boolean)

### warehouses (仓库表)
- warehouse_id (主键)
- warehouse_name (仓库名称)
- location (位置)
- capacity (容量)

### inventory (库存表)
- inventory_id (主键)
- product_id (外键 -> products)
- warehouse_id (外键 -> warehouses)
- quantity_on_hand (现有库存数量)
- last_updated (最后更新时间)

### stock_movements (库存流动表)
- movement_id (主键)
- product_id (外键 -> products)
- warehouse_id (外键 -> warehouses)
- movement_type (流动类型: inbound/outbound/transfer/adjustment)
- quantity (数量:入库为正,出库为负)
- movement_date (流动日期)
- reference_number (参考号)

## 业务规则

**可用库存定义**:
inventory 表中 quantity_on_hand > 0 的库存。

**需要补货的产品**:
所有仓库的 quantity_on_hand 总和 <= 产品的 reorder_point。

**活跃产品规则**:
排除 discontinued = true 的产品(除非专门分析停产商品)。

**库存估值计算**:
quantity_on_hand * unit_cost

## 示例查询
```sql
-- 查找需要补货的产品
SELECT 
    p.product_id,
    p.product_name,
    p.reorder_point,
    SUM(i.quantity_on_hand) as total_stock,
    p.unit_cost,
    (p.reorder_point - SUM(i.quantity_on_hand)) as units_to_reorder
FROM products p
JOIN inventory i ON p.product_id = i.product_id
WHERE p.discontinued = false
GROUP BY p.product_id, p.product_name, p.reorder_point, p.unit_cost
HAVING SUM(i.quantity_on_hand) <= p.reorder_point
ORDER BY units_to_reorder DESC;
```

**重要注意事项**:
- 始终使用 SUM(quantity_on_hand) 跨所有仓库汇总
- 默认排除停产产品
- 负数 quantity 表示出库
""",
    },
]
Step 3: 创建技能加载工具
from langchain.tools import tool

@tool
def load_skill(skill_name: str) -> str:
    """加载技能的完整内容到Agent上下文中。
    
    当你需要关于如何处理特定类型请求的详细信息时使用此工具。
    这将为你提供该技能领域的全面说明、策略和指南。
    
    Args:
        skill_name: 要加载的技能名称(如 "sales_analytics", "inventory_management")
    """
    for skill in SKILLS:
        if skill["name"] == skill_name:
            return f"✅ 已加载技能:{skill_name}\n\n{skill['content']}"
    
    available = ", ".join(s["name"] for s in SKILLS)
    return f"❌ 技能 '{skill_name}' 未找到。可用技能:{available}"
Step 4: 创建技能中间件
from langchain.agents.middleware import ModelRequest, ModelResponse, AgentMiddleware
from langchain.messages import SystemMessage
from typing import Callable


class SkillMiddleware(AgentMiddleware):
    """将技能描述注入系统提示词的中间件"""

    tools = [load_skill]

    def __init__(self):
        skills_list = []
        for skill in SKILLS:
            skills_list.append(f"- **{skill['name']}**: {skill['description']}")
        self.skills_prompt = "\n".join(skills_list)

    def wrap_model_call(
            self,
            request: ModelRequest,
            handler: Callable[[ModelRequest], ModelResponse],
    ) -> ModelResponse:
        skills_addendum = (
            f"\n\n## 可用技能\n\n{self.skills_prompt}\n\n"
            "当你需要处理特定类型请求的详细信息时,使用 load_skill 工具。"
        )

        new_content = list(request.system_message.content_blocks) + [
            {"type": "text", "text": skills_addendum}
        ]
        new_system_message = SystemMessage(content=new_content)

        modified_request = request.override(system_message=new_system_message)
        return handler(modified_request)
Step 5: 创建 Agent
import os
from langchain.agents import create_agent
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
from langgraph.checkpoint.memory import InMemorySaver


load_dotenv()

model = ChatTongyi(
    model="qwen-max",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
)

agent = create_agent(
    model,
    system_prompt="你是一个SQL查询助手,帮助用户编写业务数据库查询。",
    middleware=[SkillMiddleware()],
    checkpointer=InMemorySaver(),
)
Step 6: 测试SQL 助手
import uuid

thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}

result = agent.invoke(
    {
        "messages": [
            {
                "role": "user",
                "content": "写一个SQL查询,找出上个月订单金额超过$1000的所有客户"
            }
        ]
    },
    config
)

for message in result["messages"]:
    if hasattr(message, 'pretty_print'):
        message.pretty_print()

五、Router 模式:知识库实战

5.2 实战案例:企业多源知识库

Step 1: 定义状态
from typing import Annotated, Literal, TypedDict
import operator

class AgentInput(TypedDict):
    query: str

class AgentOutput(TypedDict):
    source: str
    result: str

class Classification(TypedDict):
    source: Literal["github", "notion", "slack"]
    query: str

class RouterState(TypedDict):
    query: str
    classifications: list[Classification]
    results: Annotated[list[AgentOutput], operator.add]
    final_answer: str
Step 2: 定义各垂直领域的工具
from langchain.tools import tool

@tool
def search_code(query: str) -> str:
    """在GitHub仓库中搜索代码"""
    return f"在代码库找到匹配 '{query}' 的结果:src/auth.py 中的认证中间件"

@tool
def search_issues(query: str) -> str:
    """搜索GitHub问题和PR"""
    return f"找到3个匹配 '{query}' 的issue:#142, #89, #203"

@tool
def search_notion(query: str) -> str:
    """在Notion工作空间搜索文档"""
    return f"找到文档:'API认证指南' - 涵盖OAuth2流程和JWT令牌"

@tool
def get_page(page_id: str) -> str:
    """获取特定Notion页面"""
    return f"页面内容:认证设置的分步说明"

@tool
def search_slack(query: str) -> str:
    """搜索Slack消息和线程"""
    return f"在 #engineering 发现讨论:'使用Bearer令牌进行API认证'"

@tool
def get_thread(thread_id: str) -> str:
    """获取特定Slack线程"""
    return f"线程讨论了API密钥轮换的最佳实践"
Step 3: 创建专业 Agent
import os
from langchain.agents import create_agent
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi


load_dotenv()

model = ChatTongyi(
    model="qwen-max",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
)

github_agent = create_agent(
    model,
    tools=[search_code, search_issues],
    system_prompt="你是GitHub专家。搜索代码、issue和PR回答问题。"
)

notion_agent = create_agent(
    model,
    tools=[search_notion, get_page],
    system_prompt="你是Notion专家。搜索文档回答问题。"
)

slack_agent = create_agent(
    model,
    tools=[search_slack, get_thread],
    system_prompt="你是Slack专家。搜索讨论回答问题。"
)
Step 4: 构建路由工作流

4.1 分类器

from pydantic import BaseModel, Field

class ClassificationResult(BaseModel):
    classifications: list[Classification] = Field(
        description="要调用的Agent列表及其针对性子问题"
    )

def classify_query(state: RouterState) -> dict:
    router_llm = model
    structured_llm = router_llm.with_structured_output(ClassificationResult)

    result = structured_llm.invoke([
        {
            "role": "system",
            "content": """分析查询并确定要咨询哪些知识库。

                可用来源:
                - github: 代码、API参考、实现细节
                - notion: 内部文档、流程、策略
                - slack: 团队讨论、非正式知识
                
                仅返回与查询相关的来源。
            """
        },
        {"role": "user", "content": state["query"]}
    ])

    return {"classifications": result.classifications}

4.2 路由函数

from langgraph.types import Send

def route_to_agents(state: RouterState) -> list[Send]:
    return [
        Send(c["source"], {"query": c["query"]})
        for c in state["classifications"]
    ]

4.3 Agent节点

def query_github(state: AgentInput) -> dict:
    result = github_agent.invoke({
        "messages": [{"role": "user", "content": state["query"]}]
    })
    return {"results": [{"source": "github", "result": result["messages"][-1].content}]}

def query_notion(state: AgentInput) -> dict:
    result = notion_agent.invoke({
        "messages": [{"role": "user", "content": state["query"]}]
    })
    return {"results": [{"source": "notion", "result": result["messages"][-1].content}]}

def query_slack(state: AgentInput) -> dict:
    result = slack_agent.invoke({
        "messages": [{"role": "user", "content": state["query"]}]
    })
    return {"results": [{"source": "slack", "result": result["messages"][-1].content}]}

4.4 综合器

def synthesize_results(state: RouterState) -> dict:
    if not state["results"]:
        return {"final_answer": "未从任何知识源找到结果。"}

    formatted = [
        f"**来自 {r['source'].title()}:**\n{r['result']}"
        for r in state["results"]
    ]

    synthesis_llm = model
    synthesis_response = synthesis_llm.invoke([
        {
            "role": "system",
            "content": f"""综合搜索结果回答:"{state['query']}"
合并信息,避免冗余,保持简洁。"""
        },
        {"role": "user", "content": "\n\n".join(formatted)}
    ])

    return {"final_answer": synthesis_response.content}
Step 5: 编译工作流
from langgraph.graph import StateGraph, START, END

workflow = (
    StateGraph(RouterState)
    .add_node("classify", classify_query)
    .add_node("github", query_github)
    .add_node("notion", query_notion)
    .add_node("slack", query_slack)
    .add_node("synthesize", synthesize_results)
    .add_edge(START, "classify")
    .add_conditional_edges("classify", route_to_agents, ["github", "notion", "slack"])
    .add_edge("github", "synthesize")
    .add_edge("notion", "synthesize")
    .add_edge("slack", "synthesize")
    .add_edge("synthesize", END)
    .compile()
)
Step 6: 使用路由器
result = workflow.invoke({
    "query": "如何认证API请求?"
})

print("原始查询:", result["query"])
print("\n分类:")
for c in result["classifications"]:
    print(f"  {c['source']}: {c['query']}")
print("\n最终答案:")
print(result["final_answer"])

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值