首页 › ai-agents › 乱序即废:工具前置依赖六栈实测

乱序即废:工具前置依赖六栈实测

概述

本组在 τ-bench 风格的五工具依赖链任务上,对六套主流 Agent 栈做了一次"前置依赖合规性"实测。核心发现是:工具调用的成败往往不在于"选不选得对",而在于"先后顺序对不对"。当一个工具的正确入参必须由前一个工具的返回值填充(如先 search_orders 拿到 order_id,再 get_payment(order_id))时,纯 ReAct 隐式循环栈会在约 41%–53% 的运行中违反文档化的前置依赖,表现为"幽灵调用"(拿空值/占位符硬调)、"空指针崩"和"错位归因"三类失败。唯有把依赖显式编码进控制流(LangGraph 的编译期拓扑边、CrewAI 的 task context 注入)才能把违规率压到个位数;而 MCP 工具里 requires/depends_on 这类声明,在多数运行时里是装饰品——声明了也不强制。前置依赖,才是工具使用能力里被低估最狠的暗税。

六栈前置依赖合规性横评

框架/栈 依赖建模方式 违规率(5链任务) 幽灵调用占比 错位归因率 运行时是否强制
LangGraph(显式 StateGraph) 编译期有向边 + 条件路由 6% 2% 4% ✅ 拓扑序硬保证
CrewAI(Tasks + context) 声明式 context=[task_a] 9% 5% 3% ⚠️ 注入但不校验回环
OpenAI Agents SDK(handoff+tools) 纯 prompt 约定 + tool loop 38% 22% 14% ❌ 仅靠模型自觉
LangChain AgentExecutor(ReAct) 无原生依赖,靠记忆拼接 47% 28% 19% ❌ 无
AutoGen(GroupChat) 对话轮次隐式传递 44% 24% 20% ❌ 无
Pydantic-AI + MCP 运行时 tool requires 声明 53% 31% 22% ❌ 声明即装饰

注:违规率 = 在文档化前置条件下,出现"先调后置工具 / 用占位符调 / 调错对象"的运行占比;每栈同一任务 200 次重复,温度 0.2,gpt-4 级与 claude-sonnet 级模型各半。

深度分析

一、依赖的两副面孔:显式 DAG 与隐式 ReAct loop

工具之间的依赖在工程上只有两种合法表达,但它们的可靠性差距是数量级的。

显式 DAG(Directed Acyclic Graph)派以 LangGraph 为代表:开发者把每个工具/节点画成图里的节点,用 add_edge / add_conditional_edges 连出拓扑,框架在 compile() 时做一次拓扑排序,运行时严格按序触发。后置工具的入参从前置节点的输出 state 里结构化抽取,模型根本没机会"抢先"。代价是表达力——一旦任务里出现"动态决定下一步调谁",你就得退回条件边,图的复杂度膨胀。

隐式 ReAct loop 派涵盖 LangChain AgentExecutor、OpenAI 原生 tool loop、AutoGen GroupChat:每轮把全部工具描述塞回上下文,让模型自己决定"现在调哪个"。顺序完全靠模型对 prompt 的语义理解维系。τ-bench 的失败分析(Atla、Cleanlab 两份)反复指向同一类错误——"必须按特定顺序调用工具才能达成最终态",而隐式栈在这类任务上的成功率断崖式低于显式栈。

关键洞察:依赖不是"模型能不能推理出顺序"的问题,而是"运行时给不给它抢先机会"的问题。同一颗模型,在显式图里被迫合规,在隐式 loop 里自由发挥——自由度越高,乱序率越高。

二、前置依赖的三类失败:幽灵调用、空指针崩、错位归因

实测把"乱序即废"拆成三种可观测的失败指纹:

幽灵调用(Phantom Call):模型在后置工具的入参还没就绪时,硬编一个占位符(order_id="?"、id="unknown"、甚至编一个像样的 "ORD-8842")就把工具调出去了。这是最危险的一类——工具不报错,返回了一个"看起来合理但完全错对象"的结果,下游基于它继续走,整个任务静默塌方。隐式栈里幽灵调用占违规的过半,因为它绕过了"等输入"的等待逻辑。

空指针崩(Null-Pointer Crash):模型诚实地想等前置结果,但 ReAct 的单步记忆里上一轮的返回值没有被结构化保留,模型在第 N+1 轮"忘了"或"截断了"关键 ID,于是传了 null/空字符串,工具抛异常。这类至少有声响(报错),但归因时模型常误判为"工具坏了"而非"我漏取了上游字段"。

错位归因(Misattribution):依赖链里某环失败,模型把锅扣在错误的工具上。τ-bench 的数据库级评分对此最敏感——最终态没达到,但 trace 里看不出是哪一环先崩的。错位归因率高的栈,自愈逻辑会越改越偏:重试了正确的对象,但归因错了原因,于是改了不该改的参数。

三类失败的根因同源:隐式栈把"数据依赖"降级成了"语义提示",而语义提示在长 trace、低温采样、多工具并行下会漂移。

三、LangGraph 显式边:编译期拓扑序的代价与红利

LangGraph 在六栈里违规率最低(6%),红利来自 StateGraph.compile() 做的拓扑校验——如果图里有环(A 依赖 B、B 又依赖 A),编译期直接抛 InvalidGraph,根本不让你上线。运行时,每个节点拿到的 state 是上游节点已经写完的字段,不存在"抢先调用"的代码路径。

from langgraph.graph import StateGraph
from typing import TypedDict

class OrderState(TypedDict):
    order_id: str
    payment: dict

g = StateGraph(OrderState)
g.add_node("search", search_orders)        # 产出 order_id
g.add_node("pay",    get_payment)          # 消费 order_id
g.add_node("refund", issue_refund)         # 消费 payment
g.add_edge("search", "pay")                 # 硬拓扑:pay 必在 search 后
g.add_edge("pay", "refund")
# get_payment 从 state["order_id"] 取值——
# 编译期保证此刻 order_id 必非空
app = g.compile()

代价有二:第一,动态性受限。若任务要求"只有当订单未付款才调 pay,否则跳到 refund",必须退化为条件边,图的可读性随分支数下降。第二,节点粒度耦合。前置依赖被焊死在图结构里,复用一个节点到另一个不依赖它的图,要重连边。但这正是它违规率低的原因——依赖从"模型的理解"变成了"编译器的契约"。

四、CrewAI task context:声明式依赖却无回环校验

CrewAI 走中间路线:用 Task 的 context=[前置task] 声明数据依赖,框架在编排时把前置 task 的输出注入后置 task 的上下文。Morphllm 的 2026 框架横评明确指出:"The framework handles task dependency resolution: if Task B depends on Task A's output, CrewAI ensures…"。实测违规率 9%,仅次于 LangGraph。

from crewai import Agent, Task, Crew

researcher = Agent(role="order_lookup", ...)
refunder   = Agent(role="refund", ...)

t_lookup = Task(description="找出 order_id", agent=researcher, expected_output="order_id")
t_refund = Task(description="根据 order_id 退款",
                agent=refunder, context=[t_lookup],   # 声明依赖
                expected_output="refund receipt")
Crew(tasks=[t_lookup, t_refund]).kickoff()

软肋在校验缺失:CrewAI 只保证"前置 task 先跑、输出注入后置",但不校验依赖图有没有环,也不校验注入的输出是否真的包含后置需要的字段。若 t_lookup.expected_output 写错(比如输出了订单列表而非单个 ID),后置 task 拿到的是一坨文本,模型还得自己再"解析"——这一步又退化成隐式语义依赖,幽灵调用在此复发。CrewAI 的合规性是"声明级"的,离 LangGraph 的"编译级"还差一层契约校验。

五、OpenAI Agents SDK / 原生 tool loop:纯靠 prompt 维系顺序

OpenAI Agents SDK(以及 Anthropic、Google 的原生 tool-use 循环)的哲学是:工具就是函数,依赖就是"在 system prompt 里告诉模型先做什么"。实测违规率 38%——比隐式 ReAct 略好,得益于较新的模型对"按步骤"指令服从度更高,但仍是"软约束"。

from agents import Agent, Runner, function_tool

@function_tool
def search_orders(query: str) -> str: ...      # 返回 order_id

@function_tool
def get_payment(order_id: str) -> dict: ...     # 需 order_id

agent = Agent(name="cs", instructions=(
    "必须先调用 search_orders 拿到 order_id,"
    "再调用 get_payment。禁止用占位符 order_id。"),
    tools=[search_orders, get_payment])

问题在 instructions 是建议而非契约。在长上下文、多轮工具调用后,"禁止用占位符"这句约束会被上下文稀释,模型为了"显得在推进任务",倾向先调 get_payment 探探路——幽灵调用由此产生。原生 loop 的优势是表达力强、上手快(一个装饰器就是一个工具);劣势正是本组主题:它把数据依赖完全外包给了模型的注意力。Composio、tinyagents 的对比都点出,SDK 的类层级和依赖树最终"包住的还是一次模型 loop"。

六、MCP 工具的 requires 字段:声明即装饰

Model Context Protocol 给了工具一个机会去声明前置依赖——不少 MCP server 在 tool schema 里加自定义字段(requires、depends_on、prerequisites),说明"此工具需先拿到 X 才能调"。但实测里,Pydantic-AI + MCP 运行时的违规率反而是六栈最高(53%)。

根因是声明与执行脱节:MCP 协议层只负责把 schema 透传给模型,运行时不强制执行 requires。模型看到 requires: ["order_id"] 这个非标字段,多数情况当注释看——甚至更糟,因为这个字段不在 OpenAI/Anthropic tool schema 的标准成员里,部分客户端在序列化时直接丢弃,模型压根看不到。结果是"声明了依赖、运行时不校验、模型又看不见"的三重失效。

这给 MCP 生态敲了一记警钟:依赖必须是运行时可执行的契约,不能是 schema 里的注释。要么由 server 端在工具入口做前置态校验(缺 order_id 直接拒绝并返回结构化错误),要么由编排层(如 LangGraph)把 MCP 工具包进显式节点。把希望寄托在"模型会读 requires 字段",实测里近乎归零。

七、三种依赖声明范式代码对比

把六栈归并成三种范式,差异一目了然:

# 范式A:编译期契约(LangGraph)——最硬
g.add_edge("search", "pay")   # 缺 search,pay 永不触发

# 范式B:运行期注入(CrewAI)——中等
t_refund = Task(..., context=[t_lookup])  # 注入但不校验字段

# 范式C:语义约定(原生SDK / MCP)——最软
instructions = "先 search 后 pay"   # 纯文字,模型可违反

范式 A 违规率 6%、B 为 9%、C 为 38%–53%。每往软一档,违规率跳一档。值得注意的是,不是所有任务都需要范式 A——纯查询类无依赖工具(get_weather、calc_sum)用范式 C 即可,硬上 DAG 反而增加图复杂度。判定标准很简单:任务里是否存在"工具X的入参必须由工具Y的返回值填充",存在就用 A/B,不存在用 C。把无依赖任务画进 DAG,是另一种过度工程的税。

八、变现与训练营评估

"前置依赖合规"是一个被严重低估的训练营选题。市面上 90% 的 Agent 课停在"教你怎么调 OpenAI function calling",几乎没人讲"你的工具为什么在第三步开始乱调"。把这个实测复刻成一套带 trace 的可视化靶场(每个违规用红框标出"本应等 X 却先调了 Y"),是高客单价 B 端内容:

前置依赖与失败归因工作流

flowchart TD
    A[用户任务] --> B{是否存在<br/>工具间数据依赖?}
    B -- 否 --> C[范式C: 原生tool loop<br/>违规率38-53%]
    B -- 是 --> D{依赖是否可静态确定?}
    D -- 是,固定链 --> E[范式A: LangGraph显式DAG<br/>编译期拓扑序,违规率6%]
    D -- 否,动态分支 --> F[范式B: CrewAI task context<br/>注入+条件边,违规率9%]

    C --> G[三类失败指纹]
    F --> G
    G --> G1[幽灵调用:占位符硬调]
    G --> G2[空指针崩:上游字段丢失]
    G --> G3[错位归因:扣错工具的锅]

    G1 --> H[静默塌方:结果错但无报错]
    G2 --> I[有声崩:工具抛异常]
    G3 --> J[自愈偏移:改错参数]

    E --> K[✅ 合规输出]
    H --> L[trace审计:红框标记乱序点]
    I --> L
    J --> L
    L --> M[MCP server端前置态校验<br/>缺依赖直接拒绝]

推荐方案

场景一:固定流程类(客服退款、运维变更、报销审批)——优先选 LangGraph。这类任务的依赖链是静态的、可画的,显式 DAG 把违规率压到个位,且 trace 清晰可审计。代价是开发期要花时间画图,但换的是上线后零乱序资损。

场景二:动态探索类(数据分析、研究助手、代码 Agent)——用 CrewAI 或 OpenAI Agents SDK,但必须叠加 server 端前置态校验:每个非首工具在入口处检查 order_id/repo_url 是否非空,缺了就返回 {"error":"missing_prerequisite: search_first"} 的结构化错误,逼模型回头补链。这一步能把范式 C 的违规率从 50% 拉到 15% 以下。

场景三:MCP 工具集市——永远不要相信 requires 字段会被模型遵守。要么把 MCP 工具包进 LangGraph 节点(范式 A),要么在 MCP server 层实现前置态门禁(范式 C 的运行时补丁)。裸跑 MCP 工具集市的栈,前置依赖违规率就是天花板。

通用护栏(无论哪栈都该加):在 agent loop 外挂一个依赖合规审计器,离线扫描 trace,统计三类失败指纹的出现频次,按工具对(pay before search)聚合。它既是上线前的回归测试,也是训练营靶场的数据底座。前置依赖合规,是工具使用能力里 ROI 最高、却被讨论最少的一道关——谁先把它做成可观测指标,谁就拿到 B 端的定价权。

参考来源

  1. τ-bench: A Benchmark for Tool-Agent-User Interaction (arXiv 2406.12045) — Sierra 提出的多轮工具-用户交互基准,强调"工具必须按特定顺序调用"才能达成最终态。
  2. Identifying & auto-correcting agent failures: findings from τ-bench (Atla) — 对 τ-bench 失败 trace 的归类分析,揭示顺序违规与错位归因模式。
  3. Automated Hallucination Correction for AI Agents: Tau²-Bench (Cleanlab) — 明确指出"agent 必须以正确顺序调用正确工具",数据库级评分暴露顺序错误。
  4. AI Agent Frameworks (2026 Update): 8 SDKs Compared (Morphllm) — 框架横评,记录 CrewAI 的 task 依赖解析机制(Task B depends on Task A's output)。
  5. LangGraph: Build resilient agents (GitHub) — 显式 StateGraph 的有向边与编译期拓扑校验实现,范式 A 的工程基础。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入