智能体面试准备(九):工作流编排------DAG 与状态机两种范式的取舍与手写实现
上一篇(B8)对比三大框架时留了个尾巴:LangGraph 为什么坚持用「图 + 状态机」而不是链式调用?这一篇把这个问题展开成 Agent 工程里的一个核心议题------工作流编排(workflow orchestration)。面试里它通常以两种面目出现:概念题「DAG 和状态机有什么区别,Agent 该用哪个」,以及手撕题「实现一个支持条件分支和重试的任务编排器」。这篇两个都覆盖。
一、为什么 Agent 需要编排:从「一条链」到「一张图」
最朴素的 LLM 应用是线性管道:检索 → 拼 prompt → 生成 → 解析输出。链式抽象(Chain)对此足够。但真实 Agent 业务很快会遇到三种链撑不住的结构:
分支:意图分类后,闲聊走小模型、专业问题走 RAG、投诉走人工升级------执行路径在运行时才确定。
循环:生成代码 → 跑测试 → 失败则带报错重新生成,直到通过或超过预算。ReAct 本质就是「思考→行动→观察」的循环,链表达不了回边。
并行与汇聚:同时查天气、查日历、查票价,三路结果汇聚后再做行程规划。串行做延迟是三倍。
一旦出现分支、循环、并行汇聚,你需要的就不再是链,而是图。工程上有两种主流建模:DAG(有向无环图) 和状态机(state machine)。
二、DAG vs 状态机:一张表说清楚
| 维度 | DAG | 状态机 |
|---|---|---|
| 数学结构 | 有向无环图,节点=任务,边=依赖 | 有向图(允许环),节点=状态,边=转移条件 |
| 能否表达循环 | 不能(无环是定义) | 天然支持,回边即循环 |
| 执行模型 | 拓扑排序,依赖就绪即可跑,天然并行 | 同一时刻只在一个状态,顺着转移走 |
| 终止性 | 天然保证(节点有限且无环) | 不保证,需要显式的最大步数/预算护栏 |
| 典型代表 | Airflow、Dagster、LLMCompiler | LangGraph、AWS Step Functions |
| 适合场景 | 批处理、数据管道、可预先规划的多任务 | 交互式 Agent、需要反思重试的流程 |
| 面试一句话 | 「结构即计划」:先画好图再执行 | 「转移即决策」:每一步动态决定去哪 |
关键结论:DAG 表达「依赖」,状态机表达「决策」。RAG 离线索引管道(抽取→切块→嵌入→入库)是纯依赖关系,用 DAG;带自我修正的 Agent(生成→评审→不合格打回重写)核心是循环决策,用状态机。LangGraph 选状态机路线,就是因为 Agent 的本质特征是循环------这直接回应了 B8 的尾巴。
进阶追问:「状态机允许环,怎么防止 Agent 死循环?」标准答案是三层护栏:最大步数硬上限、单节点重试上限、token/费用预算熔断。任何一层触发就强制转移到失败兜底状态。这是生产事故高发区,面试官非常爱问。
三、手写一个带条件路由和重试的状态机编排器
下面约 80 行纯标准库代码,实现一个最小但结构完整的编排器:节点函数读写共享状态,路由函数决定下一个节点,内置重试与最大步数护栏。用「写代码→检查→不合格返工」的场景演示循环:
python
import random
from dataclasses import dataclass, field
from typing import Callable
@dataclass
class Flow:
nodes: dict[str, Callable] = field(default_factory=dict)
routers: dict[str, Callable] = field(default_factory=dict)
max_steps: int = 20
max_retries: int = 2
def node(self, name): # 注册节点
def deco(fn):
self.nodes[name] = fn
return fn
return deco
def route(self, name): # 注册路由:返回下一节点名或 END
def deco(fn):
self.routers[name] = fn
return fn
return deco
def run(self, entry: str, state: dict) -> dict:
cur, steps = entry, 0
while cur != "END":
if steps >= self.max_steps: # 护栏1:最大步数
state["status"] = "aborted:max_steps"
break
for attempt in range(self.max_retries + 1):
try: # 护栏2:节点级重试
self.nodes[cur](state)
break
except Exception as e:
if attempt == self.max_retries:
state["status"] = f"failed:{cur}:{e}"
return state
router = self.routers.get(cur)
nxt = router(state) if router else "END"
print(f"[step {steps}] {cur} -> {nxt} | state={ {k:v for k,v in state.items() if k!='history'} }")
cur, steps = nxt, steps + 1
return state
flow = Flow()
@flow.node("write")
def write(state): # 模拟 LLM 写代码:60% 概率有 bug
state["revision"] = state.get("revision", 0) + 1
state["code"] = f"solution_v{state['revision']}"
state["has_bug"] = random.random() < 0.6
@flow.node("review")
def review(state): # 模拟评审节点
state["verdict"] = "fail" if state["has_bug"] else "pass"
@flow.node("finalize")
def finalize(state):
state["status"] = f"shipped {state['code']} after {state['revision']} revision(s)"
@flow.route("write")
def after_write(state):
return "review"
@flow.route("review")
def after_review(state): # 条件路由:不合格且未超预算则返工
if state["verdict"] == "pass":
return "finalize"
return "write" if state["revision"] < 4 else "END"
if __name__ == "__main__":
random.seed(42)
result = flow.run("write", {})
print("最终结果:", result.get("status", "gave up"))
运行会看到 write -> review -> write -> review -> finalize 之类的循环轨迹。这段代码浓缩了 LangGraph 的四个核心概念,面试时可以逐一对应:共享 state 字典对应 LangGraph 的 State Schema;@flow.node 对应 add_node;@flow.route 对应 add_conditional_edges;max_steps 对应 recursion_limit。能手写这个骨架,再聊框架就是降维打击。
生产化还差三块,口头补充即可:持久化 (每步执行后把 state 快照到数据库,支持断点续跑与人工介入,即 LangGraph 的 checkpointer);并行汇聚 (DAG 型子结构用队列+依赖计数实现,B5 的 planner 代码已演示过);幂等性(节点可能被重试,写外部系统的节点必须幂等,比如用请求 ID 去重)。
四、选型答题框架:什么时候用哪个
面试收尾常问「你们业务该选 DAG 还是状态机还是干脆裸写」,给一个三问定位法:
- 流程结构是否预先确定? 完全确定且无循环 → DAG(离线管道、报表生成);运行时才能决定走向 → 状态机。
- 是否需要人机协同或长时间挂起? 需要(审批、人工复核)→ 必须选带持久化的状态机方案,state 要能落库并在几小时后恢复。
- 循环由谁驱动? 由代码规则驱动(测试失败就重试)→ 显式状态机最可控;完全由 LLM 自主决定下一步 → 那是 ReAct 式自治 Agent,编排器退化为带护栏的循环,此时重点转向 B11 要讲的评估与可观测。
一个值得主动抛出的观点:2025 年之后的工程共识是「能用 workflow 就不用 agent」------确定性流程用显式编排跑,只把真正需要开放决策的环节交给 LLM 自主循环。这句出自 Anthropic《Building effective agents》的原则在面试里非常好用,既表明你读过一手材料,也表明你有成本与可控性意识。
下一篇(B10)把编排能力和检索结合起来:Agentic RAG------让 Agent 自己决定查什么、查几轮、什么时候停。