LLM Compiler:把任务编译成依赖图
有些任务既不是简单链条,也不是完全独立的一批。
旅游助手写一份杭州报告:先查城市背景,然后基于背景推荐路线,最后综合报告。"查背景"和"推荐路线"之间有依赖,但"查天气"和"查背景"可以并行。你需要知道哪些能同时跑,哪些必须等。
LLM Compiler 让模型输出一个任务图(DAG),Python 按依赖关系调度执行。
一句话说清楚
LLM Compiler 把"顺序执行"改成"依赖 DAG + 调度器",让无依赖的子任务不用互相等待。
解决什么问题
| 问题 | 表面看起来 | 实际风险 |
|---|---|---|
| 所有任务串行跑 | 简单 | 独立任务白等 |
| 所有任务并行跑 | 快 | 依赖被忽略,下游缺上游结果 |
| 依赖关系写在自然语言里 | 灵活 | Python 无法调度和审计 |
引入什么复杂度
- 模型需要输出结构化 DAG(task id、instruction、deps 列表)。
- DAG 可能有错:循环依赖、遗漏依赖、节点太碎。
- 调度器本身有开销——如果任务只有 2-3 步且都是串行的,不如直接用 Plan & Solve。
和其他模式的关系
| 模式 | 谁决定下一步 | 什么时候用 |
|---|---|---|
| Prompt Chaining | Python 写死线性步骤 | 顺序已知 |
| ReWOO | 排一批独立工具调用 | 调用大部分独立 |
| LLM Compiler | 模型建依赖图 | 有依赖也有并行 |
| Manager-Worker | Manager 动态分配 | 任务类型在运行时才确定 |
LLM Compiler = ReWOO + 依赖图 + 并行执行。比 ReWOO 重(要处理依赖),比 Manager-Worker 静态(图在执行前确定)。
这个模式改变了什么
| 谁 | 负责什么 |
|---|---|
| Compiler 模型 | 输出任务节点、依赖关系、最终合并指令 |
| Python 调度器 | 按依赖拓扑序执行,无依赖的并行跑 |
| Worker 模型/工具 | 执行每个节点 |
| Final 步骤 | 把所有依赖结果合并成答案 |
- 谁决定下一步:拓扑排序。某节点的所有 deps 完成后才执行。
- 谁拥有状态:Python 持有 DAG 和每个节点的输出。
- 什么时候停止:所有节点执行完 + final 步骤返回。
走一遍真实轨迹
用户请求:"写一份杭州三天旅游报告,包含城市背景、天气、推荐路线。"
DAG:
t1: 查杭州城市背景 deps=[]
t2: 查杭州天气 deps=[]
t3: 推荐三天路线 deps=[t1, t2] ← 需要背景 + 天气
final: 综合报告 deps=[t1, t2, t3]
| 阶段 | 执行 | 输出 |
|---|---|---|
| 第一批(并行) | t1 查背景、t2 查天气 | "杭州,浙江省会..."、"Day1 晴..." |
| 第二批 | t3 推荐路线(用 t1+t2 结果) | "Day1 西湖...Day2 茶博物馆..." |
| final | 综合 t1+t2+t3 | 完整报告 |
t1 和 t2 可以并行,t3 必须等两者完成。这就是 DAG 调度的价值。
流程图
flowchart TD
T["用户任务"] --> C["模型生成 DAG"]
C --> A["t1: 查背景(无依赖)"]
C --> B["t2: 查天气(无依赖)"]
A --> D["t3: 推荐路线(依赖 t1, t2)"]
B --> D
D --> F["final: 综合报告"]
完整实现
from __future__ import annotations
from pathlib import Path
from agent_patterns_lab.patterns.llm_compiler import llm_compiler
from agent_patterns_lab.runtime import MockLLM, Tracer
def main() -> None:
tracer = Tracer()
model = MockLLM(
[
# 第一次调用:生成 DAG
'{"tasks":['
'{"id":"t1","instruction":"查杭州城市背景","deps":[]},'
'{"id":"t2","instruction":"查杭州未来三天天气","deps":[]},'
'{"id":"t3","instruction":"基于背景和天气推荐三天路线","deps":["t1","t2"]}'
'],"final":{"instruction":"综合所有信息写旅游报告"}}',
# t1 输出
"杭州,浙江省会,西湖闻名,丝绸之府,茶叶之都。人口约 1200 万。",
# t2 输出
"Day1 晴 28°C,Day2 多云转小雨 25°C,Day3 晴 29°C。",
# t3 输出(会用到 t1 和 t2 的结果)
(
"Day1:西湖环湖(天气好适合户外)→ 灵隐寺\n"
"Day2:中国茶叶博物馆(室内为主,防小雨)→ 河坊街\n"
"Day3:良渚博物院 → 返程"
),
# final 综合
(
"【杭州三天旅游报告】\n"
"城市简介:杭州是浙江省会,以西湖闻名。\n"
"天气:前三天以晴为主,Day2 有小雨。\n"
"推荐路线:\n"
" Day1:西湖环湖 → 灵隐寺\n"
" Day2:茶博物馆 → 河坊街\n"
" Day3:良渚博物院 → 返程\n"
"注意事项:Day2 带伞。"
),
]
)
out = llm_compiler(
model,
task="写一份杭州三天旅游报告,包含城市背景、天气、推荐路线。",
tracer=tracer,
)
print(out)
trace_path = tracer.export_jsonl(Path(".traces") / "53_llm_compiler_zh.jsonl")
print(f"\n[trace] {trace_path}")
if __name__ == "__main__":
main()
运行:
PYTHONPATH=src python examples/53_llm_compiler.py
完整运行记录
【杭州三天旅游报告】
城市简介:杭州是浙江省会,以西湖闻名。
天气:前三天以晴为主,Day2 有小雨。
推荐路线:
Day1:西湖环湖 → 灵隐寺
Day2:茶博物馆 → 河坊街
Day3:良渚博物院 → 返程
注意事项:Day2 带伞。
[trace] .traces/53_llm_compiler_zh.jsonl
trace 事件序列:
llm_compiler.dag tasks=3
llm_compiler.execute id=t1 deps=[]
llm_compiler.execute id=t2 deps=[]
llm_compiler.execute id=t3 deps=[t1, t2]
llm_compiler.final
注意 t1 和 t2 没有依赖,调度器可以并行执行。
踩坑与诊断
| 坑 | 现象 | trace 信号 | 修法 |
|---|---|---|---|
| 依赖写错 | t3 缺上游结果 | t3 执行时 deps 列表比预期短 | DAG 校验:检查所有被引用的 id 是否存在 |
| 循环依赖 | 调度器死锁 | 无节点可执行 | 构建 DAG 时检测环(拓扑排序失败则报错) |
| 节点太碎 | 10+ 节点,每个只做一句话 | 节点数远超预期 | 合并小任务 |
| final 丢信息 | 报告漏掉某个节点的内容 | final prompt 里没带某节点输出 | 确保 final 能看到所有依赖节点的结果 |
关键诊断技巧:在 trace 里看每个节点的 deps 和实际执行顺序。如果 t3 在 t1 之前执行了,说明调度器有 bug。
工程备忘
成本公式:
模型调用 = 1(编译 DAG) + N(节点) + 1(final) = N + 2
延迟 ≈ 模型延迟 × (DAG 最长路径长度 + 2)
DAG 的价值在延迟——如果 3 个节点中 2 个可并行,总延迟从 3 步降到 2 步。任务越多、并行度越高,收益越大。
生产注意事项:
- DAG 校验必须在执行前做:检查循环、检查 id 唯一性、检查 deps 引用合法。
- 当前实现是串行按拓扑序执行。生产环境改成
asyncio并行可以拿到真正的延迟收益。 - 如果任务 DAG 经常需要执行中修改(比如 t1 结果改变了 t3 的指令),LLM Compiler 不够用,考虑 PER 或 Magentic Orchestration。
- 节点数建议不超过 6。超过 6 个节点时模型生成的 DAG 质量下降明显。
读完以后
LLM Compiler 适合有明确依赖关系的多步任务。
- 如果步骤都是线性的,退回 Plan & Solve。
- 如果工具调用都独立,用更简单的 ReWOO。
- 如果需要动态委派专家,读 Manager-Worker。