# Agent 里的 Skill 依赖关系应该怎么设计

一句话导读:Skill 不是一堆互不相关的 Prompt 或函数。复杂 Agent 里的 Skill 会形成依赖图,需要显式声明、自动排序、版本治理、隔离运行和回归测试。

当 Agent 系统变复杂后,Skill 很快不再是几个孤立能力。

比如:

生成报告 Skill
  -> 依赖读取数据 Skill
  -> 依赖生成图表 Skill
  -> 依赖摘要生成 Skill

发送邮件 Skill
  -> 依赖鉴权 Skill
  -> 依赖联系人查询 Skill
  -> 依赖附件生成 Skill
1
2
3
4
5
6
7
8
9

如果这些依赖只靠人脑记、靠文档写、靠 Prompt 约定,系统很快会变成一团胶水:

不知道谁依赖谁
不知道应该先加载哪个
底层 Skill 一改,上层一片炸
循环依赖上线后才发现
测试只测了当前 Skill,没测依赖它的上游
1
2
3
4
5

所以 Skill 依赖不是一个文档问题,而是一个工程治理问题。


# 一、先定义什么是 Skill 依赖

Skill 依赖指的是:

一个 Skill 的执行,需要另一个 Skill 的能力、输出、资源或约束。
1

常见依赖有几类。

# 1. 数据依赖

一个 Skill 需要另一个 Skill 的输出。

比如:

生成报告 Skill
  -> 依赖读取销售数据 Skill 的输出
1
2

# 2. 能力依赖

一个 Skill 内部需要调用另一个能力。

比如:

报销判断 Skill
  -> 依赖员工身份查询 Skill
  -> 依赖预算查询 Skill
  -> 依赖制度检索 Skill
1
2
3
4

# 3. 运行时依赖

多个 Skill 依赖同一个基础服务。

比如:

订单查询 Skill
退款查询 Skill
工单创建 Skill
  -> 都依赖鉴权 Skill
1
2
3
4

# 4. 版本依赖

上层 Skill 依赖某个版本范围的底层 Skill。

比如:

report-generator@2.x
  -> depends on chart-generator@^1.4.0
1
2

# 5. 隐式依赖

最危险的是隐式依赖。

比如某个 Skill 的 Prompt 里写:

请参考订单查询规则。
1

但系统里没有机器可读的依赖声明。

这种依赖人能看懂,系统看不懂。

后续就没法自动加载、排序、检测和测试。


# 二、第一步:依赖必须显式声明

Skill 依赖治理的第一步,是把隐式依赖变成显式依赖。

每个 Skill 都应该有元数据。

比如:

name: refund-status
version: 1.3.0
description: 查询并解释退款状态

dependencies:
  - name: order-query
    version: "^1.2.0"
    type: required
  - name: payment-query
    version: "^2.0.0"
    type: required
  - name: ticket-create
    version: "^1.0.0"
    type: optional

tools:
  - refund.getStatus
  - order.getRecent

inputs:
  - userId
  - orderId?

outputs:
  - refundStatus
  - estimatedArrival
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

重点不是 YAML 格式,而是这件事:

依赖关系必须机器可读。
1

只有机器可读,后面才能:

自动加载
自动排序
自动检测循环依赖
自动计算影响范围
自动跑回归测试
1
2
3
4
5

# 三、第二步:用依赖图管理 Skill

一旦每个 Skill 都声明了依赖,就可以把系统抽象成一张有向图。

比如:

refund-response
  -> refund-status
  -> order-query
  -> auth

refund-response
  -> payment-query
  -> auth

refund-response
  -> ticket-create
  -> auth
1
2
3
4
5
6
7
8
9
10
11
12

这张图可以用来做几件事。

# 1. 推导加载顺序

底层依赖先加载,上层 Skill 后加载。

auth
  -> order-query
  -> refund-status
  -> refund-response
1
2
3
4

# 2. 推导调用顺序

如果某个 Skill 的输入依赖另一个 Skill 的输出,系统可以自动知道先后关系。

# 3. 计算影响范围

如果 auth 改了,可以知道哪些 Skill 受影响:

order-query
payment-query
refund-status
ticket-create
refund-response
1
2
3
4
5

# 4. 检测循环依赖

如果出现:

A -> B -> C -> A
1

系统应该在注册期或构建期直接报错。

不要等运行时炸。


# 四、第三步:拓扑排序和循环依赖检测

依赖图最常见的处理方式是拓扑排序。

如果依赖图是 DAG,也就是有向无环图,就可以得到一个合法顺序:

auth
order-query
payment-query
refund-status
refund-response
1
2
3
4
5

如果无法排序,说明有循环依赖。

循环依赖要尽早拦截。

比如:

report-generator
  -> chart-generator
  -> report-generator
1
2
3

这通常说明 Skill 边界设计有问题。

解决方式不是硬让它们互相调用,而是重新拆边界:

report-generator
chart-generator
  -> 都依赖 shared-data-model
1
2
3

或者用事件解耦:

report-generator 发布 ReportRequested
chart-generator 监听事件生成图表
report-generator 再汇总结果
1
2
3

简单原则:

同步互调容易形成循环依赖。
共享底层能力或事件驱动更容易解耦。
1
2

# 五、第四步:区分强依赖和弱依赖

不是所有依赖都一样。

可以分成:

required dependency
optional dependency
runtime dependency
dev dependency
1
2
3
4

# 强依赖

没有它,Skill 不能运行。

比如:

退款状态解释 Skill
必须依赖退款查询 Skill。
1
2

# 弱依赖

有它更好,没有也能降级。

比如:

退款超时场景可以依赖工单创建 Skill。
如果工单系统不可用,仍然可以回复用户转人工。
1
2

# 运行时依赖

只有走到某个分支才需要。

比如:

只有退款超时才需要 ticket-create。
1

# 开发依赖

只在测试或评估时需要。

比如:

mock-payment-service
eval-dataset-loader
1
2

依赖类型不同,加载策略和失败策略也不同。


# 六、第五步:语义化版本管理

Skill 也应该有版本。

推荐用 SemVer:

MAJOR.MINOR.PATCH
1

比如:

refund-status@1.4.2
1

版本语义可以这样定义:

PATCH:
修 bug,不改变输入输出契约。

MINOR:
增加兼容能力,比如新增可选字段。

MAJOR:
破坏性变更,比如输入输出结构改变。
1
2
3
4
5
6
7
8

上层 Skill 不应该随便写死某个版本:

dependencies:
  - name: refund-status
    version: "1.4.2"
1
2
3

更适合声明范围:

dependencies:
  - name: refund-status
    version: "^1.4.0"
1
2
3

这样兼容升级可以自动接入。

破坏性升级需要显式处理。


# 七、第六步:懒加载和按需装配

Skill 多了以后,不应该所有 Skill 启动时一次性全加载。

全量加载的问题是:

启动慢
内存占用高
依赖错误影响面大
低频 Skill 拖累高频路径
1
2
3
4

更好的方式是:

高频核心 Skill 预加载。
低频长尾 Skill 懒加载。
运行到对应 Intent 或 Capability 时,再解析依赖链。
1
2
3

比如:

订单查询、退款查询、物流查询
  -> 高频,预加载

合同审查、复杂报表生成、异常申诉
  -> 低频,懒加载
1
2
3
4
5

懒加载不是越多越好。

它有首次调用延迟。

所以取舍要看:

QPS
延迟要求
Skill 体积
依赖链深度
失败影响面
1
2
3
4
5

# 八、第七步:沙箱隔离,控制故障传播

一个 Skill 出错,不应该拖垮整个 Agent。

尤其是复杂垂类 Agent:

退款查询失败
不应该导致整个客服 Agent 崩掉。
1
2

可以做几层隔离:

超时控制
异常捕获
资源限制
权限限制
工具白名单
降级策略
1
2
3
4
5
6

比如:

ticket-create Skill 失败
  -> 不影响 refund-status Skill
  -> 回复用户“当前无法自动创建工单,可以转人工处理”
1
2
3

这叫故障边界。

Skill 依赖越复杂,越需要明确故障边界。


# 九、第八步:依赖注入,降低耦合

Skill 内部不要写死依赖。

不要这样:

import { refundStatusSkill } from "../refund-status";

export async function run(input) {
  const status = await refundStatusSkill.run(input);
}
1
2
3
4
5

更好的方式是通过接口注入:

type RefundStatusProvider = {
  getStatus(input: RefundInput): Promise<RefundState>;
};

export function createRefundResponseSkill(deps: {
  refundStatus: RefundStatusProvider;
  ticket?: TicketProvider;
}) {
  return {
    async run(input) {
      const status = await deps.refundStatus.getStatus(input);
      return generateResponse(status);
    },
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

好处是:

方便替换实现
方便单元测试 mock
方便灰度新版本
避免代码层硬耦合
1
2
3
4

依赖声明解决系统级依赖关系。

依赖注入解决代码级耦合问题。

两个都需要。


# 十、第九步:变更后的影响分析和回归测试

Skill 依赖治理最容易被忽略的是测试闭环。

如果修改了一个底层 Skill:

auth
order-query
payment-query
refund-status
1
2
3
4

系统应该自动知道哪些上层 Skill 受影响,并触发相关测试。

比如:

refund-status 改了
  -> 跑 refund-response 测试
  -> 跑 overdue-refund 测试
  -> 跑 ticket-create 集成测试
  -> 跑相关 Eval Set
1
2
3
4
5

这可以通过依赖图做影响分析。

简单流程:

Skill changed
  -> 找到所有反向依赖
  -> 收集受影响测试
  -> 跑回归
  -> 阻止不兼容变更上线
1
2
3
4
5

这一步最能体现工程成熟度。

因为它解决的是:

改一个底层能力,炸一片上层能力。
1

# 十一、运行时应该怎么处理循环依赖

循环依赖不要留到运行时处理。

最好在:

注册期
构建期
发布前检查
1
2
3

直接拦截。

如果发现:

A depends on B
B depends on C
C depends on A
1
2
3

应该失败,并提示依赖链。

比如:

Circular dependency detected:
report-generator -> chart-generator -> report-generator
1
2

如果业务上确实需要双向协作,通常说明:

Skill 边界切错了。
1

可以考虑:

抽出共享底层 Skill
改成事件驱动
改成工作流节点编排
让上层 Workflow 负责协调,而不是 Skill 互相调用
1
2
3
4

# 十二、和垂类 Agent 的关系

垂类 Agent 里 Skill 依赖尤其常见。

比如报销 Agent:

报销判断 Skill
  -> 员工身份 Skill
  -> 部门政策 Skill
  -> 预算查询 Skill
  -> 订单识别 Skill
  -> 风险检查 Skill
1
2
3
4
5
6

如果这些依赖不治理,系统会很难维护。

更推荐的方式是:

Intent 识别用户要做什么
Business Resolver 查询业务事实
Capability Router 选择能力
Skill Registry 解析依赖
Workflow 编排执行顺序
LLM 组织最终回复
1
2
3
4
5
6

也就是说:

不要让 Skill 之间随便互相调用。
让上层 Router / Workflow 根据依赖图统一编排。
1
2

# 十三、和 MCP 的关系

MCP 是工具接入协议。

Skill 可以依赖 MCP Tool。

比如:

name: refund-status
tools:
  - refund.getStatus
  - order.getRecent
1
2
3
4

但 MCP 不负责 Skill 依赖治理。

MCP 解决的是:

Agent 怎么调用外部工具。
1

Skill 依赖治理解决的是:

能力模块之间如何声明、加载、排序、隔离、升级和测试。
1

所以关系是:

Skill Registry 管 Skill。
MCP Registry 管工具。
Skill 可以引用 MCP Tool。
Workflow / Harness 负责装配它们。
1
2
3
4

不要把 MCP 当成 Skill 依赖系统。


# 十四、面试回答模板

如果面试官问:

如果 Agent 里有很多 Skill,Skill 之间还有依赖关系,你怎么设计?
1

可以这样答:

我会把 Skill 依赖当成工程治理问题,而不是靠文档约定。

第一,每个 Skill 都要有结构化元数据,显式声明依赖的 Skill、版本范围、依赖类型、输入输出和需要的工具。这样依赖关系机器可读。

第二,系统把 Skill 依赖构建成有向图,用拓扑排序推导加载和执行顺序,并在注册期或构建期检测循环依赖,避免运行时才炸。

第三,引入语义化版本管理,上层声明依赖范围,兼容升级自动接入,破坏性变更通过主版本号和回归测试拦截。

第四,加载策略上区分高频核心 Skill 和低频长尾 Skill。核心 Skill 预加载,长尾 Skill 懒加载,按 Intent 或 Capability 触发时再解析依赖链。

第五,运行时用沙箱隔离和超时、降级机制控制故障传播,同时用依赖注入降低代码层耦合,方便替换、灰度和测试。

最后,底层 Skill 变更时通过依赖图做影响分析,自动触发所有上游 Skill 的回归测试和相关 Eval Set,防止改一个炸三个。
1
2
3
4
5
6
7
8
9
10
11
12
13

这个回答的主线是:

隐式变显式
人工变自动
耦合变隔离
变更有回归
1
2
3
4

# 十五、总结

Skill 依赖治理可以记成九步:

1. 显式声明依赖
2. 建 Skill 依赖图
3. 拓扑排序推导顺序
4. 循环依赖提前检测
5. 区分强依赖和弱依赖
6. 用 SemVer 做版本管理
7. 高频预加载,长尾懒加载
8. 沙箱隔离和依赖注入
9. 变更后做影响分析和回归测试
1
2
3
4
5
6
7
8
9

真正成熟的 Agent 系统,不会把 Skill 当成散落的 Prompt 片段。

它会把 Skill 当成可注册、可依赖、可版本化、可测试、可隔离的能力模块。

一句话总结:

Skill 越多,越不能靠人脑维护依赖;必须用依赖图、版本、隔离和测试,把 Skill 从胶水代码治理成工程资产。
1