LangGraph多Agent协作实战:以物流售前场景为例的5 Agent工作流设计

本文基于LangGraph StateGraph实现了一个5 Agent协作的智能物流售前系统,覆盖方案设计、设备选型、成本核算、标书撰写和质量审查全链路。代码可直接运行,架构可复用。


一、为什么物流售前需要多Agent

物流售前是一个典型的多步骤、跨领域协作场景。一个完整的售前方案需要:理解客户需求并设计仓库布局、从数万SKU的设备库中匹配货架和传送带、核算ROI和TCO、最后输出符合招投标规范的标书文档。每个环节涉及的知识域完全不同------仓库规划师不会算财务模型,设备工程师不写标书。

如果试图用单一Prompt + 巨型上下文来解决,会遇到三个致命问题:

知识域污染。将仓储布局知识、设备参数表、成本核算公式和标书模板全部塞进一个Prompt,模型会在不同知识域之间产生幻觉交叉。例如在描述货架承重时,模型可能混入成本核算中的折旧系数。

无独立校验。单Agent输出是一锤子买卖------如果设备选型错误(选了承重500kg的货架去放2吨货物),下游没有独立的Agent来做交叉验证。

错误难以定位。单Agent输出几千字的方案,当客户反馈"第三页的传送带型号不对"时,只能整体重新生成,无法定向修复。

相比之下,5 Agent架构将每个环节封装为独立节点,共享StateGraph作为数据总线,Supervisor根据当前状态决定下一步路由。每个Agent只关注自己的知识域,输出经过下一条Agent的隐式校验------成本Agent发现设备总价异常时会通过StateGraph回写给设备Agent触发重选。

scss 复制代码
┌─────────────────────────────────────────────────────────┐
│                    Supervisor Node                       │
│          (LLM路由: 根据state.next决定下一步)              │
└────┬──────────┬──────────┬──────────┬──────────┬────────┘
     │          │          │          │          │
     ▼          ▼          ▼          ▼          ▼
┌─────────┐┌─────────┐┌─────────┐┌─────────┐┌─────────┐
│ Solution││Equipment││  Cost   ││   Bid   ││ Quality │
│ Design  ││ Select  ││  Calc   ││ Writing ││ Review  │
│ Agent   ││ Agent   ││  Agent  ││  Agent  ││  Agent  │
└────┬────┘└────┬────┘└────┬────┘└────┬────┘└────┬────┘
     │          │          │          │          │
     └──────────┴──────────┴──────────┴──────────┘
                        │
                        ▼
              ┌──────────────────┐
              │   SharedState    │
              │ (TypedDict,      │
              │  LangGraph       │
              │  StateGraph)     │
              └──────────────────┘

二、StateGraph:多Agent协作的数据总线

LangGraph的核心抽象是StateGraph------一个带类型的有限状态机,其中状态是Agent间共享的数据结构,节点是Agent执行逻辑,边(包括条件边)定义了流转规则。

2.1 状态Schema定义

python 复制代码
from typing import TypedDict, List, Optional, Annotated
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage

class SLSExpertState(TypedDict):
    """SLS Expert 共享状态定义

    LangGraph的StateGraph依赖此Schema做状态合并。
    messages字段使用add_messages reducer实现增量追加而非覆盖。
    """
    # === 消息历史(增量追加) ===
    messages: Annotated[List[BaseMessage], add_messages]

    # === 业务上下文 ===
    project_id: str                    # 项目唯一标识
    customer_requirement: str          # 客户原始需求文本
    parsed_requirements: Optional[dict] # Solution Agent解析后的结构化需求

    # === 各Agent输出 ===
    solution_plan: Optional[dict]      # 方案设计:仓库布局、动线、分区
    equipment_list: Optional[list]     # 设备清单:[{name, model, qty, specs}]
    cost_report: Optional[dict]        # 成本报告:{breakdown, total, roi, tco}
    bid_document: Optional[str]        # 标书Markdown全文

    # === 质量控制 ===
    validation_results: Optional[list] # Quality Agent的校验结果
    human_approvals: Optional[dict]    # 人工审批钩子:{step: approved|rejected}

    # === 路由控制 ===
    next: str                          # Supervisor路由目标
    error_count: int                   # 全局错误计数
    max_retries: int                   # 最大重试次数

2.2 条件边与Supervisor路由

Supervisor节点本身是一个LLM调用,它根据当前state决定下一个应该激活的Agent。这里有两个设计选择:LLM动态路由 vs 确定性规则路由。本文选择了混合策略------常规流程用确定性规则,异常情况才让LLM介入。

python 复制代码
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver

def supervisor_router(state: SLSExpertState) -> str:
    """Supervisor路由逻辑:确定性优先,异常兜底用LLM"""

    # 错误边界:超过重试次数直接终止
    if state.get("error_count", 0) >= state.get("max_retries", 3):
        return "error_handler"

    current = state.get("next", "")

    # 确定性路由规则 ------ 覆盖90%的正常流程
    deterministic_routes = {
        "solution_design": "equipment_select",
        "equipment_select": "cost_calculation",
        "cost_calculation": "bid_writing",
        "bid_writing": "quality_review",
        "quality_review": "check_approval",
    }

    if current in deterministic_routes:
        return deterministic_routes[current]

    # 异常路由:成本超标需要回退重选设备
    if current == "cost_over_budget":
        return "equipment_select"  # 回退

    # 审批相关路由
    if current == "check_approval":
        cost = state.get("cost_report", {})
        threshold = cost.get("total", 0) > 5000000  # 500万以上人工审批
        return "human_approval" if threshold else END

    # LLM兜底路由 ------ 仅处理未预见的异常路径
    return _llm_fallback_route(state)

def _llm_fallback_route(state: SLSExpertState) -> str:
    """LLM兜底:分析state中的错误信息决定回退或跳过"""
    # 此处是精简示例,生产代码中会组装prompt调用DeepSeek
    last_error = state.get("messages", [])[-1] if state.get("messages") else None
    if last_error and "timeout" in str(last_error).lower():
        return "retry_current"
    return END

2.3 图组装

python 复制代码
def build_workflow() -> StateGraph:
    """组装5 Agent工作流"""
    workflow = StateGraph(SLSExpertState)

    # 注册Agent节点
    workflow.add_node("solution_design", solution_design_agent)
    workflow.add_node("equipment_select", equipment_select_agent)
    workflow.add_node("cost_calculation", cost_calculation_agent)
    workflow.add_node("bid_writing", bid_writing_agent)
    workflow.add_node("quality_review", quality_review_agent)
    workflow.add_node("human_approval", human_approval_handler)
    workflow.add_node("error_handler", error_handler)

    # 入口:从方案设计开始
    workflow.set_entry_point("solution_design")

    # 每个Agent执行完后统一回到Supervisor路由
    for node in ["solution_design", "equipment_select",
                 "cost_calculation", "bid_writing", "quality_review"]:
        workflow.add_edge(node, "supervisor")

    # Supervisor根据state决定下一步
    workflow.add_conditional_edges(
        "supervisor",
        supervisor_router,
        {
            "equipment_select": "equipment_select",
            "cost_calculation": "cost_calculation",
            "bid_writing": "bid_writing",
            "quality_review": "quality_review",
            "human_approval": "human_approval",
            "error_handler": "error_handler",
            "check_approval": "supervisor",
            "retry_current": "supervisor",
            END: END,
        }
    )

    # 人工审批是阻塞点
    workflow.add_edge("human_approval", "supervisor")
    workflow.add_edge("error_handler", END)

    # 使用MemorySaver支持checkpoint/断点续跑
    return workflow.compile(checkpointer=MemorySaver())

三、Agent实现深挖

3.1 Solution Design Agent:结构化需求解析

Solution Design Agent负责将客户自然语言需求(例如"需要一个日处理5000单的电商仓库,SKU约3000个,以服装为主")解析为结构化的仓库规划参数。核心难点不是生成文本,而是将模糊描述映射为可计算的参数(面积、货架排数、动线类型、人员配置)。

python 复制代码
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field

class WarehouseSpec(BaseModel):
    """仓库规划的结构化输出Schema"""
    daily_order_volume: int = Field(description="日处理订单量")
    sku_count: int = Field(description="SKU数量")
    category: str = Field(description="商品品类")
    storage_area_sqm: float = Field(description="预估存储面积(平方米)")
    picking_area_sqm: float = Field(description="预估拣选面积")
    dock_count: int = Field(description="月台数量")
    flow_type: str = Field(description="动线类型: U型/L型/I型")
    automation_level: str = Field(description="自动化等级: manual/semi/full")

SOLUTION_PROMPT = ChatPromptTemplate.from_messages([
    ("system", """你是仓储规划专家。将客户需求解析为结构化仓库参数。

规则:
- 服装类SKU系数1.3(需更大存储空间)
- 日处理5000单以下推荐U型动线
- 3000+ SKU自动推荐半自动化方案
- 未知参数使用行业均值并标注confidence<0.7
"""),
    ("human", "客户需求:{requirement}"),
])

async def solution_design_agent(state: SLSExpertState) -> dict:
    """方案设计Agent:需求解析 + 知识库检索 + 结构化输出"""
    requirement = state["customer_requirement"]

    # Step 1: 从向量库检索历史相似项目参数
    from app.services.vector_store import MilvusClient
    milvus = MilvusClient()
    similar_projects = await milvus.search(
        collection="warehouse_projects",
        query_text=requirement,
        top_k=3,
    )

    # Step 2: 组装Prompt上下文(历史案例 + 行业规则)
    past_cases = "\n".join([
        f"- {p['area']}sqm, {p['category']}, ROI={p['roi']}"
        for p in similar_projects
    ])

    # Step 3: LLM调用
    parser = PydanticOutputParser(pydantic_object=WarehouseSpec)
    chain = SOLUTION_PROMPT | llm | parser

    result: WarehouseSpec = await chain.ainvoke({
        "requirement": requirement,
        "past_cases": past_cases,
        "format_instructions": parser.get_format_instructions(),
    })

    return {
        "parsed_requirements": result.model_dump(),
        "next": "solution_design",
        "messages": [AIMessage(content=f"方案设计完成: {result.flow_type}动线, "
                                      f"{result.storage_area_sqm}sqm存储区")]
    }

设计要点 :Parser使用Pydantic而非自由文本------下游Agent需要的是storage_area_sqm这个数值去计算货架数量,不是一段描述性文本。结构化输出是Agent间契约的基础。

3.2 Equipment Select Agent:知识图谱驱动的设备匹配

设备选型Agent是5个Agent中对外部知识依赖最重的。它需要从Neo4j知识图谱中查询设备信息,根据方案参数做匹配推荐。这里展示的是图查询 + LLM推理的混合模式。

python 复制代码
async def equipment_select_agent(state: SLSExpertState) -> dict:
    """设备选型Agent:Neo4j图查询 + LLM匹配推理"""
    specs = state["parsed_requirements"]

    # Step 1: 从Neo4j多跳查询获取候选设备
    from app.services.graph_store import Neo4jClient
    neo4j = Neo4jClient()

    cypher_query = """
    MATCH (cat:Category {name: $category})-[:REQUIRES]->(eq:Equipment)
    WHERE eq.min_area_sqm <= $area AND eq.max_area_sqm >= $area
    OPTIONAL MATCH (eq)-[:COMPATIBLE_WITH]->(compat:Equipment)
    RETURN eq, collect(compat.name) as compatibles
    ORDER BY eq.priority_rank
    LIMIT 20
    """
    candidates = await neo4j.query(cypher_query, {
        "category": specs["category"],
        "area": specs["storage_area_sqm"],
    })

    # Step 2: 组装候选设备上下文,交给LLM做最终匹配
    equipment_context = _format_candidates(candidates)

    match_prompt = f"""从以下候选设备中为仓库选择合适的设备组合:

仓库参数:
- 存储面积: {specs['storage_area_sqm']}sqm
- 动线类型: {specs['flow_type']}
- 自动化等级: {specs['automation_level']}
- SKU数量: {specs['sku_count']}

候选设备:
{equipment_context}

要求:
1. 货架承重必须≥品类标准(服装: 200kg/层)
2. 拣选设备需兼容动线类型
3. 输送线速度≥仓库吞吐量需求
4. 同品牌设备优先(降低维护复杂度)
5. 输出JSON数组,每项含 model, qty, reason
"""
    response = await llm.ainvoke(match_prompt)
    equipment = _parse_equipment_json(response.content)

    return {
        "equipment_list": equipment,
        "next": "equipment_select",
        "messages": [AIMessage(content=f"设备选型完成: {len(equipment)}类设备")]
    }

为什么用图数据库而不是向量检索:设备之间存在兼容性关系(货架型号A只能搭配传送带型号B/C),这是典型的图结构。向量检索擅长语义相似度,但无法表达"COMPATIBLE_WITH"这样的关系约束。Neo4j的多跳查询在一个Cypher语句中完成了"品类->所需设备->兼容设备"的关联。

3.3 Quality Review Agent:确定性规则校验

Quality Review Agent不走LLM------它是由确定性规则组成的校验管线。原因很简单:LLM不擅长精确数值比较("3500kg > 2000kg"这种判断确定性代码比LLM可靠且便宜100倍)。

python 复制代码
from dataclasses import dataclass
from typing import Callable, List

@dataclass
class ValidationRule:
    """一条可组合的校验规则"""
    name: str
    check: Callable[[dict, list, dict], bool]
    severity: str  # "error" | "warning"
    message: str

VALIDATION_RULES = [
    ValidationRule(
        name="load_capacity",
        check=lambda specs, eq, cost: all(
            e.get("load_kg", 0) >= specs.get("min_load_kg_per_layer", 200)
            for e in eq if "shelf" in e.get("type", "").lower()
        ),
        severity="error",
        message="存在货架承重不满足品类最低标准",
    ),
    ValidationRule(
        name="cost_consistency",
        check=lambda specs, eq, cost:
            abs(cost.get("equipment_subtotal", 0) -
                sum(e.get("unit_price", 0) * e.get("qty", 0) for e in eq)) < 0.01,
        severity="error",
        message="设备清单金额与成本核算不一致",
    ),
    ValidationRule(
        name="bid_completeness",
        check=lambda specs, eq, cost:
            cost.get("bid_document", "") != "" and
            len(cost.get("bid_document", "")) > 500,
        severity="error",
        message="标书内容为空或过短",
    ),
    ValidationRule(
        name="roi_threshold",
        check=lambda specs, eq, cost:
            cost.get("roi_years", 99) <= 5,
        severity="warning",
        message="ROI回收期超过5年,建议标注为高风险项目",
    ),
]


async def quality_review_agent(state: SLSExpertState) -> dict:
    """质量审查Agent:确定性规则管线(无LLM调用)"""
    specs = state.get("parsed_requirements", {})
    equipment = state.get("equipment_list", [])
    cost = state.get("cost_report", {})

    results = []
    passed = True

    for rule in VALIDATION_RULES:
        try:
            ok = rule.check(specs, equipment, cost)
            results.append({
                "rule": rule.name,
                "passed": ok,
                "severity": rule.severity,
                "message": rule.message if not ok else "OK",
            })
            if rule.severity == "error" and not ok:
                passed = False
        except Exception as e:
            results.append({
                "rule": rule.name,
                "passed": False,
                "severity": "error",
                "message": f"规则执行异常: {str(e)}",
            })
            passed = False

    # 未通过则设置error_count触发Supervisor异常路由
    return {
        "validation_results": results,
        "error_count": state.get("error_count", 0) + (0 if passed else 1),
        "next": "quality_review",
        "messages": [AIMessage(content=f"质量审查: {'通过' if passed else '未通过'} "
                                      f"({len(results)}项检查)")]
    }

每条规则是一个纯函数lambda,新增校验逻辑只需追加到VALIDATION_RULES列表,不需要改动Agent主体代码。这是一种策略模式在Agent场景下的应用。


四、状态管理与错误恢复

4.1 状态传递的两种模式

LangGraph的StateGraph对状态字段默认执行浅合并 (shallow merge)------如果两个Agent都返回equipment_list,后执行的Agent会覆盖前一个的值。对于需要增量追加的字段,必须显式声明reducer:

python 复制代码
# messages字段使用add_messages实现增量追加
messages: Annotated[List[BaseMessage], add_messages]

# equipment_list默认行为是覆盖------这正是我们要的(重选设备时替换)
equipment_list: Optional[list]

# 但validation_results需要追加(每轮校验结果都保留)
validation_results: Annotated[list, _append_reducer]

这是一个需要特别注意的设计细节:默认覆盖意味着如果一个Agent忘记返回某个字段,它不会被自动清空,但上一个Agent的值会残留。在实践中我们在每个Agent函数开头做显式快照:

python 复制代码
# 在每个Agent入口做状态快照日志
_start_state_snapshot = {
    k: type(v).__name__ for k, v in state.items() if v is not None
}
logger.info(f"[{agent_name}] 进入时state: {_start_state_snapshot}")

4.2 Human-in-the-Loop审批钩子

对于成本敏感决策(方案总价超过500万),系统通过LangGraph的interrupt机制插入人工审批断点:

python 复制代码
from langgraph.types import interrupt

async def human_approval_handler(state: SLSExpertState) -> dict:
    """成本超阈值时中断工作流等待人工审批"""
    cost = state["cost_report"]

    # LangGraph原生中断:抛interrupt异常,前端监听此事件
    approval = interrupt({
        "type": "cost_approval_required",
        "project_id": state["project_id"],
        "total_cost": cost["total"],
        "breakdown": cost["breakdown"],
        "message": f"项目总成本{cost['total']:,.0f}元,请审批",
    })

    return {
        "human_approvals": {
            **state.get("human_approvals", {}),
            "cost": approval,  # "approved" | "rejected" | "revise"
        },
        "next": "human_approval",
    }

前端通过监听LangGraph Server的SSE事件流来渲染审批界面。interrupt抛出的dict会被序列化为事件payload,审批人确认后通过graph.stream(None, config)Command(resume=...)恢复执行。

4.3 重试与降级策略

python 复制代码
async def error_handler(state: SLSExpertState) -> dict:
    """全局错误处理:分级响应"""
    error_count = state.get("error_count", 0)
    last_msg = state.get("messages", [])[-1] if state.get("messages") else None

    if error_count >= state.get("max_retries", 3):
        # 三级:放弃当前流程,输出部分结果
        return {
            "next": END,
            "messages": [AIMessage(
                content=f"已达到最大重试次数({error_count}),输出当前已完成部分。"
                        f"建议人工介入处理未完成环节。"
            )]
        }

    if "timeout" in str(last_msg).lower():
        # 二级:超时类错误,线性退避重试
        await asyncio.sleep(2 ** error_count)
        return {"next": "retry_current"}

    # 一级:业务级错误,记录后让Supervisor决定
    return {"next": "supervisor"}

五、工程实践与经验

5.1 确定性路由 vs LLM路由

经过多轮测试,我们最终采用了确定性路由为主(约90%路径)、LLM路由为兜底的混合策略。纯LLM路由的问题在于:每次路由判断消耗约500 tokens,且存在约3%的概率路由到错误节点(如把"设备选型完成后"路由到"标书撰写"跳过了成本核算)。确定性规则不仅零延迟零成本,还为调试提供了清晰的可追溯路径。

5.2 状态体积控制

共享StateGraph的一个隐藏陷阱是状态膨胀。经过5个Agent后,messages字段可能累积超过50条消息。我们采取了两项措施:(1) 每个Agent只返回摘要级别消息(不超过200 tokens),完整输出存入对应的业务字段;(2) 在设备选型前对parsed_requirements做截断缓存------只保留下游Agent实际使用的字段,删除中间推理过程。

5.3 Prompt缓存策略

Solution Design和Equipment Select两个Agent共享部分上下文(品类标准、行业参数)。我们将这些公共知识抽取为系统级System Prompt片段,通过DeepSeek API的cache_control标记缓存点,在连续请求中实现了约30%的成本降低。

erlang 复制代码
             缓存命中率对比
┌────────────────────────────────────┐
│ 无缓存:  每请求 ~12K tokens 全量传输 │
│ 有缓存:  首请求 12K, 后续 ~3K (节省75%)│
│ 连续5方案: 60K → 27K (节省55%)       │
└────────────────────────────────────┘

5.4 基础设施一览

系统以8容器Docker Compose编排运行:

服务 用途 Agent关联
FastAPI API网关 + LangGraph Server 全部
Neo4j 设备知识图谱 Equipment Select
Milvus 历史方案向量检索 Solution Design
PostgreSQL+pgvector 项目持久化 + 向量补充 全部
Redis 会话缓存 + 限流 全部
MinIO 标书PDF/方案附件存储 Bid Writing
DeepSeek API LLM后端 全部

六、结语

本文展示了基于LangGraph的5 Agent协作系统在物流售前场景下的完整设计------从StateGraph状态管理、确定性路由策略、知识图谱设备匹配到Human-in-the-Loop审批与错误恢复。核心设计原则可以归纳为三条:结构化输出作为Agent间契约、确定性规则兜底LLM不确定性、状态图优于链式调用

完整项目代码将在整理脱敏后开源,架构设计演进过程可以在知乎专栏找到。如果你也在用LangGraph做多Agent系统,欢迎交流。


标签:#LangGraph #MultiAgent #FastAPI #AI工程化 #Python #知识图谱

相关推荐
码云骑士8 小时前
70-多Agent协作-CrewAI-AutoGen-角色分工与信息传递协议
python
Hachi被抢先注册了8 小时前
Skills总结
python
用户0332126663679 小时前
使用 Python 在 PowerPoint 中创建折线图和条形图
python
benchmark_cc9 小时前
如何用 Python 进行多周期 K 线合成与时区对齐?基于 QuantDash 与 Pandas 的量化数据清洗实战(附 GitHub 源码)
开发语言·python·github·盯盘·pandas·quantdash·量化数据
不如语冰9 小时前
AI大模型入门-参数的传递
数据结构·人工智能·pytorch·python
用户6760840656179 小时前
GraphBLAS_01_图的稀疏表示
python
小陈工9 小时前
第7篇:Django框架核心原理与实战深度解析(下)
后端·python·面试
用户298698530149 小时前
Python 数据处理:XML 与 Excel 互转的实用指南
后端·python·excel
m0_617493949 小时前
PyCharm 新手避坑指南:一文解决“项目列表消失”与“模块导入报错”两大玄学问题
ide·python·pycharm