智能体面试准备(九):工作流编排——DAG 与状态机两种范式的取舍与手写实现

智能体面试准备(九):工作流编排------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 还是状态机还是干脆裸写」,给一个三问定位法:

  1. 流程结构是否预先确定? 完全确定且无循环 → DAG(离线管道、报表生成);运行时才能决定走向 → 状态机。
  2. 是否需要人机协同或长时间挂起? 需要(审批、人工复核)→ 必须选带持久化的状态机方案,state 要能落库并在几小时后恢复。
  3. 循环由谁驱动? 由代码规则驱动(测试失败就重试)→ 显式状态机最可控;完全由 LLM 自主决定下一步 → 那是 ReAct 式自治 Agent,编排器退化为带护栏的循环,此时重点转向 B11 要讲的评估与可观测。

一个值得主动抛出的观点:2025 年之后的工程共识是「能用 workflow 就不用 agent」------确定性流程用显式编排跑,只把真正需要开放决策的环节交给 LLM 自主循环。这句出自 Anthropic《Building effective agents》的原则在面试里非常好用,既表明你读过一手材料,也表明你有成本与可控性意识。

下一篇(B10)把编排能力和检索结合起来:Agentic RAG------让 Agent 自己决定查什么、查几轮、什么时候停。

相关推荐
Revolution611 小时前
reactive 对象重新赋值后,表单为什么没有恢复默认值
前端·vue.js·面试
HeiSenBerg1 小时前
Android Handler 机制完全解析:从源码到内存泄漏,一篇就够了
面试
only-qi3 小时前
大模型Agent面试攻略:落地工程痛点、评估体系与Agentic RAG核心精讲
人工智能·算法·面试·职场和发展·langchain·rag
MomentYY3 小时前
RAG 混合检索:关键词 + 语义
人工智能·agent·ai编程
ShineWinsu4 小时前
对于Linux:传输层协议UDP原理的解析
linux·c++·面试·udp·协议·传输层·计算机系统
GitLqr4 小时前
StatefulWidget 里的隐形炸弹:为什么不要在 State 类中使用 context.mounted
flutter·面试·dart
菜小麒4 小时前
后端/Agent后端面经-后续
agent·后台
殷紫川4 小时前
Agent Skills:把团队里"只会做一遍"的经验,变成 Agent 能反复调用的能力包
人工智能·agent