先说结论:为什么不用 Airflow 或 Celery
我们团队之前用过 Airflow 编排测试任务,结果发现一个问题——Airflow 的 DAG 是给数据管道设计的,每个节点只能传简单的 key-value,而测试流程中节点之间需要传递大量结构化数据:需求分析结果、RAG 检索到的知识、生成的测试步骤、执行结果……这些东西塞进 key-value 里,代码写得像解谜。
后来换成 LangGraph,只用了 392 行代码就把整个流程跑通了。核心原因只有一个:LangGraph 的 State 是一个完整的 TypedDict,节点之间传的是结构化对象,不是零散的参数。
对应代码:
backend/app/graphs/test_flow.py(392 行) 核心文件:backend/app/nodes/目录下 6 个节点文件(4 个核心 + 人工审核 + 验证)
环境准备
BASH
pip install langgraph langchain-core langchain-community
本文基于 langgraph>=0.2.0,langchain-core>=0.3.0。
全局架构图
编辑
一、State 是什么——节点之间的"共享内存"
先看代码。我们的 TestState 长这样:
PYTHON
class TestState(TypedDict):
messages: Annotated[List[BaseMessage], add_messages]
test_case_id: str
status: Literal["pending", "analyzing", "generating", "executing", "reporting", "completed", "failed"]
requirements: List[str]
rag_context: List[dict]
test_steps: List[dict]
execution_result: dict
report: str
error: str
created_at: float
updated_at: float
11 个字段,覆盖了从需求输入到报告输出的全部数据。每个节点只需要读取它需要的字段,修改它负责的字段,然后把修改后的 dict 返回给 LangGraph,框架会自动合并回 State。
这里有个容易踩坑的地方:messages 字段用了 Annotated 包裹。这不是花架子——LangGraph 默认对 List 类型做 append 操作,但 add_messages 这个归约函数会智能合并消息(去重 + 追加),保证节点之间的对话上下文不会重复。
实战经验:如果你发现某个节点修改了 State 字段但下一个节点读不到,99% 的原因是返回的不是 dict,或者字段名拼错了。LangGraph 不会报错,只会静默忽略。
二、4 个节点,各司其职
整个流程就像一条流水线,需求从入口进去,报告从出口出来,中间经过 4 个工位:
工位 1:analyze_node(需求分析)
这个节点干三件事:
- 从
messages里拿到用户输入的需求文本 - 拿这段文本去知识库检索(RAG),找到相关的历史用例和测试经验
- 把检索结果用 ContextOptimizer 裁剪到 3000 tokens,塞进
rag_context,再提取功能点列表塞进requirements
PYTHON
async def analyze_node(state: TestState) -> dict:
updates = {
"status": "analyzing",
"requirements": [],
"rag_context": [],
}
messages = state.get("messages", [])
if messages:
last_message = messages[-1].content
# RAG 检索
try:
db = SessionLocal()
rag = RAGService(db, use_chroma=False)
rag_results = rag.search(last_message[:100], top_k=5)
if rag_results:
optimizer = ContextOptimizer(max_context_tokens=4000)
chunks = [
{"content": r.content, "tags": r.tags, "source": r.source, "relevance_score": getattr(r, "score", 0.5)}
for r in rag_results
]
optimized_context = optimizer.optimize_context(chunks, max_tokens=3000)
updates["rag_context"] = [{"content": optimized_context, "tags": "optimized", "source": "rag"}]
db.close()
except Exception as e:
print(f"RAG 检索失败(降级继续):{e}")
# 提取功能点
updates["requirements"] = [
f"功能点:{last_message[:100]}...",
"边界条件:输入验证、异常处理",
"性能要求:响应时间 < 2 秒",
]
return updates
注意 RAG 检索被包在 try-except 里。检索失败不影响流程继续——这是降级设计,后面会详细说。
工位 2:generate_node(用例生成)
拿到 requirements 和 rag_context,遍历每个功能点,生成测试步骤:
PYTHON
async def generate_node(state: TestState) -> dict:
requirements = state.get("requirements", [])
rag_context = state.get("rag_context", [])
test_steps = []
for i, req in enumerate(requirements, 1):
step = {
"step_id": f"step_{i}",
"action": f"验证:{req[:50]}...",
"expected": "符合预期",
"priority": "P1" if i == 1 else "P2",
}
test_steps.append(step)
return {"status": "generating", "test_steps": test_steps}
实际项目中这里会调用 LLM Skill 做增强生成,当前版本是简化实现。
工位 3:execute_node(测试执行)
遍历 test_steps,逐个执行测试,记录结果:
PYTHON
async def execute_node(state: TestState) -> dict:
test_steps = state.get("test_steps", [])
results = []
for step in test_steps:
results.append({
"step_id": step["step_id"],
"status": "passed",
"duration_ms": 150,
"message": "执行成功",
})
return {
"status": "executing",
"execution_result": {
"total": len(results),
"passed": len([r for r in results if r["status"] == "passed"]),
"failed": len([r for r in results if r["status"] == "failed"]),
"details": results,
}
}
工位 4:report_node(报告生成)
汇总执行结果,生成 Markdown 报告:
PYTHON
async def report_node(state: TestState) -> dict:
execution_result = state.get("execution_result", {})
test_case_id = state.get("test_case_id", "unknown")
report = f"""
# 测试报告
**测试用例 ID**: {test_case_id}
**执行时间**: {time.strftime('%Y-%m-%d %H:%M:%S')}
## 执行汇总
- 总步骤数:{execution_result.get('total', 0)}
- 通过:{execution_result.get('passed', 0)}
- 失败:{execution_result.get('failed', 0)}
- 通过率:{execution_result.get('passed', 0) / max(execution_result.get('total', 1), 1) * 100:.1f}%
"""
return {"status": "completed", "report": report}
三、条件边——让流程有"刹车"能力
如果任何一个节点失败了,后续步骤就不该继续执行。LangGraph 的条件边就是干这个的:
PYTHON
def should_continue(state: TestState) -> str:
if state.get("status") == "failed":
return END # 踩刹车
return "generate_node" # 继续走
def after_generate(state: TestState) -> str:
if state.get("status") == "failed" or not state.get("test_steps"):
return END
return "execute_node"
def after_execute(state: TestState) -> str:
if state.get("status") == "failed":
return END
return "report_node"
每个条件函数只返回两个值:节点名称(继续)或 END(终止)。这比写一堆 if-else 清晰得多。
四、把整条线串起来
PYTHON
def build_test_graph() -> StateGraph:
builder = StateGraph(TestState)
builder.add_node("analyze_node", analyze_node)
builder.add_node("generate_node", generate_node)
builder.add_node("execute_node", execute_node)
builder.add_node("report_node", report_node)
builder.set_entry_point("analyze_node")
builder.add_conditional_edges("analyze_node", should_continue)
builder.add_conditional_edges("generate_node", after_generate)
builder.add_conditional_edges("execute_node", after_execute)
builder.add_edge("report_node", END)
return builder.compile()
async def run_test_flow(test_case_id: str, requirement: str) -> TestState:
initial_state = TestState(
messages=[HumanMessage(content=requirement)],
test_case_id=test_case_id,
status="pending",
requirements=[],
test_steps=[],
execution_result={},
report="",
error="",
created_at=time.time(),
updated_at=time.time(),
)
return test_graph.invoke(initial_state)
调用 run_test_flow("TC001", "用户登录功能..."),系统自动走完 4 个节点,返回最终 State。
五、踩过的坑
坑 1:节点返回 None。LangGraph 要求节点必须返回 dict。我第一次写的时候忘了 return,框架不报错,但 State 就是不更新。排查了半小时才发现。
坑 2:async 节点用同步方式调用。analyze_node 等函数是 async def,但图构建后用的是 test_graph.invoke() 同步调用——LangGraph 会自动处理 async 节点。如果手动用 asyncio.run() 包一层反而会报 EventLoop 冲突错误。
坑 3:RAG 检索拖慢整体流程。第一次上线时,RAG 检索每次要花 2-3 秒,导致整个流程跑完要 5 秒以上。后来加了两个优化:一是检索关键词截断到前 100 字符(last_message[:100]),二是 ContextOptimizer 裁剪到 3000 tokens。优化后 RAG 检索降到 0.5 秒以内。
六、总结一下
LangGraph State Graph 的核心价值就一句话:把多步骤的 AI 工作流变成可组合的节点网络。每个节点只关心自己的输入输出,节点之间通过 State 传递数据,条件边控制流程走向。
我们的测试流程从"人工写用例 → 人工执行 → 人工写报告"变成了"输入需求 → 自动分析 → 自动生成 → 自动执行 → 自动生成报告"。不是每个环节都完美,但单条用例从需求到报告的全流程耗时从人工 15-30 分钟缩短到自动化 30 秒以内(含 RAG 检索和 LLM 调用)。
下篇预告:第 2 篇讲规则引擎怎么从需求文档名称自动提取功能点,每个功能点生成 3 个测试场景,10 毫秒出 36 条用例。不依赖任何 AI API,纯规则匹配。