LangChain_LangGraph_知识点详解

LangChain & LangGraph 携程助手项目 - 完整知识点详解

项目概述:这是一个基于 LangChain 和 LangGraph 构建的多智能体旅行助手系统,模拟携程客服场景,实现航班查询、酒店预订、租车服务和旅游推荐等功能。


📚 目录

  1. 核心技术栈
  2. LangGraph 核心概念
  3. 状态管理(State Management)
  4. 工作流图构建(Graph Construction)
  5. 工具系统(Tools System)
  6. 多智能体架构(Multi-Agent Architecture)
  7. 持久化与检查点(Persistence & Checkpointing)
  8. 人机交互与审批流程(Human-in-the-Loop)
  9. 向量检索与 RAG
  10. FastAPI 集成
  11. 数据库操作
  12. 常见陷阱与最佳实践

核心技术栈

1.1 主要框架

# 核心依赖
langchain>=0.3.0              # LangChain 核心库
langchain-core>=0.3.0         # LangChain 核心组件
langchain-community>=0.3.0    # 社区贡献的工具和集成
langchain-openai>=0.2.0       # OpenAI API 集成
langgraph>=0.2.0              # LangGraph 工作流引擎

# Web 框架
fastapi>=0.100.0              # 高性能异步 Web 框架
uvicorn>=0.25.0               # ASGI 服务器

# 数据库
sqlalchemy>=2.0.0             # ORM 框架
sqlite3                       # 轻量级关系型数据库

# 搜索工具
tavily-python>=0.3.0          # Tavily 搜索引擎 API

# 其他
pydantic>=2.0.0               # 数据验证和设置管理
numpy                         # 数值计算
pandas                        # 数据处理
loguru                        # 日志记录
gradio                        # UI 界面(可选)

1.2 技术架构图

┌─────────────────────────────────────────────┐
│              FastAPI Server                  │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  │
│  │  Auth    │  │  CORS    │  │Middleware│  │
│  └──────────┘  └──────────┘  └──────────┘  │
└──────────────────┬──────────────────────────┘
                   │
┌──────────────────▼──────────────────────────┐
│           LangGraph Workflow                 │
│  ┌──────────────────────────────────────┐   │
│  │     Primary Assistant (主助理)        │   │
│  │  ┌────────┐ ┌────────┐ ┌────────┐  │   │
│  │  │ Flight │ │ Hotel  │ │  Car   │  │   │
│  │  │ Agent  │ │ Agent  │ │ Agent  │  │   │
│  │  └────────┘ └────────┘ └────────┘  │   │
│  │  ┌────────┐                         │   │
│  │  │Excursion│                        │   │
│  │  │ Agent  │                         │   │
│  │  └────────┘                         │   │
│  └──────────────────────────────────────┘   │
└──────────────────┬──────────────────────────┘
                   │
┌──────────────────▼──────────────────────────┐
│            Tools Layer                      │
│  ┌────────┐ ┌────────┐ ┌────────────────┐  │
│  │SQLiteDB│ │Tavily  │ │Vector Store    │  │
│  │        │ │Search  │ │(RAG Policy)    │  │
│  └────────┘ └────────┘ └────────────────┘  │
└─────────────────────────────────────────────┘

LangGraph 核心概念

2.0 LangChain 基础概念(前置知识)

在学习 LangGraph 之前,我们需要先理解一些 LangChain 的核心概念。

📝 ChatPromptTemplate - 提示模板

ChatPromptTemplate 是 LangChain 中用于构建对话提示的工具。

基本用法

from langchain_core.prompts import ChatPromptTemplate

# 创建提示模板
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个专业的{role}。"),
    ("human", "{question}"),
])

# 格式化提示
formatted = prompt.format_messages(
    role="Python 工程师",
    question="如何定义函数?"
)

# 结果: [
#   SystemMessage(content="你是一个专业的 Python 工程师。"),
#   HumanMessage(content="如何定义函数?")
# ]

项目中的应用

from datetime import datetime
from langchain_core.prompts import ChatPromptTemplate

primary_assistant_prompt = ChatPromptTemplate.from_messages([
    (
        "system",
        "您是携程瑞士航空公司的客户服务助理。\n"
        "当前用户的航班信息:\n<Flights>\n{user_info}\n</Flights>\n"
        "当前时间: {time}."
    ),
    ("placeholder", "{messages}"),  # 占位符:插入对话历史
]).partial(time=datetime.now())  # 部分填充:固定 time 参数

关键特性

  • 模板化:使用 {variable} 占位符
  • 部分填充.partial() 预先填充某些参数
  • 消息类型:支持 system/human/assistant/tool 等角色
  • 占位符("placeholder", "{messages}") 自动展开消息列表

🔗 LangChain Expression Language (LCEL)

LCEL 是 LangChain 的表达式语言,使用 | 操作符链式组合组件。

基本语法

from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

# 定义组件
prompt = ChatPromptTemplate.from_template("翻译以下文本到{language}:\n{text}")
llm = ChatOpenAI(model="gpt-4")

# 使用 | 组合(创建 Runnable)
chain = prompt | llm

# 执行
result = chain.invoke({
    "language": "中文",
    "text": "Hello, World!"
})

工作原理

prompt | llm | parser
  ↓      ↓      ↓
输入 → 提示模板 → LLM → 解析器 → 输出

项目中的应用

# 主助理 Runnable
assistant_runnable = primary_assistant_prompt | llm.bind_tools(
    primary_assistant_tools + [ToFlightBookingAssistant, ...]
)

# 分解:
# 1. primary_assistant_prompt: ChatPromptTemplate
# 2. |: LCEL 操作符
# 3. llm.bind_tools(...): ChatOpenAI 绑定工具后的 Runnable

优势

  • 声明式:清晰表达数据流
  • 可组合:轻松组合多个组件
  • 统一接口:所有 Runnable 都有 invoke/stream/batch 方法
  • 异步支持:自动支持 async/await

⚙️ Runnable - 可运行对象

Runnable 是 LangChain 的核心接口,任何实现该接口的对象都可以被调用。

常见 Runnable 类型

类型 说明 示例
ChatModel LLM 模型 ChatOpenAI
PromptTemplate 提示模板 ChatPromptTemplate
ToolNode 工具节点 ToolNode(tools)
RunnableLambda 自定义函数包装 RunnableLambda(func)
RunnableSequence 序列组合 `prompt

Runnable 的方法

from langchain_core.runnables import RunnableLambda

# 1. invoke - 单次调用
result = runnable.invoke(input_data)

# 2. stream - 流式输出
for chunk in runnable.stream(input_data):
    print(chunk)

# 3. batch - 批量处理
results = runnable.batch([input1, input2, input3])

# 4. ainvoke - 异步调用
result = await runnable.ainvoke(input_data)

RunnableLambda 示例

from langchain_core.runnables import RunnableLambda

def my_function(x: dict) -> dict:
    """自定义处理函数"""
    return {"result": x["value"] * 2}

# 包装为 Runnable
runnable = RunnableLambda(my_function)

# 使用
result = runnable.invoke({"value": 5})
# 结果: {"result": 10}

项目中的应用

# 在工具错误处理中使用 RunnableLambda
def handle_tool_error(state) -> dict:
    error = state.get("error")
    # ... 处理逻辑
    return {"messages": [...]}

# 包装为 Runnable 并作为回退
return ToolNode(tools).with_fallbacks(
    [RunnableLambda(handle_tool_error)], 
    exception_key="error"
)

🛠️ bind_tools - LLM 工具绑定

bind_tools 方法让 LLM 能够调用外部工具。

基本用法

from langchain_openai import ChatOpenAI
from langchain_core.tools import tool

@tool
def search_weather(location: str) -> str:
    """搜索天气"""
    return f"{location} 的天气晴朗"

# 创建 LLM
llm = ChatOpenAI(model="gpt-4")

# 绑定工具
llm_with_tools = llm.bind_tools([search_weather])

# 调用
response = llm_with_tools.invoke("北京天气怎么样?")

# LLM 可能返回:
# AIMessage(
#     content="",
#     tool_calls=[{
#         "name": "search_weather",
#         "args": {"location": "北京"},
#         "id": "call_xxx"
#     }]
# )

工作流程

1. 用户提问
   ↓
2. LLM 分析问题,决定是否需要调用工具
   ↓
3. LLM 返回 tool_calls(不直接执行)
   ↓
4. 代码检测 tool_calls
   ↓
5. 执行对应工具
   ↓
6. 将工具结果返回给 LLM
   ↓
7. LLM 生成最终回答

项目中的应用

# 主助理绑定工具
assistant_runnable = primary_assistant_prompt | llm.bind_tools(
    primary_assistant_tools           # 普通工具
    + [ToFlightBookingAssistant, ...] # Pydantic 模型作为工具
)

# 子助手绑定工具
update_flight_runnable = flight_booking_prompt | llm.bind_tools(
    update_flight_tools               # 航班工具
    + [CompleteOrEscalate]            # 升级工具
)

关键点

  • ✅ LLM 不会直接执行工具,只返回调用意图
  • ✅ 需要代码层面检测 tool_calls 并执行
  • ✅ 可以绑定普通工具和 Pydantic 模型
  • ✅ 工具必须有清晰的文档字符串(LLM 通过它理解工具)

🔍 tools_condition - 工具条件判断

tools_condition 是 LangGraph 提供的实用函数,用于判断是否有工具调用。

基本用法

from langgraph.prebuilt import tools_condition
from langgraph.constants import END

def route_node(state: dict):
    """路由函数"""
    route = tools_condition(state)
    
    if route == END:
        # 没有工具调用,结束工作流
        return END
    else:
        # 有工具调用,route == "tools"
        return "execute_tools"

返回值

  • END:最后一条消息没有 tool_calls
  • "tools":最后一条消息有 tool_calls

内部逻辑

def tools_condition(state):
    messages = state.get("messages", [])
    if not messages:
        return END
    
    last_message = messages[-1]
    if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
        return "tools"
    return END

项目中的应用

def route_primary_assistant(state: dict):
    route = tools_condition(state)  # 判断是否有工具调用
    if route == END:
        return END  # 无工具调用,结束
    
    # 有工具调用,根据工具名称路由
    tool_calls = state["messages"][-1].tool_calls
    if tool_calls[0]["name"] == "ToFlightBookingAssistant":
        return "enter_update_flight"
    # ...

💬 ToolMessage - 工具消息

ToolMessage 是 LangChain 中的一种消息类型,用于表示工具的执行结果。

基本用法

from langchain_core.messages import ToolMessage, AIMessage

# LLM 调用工具
ai_message = AIMessage(
    content="",
    tool_calls=[{
        "name": "search_weather",
        "args": {"location": "北京"},
        "id": "call_123"
    }]
)

# 执行工具后,创建 ToolMessage
tool_message = ToolMessage(
    content="北京的天气晴朗,温度 25°C",
    tool_call_id="call_123"  # ⭐ 必须与 tool_calls 中的 id 匹配
)

# 将 ToolMessage 添加到对话历史
messages = [ai_message, tool_message]

关键属性

属性 说明 示例
content 工具执行结果 "天气晴朗"
tool_call_id 关联的工具调用 ID "call_123"

项目中的应用

# 1. 入口节点生成 ToolMessage
from langchain_core.messages import ToolMessage

def entry_node(state: dict) -> dict:
    tool_call_id = state["messages"][-1].tool_calls[0]["id"]
    return {
        "messages": [
            ToolMessage(
                content=f"现在助手是{assistant_name}。请回顾上述对话...",
                tool_call_id=tool_call_id,
            )
        ],
        "dialog_state": new_dialog_state,
    }

# 2. 人机交互拒绝时生成 ToolMessage
if user_input.strip().lower() != "y":
    result = graph.stream({
        "messages": [
            ToolMessage(
                tool_call_id=event["messages"][-1].tool_calls[0]["id"],
                content=f"Tool的调用被用户拒绝。原因:'{user_input}'。",
            )
        ]
    }, config)

# 3. 错误处理时生成 ToolMessage
def handle_tool_error(state) -> dict:
    error = state.get("error")
    tool_calls = state["messages"][-1].tool_calls
    return {
        "messages": [
            ToolMessage(
                content=f"错误: {repr(error)}\n请修正您的错误。",
                tool_call_id=tc["id"],
            )
            for tc in tool_calls
        ]
    }

工作流程

1. LLM 返回 AIMessage(tool_calls=[...])
   ↓
2. 代码执行工具
   ↓
3. 创建 ToolMessage(content=结果, tool_call_id=...)
   ↓
4. 将 ToolMessage 添加到消息列表
   ↓
5. 再次调用 LLM(传入包含 ToolMessage 的历史)
   ↓
6. LLM 看到工具结果,生成最终回答

📬 LangChain 消息类型体系

LangChain 定义了多种消息类型,用于表示对话中的不同角色。

消息类型层次结构

BaseMessage (基类)
├── HumanMessage (用户消息)
├── AIMessage (AI 助手消息)
├── SystemMessage (系统消息)
├── ToolMessage (工具结果消息)
└── FunctionMessage (函数调用消息,已弃用)

1. HumanMessage - 用户消息

表示用户的输入。

from langchain_core.messages import HumanMessage

# 创建用户消息
user_msg = HumanMessage(content="你好,我想查询航班信息")

# 在对话中使用
messages = [
    HumanMessage(content="北京到上海的航班有哪些?")
]

使用场景

  • ✅ 用户提问
  • ✅ 用户提供指令
  • ✅ 用户反馈

2. AIMessage - AI 助手消息

表示 LLM 的回复。

from langchain_core.messages import AIMessage

# 普通文本回复
ai_msg = AIMessage(content="您好!我可以帮您查询航班信息。")

# 带工具调用的回复
ai_msg_with_tools = AIMessage(
    content="",  # 可能为空
    tool_calls=[
        {
            "name": "search_flights",
            "args": {
                "departure_airport": "PEK",
                "arrival_airport": "SHA"
            },
            "id": "call_abc123"
        }
    ]
)

关键属性

属性 说明 示例
content AI 回复的文本 "您好!..."
tool_calls 工具调用列表 [{"name": "...", ...}]
id 消息 ID "msg_123"

项目中的应用

# 检测 AI 消息
for event in events:
    if "messages" in event:
        message = event["messages"]
        if isinstance(message, list):
            message = message[-1]
        
        if message.__class__.__name__ == 'AIMessage':
            print(f"AI: {message.content}")

3. SystemMessage - 系统消息

表示系统级别的指令或上下文信息。

from langchain_core.messages import SystemMessage

# 创建系统消息
system_msg = SystemMessage(
    content="你是一个专业的旅行助手。请提供准确、友好的建议。"
)

# 在对话开始时使用
messages = [
    SystemMessage(content="你是携程客服助手。"),
    HumanMessage(content="我想订机票"),
]

使用场景

  • ✅ 设置 AI 的角色和行为
  • ✅ 提供背景信息
  • ✅ 定义对话规则

项目中的应用

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    (
        "system",
        "您是携程瑞士航空公司的客户服务助理。\n"
        "当前时间: {time}."
    ),
    ("placeholder", "{messages}"),
])

# 格式化后会自动生成 SystemMessage
formatted = prompt.format_messages(time="2026-06-03")
# formatted[0] 是 SystemMessage

4. 消息类型的完整示例
from langchain_core.messages import (
    HumanMessage,
    AIMessage,
    SystemMessage,
    ToolMessage
)

# 完整的对话流程
messages = [
    # 1. 系统指令
    SystemMessage(content="你是一个旅行助手。"),
    
    # 2. 用户提问
    HumanMessage(content="北京到上海的航班有哪些?"),
    
    # 3. AI 决定调用工具
    AIMessage(
        content="",
        tool_calls=[{
            "name": "search_flights",
            "args": {"departure": "PEK", "arrival": "SHA"},
            "id": "call_123"
        }]
    ),
    
    # 4. 工具执行结果
    ToolMessage(
        content="找到 3 个航班:CA1831, MU5101, FM9101",
        tool_call_id="call_123"
    ),
    
    # 5. AI 根据工具结果生成回答
    AIMessage(content="我找到了 3 个从北京到上海的航班:\n1. CA1831..."),
    
    # 6. 用户继续提问
    HumanMessage(content="第一个航班多少钱?"),
]

消息类型对比

类型 角色 用途 典型内容
SystemMessage 系统 设定角色和规则 "你是一个旅行助手"
HumanMessage 用户 用户输入 "我想订机票"
AIMessage AI AI 回复 "好的,我来帮您查询"
ToolMessage 工具 工具结果 "找到 3 个航班"

AnyMessage - 通用消息类型

from typing import Annotated
from langchain_core.messages import AnyMessage
from langgraph.graph import add_messages

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

# AnyMessage 可以是任何消息类型
messages: list[AnyMessage] = [
    SystemMessage(content="..."),
    HumanMessage(content="..."),
    AIMessage(content="..."),
    ToolMessage(content="..."),
]

重要性

  • 灵活性:允许列表中包含不同类型的消息
  • 类型安全:IDE 仍然可以提供类型检查
  • LangGraph 标准:状态定义中的推荐做法

🌐 TavilySearchResults - 网络搜索工具

TavilySearchResults 是 LangChain 社区提供的网络搜索工具。

基本用法

from langchain_community.tools import TavilySearchResults
import os

# 设置 API Key
os.environ["TAVILY_API_KEY"] = "your-api-key"

# 创建搜索工具
tavily_tool = TavilySearchResults(max_results=5)

# 使用
results = tavily_tool.invoke("LangGraph 最新版本")
# 返回: [{"title": "...", "url": "...", "content": "..."}, ...]

项目中的应用

# 在 llm_tavily.py 中配置
import os
from langchain_community.tools import TavilySearchResults

os.environ["TAVILY_API_KEY"] = "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"
tavily_tool = TavilySearchResults(max_results=1)

# 作为主助理的工具之一
primary_assistant_tools = [
    tavily_tool,          # 网络搜索
    search_flights,       # 航班搜索
    lookup_policy,        # 政策查询
]

📦 Pydantic - 数据验证和模型

Pydantic 是 Python 的数据验证库,在 LangChain 中用于定义工具参数和数据结构。

基本用法

from pydantic import BaseModel, Field

class User(BaseModel):
    """用户模型"""
    name: str = Field(description="用户名", min_length=1)
    age: int = Field(description="年龄", ge=0, le=150)
    email: str | None = Field(description="邮箱", default=None)

# 创建实例
user = User(name="张三", age=25)
print(user.name)  # "张三"

# 自动验证
try:
    invalid_user = User(name="", age=-1)  # ❌ 抛出 ValidationError
except Exception as e:
    print(e)

Field 的参数

参数 说明 示例
description 字段描述(LLM 通过它理解含义) Field(description="出发机场")
default 默认值 Field(default=None)
ge / le 最小/最大值 Field(ge=0, le=150)
min_length / max_length 字符串长度限制 Field(min_length=1)

在 LangChain 工具中的应用

Pydantic 模型可以作为 结构化输出工具参数 使用。

应用 1:定义委派工具(项目核心用法)
from pydantic import BaseModel, Field

class ToFlightBookingAssistant(BaseModel):
    """将工作转交给专门处理航班的助理"""
    request: str = Field(
        description="更新航班助理在继续之前需要澄清的任何后续问题。"
    )

class ToHotelBookingAssistant(BaseModel):
    """将工作转交给酒店预订助理"""
    request: str = Field(
        description="酒店助理需要澄清的问题。"
    )

# 绑定到 LLM
assistant_runnable = primary_assistant_prompt | llm.bind_tools([
    search_flights,
    ToFlightBookingAssistant,  # ⭐ Pydantic 模型作为工具
    ToHotelBookingAssistant,
])

工作流程

用户: "我想改签明天的航班"
  ↓
LLM 分析意图
  ↓
LLM 决定委派给航班助手
  ↓
LLM 返回:
AIMessage(
    content="",
    tool_calls=[{
        "name": "ToFlightBookingAssistant",
        "args": {"request": "用户想改签明天的航班"},
        "id": "call_xxx"
    }]
)
  ↓
路由函数检测到工具名称
  ↓
跳转到 enter_update_flight 节点

为什么使用 Pydantic 模型作为工具?

  • 结构化数据:强制 LLM 返回特定格式
  • 类型安全:自动验证参数类型
  • 清晰语义:通过 docstring 和 Field description 告诉 LLM 何时使用
  • 易于路由:根据工具名称判断意图

应用 2:CompleteOrEscalate 工具
class CompleteOrEscalate(BaseModel):
    """标记当前任务为已完成和/或将对话的控制权升级到主助理"""
    cancel: bool = True
    reason: str = Field(description="升级的原因")

# 在子助手中使用
update_flight_runnable = flight_booking_prompt | llm.bind_tools(
    update_flight_tools + [CompleteOrEscalate]
)

使用场景

用户在航班助手中说: "算了,我还是先看看酒店吧"
  ↓
航班助手 LLM 判断无法处理
  ↓
LLM 调用 CompleteOrEscalate(reason="用户想查看酒店")
  ↓
路由函数检测到 CompleteOrEscalate
  ↓
返回 "leave_skill" 节点
  ↓
退出航班助手,返回主助理

应用 3:FastAPI 请求/响应模型
from pydantic import BaseModel, Field
import uuid

class GraphConfigSchema(BaseModel):
    """封装配置信息"""
    configurable: dict | None = Field(
        description="配置参数",
        default=None
    )

class BaseGraphSchema(BaseModel):
    """Graph API 请求模型"""
    user_input: str = Field(
        description="用户输入的内容",
        default=None
    )
    config: GraphConfigSchema | None = Field(
        description="封装的配置信息",
        default=None
    )

class GraphRspSchema(BaseModel):
    """Graph API 响应模型"""
    assistant: str = Field(description="助手回复")

# FastAPI 中使用
@router.post("/chat", response_model=GraphRspSchema)
async def chat(request: BaseGraphSchema):
    # request 自动验证和解析
    user_input = request.user_input
    config = request.config.dict() if request.config else {}
    ...

优势

  • 自动验证:FastAPI 自动验证请求数据
  • 自动生成文档:Swagger UI 显示字段说明
  • 类型提示:IDE 自动补全
  • 数据转换:自动将 JSON 转换为 Python 对象

应用 4:JSON Schema 生成
from pydantic import BaseModel, Field

class SearchFlightsArgs(BaseModel):
    departure_airport: str | None = Field(
        description="出发机场代码",
        default=None
    )
    arrival_airport: str | None = Field(
        description="到达机场代码",
        default=None
    )
    limit: int = Field(description="返回结果数量", default=20)

# 生成 JSON Schema(LLM 用它理解参数)
schema = SearchFlightsArgs.model_json_schema()
print(schema)
# {
#   "properties": {
#     "departure_airport": {
#       "description": "出发机场代码",
#       "type": ["string", "null"]
#     },
#     ...
#   }
# }

重要性:LangChain 的 @tool 装饰器会自动从函数签名和 docstring 生成类似的 schema,供 LLM 理解工具参数。


常见错误与最佳实践

❌ 错误 1:缺少 description
# ❌ 不好:LLM 不知道何时使用
class ToFlightAssistant(BaseModel):
    request: str

# ✅ 好:清晰的描述
class ToFlightAssistant(BaseModel):
    """将工作转交给航班助理"""
    request: str = Field(
        description="航班助理需要澄清的问题或上下文"
    )
❌ 错误 2:过于复杂的嵌套
# ❌ 避免深层嵌套
class ComplexModel(BaseModel):
    data: dict[str, dict[str, list[dict[str, str]]]]

# ✅ 拆分为多个简单模型
class InnerData(BaseModel):
    items: list[str]

class OuterData(BaseModel):
    data: dict[str, InnerData]
✅ 最佳实践:使用 Optional 和默认值
from typing import Optional

class FlexibleModel(BaseModel):
    required_field: str  # 必填
    optional_field: Optional[str] = None  # 可选
    field_with_default: str = "default"  # 有默认值

2.1 StateGraph(状态图)

定义StateGraph 是 LangGraph 的核心类,用于定义有状态的对话工作流。

from langgraph.graph import StateGraph
from Embedding.ctrip_assistant.graph_chat.state import State

# 创建状态图构建器
builder = StateGraph(State)

关键特性

  • 类型安全:使用 TypedDict 定义状态结构
  • 可组合性:可以嵌套子图
  • 条件路由:支持动态决策下一步

2.2 节点(Nodes)

节点是工作流中的基本执行单元,可以是:

  • 函数节点:普通 Python 函数
  • Runnable 节点:LangChain Runnable 对象
  • 工具节点:ToolNode 封装的工具集合
# 添加函数节点
def get_user_info(state: State):
    return {"user_info": fetch_user_flight_information.invoke({})}

builder.add_node('fetch_user_info', get_user_info)

# 添加 Runnable 节点
builder.add_node('primary_assistant', CtripAssistant(assistant_runnable))

# 添加工具节点
builder.add_node(
    "primary_assistant_tools", 
    create_tool_node_with_fallback(primary_assistant_tools)
)

2.3 边(Edges)

边定义节点之间的连接关系:

普通边(Fixed Edge)

# 固定从一个节点到另一个节点
builder.add_edge(START, 'fetch_user_info')
builder.add_edge('primary_assistant_tools', 'primary_assistant')

特点

  • ✅ 简单直接
  • ✅ 执行顺序固定
  • ❌ 无法根据状态动态决策

条件边(Conditional Edge)⭐ 核心概念

add_conditional_edges 是 LangGraph 最强大的功能之一,允许根据当前状态动态决定下一步执行哪个节点。

基本语法
builder.add_conditional_edges(
    source_node,      # 源节点名称
    routing_function, # 路由函数:接收状态,返回目标节点名
    [targets]         # 可能的目标节点列表
)

参数说明

参数 类型 说明
source_node str 从哪个节点出发
routing_function Callable 路由函数,签名: func(state) -> str
targets list[str] 所有可能的目标节点名称列表
工作原理
执行流程:

1. source_node 执行完毕
   ↓
2. LangGraph 调用 routing_function(state)
   ↓
3. routing_function 分析状态,返回目标节点名
   ↓
4. LangGraph 跳转到返回的节点
完整示例
from langgraph.graph import StateGraph
from langgraph.prebuilt import tools_condition
from langgraph.constants import END

def route_primary_assistant(state: dict):
    """
    路由函数:根据状态决定下一步
    
    Args:
        state: 当前工作流状态
    
    Returns:
        str: 下一个节点的名称
    """
    # 步骤 1: 使用 tools_condition 判断是否有工具调用
    route = tools_condition(state)
    
    # 步骤 2: 如果没有工具调用,结束工作流
    if route == END:
        return END
    
    # 步骤 3: 获取最后一条消息的工具调用
    tool_calls = state["messages"][-1].tool_calls
    
    # 步骤 4: 根据工具名称决定路由
    if tool_calls:
        if tool_calls[0]["name"] == "ToFlightBookingAssistant":
            return "enter_update_flight"  # 跳转到航班助手
        elif tool_calls[0]["name"] == "ToBookCarRental":
            return "enter_book_car_rental"  # 跳转到租车助手
        elif tool_calls[0]["name"] == "ToHotelBookingAssistant":
            return "enter_book_hotel"  # 跳转到酒店助手
        elif tool_calls[0]["name"] == "ToBookExcursion":
            return "enter_book_excursion"  # 跳转到游览助手
        else:
            return "primary_assistant_tools"  # 执行主助理工具
    
    raise ValueError("无效的路由")

# 添加条件边
builder.add_conditional_edges(
    'primary_assistant',           # 从 primary_assistant 节点出发
    route_primary_assistant,       # 使用这个路由函数
    [                              # 可能的目标节点
        "enter_update_flight",
        "enter_book_car_rental",
        "enter_book_hotel",
        "enter_book_excursion",
        "primary_assistant_tools",
        END,
    ]
)
路由函数的要求

必须满足

  1. ✅ 接收一个参数:state(当前状态字典)
  2. ✅ 返回一个字符串:目标节点名称
  3. ✅ 返回值必须在 targets 列表中
# ✅ 正确的路由函数
def my_router(state: dict) -> str:
    if state["some_field"] == "value1":
        return "node_a"
    else:
        return "node_b"

# ❌ 错误的路由函数
def bad_router(state: dict):
    return None  # 必须返回字符串
    return "invalid_node"  # 不在 targets 列表中
常用路由模式
模式 1:基于工具调用路由
from langgraph.prebuilt import tools_condition

def route_by_tool(state: dict) -> str:
    """根据 LLM 调用的工具名称路由"""
    route = tools_condition(state)
    if route == END:
        return END
    
    tool_name = state["messages"][-1].tool_calls[0]["name"]
    
    # 映射工具名称到节点名称
    tool_to_node = {
        "search_flights": "flight_tools",
        "book_hotel": "hotel_tools",
        "search_cars": "car_tools",
    }
    
    return tool_to_node.get(tool_name, "default_tools")
模式 2:基于状态字段路由
def route_by_dialog_state(state: dict) -> str:
    """根据对话状态栈路由"""
    dialog_state = state.get("dialog_state", [])
    
    if not dialog_state:
        return "primary_assistant"
    
    # 返回栈顶元素(当前活跃的助手)
    return dialog_state[-1]
模式 3:基于业务逻辑路由
def route_by_intent(state: dict) -> str:
    """根据用户意图路由"""
    last_message = state["messages"][-1]
    
    # 假设有一个意图分类器
    intent = classify_intent(last_message.content)
    
    if intent == "flight_query":
        return "flight_assistant"
    elif intent == "hotel_booking":
        return "hotel_assistant"
    elif intent == "general_chat":
        return "chat_assistant"
    else:
        return "primary_assistant"
项目中的实际应用
应用 1:主助理路由(第 52-86 行)
def route_primary_assistant(state: dict):
    """根据当前状态判断路由到子助手节点"""
    route = tools_condition(state)  # 判断下一步的方向
    if route == END:
        return END
    
    tool_calls = state["messages"][-1].tool_calls
    if tool_calls:
        if tool_calls[0]["name"] == ToFlightBookingAssistant.__name__:
            return "enter_update_flight"  # 跳转至航班预订入口节点
        elif tool_calls[0]["name"] == ToBookCarRental.__name__:
            return "enter_book_car_rental"  # 跳转至租车预订入口节点
        elif tool_calls[0]["name"] == ToHotelBookingAssistant.__name__:
            return "enter_book_hotel"  # 跳转至酒店预订入口节点
        elif tool_calls[0]["name"] == ToBookExcursion.__name__:
            return "enter_book_excursion"  # 跳转至游览预订入口节点
        return "primary_assistant_tools"  # 否则跳转至主助理工具节点
    raise ValueError("无效的路由")

builder.add_conditional_edges(
    'primary_assistant',
    route_primary_assistant,
    [
        "enter_update_flight",      # 航班子助手的入口节点
        "enter_book_car_rental",    # 租车子助手的入口节点
        "enter_book_hotel",         # 酒店子助手的入口节点
        "enter_book_excursion",     # 旅游景点子助手的入口节点
        "primary_assistant_tools",  # 主助手的工具节点
        END,
    ]
)

工作流程

用户: "我想订酒店"
  ↓
primary_assistant 节点执行
  ↓
LLM 返回: AIMessage(tool_calls=[{"name": "ToHotelBookingAssistant", ...}])
  ↓
route_primary_assistant 被调用
  ↓
检测到工具名称: "ToHotelBookingAssistant"
  ↓
返回: "enter_book_hotel"
  ↓
跳转到: enter_book_hotel 节点
应用 2:工作流入口路由(第 92-104 行)
def route_to_workflow(state: dict) -> str:
    """
    如果我们在一个委托的状态中,直接路由到相应的助理。
    """
    dialog_state = state.get("dialog_state")
    if not dialog_state:
        return "primary_assistant"  # 如果没有对话状态,返回主助理
    return dialog_state[-1]  # 返回最后一个对话状态(栈顶)

builder.add_conditional_edges("fetch_user_info", route_to_workflow)

工作流程

场景 1: 首次对话
dialog_state = []
  ↓
route_to_workflow 返回 "primary_assistant"
  ↓
跳转到主助理

场景 2: 从子助手返回后
dialog_state = ["assistant", "book_hotel"]
  ↓
route_to_workflow 返回 "book_hotel" (栈顶)
  ↓
跳转到酒店助手(继续之前的对话)
应用 3:子助手内部路由(build_child_graph.py)
def route_update_flight(state: dict): 
    """根据当前状态路由航班更新流程"""
    route = tools_condition(state)
    if route == END:
        return END
    
    tool_calls = state["messages"][-1].tool_calls
    
    # 检查是否调用了 CompleteOrEscalate
    did_cancel = any(
        tc["name"] == CompleteOrEscalate.__name__ 
        for tc in tool_calls
    )
    if did_cancel:
        return "leave_skill"  # 退出子助手
    
    # 区分安全工具和敏感工具
    safe_tool_names = [t.name for t in update_flight_safe_tools]
    if all(tc["name"] in safe_tool_names for tc in tool_calls):
        return "update_flight_safe_tools"  # 安全工具
    return "update_flight_sensitive_tools"  # 敏感工具

builder.add_conditional_edges(
    "update_flight",
    route_update_flight,
    ["update_flight_sensitive_tools", "update_flight_safe_tools", 
     "leave_skill", END],
)

工作流程

update_flight 节点执行
  ↓
LLM 返回工具调用
  ↓
route_update_flight 判断:
  ├─ CompleteOrEscalate → leave_skill (返回主助手)
  ├─ search_flights → update_flight_safe_tools (直接执行)
  └─ update_ticket → update_flight_sensitive_tools (需要审批)
add_conditional_edges vs add_edge 对比
特性 add_edge add_conditional_edges
路由方式 固定 动态
决策依据 路由函数
灵活性
适用场景 顺序执行 分支逻辑
复杂度 简单 较复杂

选择建议

  • 使用 add_edge:当执行顺序固定时
  • 使用 add_conditional_edges:当需要根据状态动态决策时
常见错误与调试
错误 1:返回值不在 targets 列表中
# ❌ 错误
def bad_router(state):
    return "invalid_node"  # 不在 targets 中

builder.add_conditional_edges(
    "node_a",
    bad_router,
    ["node_b", "node_c"]  # 没有 "invalid_node"
)
# 运行时会抛出异常

# ✅ 正确
def good_router(state):
    return "node_b"  # 在 targets 中
错误 2:路由函数抛出异常
# ❌ 可能抛出异常
def risky_router(state):
    tool_name = state["messages"][-1].tool_calls[0]["name"]
    # 如果没有 tool_calls,会抛出 IndexError
    return tool_name

# ✅ 安全的写法
def safe_router(state):
    messages = state.get("messages", [])
    if not messages:
        return "default_node"
    
    last_msg = messages[-1]
    if not hasattr(last_msg, 'tool_calls') or not last_msg.tool_calls:
        return "default_node"
    
    return last_msg.tool_calls[0]["name"]
调试技巧
# 在路由函数中添加日志
def debug_router(state: dict) -> str:
    print(f"\n{'='*50}")
    print(f"路由函数被调用")
    print(f"当前状态键: {state.keys()}")
    print(f"messages 数量: {len(state.get('messages', []))}")
    
    if state.get("messages"):
        last_msg = state["messages"][-1]
        print(f"最后消息类型: {type(last_msg).__name__}")
        if hasattr(last_msg, 'tool_calls'):
            print(f"工具调用: {last_msg.tool_calls}")
    
    # 正常路由逻辑
    result = route_primary_assistant(state)
    print(f"路由结果: {result}")
    print(f"{'='*50}\n")
    
    return result
高级用法
多条件组合路由
def complex_router(state: dict) -> str:
    """结合多个条件进行路由"""
    dialog_state = state.get("dialog_state", [])
    messages = state.get("messages", [])
    
    # 条件 1: 检查是否在子助手中
    if len(dialog_state) > 1:
        current_assistant = dialog_state[-1]
        
        # 条件 2: 检查是否有工具调用
        if messages and hasattr(messages[-1], 'tool_calls'):
            tool_calls = messages[-1].tool_calls
            if tool_calls:
                # 条件 3: 检查是否是退出信号
                if any(tc["name"] == "CompleteOrEscalate" for tc in tool_calls):
                    return "leave_skill"
                
                # 条件 4: 根据当前助手和工具类型路由
                if current_assistant == "book_hotel":
                    return route_hotel_tools(tool_calls)
                elif current_assistant == "update_flight":
                    return route_flight_tools(tool_calls)
        
        # 默认返回当前助手
        return current_assistant
    
    # 默认返回主助手
    return "primary_assistant"
使用类作为路由器
class SmartRouter:
    """智能路由器:维护路由历史"""
    
    def __init__(self):
        self.route_history = []
    
    def __call__(self, state: dict) -> str:
        """使实例可调用"""
        target = self._decide_route(state)
        self.route_history.append(target)
        return target
    
    def _decide_route(self, state: dict) -> str:
        # 路由逻辑
        ...

# 使用
router = SmartRouter()
builder.add_conditional_edges("node_a", router, ["node_b", "node_c"])

2.4 特殊常量

from langgraph.constants import START, END

# START: 工作流的入口点
# END: 工作流的终止点

状态管理(State Management)

3.0 Python 类型提示基础:TypedDict 与 Annotated

在深入状态管理之前,我们需要先理解两个关键的 Python 类型提示工具:TypedDictAnnotated

📖 TypedDict:定义字典结构

TypedDict 来自 typing 模块,用于定义具有固定键和特定值类型的字典

基本概念

from typing import TypedDict

# 定义一个 TypedDict
class Person(TypedDict):
    name: str
    age: int
    email: str

# 使用
person: Person = {
    "name": "张三",
    "age": 25,
    "email": "zhangsan@example.com"
}

与普通字典的区别

# ❌ 普通字典 - 没有类型检查
person = {
    "name": "张三",
    "age": 25,
    "email": "zhangsan@example.com"
}
# IDE 不知道有哪些键,也没有自动补全

# ✅ TypedDict - 有类型检查
class Person(TypedDict):
    name: str
    age: int
    email: str

person: Person = {
    "name": "张三",
    "age": 25,
    "email": "zhangsan@example.com"
}
# IDE 会提示可用的键,并检查类型是否正确

TypedDict 的优势

  • 类型安全:编译器检查字段类型
  • IDE 支持:自动补全、代码提示
  • 文档化:清晰说明字典结构
  • 错误检测:编写时发现拼写错误

高级用法

from typing import TypedDict, Optional

# 可选字段
class Config(TypedDict):
    host: str
    port: int
    timeout: Optional[int]  # 可选字段

# 继承
class BaseState(TypedDict):
    messages: list[str]

class ExtendedState(BaseState):
    user_id: str
    session_token: str

# 所有字段可选
class FlexibleConfig(TypedDict, total=False):
    debug: bool
    verbose: bool

🎯 Annotated:附加元数据到类型

Annotated 是 Python 3.9+ 引入的类型提示工具,允许你在类型注解中附加额外的元数据

基本语法

from typing import Annotated

# 基本格式:Annotated[类型, 元数据1, 元数据2, ...]
name: Annotated[str, "这是用户名字段", "最大长度50"]
age: Annotated[int, "年龄", "范围: 0-150"]

核心作用
Annotated 本身不改变类型的行为,它只是携带额外信息,供框架或库使用。


🔧 LangGraph 中的特殊用法

在 LangGraph 中,Annotated特殊的语义:第二个参数是一个 Reducer 函数,定义如何更新这个状态字段。

from typing import Annotated
from langchain_core.messages import AnyMessage
from langgraph.graph import add_messages

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

这段代码的含义

messages: Annotated[list[AnyMessage], add_messages]
          │         │              │
          │         │              └─ Reducer 函数(如何合并新值)
          │         └─ 类型:消息列表
          └─ 字段名

翻译成人话

"messages 字段是一个消息列表,当节点返回新的 messages 时,使用 add_messages 函数来合并到现有状态中"


💡 add_messages Reducer 的工作原理

不使用 Reducer(普通方式)

class State(TypedDict):
    messages: list[AnyMessage]

# 节点返回
def my_node(state: State) -> dict:
    return {
        "messages": [new_message]  # ❌ 这会覆盖原有的 messages!
    }

问题:每次返回都会完全替换原有消息,历史对话丢失!

使用 add_messages Reducer

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

# 节点返回
def my_node(state: State) -> dict:
    return {
        "messages": [new_message]  # ✅ 自动追加到现有消息列表
    }

效果:LangGraph 会自动调用 add_messages(old_messages, [new_message]),实现追加而非覆盖

实际示例

from typing import TypedDict, Annotated
from langchain_core.messages import HumanMessage, AIMessage
from langgraph.graph import add_messages

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

# 初始状态
state = {
    "messages": [HumanMessage(content="你好")]
}

# 节点返回新消息
def chat_node(state: State) -> dict:
    return {
        "messages": [AIMessage(content="你好!有什么可以帮助你的?")]
    }

# LangGraph 内部执行
new_state = {
    "messages": add_messages(
        state["messages"],  # 原有消息
        [AIMessage(content="你好!有什么可以帮助你的?")]  # 新消息
    )
}

# 结果:messages 包含两条消息
print(new_state["messages"])
# [HumanMessage(content="你好"), AIMessage(content="你好!有什么可以帮助你的?")]

🎨 自定义 Reducer 函数

还记得项目中的 update_dialog_stack 吗?这就是一个自定义 Reducer:

def update_dialog_stack(left: list[str], right: Optional[str]) -> list[str]:
    """自定义 reducer:管理对话状态栈"""
    if right is None:
        return left  # 无变化
    if right == "pop":
        return left[:-1]  # 弹出栈顶
    return left + [right]  # 压入新值

class State(TypedDict):
    dialog_state: Annotated[list[str], update_dialog_stack]

# 使用
def enter_flight_assistant(state: State) -> dict:
    return {
        "dialog_state": "update_flight"  # 压入新状态
    }

def leave_skill(state: State) -> dict:
    return {
        "dialog_state": "pop"  # 弹出当前状态
    }

工作流程

# 初始状态
state = {"dialog_state": ["assistant"]}

# 进入航班助手
state = update_dialog_stack(["assistant"], "update_flight")
# 结果: ["assistant", "update_flight"]

# 离开技能
state = update_dialog_stack(["assistant", "update_flight"], "pop")
# 结果: ["assistant"]

📊 Reducer 函数的签名

所有 Reducer 函数都遵循相同的模式:

def reducer_function(current_value, new_value) -> updated_value:
    """
    Args:
        current_value: 当前的状态值
        new_value: 节点返回的新值
    
    Returns:
        合并后的新状态值
    """
    pass

常见 Reducer 模式

# 1. 列表追加
def add_messages(left: list, right: list) -> list:
    return left + right

# 2. 数值累加
def increment_counter(current: int, new: int) -> int:
    return current + new

# 3. 字典合并
def merge_dicts(current: dict, new: dict) -> dict:
    return {**current, **new}

# 4. 自定义逻辑(如栈操作)
def update_dialog_stack(left: list[str], right: Optional[str]) -> list[str]:
    if right == "pop":
        return left[:-1]
    return left + [right]

🎯 为什么需要 Annotated?

问题场景:如果没有 Annotated

class State(TypedDict):
    messages: list[AnyMessage]

# 每个节点都需要手动合并消息
def my_node(state: State) -> dict:
    old_messages = state["messages"]
    new_message = AIMessage(content="...")
    return {
        "messages": old_messages + [new_message]  # 😫 每个节点都要写
    }

问题

  • ❌ 代码重复
  • ❌ 容易忘记合并
  • ❌ 不同节点可能有不同的合并逻辑

使用 Annotated 后

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

# 节点只需返回新消息
def my_node(state: State) -> dict:
    return {
        "messages": [AIMessage(content="...")]  # 😊 简洁明了
    }

# LangGraph 自动调用 add_messages 合并

优势

  • ✅ 声明式:在状态定义时指定合并逻辑
  • ✅ 自动化:框架自动处理
  • ✅ 一致性:所有节点使用相同的合并规则
  • ✅ 可定制:可以为不同字段定义不同的合并策略

3.1 状态定义(TypedDict 实战)

现在我们已经理解了 TypedDictAnnotated,让我们看看项目中的实际应用:

from typing import TypedDict, Annotated, Optional, Literal
from langchain_core.messages import AnyMessage
from langgraph.graph import add_messages

class State(TypedDict):
    """对话状态的结构化定义"""
    
    # 消息列表:使用 add_messages reducer 自动合并新消息
    messages: Annotated[list[AnyMessage], add_messages]
    
    # 用户信息(没有 Annotated,直接覆盖)
    user_info: str
    
    # 对话状态栈:跟踪当前激活的子助手
    dialog_state: Annotated[
        list[Literal[
            "assistant",
            "update_flight",
            "book_car_rental",
            "book_hotel",
            "book_excursion",
        ]],
        update_dialog_stack,  # 自定义 reducer 函数
    ]

三个字段的对比

字段 类型 Reducer 行为
messages list[AnyMessage] add_messages 追加新消息
user_info str 无(默认) 直接覆盖
dialog_state list[str] update_dialog_stack 栈操作(push/pop)

3.2 Reducer 函数

Reducer 定义如何更新状态字段:

def update_dialog_stack(left: list[str], right: Optional[str]) -> list[str]:
    """
    自定义状态更新逻辑
    
    Args:
        left: 当前状态值
        right: 新值
    
    Returns:
        更新后的状态值
    """
    if right is None:
        return left  # 无变化
    if right == "pop":
        return left[:-1]  # 弹出栈顶
    return left + [right]  # 压入新值

内置 Reducer

  • add_messages: 自动追加消息到列表
  • operator.add: 列表拼接
  • 自定义函数:完全控制更新逻辑

3.3 状态传递

# 节点返回字典,只包含需要更新的字段
def entry_node(state: dict) -> dict:
    return {
        "messages": [ToolMessage(content="...", tool_call_id="...")],
        "dialog_state": "update_flight",  # 只更新这两个字段
    }

# LangGraph 自动合并返回的状态

工作流图构建(Graph Construction)

4.1 主工作流构建步骤

# 步骤 1: 创建状态图
builder = StateGraph(State)

# 步骤 2: 添加初始节点
builder.add_node('fetch_user_info', get_user_info)
builder.add_edge(START, 'fetch_user_info')

# 步骤 3: 添加子工作流(模块化设计)
builder = build_flight_graph(builder)
builder = builder_hotel_graph(builder)
builder = build_car_graph(builder)
builder = builder_excursion_graph(builder)

# 步骤 4: 添加主助理节点
builder.add_node('primary_assistant', CtripAssistant(assistant_runnable))
builder.add_node("primary_assistant_tools", create_tool_node_with_fallback(...))

# 步骤 5: 添加条件路由
builder.add_conditional_edges('primary_assistant', route_primary_assistant, [...])
builder.add_edge('primary_assistant_tools', 'primary_assistant')

# 步骤 6: 添加入口路由
builder.add_conditional_edges("fetch_user_info", route_to_workflow)

# 步骤 7: 编译图
memory = MemorySaver()
graph = builder.compile(
    checkpointer=memory,
    interrupt_before=[
        "update_flight_sensitive_tools",
        "book_car_rental_sensitive_tools",
        # ... 其他敏感操作
    ]
)

4.2 子工作流模式

每个子助手遵循相同的模式:

def build_flight_graph(builder: StateGraph) -> StateGraph:
    """构建航班预订子工作流"""
    
    # 1. 添加入口节点
    builder.add_node(
        "enter_update_flight",
        create_entry_node("Flight Assistant", "update_flight"),
    )
    
    # 2. 添加助理节点
    builder.add_node("update_flight", CtripAssistant(update_flight_runnable))
    builder.add_edge("enter_update_flight", "update_flight")
    
    # 3. 添加工具节点(区分安全和敏感)
    builder.add_node("update_flight_sensitive_tools", ...)
    builder.add_node("update_flight_safe_tools", ...)
    
    # 4. 添加工具路由逻辑
    def route_update_flight(state: dict):
        route = tools_condition(state)
        if route == END:
            return END
        
        tool_calls = state["messages"][-1].tool_calls
        did_cancel = any(tc["name"] == CompleteOrEscalate.__name__ for tc in tool_calls)
        if did_cancel:
            return "leave_skill"
        
        safe_tool_names = [t.name for t in update_flight_safe_tools]
        if all(tc["name"] in safe_tool_names for tc in tool_calls):
            return "update_flight_safe_tools"
        return "update_flight_sensitive_tools"
    
    # 5. 添加条件边
    builder.add_conditional_edges(
        "update_flight",
        route_update_flight,
        ["update_flight_sensitive_tools", "update_flight_safe_tools", "leave_skill", END],
    )
    
    # 6. 添加工具返回边
    builder.add_edge("update_flight_sensitive_tools", "update_flight")
    builder.add_edge("update_flight_safe_tools", "update_flight")
    
    # 7. 添加退出节点
    builder.add_node("leave_skill", pop_dialog_state)
    builder.add_edge("leave_skill", "primary_assistant")
    
    return builder

4.3 入口节点工厂函数

def create_entry_node(assistant_name: str, new_dialog_state: str) -> Callable:
    """
    工厂函数:创建入口节点
    
    作用:
    1. 生成 ToolMessage 响应主助理的工具调用
    2. 更新 dialog_state 切换到子助手
    """
    def entry_node(state: dict) -> dict:
        tool_call_id = state["messages"][-1].tool_calls[0]["id"]
        return {
            "messages": [
                ToolMessage(
                    content=f"现在助手是{assistant_name}。请回顾上述对话...",
                    tool_call_id=tool_call_id,
                )
            ],
            "dialog_state": new_dialog_state,
        }
    return entry_node

工具系统(Tools System)

5.0 LangChain 工具基础(前置知识)

在深入工具分类和错误处理之前,我们需要先理解如何定义和使用 LangChain 工具。

🛠️ @tool 装饰器 - 定义工具

@tool 是 LangChain 提供的装饰器,用于将普通 Python 函数转换为 LLM 可调用的工具。

基本语法

from langchain_core.tools import tool

@tool
def my_tool(param1: str, param2: int = 10) -> str:
    """
    工具的简短描述(LLM 通过它理解用途)
    
    Args:
        param1: 参数1的描述
        param2: 参数2的描述
    
    Returns:
        返回值的描述
    """
    # 实现逻辑
    return f"结果: {param1}, {param2}"

关键要素

  1. 函数签名:类型注解帮助 LLM 理解参数类型
  2. 文档字符串:详细描述工具用途、参数和返回值
  3. 返回值:必须是字符串或可序列化的对象

完整示例:航班搜索工具

from typing import Optional, List, Dict
from datetime import date, datetime
from langchain_core.tools import tool
import sqlite3

@tool
def search_flights(
    departure_airport: Optional[str] = None,
    arrival_airport: Optional[str] = None,
    start_time: Optional[date | datetime] = None,
    end_time: Optional[date | datetime] = None,
    limit: int = 20,
) -> List[Dict]:
    """
    根据指定的参数搜索航班,并返回匹配的航班列表。
    
    如果未提供参数,则返回所有可用的航班。
    
    Args:
        departure_airport: 出发机场代码(如 "SFO", "JFK")。可选。
        arrival_airport: 到达机场代码。可选。
        start_time: 最早出发时间。可选。
        end_time: 最晚出发时间。可选。
        limit: 返回结果数量限制,默认 20。
    
    Returns:
        匹配条件的航班信息列表,每个元素是一个字典。
    
    示例:
        >>> search_flights(departure_airport="SFO", arrival_airport="JFK")
        [{"flight_id": 1, "departure": "SFO", ...}, ...]
    """
    conn = sqlite3.connect("travel.db")
    cursor = conn.cursor()
    
    query = "SELECT * FROM flights WHERE 1 = 1"
    params = []
    
    if departure_airport:
        query += " AND departure_airport = ?"
        params.append(departure_airport)
    
    if arrival_airport:
        query += " AND arrival_airport = ?"
        params.append(arrival_airport)
    
    query += " LIMIT ?"
    params.append(limit)
    
    cursor.execute(query, params)
    rows = cursor.fetchall()
    column_names = [column[0] for column in cursor.description]
    results = [dict(zip(column_names, row)) for row in rows]
    
    cursor.close()
    conn.close()
    return results

工具的工作原理

1. 定义工具(使用 @tool 装饰器)
   ↓
2. LangChain 自动提取:
   - 函数名 → 工具名称
   - 类型注解 → 参数类型
   - 文档字符串 → 工具描述
   ↓
3. 生成 JSON Schema(供 LLM 理解)
   ↓
4. 绑定到 LLM(llm.bind_tools([my_tool]))
   ↓
5. LLM 决定调用工具时,返回 tool_calls
   ↓
6. 代码执行工具函数
   ↓
7. 将结果包装为 ToolMessage
   ↓
8. 返回给 LLM 生成最终回答

项目中的实际应用

应用 1:带配置参数的工具
from langchain_core.tools import tool
from langchain_core.runnables import RunnableConfig

@tool
def fetch_user_flight_information(config: RunnableConfig) -> List[Dict]:
    """
    从数据库中获取当前乘客的航班预订信息。
    
    Args:
        config: 运行时配置,包含 passenger_id 等信息。
    
    Returns:
        乘客的航班信息列表。
    """
    configuration = config.get("configurable", {})
    passenger_id = configuration.get("passenger_id", None)
    
    if not passenger_id:
        raise ValueError("未配置乘客 ID。请在 config 中设置 passenger_id。")
    
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()
    
    cursor.execute(
        "SELECT * FROM bookings WHERE passenger_id = ?",
        (passenger_id,)
    )
    
    rows = cursor.fetchall()
    column_names = [column[0] for column in cursor.description]
    results = [dict(zip(column_names, row)) for row in rows]
    
    cursor.close()
    conn.close()
    return results

关键点

  • ✅ 使用 RunnableConfig 访问上下文信息
  • ✅ 从 config["configurable"] 获取业务参数
  • ✅ 验证必需参数,抛出清晰的错误

应用 2:写操作工具(敏感工具)
@tool
def update_ticket_to_new_flight(
    ticket_no: str,
    new_flight_id: int,
    *,  # ⭐ 强制关键字参数
    config: RunnableConfig,
) -> str:
    """
    更新机票到新航班。
    
    Args:
        ticket_no: 票号(如 "TICKET-001")。
        new_flight_id: 新航班的 ID。
        config: 运行时配置。
    
    Returns:
        操作结果消息。
    """
    configuration = config.get("configurable", {})
    passenger_id = configuration.get("passenger_id", None)
    
    if not passenger_id:
        raise ValueError("未配置乘客 ID。")
    
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()
    
    # 验证票号属于当前乘客
    cursor.execute(
        "SELECT * FROM bookings WHERE ticket_no = ? AND passenger_id = ?",
        (ticket_no, passenger_id)
    )
    booking = cursor.fetchone()
    
    if not booking:
        return f"错误:票号 {ticket_no} 不存在或不属于当前乘客。"
    
    # 更新航班
    cursor.execute(
        "UPDATE bookings SET flight_id = ? WHERE ticket_no = ?",
        (new_flight_id, ticket_no)
    )
    
    conn.commit()
    cursor.close()
    conn.close()
    
    return f"成功将票号 {ticket_no} 更新到航班 {new_flight_id}。"

关键点

  • ✅ 使用 *, 强制关键字参数(避免位置参数混淆)
  • ✅ 验证权限(确保票号属于当前乘客)
  • ✅ 返回清晰的成功/失败消息
  • ✅ 这是敏感工具,需要用户审批

应用 3:RAG 检索工具
@tool
def lookup_policy(query: str) -> str:
    """
    查询公司政策文档。
    
    在进行航班变更或其他'写'操作之前使用此函数,
    以确保符合公司政策。
    
    Args:
        query: 政策查询问题(如 "改签费用是多少?")。
    
    Returns:
        相关的政策文本。
    """
    # 使用向量检索器查找相关政策
    docs = retriever.query(query, k=2)
    
    if not docs:
        return "未找到相关政策。"
    
    # 格式化返回结果
    formatted_docs = [
        f"来源: {doc.get('source', '未知')}\n{doc['page_content']}"
        for doc in docs
    ]
    
    return "\n\n".join(formatted_docs)

关键点

  • ✅ 明确说明使用时机("在进行写操作之前使用")
  • ✅ 结合向量检索实现 RAG
  • ✅ 格式化输出,包含来源信息

常见错误与最佳实践

❌ 错误 1:缺少文档字符串
# ❌ 不好:LLM 不知道工具用途
@tool
def search(x: str, y: int) -> str:
    return f"{x}: {y}"

# ✅ 好:清晰的描述
@tool
def search_flights(departure_airport: str) -> List[Dict]:
    """
    根据出发机场搜索航班。
    
    Args:
        departure_airport: 出发机场代码(如 "SFO")。
    
    Returns:
        航班信息列表。
    """
    ...
❌ 错误 2:缺少类型注解
# ❌ 不好:LLM 不知道参数类型
@tool
def process_data(data, count):
    ...

# ✅ 好:明确的类型
@tool
def process_data(data: str, count: int = 10) -> str:
    ...
❌ 错误 3:返回不可序列化的对象
# ❌ 错误:返回复杂对象
@tool
def get_user() -> User:
    return User(...)  # Pydantic 模型可能无法正确序列化

# ✅ 正确:返回字符串或字典
@tool
def get_user() -> str:
    user = fetch_user()
    return f"姓名: {user.name}, 年龄: {user.age}"
✅ 最佳实践:使用 Optional 和默认值
@tool
def search(
    departure: Optional[str] = None,  # 可选参数
    arrival: Optional[str] = None,
    limit: int = 20,  # 有默认值
) -> List[Dict]:
    """灵活的搜索工具"""
    ...
✅ 最佳实践:清晰的错误消息
@tool
def book_hotel(hotel_id: int, config: RunnableConfig) -> str:
    if not validate_availability(hotel_id):
        return "错误:该酒店在当前日期无空房。请尝试其他日期或酒店。"
    
    # 正常逻辑...
    return f"成功预订酒店 {hotel_id}。"

工具的分类

类型 特点 示例
安全工具 只读操作,无需审批 search_flights, lookup_policy
敏感工具 写操作,需要审批 book_hotel, update_ticket
委派工具 Pydantic 模型,用于路由 ToFlightBookingAssistant
升级工具 退出子助手 CompleteOrEscalate

5.1 工具定义

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

from langchain_core.tools import tool
from langchain_core.runnables import RunnableConfig

@tool
def search_flights(
    departure_airport: Optional[str] = None,
    arrival_airport: Optional[str] = None,
    start_time: Optional[date | datetime] = None,
    end_time: Optional[date | datetime] = None,
    limit: int = 20,
) -> List[Dict]:
    """
    搜索航班
    
    Args:
        departure_airport: 出发机场代码
        arrival_airport: 到达机场代码
        start_time: 最早出发时间
        end_time: 最晚出发时间
        limit: 返回结果数量限制
    
    Returns:
        航班信息列表
    """
    # 实现逻辑...
    return results

5.2 工具分类

安全工具(Safe Tools)

  • 特点:只读操作,不修改数据
  • 示例search_flights, search_hotels, search_car_rentals
  • 无需用户确认:可以直接执行

敏感工具(Sensitive Tools)

  • 特点:写操作,修改数据
  • 示例update_ticket_to_new_flight, book_hotel, cancel_ticket
  • 需要用户确认:通过 interrupt_before 中断等待审批
# 主助理工具
primary_assistant_tools = [
    tavily_tool,          # 网络搜索
    search_flights,       # 航班搜索(安全)
    lookup_policy,        # 政策查询(安全)
]

# 航班助手工具
update_flight_safe_tools = [search_flights]
update_flight_sensitive_tools = [update_ticket_to_new_flight, cancel_ticket]

5.3 工具错误处理

def handle_tool_error(state) -> dict:
    """统一处理工具执行错误"""
    error = state.get("error")
    tool_calls = state["messages"][-1].tool_calls
    return {
        "messages": [
            ToolMessage(
                content=f"错误: {repr(error)}\n请修正您的错误。",
                tool_call_id=tc["id"],
            )
            for tc in tool_calls
        ]
    }

def create_tool_node_with_fallback(tools: list):
    """创建带错误回退的工具节点"""
    return ToolNode(tools).with_fallbacks(
        [RunnableLambda(handle_tool_error)], 
        exception_key="error"
    )

5.4 配置传递

工具可以通过 config 参数访问上下文信息:

@tool
def fetch_user_flight_information(config: RunnableConfig) -> List[Dict]:
    """从配置中获取乘客 ID"""
    configuration = config.get("configurable", {})
    passenger_id = configuration.get("passenger_id", None)
    if not passenger_id:
        raise ValueError("未配置乘客 ID。")
    
    # 使用 passenger_id 查询数据库
    # ...

多智能体架构(Multi-Agent Architecture)

6.1 架构设计原则

┌─────────────────────────────────────┐
│      Primary Assistant (主助理)      │
│  职责:                              │
│  - 理解用户意图                      │
│  - 路由到专业助手                    │
│  - 提供通用信息查询                  │
└──────────┬──────────────────────────┘
           │ 委派任务
    ┌──────┼──────────┬──────────┐
    ▼      ▼          ▼          ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌──────────┐
│ Flight │ │ Hotel  │ │  Car   │ │Excursion │
│ Agent  │ │ Agent  │ │ Agent  │ │  Agent   │
└────────┘ └────────┘ └────────┘ └──────────┘
   每个子助手:
   - 专注特定领域
   - 拥有专用工具集
   - 可升级回主助理

6.2 主助理 Prompt 设计

primary_assistant_prompt = ChatPromptTemplate.from_messages([
    ("system", """
    您是携程瑞士航空公司的客户服务助理。
    
    主要职责:
    1. 搜索航班信息和公司政策回答客户查询
    2. 如果客户请求更新或取消航班、预订租车、预订酒店或获取旅行推荐,
       通过调用相应的工具将任务委派给合适的专门助理
    3. 您自己无法进行这些类型的更改,只有专门助理才有权限
    
    重要规则:
    - 用户并不知道有不同的专门助理存在,因此请不要提及他们
    - 只需通过函数调用来安静地委派任务
    - 向客户提供详细的信息,并且在确定信息不可用之前总是复查数据库
    - 在搜索时,请坚持不懈。如果第一次搜索没有结果,请扩大查询范围
    
    当前用户的航班信息:
    <Flights>
    {user_info}
    </Flights>
    
    当前时间: {time}.
    """),
    ("placeholder", "{messages}"),
]).partial(time=datetime.now())

6.3 子助手 Prompt 设计

flight_booking_prompt = ChatPromptTemplate.from_messages([
    ("system", """
    您是专门处理航班查询、改签和预定的助理。
    
    工作流程:
    1. 当用户需要帮助更新他们的预订时,主助理会将工作委托给您
    2. 请与客户确认更新后的航班详情,并告知他们任何额外费用
    3. 在搜索时,请坚持不懈。如果第一次搜索没有结果,请扩大查询范围
    4. 如果您需要更多信息或客户改变主意,请将任务升级回主助理
    5. 请记住,在相关工具成功使用后,预订才算完成
    
    当前用户的航班信息:
    <Flights>
    {user_info}
    </Flights>
    
    当前时间: {time}.
    
    如果用户需要帮助,并且您的工具都不适用,则
    "CompleteOrEscalate"对话给主助理。不要浪费用户的时间。
    不要编造无效的工具或功能。
    """),
    ("placeholder", "{messages}"),
]).partial(time=datetime.now())

6.4 委派机制

主助理 → 子助手

使用 Pydantic 模型作为工具:

from pydantic import BaseModel, Field

class ToFlightBookingAssistant(BaseModel):
    """将工作转交给专门处理航班的助理"""
    request: str = Field(
        description="更新航班助理在继续之前需要澄清的任何后续问题。"
    )

# 绑定到 LLM
assistant_runnable = primary_assistant_prompt | llm.bind_tools(
    primary_assistant_tools + [ToFlightBookingAssistant, ...]
)

子助手 → 主助理

使用 CompleteOrEscalate 工具:

class CompleteOrEscalate(BaseModel):
    """标记当前任务为已完成和/或将对话的控制权升级到主助理"""
    cancel: bool = True
    reason: str

# 在每个子助手的工具集中包含
update_flight_runnable = flight_booking_prompt | llm.bind_tools(
    update_flight_tools + [CompleteOrEscalate]
)

6.5 对话状态栈管理

def route_to_workflow(state: dict) -> str:
    """
    根据 dialog_state 路由到当前激活的助手
    
    工作原理:
    - dialog_state 是一个栈结构
    - 栈顶元素表示当前活跃的助手
    - 如果没有 dialog_state,返回主助理
    """
    dialog_state = state.get("dialog_state")
    if not dialog_state:
        return "primary_assistant"
    return dialog_state[-1]  # 返回栈顶

持久化与检查点(Persistence & Checkpointing)

7.1 MemorySaver

from langgraph.checkpoint.memory import MemorySaver

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

# 编译时传入
graph = builder.compile(checkpointer=memory)

功能

  • 自动保存每个步骤的状态
  • 支持中断和恢复
  • 多线程隔离(通过 thread_id

7.2 配置管理

import uuid

session_id = str(uuid.uuid4())

config = {
    "configurable": {
        "passenger_id": "3442 587242",  # 业务参数
        "thread_id": session_id,         # 会话标识
    }
}

# 使用时传入
events = graph.stream({'messages': ('user', question)}, config)

7.3 状态恢复

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

# 查看下一步要执行的节点
if current_state.next:
    print(f"等待执行: {current_state.next}")

# 从中断处继续
events = graph.stream(None, config)  # 传入 None 表示继续执行

人机交互与审批流程(Human-in-the-Loop)

8.1 中断配置

graph = builder.compile(
    checkpointer=memory,
    interrupt_before=[
        "update_flight_sensitive_tools",   # 航班敏感操作前中断
        "book_car_rental_sensitive_tools",  # 租车敏感操作前中断
        "book_hotel_sensitive_tools",       # 酒店敏感操作前中断
        "book_excursion_sensitive_tools",   # 游览敏感操作前中断
    ]
)

8.2 审批流程实现

while True:
    question = input('用户:')
    
    # 执行工作流
    events = graph.stream(
        {'messages': ('user', question)}, 
        config, 
        stream_mode='values'
    )
    
    # 打印事件
    for event in events:
        _print_event(event, _printed)
    
    # 检查是否需要人工审批
    current_state = graph.get_state(config)
    if current_state.next:
        user_input = input(
            "您是否批准上述操作?输入'y'继续;否则,请说明您请求的更改。\n"
        )
        
        if user_input.strip().lower() == "y":
            # 批准:继续执行
            events = graph.stream(None, config, stream_mode='values')
            for event in events:
                _print_event(event, _printed)
        else:
            # 拒绝:发送 ToolMessage 拒绝工具调用
            result = graph.stream(
                {
                    "messages": [
                        ToolMessage(
                            tool_call_id=event["messages"][-1].tool_calls[0]["id"],
                            content=f"Tool的调用被用户拒绝。原因:'{user_input}'。",
                        )
                    ]
                },
                config,
            )
            for event in result:
                _print_event(event, _printed)

8.3 关键要点

  1. 中断时机:在敏感工具执行前中断
  2. 状态持久化:中断后状态已保存,可随时恢复
  3. 用户反馈:通过 ToolMessage 将拒绝原因传回 LLM
  4. LLM 自适应:LLM 根据拒绝原因调整策略

向量检索与 RAG

9.1 向量存储实现

import numpy as np
from langchain_openai import OpenAIEmbeddings

class VectorStoreRetriever:
    """自定义向量检索器"""
    
    def __init__(self, docs: list, vectors: list):
        self._arr = np.array(vectors)
        self._docs = docs
    
    @classmethod
    def from_docs(cls, docs):
        """从文档构建向量索引"""
        embeddings_model = OpenAIEmbeddings(...)
        embeddings = embeddings_model.embed_documents(
            [doc["page_content"] for doc in docs]
        )
        return cls(docs, embeddings)
    
    def query(self, query: str, k: int = 5) -> list[dict]:
        """相似度检索"""
        # 1. 嵌入查询
        embed = embeddings_model.embed_query(query)
        
        # 2. 计算余弦相似度(点积)
        scores = np.array(embed) @ self._arr.T
        
        # 3. 获取 top-k
        top_k_idx = np.argpartition(scores, -k)[-k:]
        top_k_idx_sorted = top_k_idx[np.argsort(-scores[top_k_idx])]
        
        # 4. 返回结果
        return [
            {**self._docs[idx], "similarity": scores[idx]} 
            for idx in top_k_idx_sorted
        ]

# 使用
retriever = VectorStoreRetriever.from_docs(docs)

9.2 RAG 工具

@tool
def lookup_policy(query: str) -> str:
    """
    查询公司政策
    
    用途:在进行航班变更或其他'写'操作之前使用此函数
    """
    docs = retriever.query(query, k=2)
    return "\n\n".join([doc["page_content"] for doc in docs])

9.3 FAQ 文档分割

import re

# 读取 FAQ 文本
with open('order_faq.md', encoding='utf8') as f:
    faq_text = f.read()

# 按 Markdown 二级标题分割
docs = [{"page_content": txt} for txt in re.split(r"(?=\n##)", faq_text)]

FastAPI 集成

10.1 应用初始化

from fastapi import FastAPI, Depends
from starlette.staticfiles import StaticFiles

class Server:
    def __init__(self):
        # 创建 FastAPI 应用,添加全局依赖(OAuth2 认证)
        my_oauth2 = MyOAuth2PasswordBearer(tokenUrl='/api/auth/', schema='JWT')
        self.app = FastAPI(dependencies=[Depends(my_oauth2)])
        
        # 挂载静态文件
        self.app.mount('/static', StaticFiles(directory='static'), name='my_static')
    
    def init_app(self):
        # 初始化全局组件
        handler_error.init_handler_errors(self.app)
        middlewares.init_middleware(self.app)
        cors.init_cors(self.app)
        routers.init_routers(self.app)
    
    def run(self):
        self.init_app()
        uvicorn.run(app=self.app, host=settings.HOST, port=settings.PORT)

10.2 路由注册

# api/routers.py
def init_routers(app: FastAPI):
    """注册所有路由"""
    from .graph_api import graph_views
    from .system_mgt import user_views
    
    app.include_router(graph_views.router, prefix="/api/graph")
    app.include_router(user_views.router, prefix="/api/users")

10.3 Graph API 视图

# api/graph_api/graph_views.py
from fastapi import APIRouter
from .graph_schemas import BaseGraphSchema, GraphRspSchema

router = APIRouter()

@router.post("/chat", response_model=GraphRspSchema)
async def chat(request: BaseGraphSchema):
    """调用 LangGraph 工作流"""
    config = request.config.dict() if request.config else {}
    
    # 执行工作流
    events = graph.stream(
        {'messages': ('user', request.user_input)}, 
        config
    )
    
    # 收集响应
    assistant_response = ""
    for event in events:
        if "messages" in event:
            # 提取 AI 回复
            ...
    
    return GraphRspSchema(assistant=assistant_response)

10.4 YAML 配置管理

# config/development.yml
LOG_LEVEL: INFO
HOST: 127.0.0.1
PORT: 8000
ORIGINS: ['http://localhost:8080']
DATABASE:
  DRIVER: mysql
  NAME: test_db4
  HOST: 127.0.0.1
  PORT: 3306
  USERNAME: root
  PASSWORD: 123123
JWT_SECRET_KEY: "..."
ALGORITHM: HS256
ACCESS_TOKEN_EXPIRE_MINUTES: 30
WHITE_LIST: ['/api/login', '/docs', ...]
# config/__init__.py
import yaml
from pathlib import Path

class Settings:
    def __init__(self):
        config_path = Path(__file__).parent / "development.yml"
        with open(config_path, 'r', encoding='utf-8') as f:
            config = yaml.safe_load(f)
        
        self.HOST = config['HOST']
        self.PORT = config['PORT']
        # ... 其他配置

settings = Settings()

数据库操作

11.1 SQLite 数据库

import sqlite3
from langchain_core.tools import tool

@tool
def search_flights(departure_airport: str = None, ...) -> List[Dict]:
    """搜索航班"""
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()
    
    # 动态构建查询
    query = "SELECT * FROM flights WHERE 1 = 1"
    params = []
    
    if departure_airport:
        query += " AND departure_airport = ?"
        params.append(departure_airport)
    
    query += " LIMIT ?"
    params.append(limit)
    
    cursor.execute(query, params)
    
    # 转换为字典列表
    rows = cursor.fetchall()
    column_names = [column[0] for column in cursor.description]
    results = [dict(zip(column_names, row)) for row in rows]
    
    cursor.close()
    conn.close()
    return results

11.2 日期对齐

def update_dates():
    """
    将数据库中的日期对齐到当前时间
    
    目的:确保测试数据始终是"最近"的
    """
    import shutil
    import pandas as pd
    
    # 1. 重置数据库(从备份复制)
    shutil.copy(backup_file, local_file)
    
    conn = sqlite3.connect(local_file)
    
    # 2. 读取所有表
    tables = pd.read_sql("SELECT name FROM sqlite_master WHERE type='table';", conn)
    tdf = {}
    for t in tables['name']:
        tdf[t] = pd.read_sql(f"SELECT * from {t}", conn)
    
    # 3. 计算时间差
    example_time = pd.to_datetime(tdf["flights"]["actual_departure"].replace("\\N", pd.NaT)).max()
    current_time = pd.to_datetime("now").tz_localize(example_time.tz)
    time_diff = current_time - example_time
    
    # 4. 更新日期字段
    datetime_columns = ["scheduled_departure", "scheduled_arrival", ...]
    for column in datetime_columns:
        tdf["flights"][column] = (
            pd.to_datetime(tdf["flights"][column].replace("\\N", pd.NaT)) + time_diff
        )
    
    # 5. 写回数据库
    for table_name, df in tdf.items():
        df.to_sql(table_name, conn, if_exists="replace", index=False)
    
    conn.commit()
    conn.close()

11.3 SQLAlchemy ORM(系统管理模块)

# db/system_mgt/models.py
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    username = Column(String(50), unique=True)
    password_hash = Column(String(255))

常见陷阱与最佳实践

12.1 依赖管理陷阱

❌ 问题:uuid_utils 版本冲突

ImportError: cannot import name 'reseed_rng' from 'uuid_utils._uuid_utils'

✅ 解决方案

# 方案 1:升级
pip install --upgrade uuid-utils

# 方案 2:降级到稳定版
pip install uuid-utils==0.9.0

# 方案 3:清理缓存重装
pip cache purge
pip uninstall uuid-utils -y
pip install uuid-utils --no-cache-dir

📝 预防措施

requirements.txt 中明确指定兼容版本:

langchain>=0.3.0
langchain-core>=0.3.0
langgraph>=0.2.0
uuid-utils>=0.9.0

12.2 模块导入陷阱

❌ 问题:直接运行脚本导致导入失败

python Embedding/ctrip_assistant/graph_chat/第三个流程图.py
# ModuleNotFoundError: No module named 'Embedding'

✅ 解决方案

# 方案 1:使用 -m 模块方式(推荐)
cd E:\PycharmProjects\aiStudy
python -m Embedding.ctrip_assistant.graph_chat.第三个流程图

# 方案 2:PyCharm 标记 Sources Root
# 右键 Embedding 目录 → Mark Directory as → Sources Root

# 方案 3:使用绝对导入
from Embedding.ctrip_assistant.graph_chat.state import State

12.3 状态管理最佳实践

✅ 使用 TypedDict 定义状态

class State(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]
    user_info: str
    dialog_state: Annotated[list[str], update_dialog_stack]

优点

  • 类型安全
  • IDE 自动补全
  • 编译时检查

✅ 使用 Reducer 函数

def update_dialog_stack(left: list[str], right: Optional[str]) -> list[str]:
    if right == "pop":
        return left[:-1]
    return left + [right]

优点

  • 集中管理状态更新逻辑
  • 避免状态不一致

12.4 工具设计最佳实践

✅ 清晰的文档字符串

@tool
def search_flights(
    departure_airport: Optional[str] = None,
    ...
) -> List[Dict]:
    """
    根据指定的参数搜索航班,并返回匹配的航班列表。
    
    Args:
        departure_airport: 出发机场(可选)。
        arrival_airport: 到达机场(可选)。
        ...
    
    Returns:
        匹配条件的航班信息列表。
    """

重要性:LLM 通过文档字符串理解工具用途

✅ 区分安全和敏感工具

# 安全工具(只读)
safe_tools = [search_flights, search_hotels]

# 敏感工具(写操作)
sensitive_tools = [book_hotel, cancel_ticket]

# 在图中分别处理
builder.add_node("safe_tools", create_tool_node_with_fallback(safe_tools))
builder.add_node("sensitive_tools", create_tool_node_with_fallback(sensitive_tools))

✅ 统一的错误处理

def handle_tool_error(state) -> dict:
    error = state.get("error")
    tool_calls = state["messages"][-1].tool_calls
    return {
        "messages": [
            ToolMessage(
                content=f"错误: {repr(error)}\n请修正您的错误。",
                tool_call_id=tc["id"],
            )
            for tc in tool_calls
        ]
    }

12.5 Prompt 工程最佳实践

✅ 明确的职责边界

"您是专门处理航班查询的助理。"
"您自己无法进行这些类型的更改,只有专门助理才有权限。"
"用户并不知道有不同的专门助理存在,因此请不要提及他们。"

✅ 提供具体示例

"以下是一些你应该CompleteOrEscalate的例子:
 - '这个季节的天气怎么样?'
 - '我再考虑一下,可能单独预订'
 - '我需要弄清楚我在那里的交通方式'"

✅ 强调关键规则

"在搜索时,请坚持不懈。如果第一次搜索没有结果,请扩大查询范围。"
"不要编造无效的工具或功能。"
"请记住,在相关工具成功使用后,预订才算完成。"

12.6 性能优化建议

✅ 流式输出

events = graph.stream(
    {'messages': ('user', question)}, 
    config, 
    stream_mode='values'  # 流式返回
)

for event in events:
    _print_event(event, _printed)  # 实时显示

✅ 避免重复打印

_printed = set()

def _print_event(event: dict, _printed: set, max_length=1500):
    message = event.get("messages")
    if message:
        if isinstance(message, list):
            message = message[-1]
        if message.id not in _printed:  # 去重
            print(message.pretty_repr(html=True))
            _printed.add(message.id)

✅ 数据库连接管理

@tool
def search_flights(...):
    conn = sqlite3.connect(db)
    try:
        cursor = conn.cursor()
        # 执行查询
        ...
    finally:
        cursor.close()
        conn.close()  # 确保关闭连接

12.7 调试技巧

✅ 可视化工作流

from Embedding.ctrip_assistant.graph_chat.draw_png import draw_graph

draw_graph(graph, 'graph.png')

✅ 打印状态变化

def _print_event(event: dict, _printed: set):
    current_state = event.get("dialog_state")
    if current_state:
        print("当前处于: ", current_state[-1])  # 显示当前助手
    
    message = event.get("messages")
    if message:
        print(message.pretty_repr(html=True))

✅ 检查点状态查询

current_state = graph.get_state(config)
print(f"当前节点: {current_state.values()}")
print(f"下一个节点: {current_state.next}")

12.8 安全注意事项

⚠️ API Key 管理

# ❌ 硬编码密钥(危险)
api_key = "sk-doD81WgxSoF9A6xYzhgW7GUh5frRwPETI8mDq3ce4UaWnCPF"

# ✅ 使用环境变量
import os
api_key = os.getenv("OPENAI_API_KEY")

# ✅ 使用配置文件(不提交到 Git)
# config/production.yml(添加到 .gitignore)

⚠️ SQL 注入防护

# ❌ 字符串拼接(危险)
query = f"SELECT * FROM users WHERE id = {user_id}"

# ✅ 参数化查询
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))

⚠️ 敏感操作审批

# 始终对写操作启用中断
graph = builder.compile(
    interrupt_before=[
        "update_flight_sensitive_tools",
        "book_hotel_sensitive_tools",
        # ...
    ]
)

🎯 学习路径建议

初级阶段

  1. 理解基础概念

    • LangChain Runnable 接口
    • LangGraph StateGraph
    • 节点和边的基本概念
  2. 实践简单工作流

    • 单节点工作流
    • 固定路由
    • 基础工具调用

中级阶段

  1. 掌握状态管理

    • TypedDict 状态定义
    • Reducer 函数
    • 状态持久化
  2. 学习条件路由

    • tools_condition
    • 自定义路由函数
    • 多分支逻辑

高级阶段

  1. 多智能体架构

    • 主子助手模式
    • 对话状态栈
    • 任务委派机制
  2. 人机交互

    • 中断和恢复
    • 审批流程
    • ToolMessage 反馈
  3. 生产部署

    • FastAPI 集成
    • OAuth2 认证
    • 错误处理和日志

📖 参考资源

官方文档

关键概念

  • StateGraph: 有状态工作流图
  • Checkpointing: 状态持久化
  • Human-in-the-Loop: 人机协作
  • Tool Calling: LLM 工具调用
  • RAG: 检索增强生成

设计模式

  • 多智能体协作: 主助手 + 专业助手
  • 工具分类: 安全工具 vs 敏感工具
  • 状态栈管理: 跟踪对话上下文
  • 错误回退: 优雅的错误处理

🔧 常见问题 FAQ

Q1: 如何处理长对话历史?

A: 使用消息修剪或摘要:

from langchain_core.messages import trim_messages

trimmed_messages = trim_messages(
    messages,
    max_tokens=1000,
    strategy="last",
    token_counter=len,
)

Q2: 如何实现并行工具调用?

A: LangGraph 默认支持并行:

# LLM 可以同时调用多个工具
# LangGraph 会自动并行执行
tool_calls = [
    {"name": "search_flights", "args": {...}},
    {"name": "search_hotels", "args": {...}},
]

Q3: 如何调试复杂工作流?

A:

  1. 使用 draw_graph() 可视化
  2. 打印每个节点的状态
  3. 使用检查点查看中间状态
  4. 逐步测试每个子图

Q4: 如何优化响应速度?

A:

  1. 使用流式输出
  2. 缓存常用查询结果
  3. 并行执行独立工具
  4. 限制搜索范围(limit 参数)

Q5: 如何处理并发请求?

A:

  1. 每个请求使用独立的 thread_id
  2. 使用异步 FastAPI 端点
  3. 数据库连接池
  4. 避免全局状态

🎓 总结

本项目涵盖了 LangChain 和 LangGraph 的核心概念:

状态管理工作流
多智能体协作
工具调用系统
人机交互审批
向量检索 RAG
FastAPI 集成
持久化检查点
错误处理最佳实践

通过学习这个项目,你将掌握构建复杂 AI 应用的完整技能栈!


最后更新: 2026-06-03
作者: AI Study Project
版本: v1.0

posted @ 2026-06-16 22:48  MrSponge  Views(8)  Comments(0)    收藏  举报