本文基于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 #知识图谱