LangChain_LangGraph_知识点详解
LangChain & LangGraph 携程助手项目 - 完整知识点详解
项目概述:这是一个基于 LangChain 和 LangGraph 构建的多智能体旅行助手系统,模拟携程客服场景,实现航班查询、酒店预订、租车服务和旅游推荐等功能。
📚 目录
- 核心技术栈
- LangGraph 核心概念
- 状态管理(State Management)
- 工作流图构建(Graph Construction)
- 工具系统(Tools System)
- 多智能体架构(Multi-Agent Architecture)
- 持久化与检查点(Persistence & Checkpointing)
- 人机交互与审批流程(Human-in-the-Loop)
- 向量检索与 RAG
- FastAPI 集成
- 数据库操作
- 常见陷阱与最佳实践
核心技术栈
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,
]
)
路由函数的要求
必须满足:
- ✅ 接收一个参数:
state(当前状态字典) - ✅ 返回一个字符串:目标节点名称
- ✅ 返回值必须在
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 类型提示工具:TypedDict 和 Annotated。
📖 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 实战)
现在我们已经理解了 TypedDict 和 Annotated,让我们看看项目中的实际应用:
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}"
关键要素:
- 函数签名:类型注解帮助 LLM 理解参数类型
- 文档字符串:详细描述工具用途、参数和返回值
- 返回值:必须是字符串或可序列化的对象
完整示例:航班搜索工具
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 关键要点
- 中断时机:在敏感工具执行前中断
- 状态持久化:中断后状态已保存,可随时恢复
- 用户反馈:通过 ToolMessage 将拒绝原因传回 LLM
- 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",
# ...
]
)
🎯 学习路径建议
初级阶段
-
理解基础概念
- LangChain Runnable 接口
- LangGraph StateGraph
- 节点和边的基本概念
-
实践简单工作流
- 单节点工作流
- 固定路由
- 基础工具调用
中级阶段
-
掌握状态管理
- TypedDict 状态定义
- Reducer 函数
- 状态持久化
-
学习条件路由
- tools_condition
- 自定义路由函数
- 多分支逻辑
高级阶段
-
多智能体架构
- 主子助手模式
- 对话状态栈
- 任务委派机制
-
人机交互
- 中断和恢复
- 审批流程
- ToolMessage 反馈
-
生产部署
- 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:
- 使用
draw_graph()可视化 - 打印每个节点的状态
- 使用检查点查看中间状态
- 逐步测试每个子图
Q4: 如何优化响应速度?
A:
- 使用流式输出
- 缓存常用查询结果
- 并行执行独立工具
- 限制搜索范围(limit 参数)
Q5: 如何处理并发请求?
A:
- 每个请求使用独立的
thread_id - 使用异步 FastAPI 端点
- 数据库连接池
- 避免全局状态
🎓 总结
本项目涵盖了 LangChain 和 LangGraph 的核心概念:
✅ 状态管理工作流
✅ 多智能体协作
✅ 工具调用系统
✅ 人机交互审批
✅ 向量检索 RAG
✅ FastAPI 集成
✅ 持久化检查点
✅ 错误处理最佳实践
通过学习这个项目,你将掌握构建复杂 AI 应用的完整技能栈!
最后更新: 2026-06-03
作者: AI Study Project
版本: v1.0

浙公网安备 33010602011771号