LangGraph流程编排:把7个AI服务串成一条生产线
从意大利面代码到优雅的DAG工作流
重构前的噩梦
重构前的代码长这样:
ini
def search_products(query):
# 查询改写
rewritten = rewrite_query(query)
filters = extract_filters(query)
# 向量检索
vector_results = []
for q in rewritten:
results = vector_search(q, filters)
vector_results.extend(results)
# 关键词检索
keyword_results = keyword_search(rewritten, filters)
# 合并去重
candidates = merge_results(vector_results, keyword_results)
# 重排序
ranked = rerank(query, candidates)
# 构建上下文
context = []
for sku in ranked[:5]:
ctx = build_context(sku)
context.append(ctx)
# 获取实时数据
realtime = {}
for sku in ranked[:5]:
data = get_realtime_data(sku)
realtime[sku] = data
# LLM生成
response = llm_generate(query, context, realtime)
return response
看起来还行?
实际上问题一堆:
- 错误处理混乱
bash
# 某个步骤失败了,整个流程崩溃
# 没有重试机制
# 没有降级方案
- 状态传递靠参数
python
# 参数越传越多
def process(query, rewritten, filters, vector_results,
keyword_results, candidates, ranked, context,
realtime, ...): # 😱
- 调试困难
bash
# 中间结果看不到
# 不知道哪一步出错
# 无法单独测试某个步骤
- 扩展性差
bash
# 想加一个新步骤?
# 要改10个地方
# 参数传递链要全部改
- 无法并行
bash
# 向量检索和关键词检索可以并行
# 但现在是串行的
# 浪费时间
更要命的是:这段代码有400行,都在一个函数里!
LangGraph:流程编排的正确姿势
LangGraph是LangChain生态的工作流编排框架,核心思想:
把复杂流程表示为有向无环图(DAG) ,每个节点是一个独立的服务,通过状态传递数据。
核心概念
1. State(状态)
所有节点共享的数据结构:
yaml
from typing import TypedDict
class GraphState(TypedDict):
raw_query: str # 原始查询
rewritten_queries: list # 改写查询
filters: dict # 过滤条件
candidates: list # 候选商品
ranked_skus: list # 排序后SKU
product_context: list # 商品上下文
real_time_data: dict # 实时数据
final_response: str # 最终回答
2. Node(节点)
每个节点是一个函数,输入state,返回partial state:
python
def query_rewrite_node(state: GraphState) -> dict:
"""查询改写节点"""
query = state["raw_query"]
# 调用查询改写服务
result = query_rewrite_service(query)
# 返回部分状态更新
return {
"rewritten_queries": result.queries,
"filters": result.filters
}
3. Edge(边)
定义节点之间的连接:
bash
workflow.add_edge("query_rewrite", "hybrid_search")
# query_rewrite执行完 → 执行hybrid_search
4. Graph(图)
组装所有节点和边:
python
from langgraph.graph import StateGraph, START, END
workflow = StateGraph(GraphState)
# 添加节点
workflow.add_node("query_rewrite", query_rewrite_node)
workflow.add_node("hybrid_search", hybrid_search_node)
workflow.add_node("rerank", rerank_node)
# 定义流程
workflow.add_edge(START, "query_rewrite")
workflow.add_edge("query_rewrite", "hybrid_search")
workflow.add_edge("hybrid_search", "rerank")
workflow.add_edge("rerank", END)
# 编译
graph = workflow.compile()
完整RAG流程设计
我们的商品搜索RAG流程有6个步骤:
objectivec
START
↓
┌─────────────────┐
│ 1. QueryRewrite │ 查询改写
│ │ 输入: "不上火的奶粉"
│ │ 输出: ["温和配方 奶粉", "易消化 配方奶粉"]
└─────────────────┘
↓
┌─────────────────┐
│ 2. HybridSearch │ 混合检索
│ │ 输入: 改写查询 + 过滤条件
│ │ 输出: 50个候选商品
└─────────────────┘
↓
┌─────────────────┐
│ 3. Rerank │ 重排序
│ │ 输入: 50个候选
│ │ 输出: Top 5 SKU
└─────────────────┘
↓
┌─────────────────┐
│ 4. ContextBuild │ 构建上下文
│ │ 输入: Top 5 SKU
│ │ 输出: 商品详细信息
└─────────────────┘
↓
┌─────────────────┐
│ 5. RealtimeData │ 获取实时数据
│ │ 输入: Top 5 SKU
│ │ 输出: 价格、库存、促销
└─────────────────┘
↓
┌─────────────────┐
│ 6. LLMGenerate │ 生成回答
│ │ 输入: 查询 + 上下文 + 实时数据
│ │ 输出: 导购回答
└─────────────────┘
↓
END
完整代码实现
python
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class ProductRAGPipeline:
"""商品RAG完整流程编排"""
def __init__(self, llm_model="gpt-4o-mini"):
# 初始化LLM
self.llm = ChatOpenAI(model=llm_model, temperature=0)
# 初始化所有服务
self.query_rewrite_service = QueryRewriteService(llm=self.llm)
self.hybrid_search_service = HybridSearchService()
self.rerank_service = create_reranker("bge", top_n=5)
self.context_builder_service = ContextBuilderService()
self.realtime_data_service = RealTimeDataService()
self.llm_generate_service = LLMGenerateService(llm=self.llm)
# 构建图
self.graph = self._build_graph()
def _build_graph(self):
"""构建LangGraph工作流"""
# 1. 定义状态
class GraphState(TypedDict):
raw_query: str
rewritten_queries: list
filters: dict
candidates: list
ranked_skus: list
product_context: list
real_time_data: dict
final_response: str
referenced_skus: list
# 2. 创建图
workflow = StateGraph(GraphState)
# 3. 添加节点(用lambda包装服务的__call__方法)
workflow.add_node(
"query_rewrite",
lambda state: self.query_rewrite_service(state)
)
workflow.add_node(
"hybrid_search",
lambda state: self.hybrid_search_service(state)
)
workflow.add_node(
"rerank",
lambda state: self.rerank_service(state)
)
workflow.add_node(
"context_builder",
lambda state: self.context_builder_service(state)
)
workflow.add_node(
"realtime_data",
lambda state: self.realtime_data_service(state)
)
workflow.add_node(
"llm_generate",
lambda state: self.llm_generate_service(state)
)
# 4. 定义流程边
workflow.add_edge(START, "query_rewrite")
workflow.add_edge("query_rewrite", "hybrid_search")
workflow.add_edge("hybrid_search", "rerank")
workflow.add_edge("rerank", "context_builder")
workflow.add_edge("context_builder", "realtime_data")
workflow.add_edge("realtime_data", "llm_generate")
workflow.add_edge("llm_generate", END)
# 5. 编译
return workflow.compile()
def run(self, query: str, user_context: dict = None):
"""执行完整流程"""
# 初始化状态
initial_state = {
"raw_query": query,
"rewritten_queries": [],
"filters": {},
"candidates": [],
"ranked_skus": [],
"product_context": [],
"real_time_data": {},
"final_response": "",
"referenced_skus": []
}
# 执行图
result = self.graph.invoke(initial_state)
return result
每个节点的实现
每个服务都实现__call__方法作为节点函数:
python
class QueryRewriteService:
def __call__(self, input_data: dict) -> dict:
"""LangGraph节点调用接口"""
query = input_data.get("raw_query", "")
# 执行查询改写
result = self.rewrite(query)
# 返回状态更新
return {
"rewritten_queries": result.rewritten_queries,
"filters": result.filters
}
class HybridSearchService:
def __call__(self, input_data: dict) -> dict:
"""LangGraph节点调用接口"""
rewritten_queries = input_data.get("rewritten_queries", [])
filters = input_data.get("filters", {})
# 执行混合检索
candidates = self.search(rewritten_queries, filters)
return {
"candidates": candidates
}
# 其他服务类似...
为什么用__call__?
因为LangGraph的节点需要是callable,签名为:
python
node(state: dict) -> partial_state: dict
__call__让类的实例可以像函数一样调用。
LangGraph的核心优势
1. 状态管理自动化
重构前:
ini
def process(query):
rewritten = rewrite(query) # 状态1
filters = extract_filters(query) # 状态2
# 手动传递状态
results = search(rewritten, filters) # 状态3
# 传来传去...
ranked = rerank(query, results)
context = build_context(ranked)
# ...
重构后:
perl
# 每个节点只需要读取需要的状态
def query_rewrite_node(state):
query = state["raw_query"] # 只读需要的
# ...
return {"rewritten_queries": result} # 只写产生的
# LangGraph自动合并状态
状态传递流程:
css
初始状态: {
"raw_query": "不上火的奶粉",
"rewritten_queries": [],
"filters": {},
...
}
↓
QueryRewrite节点返回: {
"rewritten_queries": ["温和配方 奶粉", ...],
"filters": {"category": "奶粉"}
}
↓
LangGraph自动合并: {
"raw_query": "不上火的奶粉", ← 保留
"rewritten_queries": ["温和配方 奶粉", ...], ← 更新
"filters": {"category": "奶粉"}, ← 更新
...
}
↓
传递给下一个节点
2. 错误隔离
重构前:
ini
def process(query):
rewritten = rewrite(query)
# rewrite失败,整个流程崩溃
results = search(rewritten)
# ...
重构后:
python
# 每个节点独立处理错误
def query_rewrite_node(state):
try:
result = query_rewrite_service(state)
return result
except Exception as e:
print(f"查询改写失败: {e}")
# 返回降级结果
return {
"rewritten_queries": [state["raw_query"]], # 使用原始查询
"filters": {}
}
# 不影响其他节点
3. 可观测性
LangGraph提供了强大的调试工具:
shell
# 查看执行过程
for event in graph.stream(initial_state):
print(event)
# 输出:
# {'query_rewrite': {'rewritten_queries': [...]}}
# {'hybrid_search': {'candidates': [...]}}
# {'rerank': {'ranked_skus': [...]}}
# ...
自定义日志:
python
class ProductRAGPipeline:
def run(self, query: str):
print(f"\n{'='*60}")
print(f"开始处理查询: {query}")
print(f"{'='*60}\n")
# 使用stream查看每一步
for step_output in self.graph.stream(initial_state):
for node_name, node_output in step_output.items():
print(f"\n[{node_name}] 完成")
if "rewritten_queries" in node_output:
print(f" 改写结果: {node_output['rewritten_queries']}")
if "candidates" in node_output:
print(f" 候选数量: {len(node_output['candidates'])}")
if "ranked_skus" in node_output:
print(f" 排序结果: {node_output['ranked_skus']}")
return final_result
输出示例:
less
============================================================
开始处理查询: 不上火的奶粉
============================================================
[query_rewrite] 完成
改写结果: ['温和配方 奶粉', '易消化 配方奶粉', '低热量 儿童奶粉']
[hybrid_search] 完成
候选数量: 48
[rerank] 完成
排序结果: ['SKU_1001', 'SKU_3003', 'SKU_5005', 'SKU_2002', 'SKU_4004']
[context_builder] 完成
[realtime_data] 完成
[llm_generate] 完成
4. 单元测试友好
重构前:
ini
# 要测试rerank,必须先执行前面所有步骤
def test_rerank():
query = "test"
rewritten = rewrite(query)
filters = extract_filters(query)
vector_results = vector_search(rewritten, filters)
keyword_results = keyword_search(rewritten, filters)
candidates = merge_results(vector_results, keyword_results)
# 终于可以测试rerank了
result = rerank(query, candidates)
assert len(result) == 5
重构后:
python
# 直接测试单个节点
def test_rerank_node():
# 构造状态
state = {
"raw_query": "test",
"candidates": [mock_candidate1, mock_candidate2, ...]
}
# 直接调用节点
result = rerank_service(state)
assert len(result["ranked_skus"]) == 5
5. 扩展性强
新增一个节点:商品过滤
python
# 1. 定义新节点
def product_filter_node(state):
"""过滤掉不合规商品"""
candidates = state["candidates"]
# 过滤逻辑
filtered = [c for c in candidates if is_valid(c)]
return {"candidates": filtered}
# 2. 添加到图中
workflow.add_node("product_filter", product_filter_node)
# 3. 调整边
workflow.add_edge("hybrid_search", "product_filter") # 新增
workflow.add_edge("product_filter", "rerank") # 替换原来的边
只需3行代码,不用改其他任何地方!
高级特性:条件路由
有时候需要根据状态动态选择下一步:
python
def should_use_llm_rerank(state):
"""判断是否使用LLM重排序"""
candidates = state["candidates"]
# 候选商品少于10个,直接用BGE
if len(candidates) < 10:
return "bge_rerank"
# 高价值用户,使用LLM精排
user_context = state.get("user_context", {})
if user_context.get("is_vip"):
return "llm_rerank"
# 默认BGE
return "bge_rerank"
# 添加条件边
workflow.add_conditional_edges(
"hybrid_search",
should_use_llm_rerank,
{
"bge_rerank": "bge_rerank_node",
"llm_rerank": "llm_rerank_node"
}
)
流程图:
markdown
hybrid_search
↓
├─ 候选<10 → bge_rerank
├─ VIP用户 → llm_rerank
└─ 其他 → bge_rerank
并行执行优化
有些节点可以并行执行,比如:
python
# ContextBuilder和RealtimeData可以并行
def _build_graph_parallel(self):
workflow = StateGraph(GraphState)
# ... 前面的节点
workflow.add_edge("rerank", "parallel_start")
# 并行节点
workflow.add_node("context_builder", context_builder_node)
workflow.add_node("realtime_data", realtime_data_node)
# 从parallel_start分叉
workflow.add_edge("parallel_start", "context_builder")
workflow.add_edge("parallel_start", "realtime_data")
# 汇聚到llm_generate
workflow.add_node("parallel_end", lambda state: {})
workflow.add_edge("context_builder", "parallel_end")
workflow.add_edge("realtime_data", "parallel_end")
workflow.add_edge("parallel_end", "llm_generate")
return workflow.compile()
但LangGraph当前版本对并行支持有限,通常我们会在单个节点内部并行:
python
import asyncio
async def parallel_data_fetch_node(state):
"""并行获取上下文和实时数据"""
ranked_skus = state["ranked_skus"]
# 并行执行
context_task = asyncio.create_task(
context_builder_service.build_context_async(ranked_skus)
)
realtime_task = asyncio.create_task(
realtime_data_service.get_realtime_data_async(ranked_skus)
)
# 等待完成
context = await context_task
realtime = await realtime_task
return {
"product_context": context,
"real_time_data": realtime
}
可观测性增强
除了日志,我们还加了一些可观测性字段:
yaml
class GraphState(TypedDict):
# ... 原有字段
# 可观测性字段
rerank_type: str # "bge" or "llm"
generation_type: str # "llm" or "fallback"
recommended_skus: list # LLM推荐的SKU
referenced_skus: list # 实际引用的SKU
为什么需要这些?
- rerank_type: 知道用的哪种重排序,便于A/B测试
- generation_type: 知道是否降级到Fallback
- recommended_skus vs referenced_skus: 验证LLM是否按要求推荐
监控示例:
scss
def run_with_monitoring(self, query):
result = self.graph.invoke(initial_state)
# 记录指标
metrics = {
"query": query,
"rerank_type": result.get("rerank_type"),
"generation_type": result.get("generation_type"),
"fallback_used": result.get("generation_type") == "fallback",
"recommended_count": len(result.get("recommended_skus", [])),
"referenced_count": len(result.get("referenced_skus", []))
}
# 发送到监控系统
send_to_monitoring(metrics)
# 告警:Fallback使用率过高
if metrics["fallback_used"]:
alert("LLM生成降级到Fallback")
return result
测试Mock:不依赖真实服务
开发时不想每次都调用真实的LLM API,可以用Mock:
python
class MockLLM:
"""Mock LLM用于测试"""
def invoke(self, messages):
message_text = str(messages)
class Response:
content = ""
response = Response()
# 根据消息内容判断是哪个服务
if "改写" in message_text:
# 查询改写
response.content = json.dumps({
"rewritten_queries": ["低乳糖 儿童 奶粉", "益生菌 奶粉"],
"filters": {"category": "奶粉"}
})
elif "排序" in message_text:
# 重排序
response.content = json.dumps({
"ranked_skus": ["SKU_1001", "SKU_3003"]
})
else:
# LLM生成
response.content = "根据您的需求,推荐以下商品..."
return response
# 使用Mock
pipeline = ProductRAGPipeline(use_mock=True)
result = pipeline.run("不上火的奶粉")
好处:
- ✅ 不消耗API费用
- ✅ 响应速度快
- ✅ 结果可预测
- ✅ 便于单元测试
实战效果对比
代码质量
| 指标 | 重构前 | 重构后 | 改进 |
|---|---|---|---|
| 单函数行数 | 400行 | <50行/节点 | ✅ 模块化 |
| 参数数量 | 10+ | 1个(state) | ✅ 简化 |
| 错误处理 | 集中式 | 节点级 | ✅ 隔离 |
| 单元测试 | 困难 | 容易 | ✅ 可测试 |
| 扩展新功能 | 改N处 | 加1个节点 | ✅ 可扩展 |
运行性能
| 指标 | 重构前 | 重构后 | 说明 |
|---|---|---|---|
| 平均耗时 | 1.2s | 1.1s | 略快(优化了并行) |
| 内存占用 | 不稳定 | 稳定 | 状态管理更好 |
| 错误率 | 5% | 0.2% | 错误隔离生效 |
| 可观测性 | 无 | 完整 | 每步都可见 |
开发效率
| 任务 | 重构前 | 重构后 |
|---|---|---|
| 新增节点 | 2小时 | 30分钟 |
| 调整流程 | 1小时 | 10分钟 |
| 单元测试 | 困难 | 容易 |
| Bug定位 | 1小时 | 10分钟 |
最佳实践总结
1. 节点设计原则
单一职责:
python
# ✅ 好:每个节点做一件事
def query_rewrite_node(state):
return {"rewritten_queries": [...]}
# ❌ 坏:一个节点做多件事
def query_process_node(state):
# 改写 + 过滤 + 扩展 + ...
return {...}
无副作用:
python
# ✅ 好:纯函数,不修改外部状态
def rerank_node(state):
candidates = state["candidates"]
ranked = rerank(candidates)
return {"ranked_skus": ranked}
# ❌ 坏:修改全局变量
global_cache = {}
def rerank_node(state):
global global_cache
global_cache[state["query"]] = ... # 副作用
幂等性:
perl
# ✅ 好:多次执行结果相同
def rerank_node(state):
return {"ranked_skus": deterministic_rerank(state["candidates"])}
# ❌ 坏:每次结果不同
def rerank_node(state):
return {"ranked_skus": random.sample(state["candidates"], 5)}
2. 状态设计原则
最小化:
yaml
# ✅ 只放必要的
class GraphState(TypedDict):
raw_query: str
ranked_skus: list
# ❌ 放太多中间结果
class GraphState(TypedDict):
raw_query: str
query_tokens: list
query_embeddings: np.ndarray
intermediate_results_1: list
intermediate_results_2: list
# ...
类型明确:
python
from typing import TypedDict, List, Dict
class GraphState(TypedDict):
raw_query: str # 不是 Any
ranked_skus: List[str] # 不是 list
filters: Dict[str, Any] # 不是 dict
3. 错误处理
节点级降级:
python
def rerank_node(state):
try:
return bge_rerank(state)
except Exception as e:
print(f"BGE重排失败: {e},使用基于分数排序")
return fallback_rerank(state)
全局异常捕获:
python
def run(self, query):
try:
result = self.graph.invoke(initial_state)
return result
except Exception as e:
print(f"流程执行失败: {e}")
# 返回降级结果
return {
"final_response": "抱歉,系统繁忙,请稍后再试。",
"error": str(e)
}
4. 监控和日志
关键节点打日志:
python
def query_rewrite_node(state):
start_time = time.time()
result = query_rewrite_service(state)
duration = time.time() - start_time
print(f"[QueryRewrite] 耗时: {duration:.3f}s")
print(f"[QueryRewrite] 改写数量: {len(result['rewritten_queries'])}")
return result
记录异常:
python
import logging
logger = logging.getLogger(__name__)
def rerank_node(state):
try:
return bge_rerank(state)
except Exception as e:
logger.error(f"重排序失败: {e}", exc_info=True)
return fallback_rerank(state)
写在最后
LangGraph把复杂的AI流程编排变成了优雅的DAG工作流。
核心价值:
- 状态管理自动化:不再手动传递参数
- 错误隔离:一个节点失败不影响其他
- 可观测性:每一步都清晰可见
- 可测试性:节点独立,易于测试
- 可扩展性:新增功能只需加节点
技术选型建议:
- 简单流程(<3步):直接写函数调用即可
- 中等流程(3-10步):LangGraph(本文方案)
- 复杂流程(>10步):考虑Airflow、Prefect等专业工作流引擎
关键原则:
把复杂性分解到节点中,让流程本身保持简单。
希望这篇文章能帮到正在做AI应用编排的同学。欢迎交流~
参考资源
- LangGraph : langchain-ai.github.io/langgraph/
- LangChain : python.langchain.com/
- Workflow Patterns : www.workflowpatterns.com/
技术栈
ini
langgraph==1.0.4 # 工作流编排
langchain-core # 核心组件
typing-extensions # 类型支持
本文基于真实生产环境的RAG流程编排经验。LangGraph让AI应用开发更优雅、更可靠。