如果把 LLM Agent 比作「会干活的员工」,编排系统就是「派活的中台」——它决定谁先干、谁并行、干砸了怎么办、挂了怎么续。 编排框架层(LangGraph / AutoGen / CrewAI / 自研编排器)有自己的正确性契约:调度、重试、状态持久化、并发隔离。这一层与模型无关,可以像测分布式系统一样测——这正是本篇文章的视角。
一、编排层测试与 Agent 行为测试的分工
1.1 两层测试的边界
| 层 | 关注点 | 测试对象 | 可测性 |
|---|---|---|---|
| Agent 行为层 | 模型规划、工具选择、输出质量 | Agent 本身 | 概率性、用指标 |
| 编排框架层 | 生命周期、调度、重试、状态、并发 | 编排器/运行时 | 确定性、用断言 |
一句话:编排层测试把 Agent 当成「有契约的黑盒」,只验证编排器对 Agent 的调度、驱动与恢复是否正确——用 fake Agent 驱动编排器,把不确定性隔离在测试之外。
1.2 编排器的核心职责(即可测契约)
编排器的职责:
① 生命周期:pending → running → succeeded / failed / cancelled
② 调度:任务依赖顺序、并行任务、超时
③ 重试:失败重试、退避、最大次数、死信
④ 状态持久化:每步快照、断点恢复
⑤ 并发隔离:多 Agent 并行不串状态
⑥ 可观测:步级日志、指标、结果回溯
二、编排器生命周期测试
2.1 生命周期状态机测试
编排器的核心是生命周期状态机,用状态机测试方法覆盖全部迁移:
状态:PENDING → RUNNING → SUCCEEDED / FAILED / CANCELLED / TIMED_OUT
迁移矩阵(表驱动):
- 正常完成:RUNNING → SUCCEEDED
- Agent 抛错:RUNNING → FAILED(或进入重试)
- 取消:RUNNING/PENDING → CANCELLED
- 超时:RUNNING → TIMED_OUT
- 非法迁移:SUCCEEDED 后再重试 → 拒绝
# 伪代码:生命周期表驱动测试
@pytest.mark.parametrize("from_state,event,expect", [
("running", "agent_succeeded", "succeeded"),
("running", "agent_failed", "failed"),
("running", "timeout", "timed_out"),
("running", "cancel", "cancelled"),
("pending", "cancel", "cancelled"),
("succeeded", "retry", "illegal"), # 终态不可再动
("cancelled", "agent_succeeded", "illegal"),
])
def test_lifecycle_transition(from_state, event, expect):
orc = Orchestrator(start_state=from_state)
outcome = orc.handle(event)
assert outcome == expect
2.2 非法操作防护
防误用测试:
- 重复启动同一任务 → 拒绝(幂等启动)
- 对已结束任务再注入事件 → 忽略
- 并发修改状态 → 加锁/乐观锁拒绝
三、调度与依赖测试
3.1 依赖与并行调度
调度语义测试:
- 串行依赖:A 完成后才启动 B
- 并行就绪:无依赖的任务同时启动
- 条件分支:满足条件才走分支节点
- 汇合(Join):多个前置完成后才继续
- 失败传播:前置失败时下游按配置跳过/告警/也失败
# 伪代码:DAG 调度测试
def test_parallel_after_dependency():
graph = dag({"A": [], "B": ["A"], "C": ["A"]}) # B、C 依赖 A
events = run_orchestrator(graph, fake_agents={...})
# A 启动后 B、C 才可启动
assert events.order.index("A") < events.order.index("B")
assert events.order.index("A") < events.order.index("C")
# B、C 可并行(在 A 完成后都立即运行)
assert "B" in events.parallel_with("C")
3.2 并发与调度公平
并发控制测试:
- 最大并行度:并发 Agent 数不超过配置上限
- 排队:超出并发上限的任务正确排队
- 优先级:高优先级任务插队
- 饥饿:低优先级不永久饿死
四、重试、幂等与超时
4.1 重试语义测试
重试测试场景:
- 瞬时失败 → 按退避重试 → 成功
- 达到最大次数 → 标记失败/死信
- 退避是否指数递增、是否带 jitter(防风暴)
- 幂等重试:重试不产生重复副作用
# 伪代码:重试与退避测试
def test_retry_then_success():
agent = FlakyAgent(fail_first=2) # 前两次失败
orc = Orchestrator(retry={"max": 5, "backoff": [1, 2, 4]})
result = orc.run(agent, task)
assert result.succeeded
assert agent.invocations == 3 # 失败 2 次 + 成功 1 次
assert delays == [1, 2] # 退避正确递增
def test_max_retry_deadletter():
agent = AlwaysFailAgent()
orc = Orchestrator(retry={"max": 3})
result = orc.run(agent, task)
assert result.status == "deadletter" # 超限进死信
4.2 幂等执行测试
编排器重试/恢复后可能重复驱动 Agent,幂等是硬要求:
幂等验证:
- 同一步被重复执行 → 副作用只发生一次
- 用「步 ID + 结果缓存」实现:已成功的步返回缓存结果
- 副作用工具(发通知/写库)在重试时不被二次调用
fake Agent 记录调用:
step_done(cache) → 第二次请求该步直接返回缓存,不重新执行
4.3 超时与终止语义
超时测试:
- 单步超时:Agent 长时间无响应 → 该步超时 → 走失败/重试
- 整体超时:总执行时长超限 → 强制终止
- 终止后清理:终止后 Agent 的进程/连接被释放
- 半途超时恢复:超时后状态机进入「可恢复」而非「僵尸」
五、状态持久化与断点恢复
编排系统最重要的可靠性特性是崩溃恢复——中间挂了不能从头跑。
5.1 快照与恢复测试
持久化设计:
每步完成后持久化「任务状态 + 中间结果」到存储
崩溃 → 重启 → 从最后一个快照恢复,续跑而非重跑
测试场景:
- 步骤中崩溃:恢复后从该步重跑(不重跑已成功的步)
- 步骤间崩溃:恢复后继续下一步
- 重复崩溃:多次恢复后仍正确(快照不损坏)
- 快照一致性:恢复后的状态与崩溃前一致
# 伪代码:断点恢复测试
def test_resume_after_crash():
orc = Orchestrator(persist=True)
state = orc.start(task)
# 在第 3 步制造崩溃
orc.crash_at(step=3)
orc2 = Orchestrator.restore(state.checkpoint) # 恢复
result = orc2.continue_run()
assert result.succeeded
assert orc2.executed_steps == [3, 4, 5] # 只续跑未完成步
assert orc2.executed_steps != [1, 2, 3, 4, 5] # 未重跑已完成步
5.2 恢复的幂等与一致性
- 快照版本:旧版本快照被新代码恢复 → 兼容或明确拒绝
- 恢复时上下文完整:Agent 的对话上下文/中间结果不丢
- 并发恢复:同一任务被两实例恢复 → 只有一个继续(锁)
六、多 Agent 并行的隔离与一致
6.1 状态隔离测试
并行多 Agent 最怕状态串扰——A 的上下文污染 B:
# 伪代码:并行隔离测试
def test_parallel_agents_isolated():
agent_a = RecordingAgent()
agent_b = RecordingAgent()
run_in_parallel([(agent_a, task_x), (agent_b, task_y)])
# A 只看到 task_x 的上下文,B 只看到 task_y 的
assert task_y not in agent_a.observed_context
assert task_x not in agent_b.observed_context
隔离验证点:
- 上下文隔离:每 Agent 独立会话,不共享
- 变量/工具状态隔离:并行任务不踩同一份可变状态
- 全局资源竞争:共享资源(限流器/连接池)不互相饿死
- 失败隔离:一个 Agent 失败不影响其他并行 Agent
6.2 多 Agent 结果一致
编排层需要保证「并行结果汇总的一致」:
- 汇总顺序:结果按任务 ID 而非完成顺序汇总(确定性)
- 重复结果去重:同一结果不重复计入
- 汇总完整性:所有成功 Agent 的结果都被收集
七、编排可观测性与回滚
7.1 步级可观测性测试
编排器必须有可观测输出,且这些输出本身要被测试:
可观测断言:
- 步级日志:每步开始/结束/耗时/结果都有结构化日志
- 指标:成功/失败/重试次数、执行时长分布、并发度
- 追踪:一次编排的完整执行链可回溯(trace_id)
- 结果回溯:给定编排 ID 可查询每一步的输入输出
7.2 回滚与补偿
编排失败后是否需要回滚已完成步的副作用:
补偿/回滚测试:
- 编排失败 → 按逆序执行补偿动作(类似 Saga)
- 补偿幂等:补偿动作可重复执行不重复影响
- 补偿失败 → 升级人工(半补偿状态可标识)
- 只读步不补偿:只有副作用步才需要补偿
八、常见陷阱
| 陷阱 | 现象 | 规避 |
|---|---|---|
| 用真实 Agent 测编排 | 不确定性和编排 bug 混一起 | fake Agent 隔离 |
| 不测生命周期非法迁移 | 状态机错乱 | 表驱动迁移矩阵 |
| 不测崩溃恢复 | 一挂就从头跑 | 快照 + 断点续跑测试 |
| 重试不幂等 | 重复副作用 | 步 ID + 结果缓存 |
| 并行不隔离 | 上下文串扰 | 并行隔离测试 |
| 无超时 | Agent 挂起烧钱 | 单步/整体双重超时 |
九、总结
编排框架层测试用确定性断言守护编排器的全部可靠性契约:生命周期状态机用迁移矩阵表驱动覆盖、调度与依赖用 DAG 场景验证串并行语义、重试用「失败次数 + 退避序列」断言、幂等用「步结果缓存」验证重试与恢复不重复副作用、崩溃恢复用「快照 + 断点续跑」测试保证只续未完成步、并行用隔离测试守护多 Agent 不串扰、可观测与回滚用步级日志与补偿语义兜底。关键手法是用 fake Agent 把不确定性隔离在测试之外——编排器与模型无关,就可以像测分布式系统一样,把每一条契约都变成稳定的断言。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。