当 Agent 开始调用 Skill:复杂度是如何被指数放大的?
传统软件是确定性的:给定相同的输入,你总是得到相同的输出。你可以画出调用图,可以计算圈复杂度,可以识别出所有可能的执行路径。你增加一个模块,系统的复杂度增加一些。你尝试复现,但你的测试环境和生产环境的对话历史、缓存状态、外部服务状态都不同。的问题、上下文的问题、模型本身的问题,还是纯粹的随机性。而概率本身是模型的一个属性,随着模型版本、温度参数、甚至硬件的变化而变化。或者说,有经验的架构师可以通过
一、从“线性”到“指数”:Agent 系统的复杂度本质
在传统软件工程中,复杂度通常是线性增长的。你增加一个函数,系统的复杂度增加一点;你增加一个模块,系统的复杂度增加一些。虽然有“偶然复杂度”和“本质复杂度”之分,但总体来说,复杂度与代码规模大致呈线性关系——或者说,有经验的架构师可以通过良好的设计将复杂度控制在可接受的范围内。
但 Agent 系统完全不同。当 Agent 开始动态调用 Skill 时,系统的复杂度不是线性增长,而是指数级放大。 这不是夸张,而是由 Agent 系统的三个根本特征决定的:动态决策、组合爆炸、反馈循环。
理解这个复杂度是如何被放大的,是理解“为什么 MCP 不是锦上添花而是必需品”的关键。如果你不相信复杂度会指数放大,你就不会理解为什么需要协议层来约束它。如果你不理解约束的必要性,你就会成为第四章里那个“等到系统崩溃才追悔莫及”的陈浩。
本章将从数学和工程的角度,拆解这个复杂度放大过程。
二、复杂度的第一个放大器:动态决策 vs 静态路由
静态系统的可预测性
在传统软件中,调用关系是静态确定的。当你在代码中写下 if user_input == "A": call_function_a(),这个调用关系在编译时就已经确定。你可以在部署之前通过代码审查、单元测试、集成测试来验证所有的调用路径。
即使是最复杂的静态系统——比如一个拥有上千个函数的 ERP 系统——其调用关系也是有限的、可枚举的。你可以画出调用图,可以计算圈复杂度,可以识别出所有可能的执行路径。虽然路径数量可能很大,但它是有限的、确定的。
Agent 系统的动态性
Agent 系统的调用关系是在运行时动态决定的。Agent 收到用户输入后,通过大模型的推理来决定“下一步调用哪个 Skill”。这个决策不是基于 if-else 逻辑,而是基于:
- 用户的自然语言输入(无限可能)
- 当前的对话历史(不断变化)
- Agent 的系统 Prompt(静态但可被上下文覆盖)
- 大模型的概率性输出(相同输入可能产生不同输出)
这意味着:你无法在部署之前枚举 Agent 所有可能的调用路径。 调用关系不是代码中写死的,而是模型“想”出来的。
复杂度放大的数学表达
假设一个 Agent 有 N 个可用的 Skill。在静态系统中,调用关系是固定的——最多 N 条有向边。但在 Agent 系统中,Agent 可以在任何时候调用任何 Skill,并且可以以任意顺序组合它们。
对于一个需要 T 步才能完成的任务,Agent 可能的调用序列数量是 N^T(N 的 T 次方)。当 N=20、T=5 时,可能的序列数量是 320 万。当 N=50、T=10 时,这个数字是 50^10 ≈ 9.7 × 10^16——比地球上的沙粒还多。
当然,实际路径远少于这个理论上界,因为大模型的输出受到训练数据和 Prompt 的约束。但关键在于:这个空间大到无法穷举测试,大到无法用静态规则覆盖,大到必然存在你从未预料到的调用路径。
而这些“从未预料到的路径”,正是事故的源头。
三、复杂度的第二个放大器:Skill 之间的隐式依赖
显式依赖 vs 隐式依赖
在传统软件中,依赖关系是显式声明的。函数 A 调用函数 B,这个依赖在代码中清晰可见。如果你要修改函数 B,你可以通过静态分析找到所有调用它的函数,评估影响范围。
但在 Agent 系统中,Skill 之间的依赖是隐式的、动态的、不可预测的。
隐式依赖的具体表现
当一个 Agent 按顺序调用 Skill A 和 Skill B,即使 Skill A 和 Skill B 在代码层面没有任何关系,它们之间也产生了时序依赖——Skill B 的执行依赖于 Skill A 的输出。
这种依赖不是在代码中声明的,而是 Agent 在运行时“即兴”创造的。开发者无法提前知道 Agent 会把哪些Skill 组合在一起,也无法预测这些组合会产生什么样的副作用。
隐式依赖带来的问题
第一,变更影响范围不可控。当你修改 Skill A 的输出格式时,你无法知道 Agent 是否会在某个从未测试过的场景下,把这个输出传给 Skill B。如果 Skill B 不兼容新的输出格式,系统就会在运行时崩溃。
第二,状态污染。Skill A 可能修改了某个系统状态(比如更新了缓存、修改了全局变量),Skill B 假设这个状态是某种样子,但 Skill A 的修改可能超出了 Skill B 的预期。因为没有显式的依赖声明,这种状态污染很难被定位。
第三,事务边界模糊。Agent 调用 Skill A 成功,调用 Skill B 失败。此时系统处于什么状态?Skill A 的操作是否需要回滚?在静态系统中,你可以使用数据库事务来保证原子性。但在 Agent 系统中,Skill 调用可能跨越多个服务、多个事务边界,回滚几乎不可能。
复杂度放大的数学表达
有 N 个 Skill 时,可能的 Skill 对数量是 N×(N-1)/2——这是平方级增长。但 Skill 之间的隐式依赖不仅存在于二元组,还可能存在于三元组、四元组……实际上,任何长度大于 1 的调用序列都可能产生隐式依赖。
如果考虑所有可能的调用序列长度(从 2 到 L),隐式依赖的数量是:
∑_{k=2}^{L} N^k / (某个因子)
这是一个指数级增长的序列。当 N 和 L 都增长时,隐式依赖的数量会远远超过显式依赖,成为系统复杂度的主要来源。
四、复杂度的第三个放大器:反馈循环和状态空间爆炸
反馈循环的形成
Agent 系统是一个闭环控制系统:Agent 观察当前状态(用户输入、对话历史、Skill 执行结果),做出决策(调用某个 Skill),执行决策,观察新状态,然后再次决策。
这意味着:Skill 的执行结果会成为 Agent 下一轮决策的输入。 这形成了一个反馈循环。
反馈循环在控制理论中是好事——它是系统能够“自主”的基础。但反馈循环也带来了一个经典问题:状态空间爆炸。
状态空间的定义
Agent 系统的“状态”包括:
- 对话历史(文本序列,理论上无限长)
- 用户目标(自然语言表达,无限可能)
- 已执行的 Skill 列表及其结果
- 系统内部状态(缓存、数据库、外部服务状态)
这些状态的组合数量是天文数字。Agent 的“当前状态”几乎总是独一无二的——它从未在训练数据中出现过,也从未在测试中出现过。
为什么状态空间爆炸是致命的
第一,无法穷举测试。你不可能测试 Agent 在所有可能状态下的行为。即使你只测试 1% 的状态组合,测试用例的数量也会超过宇宙的原子数量。
第二,无法保证行为边界。因为你无法预知 Agent 会进入什么样的状态,你也就无法保证 Agent 的行为不会超出预期的边界。那个“从未见过的状态”可能会触发 Agent 做出“从未见过的错误行为”。
第三,错误难以复现。用户报告了一个错误:Agent 在某种情况下调用了不该调用的 Skill。你尝试复现,但你的测试环境和生产环境的对话历史、缓存状态、外部服务状态都不同。你可能永远无法复现这个错误,也就无法修复它。
数学视角:状态空间的大小
假设 Agent 系统只有 10 个布尔状态变量,状态空间大小是 2^10 = 1024。这很小。但 Agent 的状态不是布尔变量,而是:
- 对话历史的长度:假设最多 100 条消息,每条消息来自 10000 种可能的自然语言表达——这已经是10000^100 ≈ 10^400 种可能
- Skill 执行结果:每个 Skill 可能返回成功/失败,以及不同的数据——这又是指数级
实际上,Agent 系统的状态空间不仅是指数级的,而且是超指数级的——状态数量随着对话轮数的增加而指数级增长,而对话轮数本身也是一个变量。
这种复杂度的量级,已经远远超出了传统软件工程的应对能力。这正是为什么需要 MCP 这样的协议层——不是因为它能“解决”复杂度(复杂度是本质的,无法被解决),而是因为它能“约束”复杂度,将 Agent 的行为限制在一个可治理的边界内。
五、复杂度的第四个放大器:概率性决策的不确定性
确定性与概率性的根本区别
传统软件是确定性的:给定相同的输入,你总是得到相同的输出。这意味着你可以通过一次测试来验证一个行为,并且这个验证结果是永久有效的。
Agent 系统是概率性的:大模型是一个概率模型,给定相同的 Prompt 和上下文,它每次可能生成不同的输出。这意味着:
- 同样的用户请求,Agent 可能做出不同的决策
- 同样的测试用例,通过一次不代表永远通过
- 一个今天正常工作的 Agent,明天可能因为模型版本更新而行为异常
概率性如何放大复杂度
确定性系统的复杂度是“状态空间 × 路径数量”。概率性系统的复杂度需要再乘以一个“不确定性因子”——模型输出的概率分布。
假设 Agent 在某个状态下有 3 个可能的决策,每个决策的概率分别是 0.7、0.2、0.1。这意味着系统有 3 条可能的执行路径,每条路径有特定的概率。当系统执行 100 步时,可能的执行路径数量是 3^100,每条路径有特定的概率。
你不仅要考虑“系统可能走哪些路径”,还要考虑“每条路径的概率是多少”。而概率本身是模型的一个属性,随着模型版本、温度参数、甚至硬件的变化而变化。
实际影响
概率性决策导致两个严重问题:
第一,测试失效。你测试了 100 次,Agent 都做出了正确决策。你信心满满地上线。第 101 次,模型输出了一个低概率的“错误决策”,系统出问题了。这不是 Bug,这是概率性的本质特征。你无法通过增加测试次数来消除它,你只能通过约束来降低它的发生概率和影响范围。
第二,难以调试。当系统出现问题时,你问“为什么 Agent 做出了这个错误决策?”答案是:“因为模型输出了这个 token。”但这没有提供任何可操作的信息。你不知道是 Prompt 的问题、上下文的问题、模型本身的问题,还是纯粹的随机性。调试一个概率性系统,就像调试一个每次运行结果都不同的程序。
六、复杂度的乘数效应:四个放大器如何叠加
前面我们讨论了四个复杂度放大器:
- 动态决策:调用路径数量 ~ N^T
- 隐式依赖:隐式依赖数量 ~ 指数级
- 状态空间爆炸:状态数量 ~ 超指数级
- 概率性决策:乘以不确定性因子
这些放大器不是独立的,它们会相互叠加,产生乘数效应。
一个具体的例子
假设:
- N = 20 个 Skill
- T = 5 步任务
- 每个 Skill 有 3 种可能的输出结果
- 模型在每一步有 3 个可能的决策
那么:
- 可能的调用序列数量 ≈ 20^5 = 320 万
- 可能的执行结果组合 ≈ 3^5 = 243
- 可能的决策路径 ≈ 3^5 = 243
- 总的状态空间 ≈ 320万 × 243 × 243 ≈ 1.9 × 10^10
这是 190 亿种可能的状态。而这是一个极其简单的 Agent 系统——只有 20 个 Skill,只执行 5 步,每个 Skill 只有 3 种输出。真实系统的数字会比这大无数倍。
关键洞察
这个数学分析揭示了一个关键洞察:Agent 系统的复杂度不是一个可以被“管理”的问题,而是一个可以被“约束”的问题。
你无法通过更好的代码组织、更多的测试、更精细的 Prompt 来“解决”这种指数级的复杂度。这不是工程实现的问题,而是系统本质的问题。
你能做的只有一件事:引入约束。通过协议层限制 Agent 能做什么、不能做什么;通过控制平面在运行时拦截、审计、审批 Agent 的行为;通过设计将 Agent 的行为空间限制在一个可治理的范围内。
这就是 MCP 存在的根本原因。
七、从复杂度分析到 MCP 的设计原则
理解了复杂度的来源和放大机制,就能推导出 MCP 的核心设计原则:
原则一:用协议层替代硬编码
指数级增长的调用路径无法通过硬编码覆盖。MCP 通过统一的协议,让 Agent 和 Skill 之间的交互标准化,而不是为每一对组合编写定制代码。
原则二:在 Skill 层建立治理边界
隐式依赖无法被消除,但可以被观测和约束。MCP 将所有 Skill 调用收敛到网关层,使得隐式依赖变得可观测、可审计。
原则三:用策略约束状态空间
状态空间爆炸无法被“测试覆盖”,但可以被“策略约束”。MCP 的权限模型限制了 Agent 能进入的状态子集,将无限的状态空间缩小到可治理的范围。
原则四:用控制和审计应对概率性
概率性决策无法被消除,但可以被控制和审计。MCP 的审计日志记录每一次决策和执行,人工审批机制在概率性错误发生时提供最后一道防线。
八、小结:复杂度放大是 MCP 存在的数学理由
本章从数学和工程的角度,分析了 Agent 系统复杂度的四个放大器:
- 动态决策导致调用路径数量指数级增长(N^T)
- 隐式依赖导致 Skill 之间的关系不可预测、不可管理
- 状态空间爆炸导致系统行为无法被穷举测试
- 概率性决策引入不确定性,使测试和调试失效
这四个放大器相互叠加,将 Agent 系统的复杂度推到了一个传统软件工程方法无法应对的量级。
这个分析揭示了一个核心结论:Agent 系统的复杂度不是一个可以被“管理”或“优化”的问题,而是一个必须被“约束”的问题。 你无法通过写更聪明的代码来解决指数级的问题,你只能通过引入协议层和控制平面来限制 Agent 的行为空间。
MCP 不是锦上添花,不是“有了更好的架构”,而是 Agent 系统在面对这种本质复杂度时,唯一可行的工程化路径。
在下一章,我们将进一步探讨一个反直觉的观点:Skill 越多,系统越聪明?还是越危险? 这个问题的答案,将帮助我们更深刻地理解“能力增长”和“风险增长”之间的关系。
更多推荐




所有评论(0)