模块一:LangGraph 核心概念与生态系统
-
开发环境搭建与工具链配置
3.1 环境准备
虚拟环境管理:
# 使用虚拟环境隔离项目依赖,避免与系统 Python 环境冲突
# 方案 1:pycharm
# pycharm创建项目后会自带虚拟环境,如果没有可以按下面方法创建
# 方案 2:venv(Python 内置)
# 创建虚拟环境
python -m venv venv
# 激活虚拟环境
# Windows:
venv\Scripts\activate
# macOS/Linux:
source venv/bin/activate
# 方案 3:uv
pip install uv
uv venv
uv init
source .venv/bin/activate
3.2 依赖安装策略
在项目根目录创建 requirements.txt 文件:
# ============== 核心框架 ==============
langchain==1.2.0
langchain-community==0.4.1
langchain-core~=1.2.7
langgraph==1.0.5 # 核心框架
langgraph-checkpoint==3.0.1 # 内存持久化后端
langgraph-checkpoint-postgres==3.0.2 # 数据库持久化后端
langgraph-cli[inmem]>=0.4.0 # CLI 工具
langsmith==0.4.59 # 监控工具
# ============== LLM 集成 ==============
dashscope==1.25.4 # 千问 SDK
# ============== MCP 相关 ==============
langchain-mcp-adapters==0.2.1 # MCP 客户端
fastmcp==2.13.1 # MCP 服务端(自建服务用)
# ============== 工具库 ==============
pydantic==2.12.5 # 数据验证
pydantic-settings==2.12.0 # 配置管理
python-dotenv==1.2.1 # 环境变量
tenacity==9.1.2 # 重试机制
httpx==0.28.1 # HTTP 客户端
aiohttp==3.13.2 # 异步 HTTP
安装依赖
pip install -r requirements.txt
3.3 必要配置
-
环境变量配置
创建 .env 文件:
# LangSmith 配置(用于追踪和监控)
LANGSMITH_TRACING=true
LANGSMITH_ENDPOINT=https://api.smith.langchain.com
LANGSMITH_API_KEY=你的秘钥
LANGSMITH_PROJECT=Langgraph_test
# 千问 API 配置
DASHSCOPE_API_KEY=your_qwen_api_key
# 可选:OpenAI(用于对比测试)
OPENAI_API_KEY=your_openai_api_key
-
Python 代码加载环境变量
from dotenv import load_dotenv
import os
# 加载 .env 文件
load_dotenv()
# 验证配置
def check_environment():
required_keys = [
"DASHSCOPE_API_KEY",
"LANGSMITH_API_KEY"
]
for key in required_keys:
if not os.getenv(key):
print(f"缺少环境变量: {key}")
else:
print(f"{key}: 已配置")
if __name__ == '__main__':
check_environment()
-
千问模型配置
import os
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
load_dotenv()
# 初始化千问模型
llm = ChatTongyi(
model="qwen-turbo", # 或 qwen-plus, qwen-max
api_key=os.getenv("DASHSCOPE_API_KEY"),
temperature=0.7,
top_p=0.9,
)
# 测试调用
if __name__ == '__main__':
response = llm.invoke("你好,请介绍一下 LangGraph")
print(response.content)
-
Hello World 与核心概念初探
4.1 最简单的图结构
完整代码示例:
import os
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
from langgraph.graph import StateGraph, MessagesState, START, END
load_dotenv()
# 初始化千问模型
llm = ChatTongyi(
model="qwen-turbo", # 或 qwen-plus, qwen-max
api_key=os.getenv("DASHSCOPE_API_KEY"),
temperature=0.7,
top_p=0.9,
)
# 定义节点函数:调用 LLM
def chat_node(state: MessagesState):
# 调用千问模型
response = llm.invoke(state["messages"])
# 返回新消息(LangGraph 会自动添加到 messages 列表)
return {"messages": [response]}
# 创建图:以 MessagesState 为状态模式
builder = StateGraph(MessagesState)
# 添加节点
builder.add_node("chat", chat_node)
# 定义流程:START → chat → END
builder.add_edge(START, "chat")
builder.add_edge("chat", END)
# 编译并执行
graph = builder.compile()
result = graph.invoke({"messages": [{"role": "user", "content": "你好,请介绍一下 LangGraph"}]})
# 打印 AI 的回复
print(result["messages"][-1].content)
4.5 实践练习:扩展 Hello World
import os
from typing import TypedDict
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
from langgraph.graph import StateGraph, START, END
from langchain_core.messages import HumanMessage, SystemMessage
# ==========================================
# 0. 配置 LLM (请学生填入自己的 Key)
# ==========================================
load_dotenv()
llm = ChatTongyi(
model="qwen-max",
api_key=os.getenv("DASHSCOPE_API_KEY"),
temperature=0.7
)
# ==========================================
# 1. 定义状态 (State)
# ==========================================
class AgentState(TypedDict):
topic: str # 原始输入的话题
draft: str # 节点1生成的初稿
final_post: str # 节点2润色后的最终文案
# ==========================================
# 2. 定义节点 (Nodes) - 这里引入了 LLM
# ==========================================
def write_draft(state: AgentState) -> AgentState:
"""
节点 1: 初稿生成员
任务: 根据话题写一段朴实的话
"""
topic = state["topic"]
print(f"\n--- [Node 1] 正在为 '{topic}' 撰写初稿 ---")
# 调用 LLM
messages = [
SystemMessage(content="你是一个社交媒体助手。请根据用户的话题,写一段简单的描述,不要加表情包,50字以内。"),
HumanMessage(content=topic)
]
response = llm.invoke(messages)
# 返回更新的状态
return {"draft": response.content}
def polish_post(state: AgentState) -> AgentState:
"""
节点 2: 润色编辑员
任务: 读取上一步的 draft,进行美化
"""
draft_text = state["draft"]
print(f"--- [Node 2] 正在润色初稿: {draft_text[:20]}... ---")
# 调用 LLM (注意:输入是上一个节点的产出)
messages = [
SystemMessage(content="你是一个爆款文案专家。请将用户的文字改写得更有吸引力,添加3个Emoji,并加上2个Hashtag。"),
HumanMessage(content=draft_text)
]
response = llm.invoke(messages)
# 返回更新的状态
return {"final_post": response.content}
# ==========================================
# 3. 构建图 (Build Graph)
# ==========================================
builder = StateGraph(AgentState)
# 添加节点
builder.add_node("writer", write_draft)
builder.add_node("editor", polish_post)
# 添加边 (串行逻辑)
# START -> writer -> editor -> END
builder.add_edge(START, "writer")
builder.add_edge("writer", "editor")
builder.add_edge("editor", END)
# 编译
graph = builder.compile()
# ==========================================
# 4. 运行作业
# ==========================================
# 可以在这里修改话题
user_topic = "周五下班去吃火锅"
print(f"=== 开始生成文案: {user_topic} ===")
result = graph.invoke({"topic": user_topic})
print("\n" + "=" * 30)
print("FINAL OUTPUT (最终文案)")
print("=" * 30)
print(result["final_post"])
模块二:图结构设计与状态管理
实践练习:Agent 反思循环
目标:构建一个自我反思改进的 Agent
代码示例(学生课后完成):
import os
from typing import TypedDict, Literal
from dotenv import load_dotenv
from langchain_community.chat_models.tongyi import ChatTongyi
from langgraph.graph import StateGraph, START, END
# 加载环境变量
load_dotenv()
# 初始化 LLM
llm = ChatTongyi(
model="qwen-max",
api_key=os.getenv("DASHSCOPE_API_KEY"),
temperature=0.7
)
# ============================================================
# 1. 定义 State
# ============================================================
class ReflectionState(TypedDict):
task: str # 任务描述
draft: str # 当前草稿
quality_score: float # 质量分数 (0-1)
reflection: str # 反思内容
iteration: int # 当前迭代次数
max_iterations: int # 最大迭代次数
# ============================================================
# 2. 定义 Node
# ============================================================
def generate_draft(state: ReflectionState) -> dict:
"""
Node 1: 生成初稿
使用 LLM 生成或改进内容
"""
print(f"\n{'=' * 60}")
print(f"Node 1: 生成初稿 (第 {state['iteration'] + 1} 次)")
print(f"{'=' * 60}")
task = state["task"]
iteration = state["iteration"]
if iteration == 0:
# 第一次:生成初稿
prompt = f"请写一篇关于'{task}'的文章,大约150字左右。"
print(f"提示词: {prompt}")
else:
# 后续:根据反思改进
previous_draft = state["draft"]
reflection = state["reflection"]
prompt = f"""
请改进以下文章:
原文:
{previous_draft}
改进建议:
{reflection}
请根据改进建议重写文章,保持150字左右。
"""
print(f"提示词: 根据反思改进第 {iteration} 次")
print(f"反思建议: {reflection}")
# 调用 LLM
response = llm.invoke(prompt)
draft = response.content
print(f"\n生成结果:")
print(f"{draft}")
return {
"draft": draft,
"iteration": iteration + 1
}
def evaluate_quality(state: ReflectionState) -> dict:
"""
Node 2: 评估质量
使用 LLM 评估文章质量
"""
print(f"\n{'=' * 60}")
print(f"Node 2: 评估质量")
print(f"{'=' * 60}")
draft = state["draft"]
task = state["task"]
prompt = f"""
请评估以下文章的质量。
任务要求: 写一篇关于'{task}'的文章
文章内容:
{draft}
评估标准:
1. 内容完整性 (是否涵盖主题)
2. 逻辑清晰度 (结构是否合理)
3. 语言表达 (是否流畅准确)
请给出0-1的分数(保留两位小数),只输出数字,不要其他内容。
例如:0.75
"""
# 调用 LLM 评估
response = llm.invoke(prompt)
score_text = response.content.strip()
try:
# 提取分数
score = float(score_text)
score = max(0.0, min(1.0, score)) # 确保在 0-1 范围内
except ValueError:
print(f"⚠️ 无法解析分数,使用默认值 0.5")
score = 0.5
print(f"\n质量分数: {score:.2f}")
return {"quality_score": score}
def reflect_and_improve(state: ReflectionState) -> dict:
"""
Node 3: 反思改进
使用 LLM 分析问题并给出改进建议
"""
print(f"\n{'=' * 60}")
print(f"Node 3: 反思改进")
print(f"{'=' * 60}")
draft = state["draft"]
task = state["task"]
score = state["quality_score"]
prompt = f"""
请分析以下文章存在的问题,并给出具体的改进建议。
任务要求: 写一篇关于'{task}'的文章
当前文章:
{draft}
当前评分: {score:.2f}
请简要指出1-2个主要问题,并给出改进方向(50字以内)。
"""
# 调用 LLM 反思
response = llm.invoke(prompt)
reflection = response.content
print(f"\n改进建议:")
print(f"{reflection}")
return {"reflection": reflection}
# ============================================================
# 3. 定义路由函数(条件边)
# ============================================================
def should_continue(state: ReflectionState) -> Literal["reflect", "end"]:
"""
决定是否继续改进
终止条件:
1. 质量分数 >= 0.75
2. 达到最大迭代次数
"""
score = state["quality_score"]
iteration = state["iteration"]
max_iter = state["max_iterations"]
print(f"\n{'=' * 60}")
print(f"条件判断: 是否继续改进")
print(f"{'=' * 60}")
print(f"当前分数: {score:.2f}")
print(f"迭代次数: {iteration}/{max_iter}")
# 终止条件1:质量达标
if score >= 0.9:
print(f"✅ 决策: 质量达标 (>= 0.9),结束改进")
return "end"
# 终止条件2:达到最大迭代次数
if iteration >= max_iter:
print(f"⚠️ 决策: 达到最大迭代次数,停止改进")
return "end"
# 继续改进
print(f"❌ 决策: 质量不足,继续改进")
return "reflect"
# ============================================================
# 4. 构建图
# ============================================================
def build_graph():
"""构建反思循环图"""
# 创建图
builder = StateGraph(ReflectionState)
# 添加节点
builder.add_node("generate_draft", generate_draft)
builder.add_node("evaluate_quality", evaluate_quality)
builder.add_node("reflect", reflect_and_improve)
# 添加边
# Edge 1: START → generate_draft (入口)
builder.add_edge(START, "generate_draft")
# Edge 2: generate_draft → evaluate_quality (普通边)
builder.add_edge("generate_draft", "evaluate_quality")
# Edge 3: evaluate_quality → [条件判断] (条件边)
builder.add_conditional_edges(
source="evaluate_quality",
path=should_continue,
path_map={
"reflect": "reflect", # 不合格 → 反思
"end": END # 合格 → 结束
}
)
# Edge 4: reflect → generate_draft (循环边)
builder.add_edge("reflect", "generate_draft")
# 编译
graph = builder.compile()
return graph
# ============================================================
# 5. 运行示例
# ============================================================
def run_reflection_loop(task: str, max_iterations: int = 3):
"""
运行反思循环
Args:
task: 任务描述
max_iterations: 最大迭代次数
"""
print("\n" + "=" * 60)
print("反思循环 Agent 启动")
print("=" * 60)
print(f"任务: {task}")
print(f"最大迭代次数: {max_iterations}")
print(f"质量阈值: 0.9")
# 构建图
graph = build_graph()
# 初始状态
initial_state = {
"task": task,
"draft": "",
"quality_score": 0.0,
"reflection": "",
"iteration": 0,
"max_iterations": max_iterations
}
# 执行图
final_state = graph.invoke(initial_state)
# 显示最终结果
print("\n" + "=" * 60)
print("最终结果")
print("=" * 60)
print(f"总迭代次数: {final_state['iteration']}")
print(f"最终质量分数: {final_state['quality_score']:.2f}")
print(f"\n最终文章:")
print("-" * 60)
print(final_state['draft'])
print("-" * 60)
return final_state
if __name__ == "__main__":
# 运行示例
result = run_reflection_loop(
task="人工智能的发展历程",
max_iterations=3
)
4.1 并行执行(Fan-out/Fan-in)
代码示例:
import os
from typing import Annotated, TypedDict
from operator import add
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
from langgraph.graph import StateGraph, START, END
from langgraph.types import Send
load_dotenv()
llm = ChatTongyi(
model="qwen-max",
api_key=os.getenv("DASHSCOPE_API_KEY"),
temperature=0.7
)
class State(TypedDict):
query: str
results: Annotated[list[str], add] # results 字段使用了 Annotated[list[str], add],三个并行节点返回的列表会被自动拼接,而不是相互覆盖。
final_answer: str
def search_web(state: State) -> dict:
return {"results": [f"Web 结果: {state['query']}"]}
def search_kb(state: State) -> dict:
return {"results": [f"知识库结果: {state['query']}"]}
def search_db(state: State) -> dict:
return {"results": [f"数据库结果: {state['query']}"]}
def aggregate(state: State) -> dict:
combined = "\n".join(state["results"])
answer = llm.invoke(f"综合以下信息回答:\n{combined}")
return {"final_answer": answer.content}
# 扇出路由:同时发送到多个节点
def fan_out(state: State):
return [
Send("search_web", state),
Send("search_kb", state),
Send("search_db", state),
]
builder = StateGraph(State)
builder.add_node("search_web", search_web)
builder.add_node("search_kb", search_kb)
builder.add_node("search_db", search_db)
builder.add_node("aggregate", aggregate)
builder.add_conditional_edges(START, fan_out) # 并行扇出
builder.add_edge("search_web", "aggregate") # 扇入汇聚
builder.add_edge("search_kb", "aggregate")
builder.add_edge("search_db", "aggregate")
builder.add_edge("aggregate", END)
graph = builder.compile()
if __name__ == '__main__':
print(graph.invoke({"query": "如何使用 LangGraph?"}))
4.3 分层结构(Hierarchical)
代码示例:
import os
from typing import Annotated, TypedDict
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
from langgraph.graph import StateGraph, START, END
load_dotenv()
llm = ChatTongyi(
model="qwen-max",
api_key=os.getenv("DASHSCOPE_API_KEY"),
temperature=0.7
)
# ============ 子图:深度研究 ============
class ResearchState(TypedDict):
topic: str
search_results: list[str]
summary: str
def search_node(state: ResearchState) -> dict:
return {"search_results": [f"搜索结果: {state['topic']}"]}
def summarize_node(state: ResearchState) -> dict:
return {"summary": f"总结: {state['search_results']}"}
# 构建子图
research_builder = StateGraph(ResearchState)
research_builder.add_node("search", search_node)
research_builder.add_node("summarize", summarize_node)
research_builder.add_edge(START, "search")
research_builder.add_edge("search", "summarize")
research_builder.add_edge("summarize", END)
research_subgraph = research_builder.compile()
# ============ 主图 ============
class MainState(TypedDict):
topic: str # 与子图同名,自动传递
summary: str # 与子图同名,自动回传
final_report: str
def generate_report(state: MainState) -> dict:
report = llm.invoke(f"根据以下总结生成报告:\n{state['summary']}")
return {"final_report": report.content}
# 把子图当作一个节点使用
main_builder = StateGraph(MainState)
main_builder.add_node("research", research_subgraph) # 子图作为节点
main_builder.add_node("report", generate_report)
main_builder.add_edge(START, "research")
main_builder.add_edge("research", "report")
main_builder.add_edge("report", END)
main_graph = main_builder.compile()
if __name__ == '__main__':
print(main_graph.invoke({"topic": "如何使用 LangGraph?"}))
综合实践项目:多轮对话客服系统
项目目标:构建一个完整的客服 Agent,整合本模块所有知识点
完整实现示例(学生课后完成):
import os
import uuid
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.checkpoint.memory import MemorySaver
load_dotenv()
# ============================================================
# 初始化 LLM
# ============================================================
llm = ChatTongyi(
model="qwen-max",
api_key=os.getenv("DASHSCOPE_API_KEY"),
temperature=0.7
)
# ============================================================
# State 定义(使用 MessagesState)
# ============================================================
class State(MessagesState):
"""使用内置的 MessagesState,自动管理消息历史"""
pass
# ============================================================
# 模拟数据库
# ============================================================
ORDERS = {
"ORD001": {"商品": "iPhone 15", "状态": "已发货", "价格": 5999},
"ORD002": {"商品": "AirPods", "状态": "配送中", "价格": 1299},
}
# ============================================================
# Node: 客服助手(一个节点搞定所有逻辑)
# ============================================================
def customer_service_node(state: State) -> dict:
"""
客服助手节点
处理所有用户请求:意图识别 + 订单查询 + 产品咨询
"""
# 构建系统提示
system_prompt = """你是智能客服助手。
你的能力:
1. 查询订单:当用户提供订单号(格式:ORD001、ORD002等),从数据库查询订单信息
2. 产品咨询:回答产品相关问题
订单数据库:
- ORD001: iPhone 15, 已发货, ¥5999
- ORD002: AirPods, 配送中, ¥1299
产品信息:
- iPhone 15: ¥5999
- AirPods: ¥1299
处理流程:
1. 如果用户提到"订单"或"查询",引导用户提供订单号
2. 如果用户提供了订单号(ORD开头),查询并返回订单详情
3. 如果是产品咨询,直接回答
4. 回答要简洁、友好
"""
# 获取最新用户消息
messages = state["messages"]
user_message = messages[-1].content
# 检查是否包含订单号
order_id = None
for word in user_message.split():
if word.startswith("ORD"):
order_id = word
break
# 如果有订单号,直接查询数据库
if order_id and order_id in ORDERS:
order = ORDERS[order_id]
response = f"""📦 订单详情
━━━━━━━━━━━━━━━
订单号:{order_id}
商品:{order['商品']}
状态:{order['状态']}
价格:¥{order['价格']}
━━━━━━━━━━━━━━━
还需要其他帮助吗?"""
return {"messages": [AIMessage(content=response)]}
# 否则调用 LLM 处理
full_messages = [SystemMessage(content=system_prompt)] + messages
response = llm.invoke(full_messages)
return {"messages": [response]}
# ============================================================
# 构建图
# ============================================================
def build_graph():
"""构建客服图"""
# 创建内存检查点
memory = MemorySaver()
# 创建图
graph = StateGraph(State)
# 只需要一个节点
graph.add_node("customer_service", customer_service_node)
# 简单的线性流程
graph.add_edge(START, "customer_service")
graph.add_edge("customer_service", END)
# 编译时添加 checkpointer
return graph.compile(checkpointer=memory)
# ============================================================
# 交互式运行
# ============================================================
def run_interactive():
"""交互式运行"""
graph = build_graph()
# 生成唯一会话 ID
thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}
print("\n" + "=" * 50)
print("💬 智能客服系统")
print("=" * 50)
print(f"会话 ID: {thread_id[:8]}...")
print("输入 'quit' 退出")
print("-" * 50)
# 欢迎语
print("\n🤖 您好!我是智能客服助手")
print("我可以帮您:")
print(" 1. 查询订单(请提供订单号)")
print(" 2. 产品咨询")
while True:
try:
# 获取用户输入
user_input = input("\n👤 您: ").strip()
if not user_input:
continue
if user_input.lower() in ["quit", "exit", "q"]:
print("\n👋 感谢使用,再见!")
break
# 构造输入
inputs = {"messages": [HumanMessage(content=user_input)]}
# 执行图并获取响应
result = graph.invoke(inputs, config=config)
# 获取最新的 AI 回复
messages = result.get("messages", [])
if messages:
last_message = messages[-1]
if isinstance(last_message, AIMessage):
print(f"\n🤖 {last_message.content}")
except Exception as e:
print(f"\n❌ 错误: {e}")
import traceback
traceback.print_exc()
# ============================================================
# 自动测试
# ============================================================
def run_auto_test():
"""自动测试"""
graph = build_graph()
thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}
print("\n" + "=" * 50)
print(" 智能客服系统 - 自动测试")
print("=" * 50)
test_cases = [
"我要查询订单",
"ORD001",
"iPhone多少钱",
"谢谢"
]
for user_input in test_cases:
print(f"\n👤 用户: {user_input}")
inputs = {"messages": [HumanMessage(content=user_input)]}
try:
result = graph.invoke(inputs, config=config)
messages = result.get("messages", [])
if messages:
last_message = messages[-1]
if isinstance(last_message, AIMessage):
print(f"\n🤖 {last_message.content}")
except Exception as e:
print(f"❌ 错误: {e}")
break
print("\n" + "=" * 50)
print("✅ 测试完成")
print("=" * 50)
# ============================================================
# 主函数
# ============================================================
if __name__ == "__main__":
run_auto_test()
#run_interactive()
模块三:工具系统与 Agent 增强
-
工具系统核心
1.1 工具定义与最佳实践
1.1.2 使用 @tool 装饰器定义工具
基础示例:
from langchain.tools import tool
@tool
def get_weather(city: str) -> str:
"""获取指定城市的天气信息。
Args:
city: 城市名称,例如"北京"、"上海"
Returns:
天气信息描述字符串
"""
weather_data = {
"北京": "晴天,25°C",
"上海": "多云,28°C",
"深圳": "雷阵雨,30°C"
}
return weather_data.get(city, f"{city}的天气信息暂时无法获取")
# 测试工具
print(get_weather.name) # get_weather
print(get_weather.description) # 获取指定城市的天气信息...
result = get_weather.invoke({"city": "北京"})
print(result) # 晴天,25°C
1.1.3 DocString 最佳实践
关键原则:DocString 是 LLM 理解工具用途的唯一依据!
@tool
def search_product(
keyword: str,
category: str = "all",
max_price: float = None,
min_rating: float = 4.0
) -> str:
"""在电商平台搜索商品。
使用场景:
- 用户想要查找特定商品时调用
- 用户询问"有什么XX产品"、"帮我找XX"时使用
Args:
keyword: 搜索关键词,例如"手机"、"笔记本电脑"
category: 商品分类,可选值:"electronics"(电子产品)、"clothing"(服装)、"all"(全部)
max_price: 最高价格限制,单位为元,不设置则不限制
min_rating: 最低评分要求,范围 1.0-5.0,默认 4.0
Returns:
JSON 格式的商品列表,包含名称、价格、评分等信息
注意事项:
- 关键词尽量具体,避免过于宽泛
- 价格限制时请考虑用户预算
- 评分过滤会影响结果数量
"""
# 实际实现...
pass
1.1.4 复杂参数:Pydantic 模型
对于复杂参数,使用 Pydantic 提供强类型验证:
from pydantic import BaseModel, Field
from typing import Optional
from datetime import date
class FlightSearchParams(BaseModel):
"""航班搜索参数"""
departure_city: str = Field(description="出发城市,例如'北京'")
arrival_city: str = Field(description="到达城市,例如'上海'")
departure_date: date = Field(description="出发日期,格式 YYYY-MM-DD")
return_date: Optional[date] = Field(None, description="返程日期(往返票时必填)")
passengers: int = Field(default=1, ge=1, le=9, description="乘客人数,1-9人")
cabin_class: str = Field(default="economy", description="舱位等级:economy/business/first")
@tool(args_schema=FlightSearchParams)
def search_flights(
departure_city: str,
arrival_city: str,
departure_date: date,
return_date: Optional[date] = None,
passengers: int = 1,
cabin_class: str = "economy"
) -> str:
"""搜索航班信息。
Returns:
JSON 格式的航班列表
"""
flight_info = {
"route": f"{departure_city} -> {arrival_city}",
"date": str(departure_date),
"passengers": passengers,
"class": cabin_class
}
return f"找到 3 班符合条件的航班: {flight_info}"
1.2 ToolRuntime 统一上下文访问
1.2.2 访问 State(会话状态)
from langchain.tools import tool, ToolRuntime
# ✅ 访问当前会话状态
@tool
def summarize_conversation(runtime: ToolRuntime) -> str:
"""总结当前对话内容"""
messages = runtime.state["messages"]
human_msgs = sum(1 for m in messages if m.__class__.__name__ == "HumanMessage")
ai_msgs = sum(1 for m in messages if m.__class__.__name__ == "AIMessage")
return f"对话包含 {human_msgs} 条用户消息,{ai_msgs} 条 AI 回复"
# ✅ 访问自定义状态字段
@tool
def get_user_preference(pref_name: str, runtime: ToolRuntime) -> str:
"""获取用户偏好设置"""
preferences = runtime.state.get("user_preferences", {})
return preferences.get(pref_name, "未设置")
1.2.3 更新 State(使用 Command)
from langgraph.types import Command
from langchain.messages import RemoveMessage
from langgraph.graph.message import REMOVE_ALL_MESSAGES
@tool
def clear_conversation() -> Command:
"""清空对话历史"""
return Command(
update={
"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES)],
}
)
@tool
def update_user_name(new_name: str, runtime: ToolRuntime) -> Command:
"""更新用户姓名"""
return Command(update={"user_name": new_name})
1.2.4 访问 Context(运行时配置)
from dataclasses import dataclass
from langchain.agents import create_agent
# 1. 定义 Context 结构
@dataclass
class UserContext:
user_id: str
api_key: str
# 2. 在工具中访问 Context
@tool
def get_account_info(runtime: ToolRuntime[UserContext]) -> str:
"""获取账户信息"""
user_id = runtime.context.user_id
api_key = runtime.context.api_key
# 使用 user_id 查询数据库...
return f"用户 {user_id} 的账户余额:5000 元"
# 3. 创建 Agent 时指定 context_schema
agent = create_agent(
model=model,
tools=[get_account_info],
context_schema=UserContext
)
# 4. 调用时传入 context
result = agent.invoke(
{"messages": [{"role": "user", "content": "查询我的余额"}]},
context=UserContext(user_id="user_123", api_key="sk-xxx")
)
1.2.5 访问 Store(长期记忆)
from langgraph.store.memory import InMemoryStore
@tool
def get_user_info(user_id: str, runtime: ToolRuntime) -> str:
"""查询用户信息"""
store = runtime.store
user_info = store.get(("users",), user_id)
return str(user_info.value) if user_info else "用户不存在"
@tool
def save_user_info(user_id: str, name: str, age: int, runtime: ToolRuntime) -> str:
"""保存用户信息"""
store = runtime.store
store.put(("users",), user_id, {"name": name, "age": age})
return "保存成功"
# 创建带 Store 的 Agent
store = InMemoryStore()
agent = create_agent(
model=model,
tools=[get_user_info, save_user_info],
store=store
)
1.2.6 Stream Writer(流式输出)
@tool
def process_large_file(filename: str, runtime: ToolRuntime) -> str:
"""处理大文件"""
writer = runtime.stream_writer
# 分步流式输出进度
writer(f"开始读取文件:{filename}")
# 模拟处理...
writer("文件读取完成,开始解析...")
# 模拟解析...
writer("解析完成,正在生成报告...")
return f"文件 {filename} 处理完成"
-
Agent 与工具集成
2.1 create_agent 标准实现
2.1.3 完整示例
import os
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
load_dotenv()
model = ChatTongyi(
model="qwen-max",
api_key=os.getenv("DASHSCOPE_API_KEY"),
)
from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain.tools import tool
from datetime import datetime
load_dotenv()
# 定义工具
@tool
def get_weather(city: str) -> str:
"""获取城市天气"""
return f"{city}的天气是晴天,25°C"
@tool
def get_time() -> str:
"""获取当前时间"""
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 创建 Agent
agent = create_agent(
model=model,
tools=[get_weather, get_time],
system_prompt="你是一个友好的助手,可以查询天气和时间。"
)
# 调用
result = agent.invoke({
"messages": [{"role": "user", "content": "北京今天天气怎么样?"}]
})
print(result["messages"][-1].content)
2.2 ToolNode 自动化工具调用
2.2.2 使用 ToolNode 构建 Agent
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.graph import StateGraph, MessagesState, START, END
from langchain_openai import ChatOpenAI
# 定义工具
tools = [get_weather, get_time]
# 绑定工具到模型
model = ChatTongyi(
model="qwen-max",
api_key=os.getenv("DASHSCOPE_API_KEY"),
).bind_tools(tools)
# 定义 Agent 节点
def call_model(state: MessagesState):
response = model.invoke(state["messages"])
return {"messages": [response]}
# 构建图
workflow = StateGraph(MessagesState)
workflow.add_node("agent", call_model)
workflow.add_node("tools", ToolNode(tools)) # 自动处理工具调用
# 添加边
workflow.add_edge(START, "agent")
workflow.add_conditional_edges("agent", tools_condition)
workflow.add_edge("tools", "agent")
# 编译
app = workflow.compile()
# 调用
result = app.invoke({
"messages": [{"role": "user", "content": "北京和上海的天气?"}]
})
2.2.3 ToolNode 并行执行
from langchain_core.messages import AIMessage
# 模拟 LLM 返回多个工具调用
ai_message = AIMessage(
content="",
tool_calls=[
{"name": "get_weather", "args": {"city": "北京"}, "id": "call_1"},
{"name": "get_weather", "args": {"city": "上海"}, "id": "call_2"},
{"name": "get_time", "args": {}, "id": "call_3"}
]
)
# ToolNode 会并行执行这 3 个工具
workflow = StateGraph(MessagesState)
workflow.add_node("tools", ToolNode(tools))
workflow.add_edge(START, "tools")
workflow.add_edge("tools", END)
app = workflow.compile()
result = app.invoke({"messages": [ai_message]})
# 输出包含 3 个 ToolMessage
for msg in result["messages"]:
print(f"[{msg.type}]: {msg.content}")
2.3 多轮对话与状态管理
2.3.1 使用 Checkpointer(短期记忆)
from langgraph.checkpoint.memory import MemorySaver
# 创建 Checkpointer
memory = MemorySaver()
# 创建带记忆的 Agent
agent = create_agent(
model=modle,
tools=tools,
checkpointer=memory
)
# 使用线程 ID 隔离会话
config = {"configurable": {"thread_id": "user_123"}}
# 第一轮对话
response1 = agent.invoke(
{"messages": [{"role": "user", "content": "我叫张三"}]},
config=config
)
# 第二轮对话(Agent 能记住上下文)
response2 = agent.invoke(
{"messages": [{"role": "user", "content": "我叫什么名字?"}]},
config=config
)
# 输出: "您叫张三。"
2.3.2 自定义状态扩展
from langchain.agents import AgentState
from typing_extensions import NotRequired
# 定义自定义状态
class CustomState(AgentState):
user_preferences: NotRequired[dict]
task_count: NotRequired[int]
# 方式1:通过 state_schema
agent = create_agent(
model=model,
tools=tools,
state_schema=CustomState
)
# 方式2:通过 Middleware(推荐)
from langchain.agents.middleware import AgentMiddleware
class CustomMiddleware(AgentMiddleware):
state_schema = CustomState
def before_model(self, state: CustomState, runtime):
# 可以访问 state["user_preferences"]
return None
agent = create_agent(
model=model,
tools=tools,
middleware=[CustomMiddleware()]
)
-
Middleware 系统
3.3 自定义中间件开发
3.3.1 装饰器方式(简单场景)
from langchain.agents.middleware import before_model, after_model
from langchain.agents import AgentState
from langgraph.runtime import Runtime
@before_model(can_jump_to=["end"])
def safety_check(state: AgentState, runtime: Runtime) -> dict | None:
"""安全检查:对话长度限制"""
if len(state["messages"]) >= 20:
return {
"messages": [{"role": "assistant", "content": "对话已达上限"}],
"jump_to": "end"
}
return None
@after_model
def log_response(state: AgentState, runtime: Runtime) -> dict | None:
"""记录模型响应"""
print(f"模型返回: {state['messages'][-1].content}")
return None
# 使用
agent = create_agent(
model=model,
tools=tools,
middleware=[safety_check, log_response]
)
3.3.2 类方式(复杂场景)
from typing import Any
from langchain.agents.middleware import AgentMiddleware, AgentState
from langgraph.runtime import Runtime
class LoggingMiddleware(AgentMiddleware):
"""统一日志中间件:可复用、可扩展(推荐工程化写法)"""
def before_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
print(f"[before_model] messages={len(state['messages'])}")
return None
def after_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
print(f"[after_model] assistant={state['messages'][-1].content}")
return None
agent = create_agent(
model=model,
tools=tools,
middleware=[LoggingMiddleware()],
)
3.3.3 Wrap-style Hook(重试机制)
from typing import Callable
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
@wrap_model_call
def retry_model(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
for attempt in range(3):
try:
return handler(request)
except Exception as e:
if attempt == 2:
raise
print(f"重试 {attempt + 1}/3: {e}")
agent = create_agent(
model=model,
tools=tools,
middleware=[retry_model],
)
3.3.6 Agent jumps(提前退出/跳转)
from typing import Any
from langchain.agents.middleware import after_model, hook_config, AgentState
from langchain.messages import AIMessage
from langgraph.runtime import Runtime
@after_model
@hook_config(can_jump_to=["end"])
def check_for_blocked(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
last = state["messages"][-1]
if "BLOCKED" in last.content:
return {
"messages": [AIMessage("该请求无法处理,我需要在这里停止。")],
"jump_to": "end",
}
return None
3.3.7 自定义 State schema
from typing import Any
from typing_extensions import NotRequired
from langchain.agents.middleware import AgentState, before_model, after_model
from langgraph.runtime import Runtime
class CustomState(AgentState):
model_call_count: NotRequired[int]
user_id: NotRequired[str]
@before_model(state_schema=CustomState, can_jump_to=["end"])
def check_call_limit(state: CustomState, runtime: Runtime) -> dict[str, Any] | None:
if state.get("model_call_count", 0) > 10:
return {"jump_to": "end"}
return None
@after_model(state_schema=CustomState)
def increment_counter(state: CustomState, runtime: Runtime) -> dict[str, Any] | None:
return {"model_call_count": state.get("model_call_count", 0) + 1}
-
模型上下文协议(MCP)完整指南
4.2 客户端集成与工具调用
4.2.3 Python 示例:连接多个 MCP Server
基础用法(无状态):
pip install langchain-mcp-adapters
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent
async def main():
# 连接多个 MCP Server
client = MultiServerMCPClient(
{
"math": {
"transport": "stdio", # 本地子进程通信
"command": "python",
"args": ["/path/to/math_server.py"],
},
"weather": {
"transport": "http", # HTTP 远程服务
"url": "http://localhost:8000/mcp",
},
}
)
# 获取所有工具
tools = await client.get_tools()
# 创建 Agent
agent = create_agent(
model, # 最新模型
tools
)
# 调用数学工具
result1 = await agent.ainvoke({
"messages": [{"role": "user", "content": "计算 3 × 12"}]
})
# 调用天气工具
result2 = await agent.ainvoke({
"messages": [{"role": "user", "content": "纽约天气如何?"}]
})
print(result1)
print(result2)
asyncio.run(main())
进阶用法:加载 Resources 和 Prompts :
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient({...}) # 只是保存配置,不连接
# 1. 加载工具(这里才真正连接)
tools = await client.get_tools()
print("\n=== TOOLS ===")
for t in tools:
print(f"- {t.name}: {t.description}")
print(f" args_schema: {t.args_schema}")
# 2. 加载资源(文件、数据等)
print("\n=== RESOURCES (templates/listing) ===")
math_blobs = await client.get_resources("math")
print(f"resources count: {len(math_blobs)}")
for blob in math_blobs:
print(f"- URI: {blob.metadata.get('uri')} | MIME: {blob.mimetype}")
print(blob.as_string())
print("\n=== RESOURCES (templates/listing) ===")
blobs = await client.get_resources("weather",uris=["weather://北京/history"])
print(f"resources count: {len(blobs)}")
for blob in blobs:
print(f"- URI: {blob.metadata.get('uri')} | MIME: {blob.mimetype}")
print(blob.as_string())
# 3. 加载提示模板
messages = await client.get_prompt(
server_name="math",
prompt_name="explain_math",
arguments={
"topic": "计算 3 × 12"
}
)
print(messages)
4.3 自定义 MCP Server(FastMCP)
4.3.1 本地服务器(stdio)示例
pip install fastmcp
# math_server.py
from fastmcp import FastMCP
mcp = FastMCP("Math")
@mcp.tool()
def add(a: int, b: int) -> int:
"""加法运算"""
return a + b
@mcp.tool()
def multiply(a: int, b: int) -> int:
"""乘法运算"""
return a * b
@mcp.resource("formula://basic")
async def get_formulas() -> str:
"""返回基础数学公式"""
return "面积 = 长 × 宽\n体积 = 长 × 宽 × 高"
@mcp.prompt()
def explain_math(topic: str) -> str:
"""生成数学解释提示"""
return f"请用简单的语言解释 {topic} 的概念,并举例说明。"
if __name__ == "__main__":
mcp.run(transport="stdio")
4.3.2 远程服务器(Streamable HTTP)示例
# weather_server.py
from fastmcp import FastMCP
from pydantic import BaseModel
mcp = FastMCP("Weather")
class WeatherData(BaseModel):
temperature: float
condition: str
@mcp.tool()
async def get_weather(location: str) -> str:
"""获取天气信息"""
return f"{location} 的天气是晴天,温度 25°C"
@mcp.tool()
async def get_detailed_weather(location: str) -> WeatherData:
"""获取详细天气数据(结构化输出)"""
return WeatherData(temperature=25.0, condition="sunny")
@mcp.resource("weather://{location}/history")
async def get_weather_history(location: str) -> str:
"""返回历史天气数据"""
return f"{location} 过去7天天气记录..."
if __name__ == "__main__":
mcp.run(transport="streamable-http")
4.3.5 有状态 Session 示例
from langchain_mcp_adapters.tools import load_mcp_tools
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent
client = MultiServerMCPClient({...})
# 创建持久会话
async with client.session("math") as session:
# 在会话中加载工具
tools = await load_mcp_tools(session)
agent = create_agent(
"claude-sonnet-4-5-20250929",
tools
)
# 多次调用共享会话状态
result = await agent.ainvoke({
"messages": [{"role": "user", "content": "设置变量 x=10,然后计算 x+5"}]
})
4.4 核心特性详解
4.4.1 Tools(工具)
from fastmcp import FastMCP
from pydantic import BaseModel
mcp = FastMCP("Data")
class UserProfile(BaseModel):
name: str
age: int
email: str
@mcp.tool()
async def get_user_profile(user_id: str) -> dict:
"""获取用户资料(返回结构化数据)"""
return {
"text": f"用户 {user_id} 的资料已找到",
"structured_content": {
"name": "张三",
"age": 30,
"email": "zhangsan@example.com"
}
}
# 客户端访问结构化内容
from langchain.messages import ToolMessage
result = await agent.ainvoke({"messages": [...]})
for message in result["messages"]:
if isinstance(message, ToolMessage) and message.artifact:
data = message.artifact["structured_content"]
print(f"姓名: {data['name']}, 年龄: {data['age']}")
多模态内容(Multimodal Content)
客户端示例:
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent
client = MultiServerMCPClient({...})
tools = await client.get_tools()
agent = create_agent(model, tools)
result = await agent.ainvoke(
{"messages": [{"role": "user", "content": "截取当前页面的屏幕截图"}]}
)
# 访问多模态内容
for message in result["messages"]:
if message.type == "tool":
# 以提供方本地格式提供的原始内容
print(f"Raw content: {message.content}")
# 标准化的内容块
for block in message.content_blocks:
if block["type"] == "text":
print(f"Text: {block['text']}")
elif block["type"] == "image":
print(f"Image URL: {block.get('url')}")
print(f"Image base64: {block.get('base64', '')[:50]}...")
4.4.2 Resources(资源)
客户端示例
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient({...})
# 加载所有资源
blobs = await client.get_resources("server_name")
# 或加载特定资源
blobs = await client.get_resources(
"server_name",
uris=["file:///path/to/config.json"]
)
for blob in blobs:
print(f"URI: {blob.metadata['uri']}")
print(f"MIME: {blob.mimetype}")
content = blob.as_string() # 文本内容
# 或 blob.as_bytes() 获取二进制数据
服务端示例
from fastmcp import FastMCP
mcp = FastMCP("Files")
@mcp.resource("config://app.json")
async def get_config() -> str:
"""返回配置文件"""
return '{"version": "1.0", "debug": true}'
@mcp.resource("docs://{category}/{filename}")
async def get_document(category: str, filename: str) -> str:
"""参数化资源路径"""
return f"文档内容:{category}/{filename}"
4.4.3 Prompts(提示模板)
客户端示例:
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient({...})
# 加载提示模板
messages = await client.get_prompt(
"server_name",
"code_review",
arguments={"language": "python", "focus": "security"}
)
# 使用提示
for message in messages:
print(f"{message.type}: {message.content}")
服务端示例:
from fastmcp import FastMCP
mcp = FastMCP("Prompts")
@mcp.prompt()
def code_review(language: str, focus: str = "general") -> str:
"""生成代码审查提示"""
return f"""请审查以下 {language} 代码,重点关注{focus}方面:
1. 代码质量
2. 潜在bug
3. 最佳实践
"""
@mcp.prompt()
def summarize(style: str = "concise") -> list:
"""生成摘要提示(返回消息列表)"""
return [
{"role": "system", "content": f"你是一个{style}的摘要助手"},
{"role": "user", "content": "请总结以下内容"}
]
4.5 高级特性 (扩展 可跳过)
4.5.1 工具拦截器(Tool Interceptors)
示例 1:注入用户上下文
from dataclasses import dataclass
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.interceptors import MCPToolCallRequest
from langchain.agents import create_agent
@dataclass
class Context:
user_id: str
api_key: str
async def inject_user_context(
request: MCPToolCallRequest,
handler,
):
"""将用户凭证注入到工具调用中"""
runtime = request.runtime
user_id = runtime.context.user_id # 访问运行时上下文
api_key = runtime.context.api_key
# 修改工具参数
modified_request = request.override(
args={**request.args, "user_id": user_id}
)
return await handler(modified_request)
client = MultiServerMCPClient(
{...},
tool_interceptors=[inject_user_context],
)
tools = await client.get_tools()
agent = create_agent(model, tools, context_schema=Context)
# 调用时传入上下文
result = await agent.ainvoke(
{"messages": [{"role": "user", "content": "查询我的订单"}]},
context={"user_id": "user_123", "api_key": "sk-..."}
)
示例 2:访问 Store(长期记忆)
from dataclasses import dataclass
from langchain_mcp_adapters.interceptors import MCPToolCallRequest
from langgraph.store.memory import InMemoryStore
@dataclass
class Context:
user_id: str
async def personalize_search(
request: MCPToolCallRequest,
handler,
):
"""基于用户偏好个性化搜索"""
runtime = request.runtime
user_id = runtime.context.user_id
store = runtime.store # 访问存储
# 读取用户偏好
prefs = store.get(("preferences",), user_id)
if prefs and request.name == "search":
# 应用用户的语言和结果限制偏好
modified_args = {
**request.args,
"language": prefs.value.get("language", "zh"),
"limit": prefs.value.get("result_limit", 10),
}
request = request.override(args=modified_args)
return await handler(request)
client = MultiServerMCPClient(
{...},
tool_interceptors=[personalize_search],
)
agent = create_agent(
model,
tools,
context_schema=Context,
store=InMemoryStore()
)
示例 3:基于状态的权限控制
from langchain.messages import ToolMessage
async def require_authentication(
request: MCPToolCallRequest,
handler,
):
"""阻止未认证用户调用敏感工具"""
runtime = request.runtime
state = runtime.state # 访问对话状态
is_authenticated = state.get("authenticated", False)
sensitive_tools = ["delete_file", "update_settings", "export_data"]
if request.name in sensitive_tools and not is_authenticated:
# 返回错误而不是执行工具
return ToolMessage(
content="需要认证。请先登录。",
tool_call_id=runtime.tool_call_id,# 访问工具调用 ID
)
return await handler(request)
client = MultiServerMCPClient(
{...},
tool_interceptors=[require_authentication],
)
拦截器组合(洋葱模式)
async def outer_interceptor(request, handler):
print("外层: 执行前")
result = await handler(request)
print("外层: 执行后")
return result
async def inner_interceptor(request, handler):
print("内层: 执行前")
result = await handler(request)
print("内层: 执行后")
return result
client = MultiServerMCPClient(
{...},
tool_interceptors=[outer_interceptor, inner_interceptor],
)
# 执行顺序:
# 外层: 执行前 → 内层: 执行前 → 工具执行 → 内层: 执行后 → 外层: 执行后
状态更新与命令(Command)
from langgraph.types import Command
async def handle_task_completion(
request: MCPToolCallRequest,
handler,
):
"""标记任务完成并切换到summary agent"""
result = await handler(request)
if request.name == "submit_order":
return Command(
update={
"messages": [result] if isinstance(result, ToolMessage) else [],
"task_status": "completed", # 更新状态
},
goto="summary_agent", # 切换到下一个节点
)
return result
async def end_on_success(
request: MCPToolCallRequest,
handler,
):
"""任务完成时提前结束"""
result = await handler(request)
if request.name == "mark_complete":
return Command(
update={"messages": [result], "status": "done"},
goto="__end__", # 结束执行
)
return result
4.5.2 进度通知(Progress Notifications)
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.callbacks import Callbacks, CallbackContext
async def on_progress(
progress: float,
total: float | None,
message: str | None,
context: CallbackContext,
):
"""处理进度更新"""
percent = (progress / total * 100) if total else progress
tool_info = f" ({context.tool_name})" if context.tool_name else ""
print(f"[{context.server_name}{tool_info}] 进度: {percent:.1f}% - {message}")
client = MultiServerMCPClient(
{...},
callbacks=Callbacks(on_progress=on_progress),
)
4.5.3 日志记录(Logging)
from langchain_mcp_adapters.callbacks import Callbacks, CallbackContext
from mcp.types import LoggingMessageNotificationParams
async def on_logging_message(
params: LoggingMessageNotificationParams,
context: CallbackContext,
):
"""处理服务器日志"""
print(f"[{context.server_name}] {params.level}: {params.data}")
client = MultiServerMCPClient(
{...},
callbacks=Callbacks(on_logging_message=on_logging_message),
)
4.5.4 交互式输入(Elicitation)
服务端示例:
from pydantic import BaseModel
from mcp.server.fastmcp import Context, FastMCP
server = FastMCP("Profile")
class UserDetails(BaseModel):
email: str
age: int
@server.tool()
async def create_profile(name: str, ctx: Context) -> str:
"""创建用户资料,通过 elicitation 请求详细信息"""
result = await ctx.elicit(
message=f"请提供 {name} 的详细信息:",
schema=UserDetails,
)
if result.action == "accept" and result.data:
return f"为 {name} 创建资料: email={result.data.email}, age={result.data.age}"
if result.action == "decline":
return f"用户拒绝。为 {name} 创建了最小资料。"
return "资料创建已取消。"
if __name__ == "__main__":
server.run(transport="http")
客户端处理:
from mcp.shared.context import RequestContext
from mcp.types import ElicitRequestParams, ElicitResult
async def on_elicitation(
mcp_context: RequestContext,
params: ElicitRequestParams,
context: CallbackContext,
) -> ElicitResult:
"""处理 elicitation 请求"""
# 实际应用中,这里会提示用户输入
# 基于 params.message 和 params.requestedSchema
return ElicitResult(
action="accept",
content={"email": "user@example.com", "age": 25},
)
client = MultiServerMCPClient(
{...},
callbacks=Callbacks(on_elicitation=on_elicitation),
)
模块四:持久化与内存管理
-
短期内存:Checkpointer(线程级持久化)
2.2 开发环境:InMemorySaver
2.2.1 基本使用
import os
from dotenv import load_dotenv
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.checkpoint.memory import MemorySaver
from langchain_openai import ChatOpenAI
load_dotenv()
# 步骤 1️:初始化检查点
checkpointer = MemorySaver()
# 步骤 2️:定义简单的对话节点
model = ChatOpenAI(
model=os.getenv("QWEN_MODEL_NLP", "qwen-max"),
base_url=os.getenv("QWEN_BASE_URL"),
api_key=os.getenv("QWEN_API_KEY"),
temperature=0.3,
)
def chatbot(state: MessagesState):
return {"messages": [model.invoke(state["messages"])]}
# 步骤 3️:构建图并编译(关键!)
builder = StateGraph(MessagesState)
builder.add_node("chat", chatbot)
builder.add_edge(START, "chat")
builder.add_edge("chat", END)
# 编译时传入 checkpointer
graph = builder.compile(checkpointer=checkpointer)
2.2.2 多轮对话示例
# 定义配置,区分不同会话
thread_config = {"configurable": {"thread_id": "session_user_123"}}
# 第一次对话
result1 = graph.invoke(
{"messages": [HumanMessage(content="你好,我叫张三")]},
config=thread_config
)
print(result1["messages"][-1].content)
# 第二次对话(Agent 会自动读取 thread_id 对应的历史状态)
result2 = graph.invoke(
{"messages": [HumanMessage(content="我刚才说我叫什么?")]},
config=thread_config
)
print(result2["messages"][-1].content) # 应该回答:你说你叫张三
# 其他会话(不同的 thread_id)
result3 = graph.invoke(
{"messages": [HumanMessage(content="我刚才说了什么?")]},
config={"configurable": {"thread_id": "session_user_456"}}
)
print(result3["messages"][-1].content) # 无法回答,因为这是新会话
2.3 生产环境:数据库持久化
2.3.2 PostgreSQL 实现
依赖安装
pip install langgraph langgraph-checkpoint-postgres psycopg[binary,pool]
同步版本完整代码 (可以直接跑)
import os
from typing import Annotated
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.postgres import PostgresSaver
from psycopg_pool import ConnectionPool
load_dotenv()
model = ChatOpenAI(
model=os.getenv("QWEN_MODEL_NLP", "qwen-max"),
base_url=os.getenv("QWEN_BASE_URL"),
api_key=os.getenv("QWEN_API_KEY"),
temperature=0.3,
)
# ================= 1. 定义一个简单的 Graph (为了让代码能跑) =================
class State(TypedDict):
messages: Annotated[list, add_messages]
def chatbot(state: State):
return {"messages": [model.invoke(state.get("messages"))]}
builder = StateGraph(State)
builder.add_node("chatbot", chatbot)
builder.add_edge(START, "chatbot")
builder.add_edge("chatbot", END)
# ================= 2. PostgreSQL 配置与运行 =================
# 请确保数据库已存在,替换你的连接字符串
DB_URI = os.getenv("DB_URI")
# 使用上下文管理器确保连接池正确开启和关闭
# 如果你不使用 context manager (with 语句),需要手动调用 pool.open()
with ConnectionPool(conninfo=DB_URI, min_size=1, max_size=10) as pool:
# 初始化 Checkpointer
checkpointer = PostgresSaver(pool)
# 重点: 首次运行时必须执行 setup() 创建表结构
# checkpointer.setup()
# 编译图
graph = builder.compile(checkpointer=checkpointer)
# ================= 3. 执行测试 =================
thread_id = "postgres_user_001"
config = {"configurable": {"thread_id": thread_id}}
print(f"--- 开始对话 (Thread: {thread_id}) ---")
# 第一轮对话
input_data = {"messages": [("user", "你好,Postgres!")]}
result = graph.invoke(input_data, config)
print(f"回复: {result['messages'][-1].content}")
# 验证记忆(再次发送,查看是否延续之前的 thread_id)
result2 = graph.invoke({"messages": [("user", "我刚才说了什么?")]}, config)
print(f"回复2: {result2['messages'][-1].content}")
异步版本 (FastAPI 常用)
import os
import sys # 新增
import asyncio
from typing import Annotated
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver # 修正引用位置
load_dotenv()
# 确保读取到了环境变量,否则后面会报错
if not os.getenv("DB_URI"):
raise ValueError("请在 .env 文件中配置 DB_URI")
model = ChatOpenAI(
model=os.getenv("QWEN_MODEL_NLP", "qwen-max"),
base_url=os.getenv("QWEN_BASE_URL"),
api_key=os.getenv("QWEN_API_KEY"),
temperature=0.3,
)
# ================= 1. 定义 Graph =================
class State(TypedDict):
messages: Annotated[list, add_messages]
def chatbot(state: State):
return {"messages": [model.invoke(state.get("messages"))]}
builder = StateGraph(State)
builder.add_node("chatbot", chatbot)
builder.add_edge(START, "chatbot")
builder.add_edge("chatbot", END)
# ================= 2. 异步执行主逻辑 =================
async def main():
DB_URI = os.getenv("DB_URI")
# 异步连接必须使用 context manager
async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
# 第一次运行时必须取消注释,用于创建数据库表结构
# 运行一次后可以再次注释掉,或者保留着(它是幂等的,不会重复创建)
await checkpointer.setup()
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "async_pg_002"}}
print("--- 异步 PostgreSQL 测试 ---")
# 第一轮对话
print("User: Hello Async DB")
response1 = await graph.ainvoke({"messages": [("user", "Hello Async DB")]}, config)
print(f"Bot: {response1['messages'][-1].content}")
# 第二轮对话(测试记忆)
print("\nUser: 我刚才说了什么")
response2 = await graph.ainvoke({"messages": [("user", "我刚才说了什么")]}, config)
print(f"Bot: {response2['messages'][-1].content}")
print("执行完成")
if __name__ == "__main__":
# 解决 Windows 下 psycopg "ProactorEventLoop" 错误
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
asyncio.run(main())
2.3.3 Redis 实现
依赖安装
pip install langgraph langgraph-checkpoint-redis redis
同步版本完整代码
import os
import redis
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.redis import RedisSaver
load_dotenv()
# 确保读取到了环境变量,否则后面会报错
if not os.getenv("DB_URI"):
raise ValueError("请在 .env 文件中配置 DB_URI")
model = ChatOpenAI(
model=os.getenv("QWEN_MODEL_NLP", "qwen-max"),
base_url=os.getenv("QWEN_BASE_URL"),
api_key=os.getenv("QWEN_API_KEY"),
temperature=0.3,
)
# ================= 1. 定义 Graph =================
class State(TypedDict):
messages: Annotated[list, add_messages]
def chatbot(state: State):
return {"messages": [model.invoke(state.get("messages"))]}
builder = StateGraph(State)
builder.add_node("chatbot", chatbot)
builder.add_edge(START, "chatbot")
builder.add_edge("chatbot", END)
# ================= 2. Redis 配置与运行 =================
# 建立连接
redis_client = redis.Redis(
host=os.getenv("REDIS_HOST"),
port=os.getenv("REDIS_PORT"),
db=12,
password=os.getenv("REDIS_PASSWORD"), # 如果有密码请取消注释
#decode_responses=False # 注意:LangGraph 序列化通常处理二进制,这里建议保持 False 或根据具体库版本调整
)
# 初始化 Checkpointer
checkpointer = RedisSaver(redis_client=redis_client)
# 编译
graph = builder.compile(checkpointer=checkpointer)
# ================= 3. 执行测试 =================
config = {"configurable": {"thread_id": "redis_user_999"}}
print(f"--- Redis 对话 ---")
result = graph.invoke({"messages": [("user", "Redis 准备好了吗?")]}, config)
print(f"Bot: {result['messages'][-1].content}")
# 验证 Key 是否存在
print(f"Key 数量: {len(redis_client.keys('checkpoint:*'))}")
-
长期内存:Store(跨会话持久化)
3.2 Store 基础用法
3.2.1 基本操作
from langgraph.store.memory import InMemoryStore
# 1. 初始化 Store
store = InMemoryStore()
# 2. 存储数据(使用命名空间组织)
store.put(
namespace=('user_preferences', 'user_123'),
key='language',
value={'lang': 'zh-CN', 'tone': 'professional'}
)
# 3. 检索数据
preference = store.get(('user_preferences', 'user_123'), 'language')
print(preference) # {'lang': 'zh-CN', 'tone': 'professional'}
# 4. 列出所有 key
keys = store.search(('user_preferences', 'user_123'))
3.3 语义搜索
3.3.1 配置语义搜索
from langchain_community.embeddings import DashScopeEmbeddings
from langgraph.store.memory import InMemoryStore
# 1. 配置 Embedding 模型
embeddings = DashScopeEmbeddings(
model='text-embedding-v2',
dashscope_api_key=os.getenv('DASHSCOPE_API_KEY')
)
# 2. 创建支持语义搜索的 Store
store = InMemoryStore(
index={'dims': 1536, 'embed': embeddings}
)
3.3.2 语义搜索示例
# 存入多条记忆
memories = [
'我喜欢吃意大利菜,尤其是披萨和意面',
'我最近在学习 Python 和机器学习',
'我养了一只叫 Luna 的猫',
'我每周末都去健身房锻炼'
]
for i, memory in enumerate(memories):
store.put(
namespace=('user_memories', 'user_123'),
key=str(i),
value={'text': memory}
)
# 语义搜索(根据问题找相关记忆)
results = store.search(
('user_memories', 'user_123'),
query='用户的饮食习惯是什么?',
limit=2
)
for item in results:
print(item.value['text'])
# 输出:我喜欢吃意大利菜,尤其是披萨和意面
3.4 生产环境 Store 配置
3.4.1 PostgreSQL Store(推荐)
安装依赖:
pip install -U "psycopg[binary,pool]" langgraph langgraph-checkpoint-postgres
基本使用:
from langgraph.store.postgres import PostgresStore
DB_URI = 'postgresql://user:pass@localhost:5432/langgraph_store'
# 创建 Store
with PostgresStore.from_conn_string(DB_URI) as store:
# 启用 PGVector 扩展进行向量搜索
store.setup() # 自动创建表和索引
# 配置 Embedding
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings()
# 编译图
graph = builder.compile(
store=store,
checkpointer=checkpointer
)
4.2 策略一:修剪消息(Trim Messages)
代码示例
from typing import Literal
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_core.messages.utils import trim_messages, count_tokens_approximately
from langgraph.graph import StateGraph, MessagesState, START, END
# 初始化模型
model = ChatOpenAI(model="gpt-4o-mini")
# 定义处理节点的逻辑
def call_model_with_trimming(state: MessagesState):
# --- 核心逻辑开始 ---
# 在调用模型前,先修剪消息
# 策略:保留最后 max_tokens 个 token,且保证从 Human 消息开始(避免切断对话流)
trimmed_messages = trim_messages(
state["messages"],
strategy="last", # 保留最新的
token_counter=count_tokens_approximately, # 估算 Token 数
max_tokens=100, # 设定非常严格的限制用于演示
start_on="human", # 确保第一条是用户发的消息(避免只有 AI 回复)
end_on=("human", "tool"), # 确保以有效消息结束
include_system=True # 始终保留 SystemMessage (如果有)
)
# --- 核心逻辑结束 ---
print(f"Debug: 实际发送给模型的消息数: {len(trimmed_messages)}")
response = model.invoke(trimmed_messages)
return {"messages": [response]}
# 构建图
builder = StateGraph(MessagesState)
builder.add_node("bot", call_model_with_trimming)
builder.add_edge(START, "bot")
builder.add_edge("bot", END)
# 编译
graph = builder.compile()
4.3 策略二:删除消息(Remove Messages)
代码示例
from langchain_core.messages import RemoveMessage
from langgraph.graph import StateGraph, MessagesState, START, END
# 定义清理逻辑
def manage_memory(state: MessagesState):
messages = state["messages"]
# 假设我们只想在数据库中保留最新的 4 条消息(2轮对话)
if len(messages) > 4:
# 计算需要删除的消息:除了最后 4 条之外的所有消息
num_to_remove = len(messages) - 4
# 取出最早的若干条消息
messages_to_remove = messages[:num_to_remove]
print(f"Debug: 正在物理删除 {len(messages_to_remove)} 条旧消息...")
# 返回 RemoveMessage 对象列表,LangGraph 收到后会执行删除操作
return {"messages": [RemoveMessage(id=m.id) for m in messages_to_remove]}
return {}
def simple_bot(state: MessagesState):
# 这里是一个简单的回声机器人模拟
last_msg = state["messages"][-1]
return {"messages": [("ai", f"收到:{last_msg.content}")]}
# 构建图
builder = StateGraph(MessagesState)
builder.add_node("bot", simple_bot)
builder.add_node("cleaner", manage_memory) # 专门的清理节点
# 流程:开始 -> 机器人回复 -> 清理旧消息 -> 结束
builder.add_edge(START, "bot")
builder.add_edge("bot", "cleaner")
builder.add_edge("cleaner", END)
app = builder.compile()
4.4 策略三:总结消息(Summarize Messages)
代码示例
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage, RemoveMessage
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.checkpoint.memory import MemorySaver # 必须引入检查点
load_dotenv()
# 1. 初始化 Qwen 模型
model = ChatOpenAI(
model=os.getenv("QWEN_MODEL_NLP", "qwen-max"),
base_url=os.getenv("QWEN_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1"),
api_key=os.getenv("QWEN_API_KEY"),
temperature=0.3,
)
# 定义状态
class State(MessagesState):
summary: str
# 2. 核心对话节点 (自动注入摘要)
def call_model(state: State):
msgs = state["messages"]
# 如果存在摘要,将其作为 SystemMessage 插入到队首
if state.get("summary"):
msgs = [SystemMessage(content=f"前情摘要: {state['summary']}")] + msgs
return {"messages": [model.invoke(msgs)]}
# 3. 摘要生成节点 (生成 + 清理)
def summarize_node(state: State):
summary = state.get("summary", "")
# 构造更简洁的总结 Prompt
prompt = f"当前摘要:{summary}\n请结合上述新消息更新摘要,保留关键信息(如姓名、喜好等):" if summary else "请总结上述对话的关键信息:"
# 生成新摘要 (只基于最近消息和 Prompt)
new_summary = model.invoke(state["messages"] + [HumanMessage(content=prompt)]).content
print(f"\n[系统日志] 触发记忆压缩 -> 更新摘要: {new_summary[:30]}...")
# 删除旧消息:保留最后 2 条消息用于上下文衔接,其余删除
delete_msgs = [RemoveMessage(id=m.id) for m in state["messages"][:-2]]
return {"summary": new_summary, "messages": delete_msgs}
# 4. 判断逻辑 (每 6 条消息触发一次总结)
def should_summarize(state: State):
return "summarize" if len(state["messages"]) > 6 else END
# 5. 构建图
workflow = StateGraph(State)
workflow.add_node("chat", call_model)
workflow.add_node("summarize", summarize_node)
workflow.add_edge(START, "chat")
workflow.add_conditional_edges("chat", should_summarize, {"summarize": "summarize", END: END})
workflow.add_edge("summarize", END)
# 编译 (必须加上 checkpointer 才能在多轮对话中记住状态)
app = workflow.compile(checkpointer=MemorySaver())
# 6. 运行测试
if __name__ == '__main__':
config = {"configurable": {"thread_id": "qwen_memory_test"}}
questions = [
"我叫 Alice,我有一只叫 Luna 的猫。",
"Luna 喜欢吃金枪鱼罐头。",
"它今年三岁了。",
"它最讨厌洗澡。", # 第4条
"我的猫叫什么名字?它喜欢吃什么?" # 第5条,交互后总数变多,将触发总结
]
for q in questions:
print(f"\nUser: {q}")
# 只需要传入新消息,历史记录由 checkpointer 自动管理
res = app.invoke({"messages": [HumanMessage(content=q)]}, config)
print(f"Qwen: {res['messages'][-1].content}")
-
管理检查点(Checkpoint Management)
5.1 查看线程状态
查看当前或特定检查点的状态:
config = {
"configurable": {
"thread_id": "session_user_123",
# 可选:指定特定检查点 ID
# "checkpoint_id": "1f029ca3-1f5b-6704-8004-820c16b69a5a"
}
}
# 获取当前状态
state = graph.get_state(config)
print(state)
返回结果示例:
StateSnapshot(
values= {'messages': [
HumanMessage(content="你好,我叫张三"),
AIMessage(content='你好张三!有什么可以帮你的吗?'),
HumanMessage(content="我叫什么名字?"),
AIMessage(content='你叫张三。')
]},
next=(),
config={'configurable': {'thread_id': 'session_user_123', ...}},
metadata={'source': 'loop', 'step': 4, ...},
created_at='2025-05-05T16:01:24.680462+00:00',
...
)
5.2 查看线程历史
查看一个线程的所有检查点历史(版本控制):
config = {"configurable": {"thread_id": "session_user_123"}}
# 获取所有历史快照
history = list(graph.get_state_history(config))
for snapshot in history:
print(f"步骤 {snapshot.metadata['step']}: {snapshot.values['messages'][-1].content}")
-
完整实战案例
7.3 核心功能实现
import os
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
from langchain_core.messages import SystemMessage
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.checkpoint.postgres import PostgresSaver
from langgraph.store.postgres import PostgresStore
from psycopg_pool import ConnectionPool
load_dotenv()
DB_URI = os.getenv('DATABASE_URL')
model = ChatTongyi(
model="qwen-max",
api_key=os.getenv("DASHSCOPE_API_KEY"),
temperature=0.7
)
# 初始化存储(共用同一个连接池,不需要 with 上下文管理器)
pool = ConnectionPool(conninfo=DB_URI, min_size=5, max_size=20,kwargs={"autocommit": True})
checkpointer = PostgresSaver(pool)
store = PostgresStore(pool)
# 首次运行时创建表
checkpointer.setup()
store.setup()
print("数据库表初始化成功!")
# ==================== 节点定义 ====================
def customer_service_bot(state: MessagesState, config: RunnableConfig):
"""智能客服主节点"""
# 1. 获取用户偏好
user_id = config['configurable']['user_id']
item = store.get(('user_preferences', user_id), 'settings')
# 2. 构建系统提示词
system_prompt = '你是一个专业的客服助手。'
if item:
preferences = item.value
lang = preferences.get('language', 'zh-CN')
tone = preferences.get('tone', 'professional')
system_prompt += f'\n请使用{lang}语言,保持{tone}风格。'
# 3. 调用模型
messages = [SystemMessage(content=system_prompt)] + state['messages']
response = model.invoke(messages)
return {'messages': [response]}
# ==================== 构建图 ====================
builder = StateGraph(MessagesState)
builder.add_node('customer_service', customer_service_bot)
builder.add_edge(START, 'customer_service')
builder.add_edge('customer_service', END)
app = builder.compile(
checkpointer=checkpointer, # 短期记忆
store=store # 长期记忆
)
# ==================== 使用示例 ====================
config = {
'configurable': {
'thread_id': 'customer_001',
'user_id': 'user_123'
}
}
if __name__ == '__main__':
try:
# 第一次对话
response1 = app.invoke(
{'messages': [('user', '你好,我想咨询订单问题')]},
config=config
)
print(response1['messages'][-1].content)
# 第二次对话(自动读取历史)
response2 = app.invoke(
{'messages': [('user', '我的订单号是 12345')]},
config=config
)
print(response2['messages'][-1].content)
# 第三次对话
response3 = app.invoke(
{'messages': [('user', '我刚才说的订单号是多少?')]},
config=config
)
print(response3['messages'][-1].content)
finally:
pool.close()
模块五: 人机协作与流式输出
人机协作
-
实战 :关键步骤需人工确认的助手
"""
研报助手 V4 - 人工审核闭环(单文件完整版)
=============================================
运行前请设置环境变量:
export DASHSCOPE_API_KEY=sk-xxx
export TAVILY_API_KEY=tvly-xxx
或者直接在下方【配置区域】中填写。
依赖安装:
pip install langgraph langchain langchain-community tavily-python langchain-community[tongyi]
流程:
用户输入主题 → AI 搜索撰写 → 暂停等人工审核
→ 批准 → 发布
→ 修改意见 → AI 重写 → 再次审核 → ...
"""
import os
import uuid
from typing import Annotated, Literal
from dotenv import load_dotenv
from typing_extensions import TypedDict
from langchain_core.messages import AIMessage, HumanMessage
from langchain_community.chat_models.tongyi import ChatTongyi
from langchain_community.tools.tavily_search import TavilySearchResults
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import ToolNode
load_dotenv()
# ============================================================
# 1. 状态定义
# ============================================================
class State(TypedDict):
messages: Annotated[list, add_messages]
# ============================================================
# 2. LLM 和工具初始化
# ============================================================
model = ChatTongyi(
model="qwen-max",
api_key=os.getenv("DASHSCOPE_API_KEY"),
temperature=0.7,
)
search_tool = TavilySearchResults(max_results=3)
tools = [search_tool]
model_with_tools = model.bind_tools(tools)
# ============================================================
# 3. 节点定义
# ============================================================
def writer_node(state):
"""写手节点:调用带搜索工具的 LLM 进行撰写或重写"""
messages = state["messages"]
response = model_with_tools.invoke(messages)
return {"messages": [response]}
# 工具执行节点(自动处理 LLM 发出的 tool_calls)
tool_node = ToolNode(tools=tools)
def human_review_node(state):
"""
人工审核占位节点。
本身不执行逻辑,作为 interrupt_before 的挂载点,
让程序在此处暂停,等待人工输入。
"""
return state
def publisher_node(state):
"""发布节点:审核通过后执行(可扩展为存数据库、发邮件等)"""
print("\n" + "=" * 50)
print(">>> [系统] 研报已正式发布!已归档并发送邮件。 <<<")
print("=" * 50 + "\n")
return {"messages": [AIMessage(content="✅ 研报已正式发布,流程结束。")]}
# ============================================================
# 4. 路由函数
# ============================================================
def should_use_tools(state) -> Literal["tools", "human_review"]:
"""
检查 writer 的输出:
- 如果 LLM 请求了工具调用 → 去 tools 节点执行
- 如果没有工具调用(已完成撰写)→ 去人工审核
"""
messages = state["messages"]
last_message = messages[-1]
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
return "tools"
return "human_review"
def review_router(state) -> Literal["publisher", "writer"]:
"""
根据人工审核后追加的消息,决定下一步:
- 包含"批准/通过/approve" → 发布
- 其他内容 → 视为修改意见,回退给 writer 重写
"""
messages = state["messages"]
last_message = messages[-1]
content = last_message.content.lower() if hasattr(last_message, "content") else ""
approve_keywords = ["批准", "通过", "approve", "ok", "确认", "发布"]
if any(kw in content for kw in approve_keywords):
return "publisher"
return "writer"
# ============================================================
# 5. 构建并编译图
# ============================================================
workflow = StateGraph(State)
# 添加节点
workflow.add_node("writer", writer_node)
workflow.add_node("tools", tool_node)
workflow.add_node("human_review", human_review_node)
workflow.add_node("publisher", publisher_node)
# 连线
workflow.add_edge(START, "writer")
workflow.add_conditional_edges(
"writer",
should_use_tools,
{"tools": "tools", "human_review": "human_review"},
)
workflow.add_edge("tools", "writer") # 工具执行完回到写手继续撰写
workflow.add_conditional_edges(
"human_review",
review_router,
{"publisher": "publisher", "writer": "writer"},
)
workflow.add_edge("publisher", END)
# 编译(MemorySaver 支持暂停和恢复)
memory = MemorySaver()
app_v4 = workflow.compile(
checkpointer=memory,
interrupt_before=["human_review"], # 关键:在人工审核节点前暂停
)
# ============================================================
# 6. 辅助函数(复制即可)
# ============================================================
def print_separator(char="─", length=60):
print(char * length)
def print_ai_response(messages):
"""打印 AI 最新的回复"""
for msg in reversed(messages):
if hasattr(msg, "type") and msg.type == "ai" and msg.content:
print("\n[AI 草稿]:")
print_separator()
print(msg.content)
print_separator()
return
# ============================================================
# 7. 主程序入口(复制即可)
# ============================================================
def run():
thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}
print("\n" + "=" * 60)
print(" 📊 研报助手 V4 - 人工审核版")
print("=" * 60)
# ----------------------------------------------------------
# 第一步:用户输入研报主题
# ----------------------------------------------------------
topic = input("\n请输入研报主题(例:2026年新能源汽车市场分析): ").strip()
if not topic:
topic = "2024年全球AI芯片市场分析"
print(f"使用默认主题:{topic}")
initial_prompt = f"""你是一位专业的研究分析师。请针对以下主题撰写一份研究报告草稿。
主题:{topic}
要求:
1. 先使用搜索工具查找最新的相关资料
2. 基于搜索结果,撰写一份结构清晰的研报草稿
3. 研报需包含:摘要、行业背景、市场现状、关键趋势、风险分析、结论与建议
4. 引用数据需标明来源
5. 使用中文撰写
请开始。"""
# ----------------------------------------------------------
# 第二步:AI 搜索 + 撰写(会在 human_review 前自动暂停)
# ----------------------------------------------------------
print("\n⏳ AI 正在搜索资料并撰写草稿,请稍候...\n")
for event in app_v4.stream(
{"messages": [HumanMessage(content=initial_prompt)]},
config=config,
stream_mode="values",
):
if "messages" in event:
last_msg = event["messages"][-1]
if hasattr(last_msg, "type"):
if last_msg.type == "ai" and hasattr(last_msg, "tool_calls") and last_msg.tool_calls:
for tc in last_msg.tool_calls:
print(f" 🔍 正在调用工具: {tc['name']}")
elif last_msg.type == "tool":
print(f" ✅ 工具返回结果({len(last_msg.content)} 字符)")
# 图已在 human_review 前暂停
current_state = app_v4.get_state(config)
print_ai_response(current_state.values["messages"])
# ----------------------------------------------------------
# 第三步:人工审核循环
# ----------------------------------------------------------
while True:
print("\n" + "=" * 60)
print(" 👤 人工审核环节")
print("=" * 60)
print(" 输入 '批准' / '通过' → 正式发布")
print(" 输入修改意见 → AI 将根据意见重写")
print(" 输入 'quit' → 退出程序")
print_separator()
user_input = input("\n您的审核意见: ").strip()
if not user_input:
print("⚠️ 请输入审核意见。")
continue
if user_input.lower() == "quit":
print("\n 已退出程序。")
return
# 将审核意见注入状态(human-in-the-loop 的核心)
app_v4.update_state(
config,
{"messages": [HumanMessage(content=user_input)]},
)
# 恢复执行
print("\n处理中...\n")
for event in app_v4.stream(
None, # None = 从中断点继续
config=config,
stream_mode="values",
):
if "messages" in event:
last_msg = event["messages"][-1]
if hasattr(last_msg, "type"):
if last_msg.type == "ai" and hasattr(last_msg, "tool_calls") and last_msg.tool_calls:
for tc in last_msg.tool_calls:
print(f" 🔍 正在调用工具: {tc['name']}")
elif last_msg.type == "tool":
print(f" ✅ 工具返回结果({len(last_msg.content)} 字符)")
# 检查图是否结束
current_state = app_v4.get_state(config)
if current_state.next:
# 又暂停在 human_review 前 → 打印新草稿
print_ai_response(current_state.values["messages"])
else:
# 图已结束 → 发布完成
print_ai_response(current_state.values["messages"])
print("\n流程已完成!")
return
# ============================================================
# 启动
# ============================================================
if __name__ == "__main__":
run()
Langgraph.json配置:
{
"dependencies": ["./src"],
"graphs": {
"destination_agent": "./package1/test3.py:app_v4"
},
"env": ".env",
"cors": {
"enabled": true,
"allow_origins": [
"https://smith.langchain.com",
"http://localhost:3000",
"http://192.168.25.1:3000"
],
"allow_credentials": true
}
}
模块六: LangGraph 多 Agent 系统(实战版)
二、Subagents 模式:电商运营助手实战
2.2 实战案例:智能电商运营助手
Step 1: 定义底层工具
from langchain.tools import tool
@tool
def create_product(name: str, category: str, price: float, inventory: int) -> str:
"""创建新商品并上架。"""
return f"商品已上架 - 名称: {name}, 类目: {category}, 价格: ¥{price}, 库存: {inventory}件"
@tool
def update_inventory(sku: str, quantity: int) -> str:
"""更新商品库存数量。"""
return f"库存已更新 - SKU: {sku}, 新库存: {quantity}件"
@tool
def get_product_analytics(sku: str) -> str:
"""获取商品销售数据分析。"""
return f"商品 {sku} 数据 - 浏览量: 1234, 销量: 45, 转化率: 3.6%"
@tool
def create_promotion(name: str, promotion_type: str, rules: str) -> str:
"""创建促销活动。"""
return f"促销已创建 - 名称: {name}, 类型: {promotion_type}, 规则: {rules}"
@tool
def send_marketing_push(title: str, target_audience: str, channel: str) -> str:
"""发送营销推送消息。"""
return f"推送已发送 - 标题: {title}, 渠道: {channel}, 目标: {target_audience}"
Step 2: 创建专业子 Agent
import os
from langchain.agents import create_agent
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
load_dotenv()
model = ChatTongyi(
model="qwen-max",
api_key=os.getenv("DASHSCOPE_API_KEY"),
)
# 商品管理专家
product_agent = create_agent(
model,
tools=[create_product, update_inventory, get_product_analytics],
system_prompt="""你是电商商品管理专家。
职责:
- 创建和上架新商品
- 管理商品库存
- 分析商品销售数据
工作原则:
✓ 确保商品信息完整准确
✓ 在最终消息中包含所有操作结果"""
)
# 营销推广专家
marketing_agent = create_agent(
model,
tools=[create_promotion, send_marketing_push],
system_prompt="""你是电商营销推广专家。
职责:
- 创建各类促销活动
- 制定营销推送策略
工作原则:
✓ 活动规则清晰易懂
✓ 在最终消息中包含所有操作结果"""
)
Step 3: 将子 Agent 包装为工具
from langchain.tools import InjectedToolCallId
from langchain_core.messages import ToolMessage
from langgraph.types import Command
from typing import Annotated
@tool(
"manage_product", #工具名称, 工具的唯一标识符
description="""商品管理专家。
t
何时调用:
- 用户需要上架、修改或下架商品
- 涉及库存管理和调整
- 需要商品销售数据分析
输入:自然语言描述的商品管理需求
示例:"上架一款夏季新款连衣裙,价格299元"""
)
def manage_product(
request: str,
tool_call_id: Annotated[str, InjectedToolCallId] #自动获取当前这次工具调用的唯一身份证(快递单号)
#InjectedToolCallId:这个值不要让用户或 LLM 传,请框架自动把当前这次工具调用的 ID 塞进来。
) -> Command:
"""调用商品管理专家 Agent"""
result = product_agent.invoke({
"messages": [{"role": "user", "content": request}]
})
final_message = result["messages"][-1].content
return Command(update={
"messages": [
ToolMessage(
content=final_message,
tool_call_id=tool_call_id
)
]
})
@tool(
"create_campaign",
description="""营销推广专家。
何时调用:
- 用户需要创建促销活动
- 需要发送营销推送
输入:自然语言描述的营销需求
示例:"创建618大促满减活动"""
)
def create_campaign(
request: str,
tool_call_id: Annotated[str, InjectedToolCallId]
) -> Command:
"""调用营销推广专家 Agent"""
result = marketing_agent.invoke({
"messages": [{"role": "user", "content": request}]
})
final_message = result["messages"][-1].content
return Command(update={
"messages": [
ToolMessage(
content=final_message,
tool_call_id=tool_call_id
)
]
})
Step 4: 创建 Supervisor
supervisor = create_agent(
model,
tools=[manage_product, create_campaign],
system_prompt="""你是智能电商运营助手的协调者(Supervisor)。
核心能力:
1. 理解用户的复杂运营请求
2. 将任务分解为子任务
3. 选择合适的专家处理各个子任务
4. 整合结果形成完整回复
可用专家:
- manage_product: 处理所有商品管理任务
- create_campaign: 处理所有营销推广任务
工作流程:
1. 分析请求,识别需要哪些专家
2. 可以并行调用多个专家(工具)
3. 整合所有专家结果,提供连贯回复
注意:
- 多领域任务需要调用多个专家
- 给用户完整且易懂的结果"""
)
Step 5: 使用示例
from langchain_core.messages import HumanMessage
# 示例 1:简单单领域请求
print("=== 简单请求 ===")
result = supervisor.invoke({
"messages": [
HumanMessage(content="上架一款新的蓝牙耳机,价格199元,库存500件")
]
})
print(result["messages"][-1].content)
# 示例 2:复杂多领域请求
print("\n=== 复杂请求 ===")
result = supervisor.invoke({
"messages": [
HumanMessage(content="""
上架夏季新款防晒衣(价格159元,库存1000件),
同时创建一个"清凉一夏"满199减20的促销活动
""")
]
})
print(result["messages"][-1].content)
2.4 添加人机协同
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver
# 为子Agent添加审批中间件
product_agent = create_agent(
model,
tools=[create_product, update_inventory, get_product_analytics],
system_prompt=PRODUCT_PROMPT,
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={"create_product": True},
description_prefix="📦 商品上架待审批",
)
]
)
# Supervisor需要checkpointer来暂停/恢复
supervisor = create_agent(
model,
tools=[manage_product, create_campaign],
system_prompt=SUPERVISOR_PROMPT,
checkpointer=InMemorySaver(),
)
三、Handoffs 模式:旅行规划助手实战
Step 1: 定义自定义状态
from langchain.agents import AgentState
from typing_extensions import NotRequired
from typing import Literal
PlanningStep = Literal["preference_collector", "destination_recommender", "itinerary_planner"]
class TravelPlanningState(AgentState):
"""旅行规划工作流状态"""
current_step: NotRequired[PlanningStep]
budget_level: NotRequired[Literal["economy", "comfort", "luxury"]]
travel_style: NotRequired[Literal["relaxation", "culture", "adventure"]]
destination: NotRequired[str]
travel_dates: NotRequired[str]
Step 2: 创建状态转换工具
from langchain.tools import tool, ToolRuntime
from langchain.messages import ToolMessage
from langgraph.types import Command
@tool
def record_preferences(
budget_level: Literal["economy", "comfort", "luxury"],
travel_style: Literal["relaxation", "culture", "adventure"],
travel_dates: str,
runtime: ToolRuntime[None, TravelPlanningState],
) -> Command:
"""记录用户旅行偏好并转换到目的地推荐步骤"""
return Command(
update={
"messages": [
ToolMessage(
content=f"偏好已记录 - 预算: {budget_level}, 风格: {travel_style}, 日期: {travel_dates}",
tool_call_id=runtime.tool_call_id,
)
],
"budget_level": budget_level,
"travel_style": travel_style,
"travel_dates": travel_dates,
"current_step": "destination_recommender",
}
)
@tool
def select_destination(
destination: str,
runtime: ToolRuntime[None, TravelPlanningState],
) -> Command:
"""确认目的地选择并转换到行程制定步骤"""
return Command(
update={
"messages": [
ToolMessage(
content=f"目的地已选择: {destination}",
tool_call_id=runtime.tool_call_id,
)
],
"destination": destination,
"current_step": "itinerary_planner",
}
)
@tool
def generate_itinerary(itinerary: str) -> str:
"""生成最终行程安排"""
return f"行程已生成:\n{itinerary}"
@tool
def search_flights(destination: str) -> str:
"""搜索航班信息"""
return f"找到飞往{destination}的航班 - 直飞 ¥2580, 转机 ¥1890"
@tool
def search_hotels(destination: str) -> str:
"""搜索酒店信息"""
return f"{destination}推荐酒店 - 五星 ¥800/晚, 四星 ¥450/晚, 民宿 ¥200/晚"
@tool
def search_attractions(destination: str) -> str:
"""搜索景点信息"""
return f"{destination}热门景点 - 景点A, 景点B, 景点C"
Step 3: 定义步骤配置
PREFERENCE_COLLECTOR_PROMPT = """你是旅行规划助手的第一线。
当前阶段:偏好收集
任务:
1. 热情问候用户
2. 依次询问:预算范围、旅行风格、出行日期
3. 收集完成后使用 record_preferences 记录并进入下一步
注意:友好对话,不要一次问多个问题"""
DESTINATION_RECOMMENDER_PROMPT = """你是旅行目的地推荐专家。
当前阶段:目的地推荐
用户偏好:预算 {budget_level}, 风格 {travel_style}, 日期 {travel_dates}
任务:
1. 根据偏好推荐2-3个目的地
2. 用户确认后使用 select_destination 进入下一步
可用工具:search_flights, search_hotels, search_attractions"""
ITINERARY_PLANNER_PROMPT = """你是行程规划专家。
当前阶段:行程制定
用户信息:预算 {budget_level}, 风格 {travel_style}, 日期 {travel_dates}, 目的地 {destination}
任务:
1. 使用搜索工具获取航班、酒店、景点信息
2. 制定详细行程
3. 使用 generate_itinerary 输出最终行程"""
STEP_CONFIG = {
"preference_collector": {
"prompt": PREFERENCE_COLLECTOR_PROMPT,
"tools": [record_preferences],
"requires": [],
},
"destination_recommender": {
"prompt": DESTINATION_RECOMMENDER_PROMPT,
"tools": [select_destination, search_flights, search_hotels, search_attractions],
"requires": ["budget_level", "travel_style", "travel_dates"],
},
"itinerary_planner": {
"prompt": ITINERARY_PLANNER_PROMPT,
"tools": [generate_itinerary, search_flights, search_hotels, search_attractions],
"requires": ["budget_level", "travel_style", "travel_dates", "destination"],
},
}
Step 4: 创建步骤中间件
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from langchain.messages import SystemMessage
from typing import Callable
@wrap_model_call
def apply_step_config(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
#handler是一个接收ModelRequest返回ModelResponse的可调用对象
) -> ModelResponse:
"""
动态配置Agent执行流程的核心中间件
- 根据当前对话阶段自动注入定制化系统提示
- 强制校验前置状态完整性
- 按阶段切换可用工具集
通过装饰器模式实现无侵入式流程控制
"""
# 获取当前对话阶段(默认首阶段为偏好收集器)
current_step = request.state.get("current_step", "preference_collector")
#这里的state实际上就是指Agent 的“全局共享记忆库”,等于定义的业务变量字典 + 按时间排序的 Message 列表
# 从全局配置表加载阶段专属参数
step_config = STEP_CONFIG[current_step]
# === 状态校验 ===
# 确保所有前置依赖状态已填充(如:用户意图、设备型号等)
for key in step_config["requires"]:
if request.state.get(key) is None:
raise ValueError(
f"阶段 {current_step} 需要完整状态: {key} 未设置"
f"(当前状态: {list(request.state.keys())})"
)
# === 动态提示词生成 ===
# 使用状态变量填充系统提示(示例: "{device_model}保修状态: {warranty_status}")
system_prompt = step_config["prompt"].format(**request.state)
# === 执行配置注入 ===
# 1. 替换系统提示词(实现上下文感知对话)
# 2. 切换当前阶段可用工具集(如: 诊断工具/维修工具)
request = request.override(
system_prompt=system_prompt,
tools=step_config["tools"],
)
return handler(request) # 传递配置后的请求到下游处理
Step 5: 创建 Agent
import os
from langchain.agents import create_agent
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
from langgraph.checkpoint.memory import InMemorySaver
load_dotenv()
model = ChatTongyi(
model="qwen-max",
api_key=os.getenv("DASHSCOPE_API_KEY"),
)
all_tools = [
record_preferences,
select_destination,
generate_itinerary,
search_flights,
search_hotels,
search_attractions,
]
travel_agent = create_agent(
model,
tools=all_tools,
state_schema=TravelPlanningState,
middleware=[apply_step_config],
checkpointer=InMemorySaver(),
)
Step 6: 测试完整工作流
import uuid
thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}
# 第1轮:初始问题
print("=== 第1轮 ===")
result = travel_agent.invoke(
{"messages": [{"role": "user", "content": "我想规划一次旅行"}]},
config
)
print(result["messages"][-1].content)
# 第2轮:回答偏好
print("\n=== 第2轮 ===")
result = travel_agent.invoke(
{"messages": [{"role": "user", "content": "预算中等舒适型,想体验文化,7月初出发玩5天"}]},
config
)
print(result["messages"][-1].content)
# 第3轮:选择目的地
print("\n=== 第3轮 ===")
result = travel_agent.invoke(
{"messages": [{"role": "user", "content": "我选择去西安"}]},
config
)
print(result["messages"][-1].content)
# 第4轮:生成行程
print("\n=== 第4轮 ===")
result = travel_agent.invoke(
{"messages": [{"role": "user", "content": "帮我制定详细行程"}]},
config
)
print(result["messages"][-1].content)
3.5 添加回退功能
@tool
def go_back_to_preferences(
runtime: ToolRuntime[None, TravelPlanningState]
) -> Command:
"""返回到偏好收集步骤"""
return Command(
update={
# 必须包含 ToolMessage 来响应 LLM 的调用
"messages": [
ToolMessage(
content="已重置状态,请重新提供偏好信息。",
tool_call_id=runtime.tool_call_id
)
],
"current_step": "preference_collector"
}
)
@tool
def go_back_to_destination(
runtime: ToolRuntime[None, TravelPlanningState]
) -> Command:
"""返回到目的地推荐步骤"""
return Command(
update={
# 必须包含 ToolMessage 来响应 LLM 的调用
"messages": [
ToolMessage(
content="已返回目的地推荐阶段。",
tool_call_id=runtime.tool_call_id
)
],
"current_step": "destination_recommender"
}
)
# 更新配置
STEP_CONFIG["itinerary_planner"]["tools"].extend([
go_back_to_preferences,
go_back_to_destination
])
all_tools = [
record_preferences,
select_destination,
generate_itinerary,
search_flights,
search_hotels,
search_attractions,
go_back_to_preferences, # <--- Add this
go_back_to_destination, # <--- Add this
]
ITINERARY_PLANNER_PROMPT = """你是行程规划专家。
当前阶段:行程制定
用户信息:预算 {budget_level}, 风格 {travel_style}, 日期 {travel_dates}, 目的地 {destination}
任务:
1. 使用搜索工具获取航班、酒店、景点信息
2. 制定详细行程
3. 使用 generate_itinerary 输出最终行程
4. 当用户有新的偏好更改使用go_back_to_preferences重新进行修改
5. 当用户需要重新推荐目的地使用go_back_to_destination重新推荐
"""
四、Skills 模式:SQL 助手实战
4.2 实战案例:企业 SQL 助手
Step 1: 定义技能结构
from typing import TypedDict
class Skill(TypedDict):
"""可以渐进式披露的技能"""
name: str # 唯一标识符
description: str # 1-2句简短描述(显示在系统提示词中)
content: str # 完整内容(按需加载)
Step 2: 定义业务技能
SKILLS: list[Skill] = [
{
"name": "sales_analytics",
"description": "销售数据分析的数据库schema和业务逻辑,包括客户、订单和收入。",
"content": """# 销售分析 Schema
## 表结构
### customers (客户表)
- customer_id (主键)
- name (客户名称)
- email
- signup_date (注册日期)
- status (active/inactive)
- customer_tier (客户等级: bronze/silver/gold/platinum)
### orders (订单表)
- order_id (主键)
- customer_id (外键 -> customers)
- order_date (订单日期)
- status (订单状态: pending/completed/cancelled/refunded)
- total_amount (订单总金额)
- sales_region (销售区域: north/south/east/west)
### order_items (订单明细表)
- item_id (主键)
- order_id (外键 -> orders)
- product_id (产品ID)
- quantity (数量)
- unit_price (单价)
- discount_percent (折扣百分比)
## 业务规则
**活跃客户定义**:
status = 'active' AND signup_date <= CURRENT_DATE - INTERVAL '90 days'
**收入计算规则**:
只计算 status = 'completed' 的订单。使用 orders 表的 total_amount 字段(已包含折扣)。
**客户生命周期价值 (CLV)**:
客户所有已完成订单的 total_amount 总和。
**高价值订单定义**:
total_amount > 1000 的订单。
## 示例查询
```sql
-- 查询最近一季度收入前10的客户
SELECT
c.customer_id,
c.name,
c.customer_tier,
SUM(o.total_amount) as total_revenue
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE o.status = 'completed'
AND o.order_date >= CURRENT_DATE - INTERVAL '3 months'
GROUP BY c.customer_id, c.name, c.customer_tier
ORDER BY total_revenue DESC
LIMIT 10;
```
**重要注意事项**:
- 始终在 WHERE 子句中包含 status = 'completed'
- 使用 INTERVAL 而不是固定日期
- 金额字段已包含税费和折扣
""",
},
{
"name": "inventory_management",
"description": "库存追踪的数据库schema和业务逻辑,包括产品、仓库和库存水平。",
"content": """# 库存管理 Schema
## 表结构
### products (产品表)
- product_id (主键)
- product_name (产品名称)
- sku (库存单位)
- category (类别)
- unit_cost (单位成本)
- reorder_point (补货点:最低库存警戒线)
- discontinued (是否停产: boolean)
### warehouses (仓库表)
- warehouse_id (主键)
- warehouse_name (仓库名称)
- location (位置)
- capacity (容量)
### inventory (库存表)
- inventory_id (主键)
- product_id (外键 -> products)
- warehouse_id (外键 -> warehouses)
- quantity_on_hand (现有库存数量)
- last_updated (最后更新时间)
### stock_movements (库存流动表)
- movement_id (主键)
- product_id (外键 -> products)
- warehouse_id (外键 -> warehouses)
- movement_type (流动类型: inbound/outbound/transfer/adjustment)
- quantity (数量:入库为正,出库为负)
- movement_date (流动日期)
- reference_number (参考号)
## 业务规则
**可用库存定义**:
inventory 表中 quantity_on_hand > 0 的库存。
**需要补货的产品**:
所有仓库的 quantity_on_hand 总和 <= 产品的 reorder_point。
**活跃产品规则**:
排除 discontinued = true 的产品(除非专门分析停产商品)。
**库存估值计算**:
quantity_on_hand * unit_cost
## 示例查询
```sql
-- 查找需要补货的产品
SELECT
p.product_id,
p.product_name,
p.reorder_point,
SUM(i.quantity_on_hand) as total_stock,
p.unit_cost,
(p.reorder_point - SUM(i.quantity_on_hand)) as units_to_reorder
FROM products p
JOIN inventory i ON p.product_id = i.product_id
WHERE p.discontinued = false
GROUP BY p.product_id, p.product_name, p.reorder_point, p.unit_cost
HAVING SUM(i.quantity_on_hand) <= p.reorder_point
ORDER BY units_to_reorder DESC;
```
**重要注意事项**:
- 始终使用 SUM(quantity_on_hand) 跨所有仓库汇总
- 默认排除停产产品
- 负数 quantity 表示出库
""",
},
]
Step 3: 创建技能加载工具
from langchain.tools import tool
@tool
def load_skill(skill_name: str) -> str:
"""加载技能的完整内容到Agent上下文中。
当你需要关于如何处理特定类型请求的详细信息时使用此工具。
这将为你提供该技能领域的全面说明、策略和指南。
Args:
skill_name: 要加载的技能名称(如 "sales_analytics", "inventory_management")
"""
for skill in SKILLS:
if skill["name"] == skill_name:
return f"✅ 已加载技能:{skill_name}\n\n{skill['content']}"
available = ", ".join(s["name"] for s in SKILLS)
return f"❌ 技能 '{skill_name}' 未找到。可用技能:{available}"
Step 4: 创建技能中间件
from langchain.agents.middleware import ModelRequest, ModelResponse, AgentMiddleware
from langchain.messages import SystemMessage
from typing import Callable
class SkillMiddleware(AgentMiddleware):
"""将技能描述注入系统提示词的中间件"""
tools = [load_skill]
def __init__(self):
skills_list = []
for skill in SKILLS:
skills_list.append(f"- **{skill['name']}**: {skill['description']}")
self.skills_prompt = "\n".join(skills_list)
def wrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
skills_addendum = (
f"\n\n## 可用技能\n\n{self.skills_prompt}\n\n"
"当你需要处理特定类型请求的详细信息时,使用 load_skill 工具。"
)
new_content = list(request.system_message.content_blocks) + [
{"type": "text", "text": skills_addendum}
]
new_system_message = SystemMessage(content=new_content)
modified_request = request.override(system_message=new_system_message)
return handler(modified_request)
Step 5: 创建 Agent
import os
from langchain.agents import create_agent
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
from langgraph.checkpoint.memory import InMemorySaver
load_dotenv()
model = ChatTongyi(
model="qwen-max",
api_key=os.getenv("DASHSCOPE_API_KEY"),
)
agent = create_agent(
model,
system_prompt="你是一个SQL查询助手,帮助用户编写业务数据库查询。",
middleware=[SkillMiddleware()],
checkpointer=InMemorySaver(),
)
Step 6: 测试SQL 助手
import uuid
thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}
result = agent.invoke(
{
"messages": [
{
"role": "user",
"content": "写一个SQL查询,找出上个月订单金额超过$1000的所有客户"
}
]
},
config
)
for message in result["messages"]:
if hasattr(message, 'pretty_print'):
message.pretty_print()
五、Router 模式:知识库实战
5.2 实战案例:企业多源知识库
Step 1: 定义状态
from typing import Annotated, Literal, TypedDict
import operator
class AgentInput(TypedDict):
query: str
class AgentOutput(TypedDict):
source: str
result: str
class Classification(TypedDict):
source: Literal["github", "notion", "slack"]
query: str
class RouterState(TypedDict):
query: str
classifications: list[Classification]
results: Annotated[list[AgentOutput], operator.add]
final_answer: str
Step 2: 定义各垂直领域的工具
from langchain.tools import tool
@tool
def search_code(query: str) -> str:
"""在GitHub仓库中搜索代码"""
return f"在代码库找到匹配 '{query}' 的结果:src/auth.py 中的认证中间件"
@tool
def search_issues(query: str) -> str:
"""搜索GitHub问题和PR"""
return f"找到3个匹配 '{query}' 的issue:#142, #89, #203"
@tool
def search_notion(query: str) -> str:
"""在Notion工作空间搜索文档"""
return f"找到文档:'API认证指南' - 涵盖OAuth2流程和JWT令牌"
@tool
def get_page(page_id: str) -> str:
"""获取特定Notion页面"""
return f"页面内容:认证设置的分步说明"
@tool
def search_slack(query: str) -> str:
"""搜索Slack消息和线程"""
return f"在 #engineering 发现讨论:'使用Bearer令牌进行API认证'"
@tool
def get_thread(thread_id: str) -> str:
"""获取特定Slack线程"""
return f"线程讨论了API密钥轮换的最佳实践"
Step 3: 创建专业 Agent
import os
from langchain.agents import create_agent
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
load_dotenv()
model = ChatTongyi(
model="qwen-max",
api_key=os.getenv("DASHSCOPE_API_KEY"),
)
github_agent = create_agent(
model,
tools=[search_code, search_issues],
system_prompt="你是GitHub专家。搜索代码、issue和PR回答问题。"
)
notion_agent = create_agent(
model,
tools=[search_notion, get_page],
system_prompt="你是Notion专家。搜索文档回答问题。"
)
slack_agent = create_agent(
model,
tools=[search_slack, get_thread],
system_prompt="你是Slack专家。搜索讨论回答问题。"
)
Step 4: 构建路由工作流
4.1 分类器
from pydantic import BaseModel, Field
class ClassificationResult(BaseModel):
classifications: list[Classification] = Field(
description="要调用的Agent列表及其针对性子问题"
)
def classify_query(state: RouterState) -> dict:
router_llm = model
structured_llm = router_llm.with_structured_output(ClassificationResult)
result = structured_llm.invoke([
{
"role": "system",
"content": """分析查询并确定要咨询哪些知识库。
可用来源:
- github: 代码、API参考、实现细节
- notion: 内部文档、流程、策略
- slack: 团队讨论、非正式知识
仅返回与查询相关的来源。
"""
},
{"role": "user", "content": state["query"]}
])
return {"classifications": result.classifications}
4.2 路由函数
from langgraph.types import Send
def route_to_agents(state: RouterState) -> list[Send]:
return [
Send(c["source"], {"query": c["query"]})
for c in state["classifications"]
]
4.3 Agent节点
def query_github(state: AgentInput) -> dict:
result = github_agent.invoke({
"messages": [{"role": "user", "content": state["query"]}]
})
return {"results": [{"source": "github", "result": result["messages"][-1].content}]}
def query_notion(state: AgentInput) -> dict:
result = notion_agent.invoke({
"messages": [{"role": "user", "content": state["query"]}]
})
return {"results": [{"source": "notion", "result": result["messages"][-1].content}]}
def query_slack(state: AgentInput) -> dict:
result = slack_agent.invoke({
"messages": [{"role": "user", "content": state["query"]}]
})
return {"results": [{"source": "slack", "result": result["messages"][-1].content}]}
4.4 综合器
def synthesize_results(state: RouterState) -> dict:
if not state["results"]:
return {"final_answer": "未从任何知识源找到结果。"}
formatted = [
f"**来自 {r['source'].title()}:**\n{r['result']}"
for r in state["results"]
]
synthesis_llm = model
synthesis_response = synthesis_llm.invoke([
{
"role": "system",
"content": f"""综合搜索结果回答:"{state['query']}"
合并信息,避免冗余,保持简洁。"""
},
{"role": "user", "content": "\n\n".join(formatted)}
])
return {"final_answer": synthesis_response.content}
Step 5: 编译工作流
from langgraph.graph import StateGraph, START, END
workflow = (
StateGraph(RouterState)
.add_node("classify", classify_query)
.add_node("github", query_github)
.add_node("notion", query_notion)
.add_node("slack", query_slack)
.add_node("synthesize", synthesize_results)
.add_edge(START, "classify")
.add_conditional_edges("classify", route_to_agents, ["github", "notion", "slack"])
.add_edge("github", "synthesize")
.add_edge("notion", "synthesize")
.add_edge("slack", "synthesize")
.add_edge("synthesize", END)
.compile()
)
Step 6: 使用路由器
result = workflow.invoke({
"query": "如何认证API请求?"
})
print("原始查询:", result["query"])
print("\n分类:")
for c in result["classifications"]:
print(f" {c['source']}: {c['query']}")
print("\n最终答案:")
print(result["final_answer"])

2606

被折叠的 条评论
为什么被折叠?



