本组在 τ-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(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 在六栈里违规率最低(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=[前置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(以及 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"。
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 端的定价权。
[!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 等场景全覆盖。
