Skip to content

隐式编排与显式编排

编排的两种范式

当一个任务需要多个步骤才能完成,这些步骤的执行顺序、依赖关系和数据传递就是编排问题。在 LLM 应用中,编排有两种范式。

显式编排通过外部的控制结构定义执行流程。DAG(有向无环图)、状态机、流程引擎——开发者在代码中显式声明"先做 A,再做 B,如果 B 失败则做 C"。执行流程对开发者完全透明,可以被可视化、被测试、被版本控制。

隐式编排通过数据结构本身驱动执行流程。第四章讨论的 Pydantic 模型就是一个例子:字段的顺序定义了推理的顺序,嵌套结构定义了推理的层级。开发者不声明"先分析主题,再提取观点"——结构定义里天然带着这个顺序。

两种范式的核心区别在于控制权在谁手里。显式编排把控制权交给开发者编写的编排代码;隐式编排把控制权交给数据结构的定义和 LLM 的生成过程。

隐式编排被低估了

LLM 应用领域对编排的讨论几乎被显式编排垄断——LangChain 的 Chain、LlamaIndex 的 Pipeline、各种 DAG 执行框架。但在相当多的场景中,隐式编排是更简洁、更可靠的选择。

考虑一个文档分析任务:提取摘要、识别关键词、评估质量、给出建议。

显式编排的做法:

python
# 四次独立的 LLM 调用,显式串联
summary = call_llm("提取摘要", document)
keywords = call_llm("识别关键词", document)
quality = call_llm("评估质量", document, summary, keywords)
suggestion = call_llm("给出建议", quality, summary)

隐式编排的做法:

python
class DocumentAnalysis(BaseModel):
    summary: str = Field(description="文档核心内容的三句话摘要")
    keywords: list[str] = Field(description="5个最重要的关键词")
    quality_score: float = Field(description="内容质量评分,0-1", ge=0, le=1)
    suggestion: str = Field(description="基于以上分析给出的改进建议")

result = call_llm_structured(document, DocumentAnalysis)

隐式编排用一次 LLM 调用替代了四次,字段顺序隐式地定义了推理链。这不仅减少了 API 调用次数和延迟,还避免了显式编排中的一个隐蔽问题:中间结果的信息损失。当 summary 被序列化为字符串传递给下一个调用时,LLM 对原始文档的"理解"被压缩了;而在一次结构化输出中,LLM 可以在生成 suggestion 时同时看到原始文档和前面生成的所有字段。

显式编排的适用条件

隐式编排的前提是:所有步骤可以在一次 LLM 调用内完成。当以下条件出现时,显式编排成为必要:

步骤涉及外部系统调用。 如果推理链的中间步骤需要查询数据库、调用 API 或执行代码,这些操作无法在 LLM 的单次生成中完成——必须显式地在步骤之间插入外部调用。

步骤的输入依赖运行时结果。 如果步骤 B 的 prompt 需要根据步骤 A 的输出动态构建(不仅仅是数据填充,而是逻辑分支),隐式编排做不到这种动态分支。

单次调用的上下文无法承载所有信息。 当任务涉及大量文档、多轮工具调用结果或复杂的中间状态时,一次调用的上下文窗口可能不够。显式编排允许在步骤之间管理和压缩上下文。

需要人工审核中间结果。 某些步骤的输出需要人工确认后才能继续。这种 human-in-the-loop 的需求只能通过显式编排实现。

简单的显式编排

当需要显式编排时,最简单的实现往往是最好的。

python
async def analyze_and_act(user_request: str) -> str:
    # 步骤1:理解意图
    intent = await classify_intent(user_request)

    # 步骤2:根据意图执行不同的处理路径
    if intent.action == "search":
        results = await search_database(intent.query)
        return await summarize_results(results, user_request)
    elif intent.action == "calculate":
        data = await fetch_data(intent.parameters)
        computed = perform_calculation(data)  # 确定性计算
        return await format_response(computed, user_request)
    else:
        return await direct_response(user_request)

这是纯 Python 的控制流——if/else、函数调用、await。不需要 DAG 框架、不需要流程引擎、不需要特殊的编排 DSL。Python 本身就是编排语言。

这种"无框架编排"的优势在于:

  • 完全透明:执行路径就是代码路径,IDE 可以跳转、调试器可以断点。
  • 完全灵活:任何 Python 能表达的逻辑都可以用于编排——条件分支、循环、异常处理、并发。
  • 无额外依赖:不引入框架意味着不引入框架的抽象泄漏、版本兼容性问题和学习成本。

何时才需要编排框架

编排框架(如 Prefect、Temporal、Airflow)在以下条件同时满足时才有价值:

  1. 工作流步骤数量多(> 10 步),手动管理依赖关系容易出错。
  2. 需要持久化工作流状态,支持中断后恢复。
  3. 需要分布式执行,步骤在不同的机器上运行。
  4. 需要可视化的工作流监控和管理界面。

注意这四个条件的交集——大多数 LLM 应用不满足其中的多数条件。一个 5 步的 RAG pipeline、一个 3 轮的 Agent 循环、一个线性的文档处理流程——这些用普通的 Python 函数和 async/await 就能编排得清清楚楚。

引入编排框架的决策应该遵循第二章的原则:战略决策先于战术决策。先确认工作流是否真的复杂到需要框架,再选具体用哪个。多数情况下,答案是不需要。