LangGraph流程编排:把7个AI服务串成一条生产线

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

看起来还行?

实际上问题一堆:

  1. 错误处理混乱
bash 复制代码
# 某个步骤失败了,整个流程崩溃
# 没有重试机制
# 没有降级方案
  1. 状态传递靠参数
python 复制代码
# 参数越传越多
def process(query, rewritten, filters, vector_results, 
            keyword_results, candidates, ranked, context, 
            realtime, ...):  # 😱
  1. 调试困难
bash 复制代码
# 中间结果看不到
# 不知道哪一步出错
# 无法单独测试某个步骤
  1. 扩展性差
bash 复制代码
# 想加一个新步骤?
# 要改10个地方
# 参数传递链要全部改
  1. 无法并行
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

为什么需要这些?

  1. rerank_type: 知道用的哪种重排序,便于A/B测试
  2. generation_type: 知道是否降级到Fallback
  3. 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工作流。

核心价值:

  1. 状态管理自动化:不再手动传递参数
  2. 错误隔离:一个节点失败不影响其他
  3. 可观测性:每一步都清晰可见
  4. 可测试性:节点独立,易于测试
  5. 可扩展性:新增功能只需加节点

技术选型建议:

  • 简单流程(<3步):直接写函数调用即可
  • 中等流程(3-10步):LangGraph(本文方案)
  • 复杂流程(>10步):考虑Airflow、Prefect等专业工作流引擎

关键原则:

把复杂性分解到节点中,让流程本身保持简单。

希望这篇文章能帮到正在做AI应用编排的同学。欢迎交流~


参考资源

技术栈

ini 复制代码
langgraph==1.0.4      # 工作流编排
langchain-core        # 核心组件
typing-extensions     # 类型支持

本文基于真实生产环境的RAG流程编排经验。LangGraph让AI应用开发更优雅、更可靠。

相关推荐
__True2 小时前
搜索重排序(Rerank)实战:让最相关的结果排第一
aigc
__True3 小时前
LLM导购生成:如何让AI不说谎、不编造、不乱推荐
aigc
小小工匠5 小时前
AIGC - 使用 Nano Banana Pro 生成卡通信息图的完整指南
aigc
用户5191495848456 小时前
7-ZiProwler:CVE-2025-11001 漏洞利用工具
人工智能·aigc
Mintopia7 小时前
🪄 生成式应用的 **前端 orchestration 层(编排层)指南**
人工智能·llm·aigc
Mintopia7 小时前
🤖✨ 生成式应用架构师的修炼手册
人工智能·llm·aigc
安思派Anspire8 小时前
数据科学和ML领域的趋势是什么?为2026年做准备
aigc·openai·agent
艺术是真的秃头8 小时前
Trae:当编程从“编写”转向“对话”与“委派”
人工智能·python·ai·aigc
小霖家的混江龙11 小时前
不再费脑, 写给 AI 爱好者的矩阵 (Matrix) 入门指南
人工智能·llm·aigc