首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Agent Skill 按需加载:架构设计与实现解析

Agent Skill 按需加载:架构设计与实现解析

作者头像
tunsuy
发布2026-04-09 11:13:42
发布2026-04-09 11:13:42
1970
举报

❝当 AI Agent 需要的知识越来越多,把一切都塞进 System Prompt 显然不是个好主意。本文从架构设计的角度出发,深入探讨一种优雅的解法——「Skill 渐进式加载机制」。❞


一、问题:当 Agent 需要"十八般武艺"

构建一个功能丰富的 AI Agent 时,我们不可避免地面临这样的困境:

  • 「知识膨胀」:Agent 需要掌握代码审查、数据分析、文档撰写等几十种技能,每种技能的指导文本可能上千 Token
  • 「上下文浪费」:90% 的任务只需要 1~2 个技能,但传统做法是在 System Prompt 里塞满所有技能说明
  • 「缓存失效」:如果每次请求的 System Prompt 都不一样(因为动态拼接了不同技能),LLM API 的 Prompt Caching 就无法命中
  • 「多 Agent 干扰」:在多 Agent 协作场景中,不同 Agent 加载的技能可能互相"串台"

那么,如何设计一套机制,让 Agent 能够 「按需加载」 技能内容,同时 「对 Token 友好」「对缓存友好」「对多 Agent 安全」


二、核心思想:渐进式披露(Progressive Disclosure)

解决方案的核心借鉴了 UI 设计中的经典原则——「渐进式披露」

❝不要一次性展示所有信息,先提供概览让用户决策,需要时再展开详情。❞

映射到 Skill 机制:

  • 「概览始终可见」:每次请求都告诉 Agent "你有哪些技能可用",但只给名称和一句话描述(几十个 Token)
  • 「详情按需加载」:Agent 判断当前任务需要某个技能时,主动调用工具加载完整内容(可能几千个 Token)
  • 「自动生命周期管理」:加载的内容会根据预设策略自动过期,不会无限累积

这就像餐厅里的体验——你先看菜单(概览),点了某道菜后厨师才开始做(加载),吃完自动撤盘(过期清理)。


三、架构全景图

整个 Skill 加载机制可以拆解为 「四个核心模块」「两条数据流」

代码语言:javascript
复制
┌──────────────────────────────────────────────────────────────┐
│                        Agent Runtime                         │
│                                                              │
│   ┌──────────┐    ┌────────────────────┐    ┌─────────────┐  │
│   │  Skill   │───▶│ Request Processor  │───▶│   Model     │  │
│   │Repository│    │    Pipeline        │    │  Request    │  │
│   └──────────┘    └────────────────────┘    └─────────────┘  │
│        │                    │                                │
│        │            ┌───────┴────────┐                       │
│        │            │ Session State  │                       │
│        │            │  (loaded,docs) │                       │
│        │            └───────┬────────┘                       │
│        │                    │                                │
│   ┌────┴─────┐    ┌────────┴───────┐                        │
│   │  Skill   │───▶│ Tool Execution │                        │
│   │  Tools   │    │  (StateDelta)  │                        │
│   └──────────┘    └────────────────┘                        │
└──────────────────────────────────────────────────────────────┘

「数据流一:模型决策 → 工具调用 → State 写入」

代码语言:javascript
复制
模型判断需要某技能 → 调用 skill_load 工具 → 工具执行产生 StateDelta → 写入 Session State

「数据流二:Processor 读取 State → 注入 Prompt」

代码语言:javascript
复制
新一轮请求 → Processor 从 State 读取已加载技能 → 组装完整内容 → 注入到模型 Prompt 中

两条数据流通过 「Session State」 这个"中间介质"解耦。写入和读取可以发生在不同的请求轮次中,天然支持异步和跨轮次的状态传递。


四、模块一:Skill 仓库——数据源抽象

4.1 接口设计

Skill 仓库是整个系统的数据源层。它需要回答三个问题:

  1. 「有哪些技能可用?」(概览)
  2. 「某个技能的完整内容是什么?」(详情)
  3. 「某个技能有哪些关联文档?」(辅助资料)

抽象为接口:

代码语言:javascript
复制
type Repository interface {
    // 返回所有 Skill 的摘要(名称 + 一句话描述)
    Summaries() []Summary
    // 根据名称获取完整的 Skill 定义
    Get(name string) (*Skill, error)
    // 获取 Skill 关联文档的存储路径
    Path(name string) (string, error)
}

设计要点:

  • Summaries() 返回的是 「轻量摘要」,成本极低,适合每次请求都调用
  • Get() 返回 「完整内容」,开销大,只在确定需要时调用
  • 接口而非具体实现,方便扩展(文件系统、数据库、远程服务等多种后端)

4.2 基于文件系统的默认实现

一种直观的组织方式是用文件夹来表示 Skill:

代码语言:javascript
复制
skills/
├── code-review/
│   ├── SKILL.md          # 核心定义(YAML front matter + Markdown body)
│   ├── guide.md          # 辅助文档
│   └── reference.txt     # 辅助文档
└── data-analysis/
    ├── SKILL.md
    └── templates.md

SKILL.md 使用 YAML front matter 声明元数据:

代码语言:javascript
复制
---
name: code-review
description: 代码审查技能,涵盖代码规范、安全检查和性能优化
---

## 审查指南

当你进行代码审查时,请遵循以下步骤...

文件系统实现在初始化时一次性扫描目录、解析 front matter、建立名称索引。如果 name 字段未指定,回退到文件夹名称——简单约定优于复杂配置。


五、模块二:Session State Key 设计

Session State 是 Skill 加载状态的持久化存储。Key 的设计直接影响多 Agent 隔离和查询效率。

5.1 两类核心 Key

Key 模式

含义

典型值

temp:skill:loaded:{agent}/{skill}

标记某 Skill 已被某 Agent 加载

"1"

temp:skill:docs:{agent}/{skill}

记录 Agent 对某 Skill 选择的文档

"*" 或 JSON 数组

temp: 前缀表示这些是临时状态,不参与 Session 的持久化归档。

5.2 为什么需要 Agent 作用域?

在多 Agent 协作场景中,父 Agent 可能编排多个子 Agent 在同一个 Session 里工作。如果所有 Agent 的 Skill 状态都混在一起,就会出现"串台":

代码语言:javascript
复制
场景:Agent-A 加载了 code-review,Agent-B 加载了 data-analysis

❌ 不隔离:Agent-A 的 Prompt 里也会出现 data-analysis 的内容
✅ 带作用域:每个 Agent 只看到自己加载的 Skill

Key 生成函数的核心逻辑:

代码语言:javascript
复制
func LoadedKey(agentName, skillName string) string {
    return "temp:skill:loaded:" + agentName + "/" + skillName
}

func LoadedPrefix(agentName string) string {
    return "temp:skill:loaded:" + agentName + "/"
}

通过前缀扫描 LoadedPrefix(agentName),可以高效地获取某个 Agent 的所有已加载 Skill,而不会误读其他 Agent 的状态。


六、模块三:Skill Tools——写入 State 的入口

6.1 skill_load:加载 Skill

这是最核心的 Tool。当模型决定需要某个技能时,会调用它。

「关键架构决策」:Tool 的执行函数(Call)和 State 写入(StateDelta)是 「分离的」

代码语言:javascript
复制
// Call 只负责验证和返回确认
func (t *LoadTool) Call(ctx context.Context, args LoadInput) string {
    if _, err := t.repo.Get(args.Skill); err != nil {
        return"error: skill not found"
    }
    return"loaded: " + args.Skill
}

// StateDelta 声明需要写入的状态变更
func (t *LoadTool) StateDelta(agentName string, args LoadInput) map[string][]byte {
    delta := map[string][]byte{
        LoadedKey(agentName, args.Skill): []byte("1"),
    }
    if args.IncludeAllDocs {
        delta[DocsKey(agentName, args.Skill)] = []byte("*")
    } elseiflen(args.Docs) > 0 {
        delta[DocsKey(agentName, args.Skill)] = marshalJSON(args.Docs)
    }
    return delta
}

「为什么要分离?」

  • 「可测试性」:StateDelta 是纯函数,不依赖外部状态,易于单元测试
  • 「事务性」:由框架统一提交 StateDelta,可以和其他状态变更一起原子操作
  • 「可审计性」:StateDelta 作为结构化数据,可以被记录到事件日志中

6.2 skill_select_docs:精细化文档选择

提供更精细的文档管理能力,支持三种操作模式:

模式

行为

场景

add

在已选文档基础上追加

"再帮我看看这个文件"

replace

替换整个文档选择(默认)

"换一组参考资料"

clear

清除所有已选文档

"不需要参考资料了"

同样通过 StateDelta 机制更新 DocsKey 的值,保持写入逻辑的一致性。


七、模块四:Request Processor——从 State 到 Prompt 的桥梁

Processor 是整个机制的核心调度中心。我们设计了 「两种注入策略」,各有优劣。

7.1 策略一:System Message 注入

「思路」:将 Skill 内容追加到 System Message 的尾部。

「处理流程」

代码语言:javascript
复制
ProcessRequest()
  │
  ├─ 1. 旧版 State 迁移(如需要)
  │
  ├─ 2. 清理过期的 Skill State(Turn 模式)
  │
  ├─ 3. 注入 Skill 概览列表(无条件执行)
  │     → "Available skills:\n- code-review: 代码审查\n- ..."
  │
  ├─ 4. 从 State 读取已加载的 Skill 列表
  │
  ├─ 5. 限制最大加载数量(如超限,按最近使用排序保留)
  │
  ├─ 6. 遍历已加载 Skill,构建注入内容:
  │     ├─ 获取 Skill 完整 Body
  │     ├─ 读取文档选择
  │     └─ 拼装 "[Loaded] skill-name\n{body}\n{docs}"
  │
  ├─ 7. 合并到 System Message
  │
  └─ 8. 清理一次性 Skill State(Once 模式)

注入后的 System Message 结构示例:

代码语言:javascript
复制
(原始 System Prompt 内容)

Available skills:
- code-review: 代码审查技能
- data-analysis: 数据分析技能

[Loaded] code-review

## 审查指南
当你进行代码审查时,请遵循以下步骤...

Docs loaded: guide.md
[Doc] guide.md
(guide.md 的完整内容)

「优点」:实现简单直观,模型总能看到最新的技能信息。

「缺点」:每次加载新 Skill 都会改变 System Message,导致 LLM API 的 Prompt Caching 失效。

7.2 策略二:Tool Result 注入

「思路」:不修改 System Message,而是将 Skill 内容"回填"到对话历史中 skill_load 对应的 Tool Result 消息里。

「处理流程」

代码语言:javascript
复制
ProcessRequest()
  │
  ├─ 1. 从 State 读取已加载 Skill
  │
  ├─ 2. 索引对话历史中所有的 Tool Call 消息
  │
  ├─ 3. 找到每个 Skill 最后一次 Tool Response 的位置
  │
  ├─ 4. 将 Skill 完整内容写入对应的 Tool Result 消息体
  │
  ├─ 5. 为找不到匹配 Tool Result 的 Skill 构建回退内容
  │
  └─ 6. 必要时插入回退的 System Message

「为什么这样设计?」

LLM API 的 Prompt Caching 通常基于前缀匹配——如果请求的 System Message 和前几轮对话跟上次完全一致,就能命中缓存。Tool Result 注入策略保持 System Message 不变,只修改对话历史中后面的 Tool Response,从而 「最大化缓存命中率」

「回退机制」:当对话历史被压缩(如 Session Summary 合并了多轮对话),原本的 Tool Result 消息可能已经不存在了。此时 Processor 检测到"无处回填",就会回退到 System Message 注入,确保 Skill 内容不丢失。但如果 Summary 已经涵盖了相关上下文,则跳过回退,避免重复注入。

7.3 策略对比

维度

System Message 注入

Tool Result 注入

实现复杂度

⭐⭐ 简单

⭐⭐⭐⭐ 较复杂

Prompt Caching

❌ 每次加载新 Skill 失效

✅ System Message 稳定,利于缓存

对话压缩兼容

✅ 天然兼容

⚠️ 需要回退机制

调试友好度

✅ Prompt 一目了然

⚠️ 内容分散在 Tool Results 中

在实际系统中,可以将两种策略做成可配置选项,让使用者根据场景选择。


八、加载模式(Load Mode):Skill 内容的生命周期

不同任务场景对 Skill 内容的存活时间有不同需求。设计三种加载模式来覆盖:

模式

行为

适用场景

「once」

注入一次后立即清除 State

一次性查询,防止 Prompt 无限膨胀

「turn」(默认)

当前调用内有效,下次调用时清除

多轮工具调用,同一轮任务内保持上下文

「session」

跨调用持续有效,直到 Session 结束

整个会话都需要某技能的长期任务

「Turn 模式的实现细节」

代码语言:javascript
复制
func maybeClearSkillStateForTurn(invocation Invocation) {
    // 使用一次性标记防止重复清理
    if invocation.GetFlag("skill_turn_cleared") {
        return
    }
    invocation.SetFlag("skill_turn_cleared", true)

    // 清除上一轮残留的 Skill State
    clearAllSkillKeys(invocation)
}

关键技巧是使用 「Invocation 级别的标记位」 来保证 "一个 Invocation 只清理一次"——因为一个 Invocation 内部可能有多次 Processor 调用(多轮工具调用循环),不能每次都清理,否则同一轮内加载的 Skill 也会被误删。


九、高级特性:最大加载数限制

当 Agent 在一个 Session 中累积加载了很多 Skill 时,Prompt 会变得过长。需要一个"容量控制阀"。

9.1 配置方式

代码语言:javascript
复制
agent := NewAgent("my-agent",
    WithSkills(repo),
    WithMaxLoadedSkills(3),  // 最多同时保留 3 个已加载 Skill
)

9.2 淘汰策略:最近使用优先

当已加载数量超过上限时,按以下逻辑淘汰:

  1. 「逆序扫描事件历史」,找到最近被 skill_load / skill_select_docs 调用过的 Skill
  2. 按调用时间从近到远排列,保留最新的 N 个
  3. 同时间的按字母序排列(保证确定性,避免随机性导致的不稳定行为)
  4. 未被保留的 Skill 的 State Key 被清除

「为什么选择"最近使用"而非"最常使用"?」

  • 「时效性更强」:最近加载的 Skill 最可能与当前任务相关
  • 「实现更简单」:不需要维护频率计数器
  • 「符合直觉」:类似 LRU(Least Recently Used)缓存策略,已被广泛验证

十、Processor Pipeline:执行顺序的讲究

所有 Processor 按特定顺序排列成 Pipeline,执行顺序直接影响正确性:

代码语言:javascript
复制
Request Processor Pipeline:
  │
  ├─ [1] System Prompt Processor        // 基础 System Message
  ├─ [2] Context Processor              // 注入外部上下文
  ├─ [3] Skills Request Processor       // 注入 Skill 概览 + System Message 模式的内容
  ├─ [4] Content Processor              // 组装对话历史
  ├─ [5] Post-Tool Processor            // 处理工具调用结果后的提示
  └─ [6] Skills Tool Result Processor   // Tool Result 模式的内容回填

「为什么 Skills Tool Result Processor 在最后?」

因为它需要操作已经被 Content Processor 组装好的完整对话历史。只有在对话消息列表已经构建完成后,才能定位到正确的 Tool Result 消息并回填内容。

「为什么 Skills Request Processor 在 Content Processor 之前?」

因为它需要在 System Message 被最终"冻结"之前完成概览和内容的注入。


十一、完整数据流回顾

用一个端到端的例子把所有模块串起来:

第一轮:用户提问

代码语言:javascript
复制
用户: "帮我做一下这段代码的审查"

→ Skills Request Processor 注入概览:
  System Message += "Available skills:\n- code-review: 代码审查\n- ..."

→ 模型看到概览,判断需要 code-review 技能

→ 模型调用 skill_load(skill="code-review", include_all_docs=true)

→ Tool 执行:
  - Call() 返回 "loaded: code-review"
  - StateDelta() 产生:
    {
      "temp:skill:loaded:my-agent/code-review": "1",
      "temp:skill:docs:my-agent/code-review":   "*"
    }

→ 框架将 StateDelta 写入 Session State

第二轮:Agent Loop 继续

代码语言:javascript
复制
→ Skills Request Processor 处理:
  a. 注入概览(每次都做)
  b. 从 State 发现 code-review 已加载
  c. 从 Repository 获取 code-review 的完整 Body + 所有文档
  d. 注入到 Prompt:
     "[Loaded] code-review\n\n## 审查指南\n...\n\n[Doc] guide.md\n..."

→ 模型现在拥有完整的审查指导,开始执行代码审查任务

下一次 Invocation(Turn 模式下)

代码语言:javascript
复制
→ 新的 Invocation 开始

→ maybeClearSkillStateForTurn() 清除上轮的 Skill State

→ code-review 的内容不再出现在 Prompt 中

→ 如果新任务又需要,模型会重新加载

十二、设计决策总结

设计点

决策

价值

渐进式披露

概览常驻 + 详情按需加载

节省 90%+ 的冗余 Token 消耗

State 作为中间介质

工具写入 State,Processor 读取 State

写入和读取完全解耦,支持跨轮次传递

Agent 作用域隔离

Key 中嵌入 Agent 名称

多 Agent 共享 Session 时互不干扰

双注入策略

System Message 注入 + Tool Result 注入

简单场景与缓存优化场景各取所需

StateDelta 机制

Tool 声明 Delta,框架统一提交

可测试、可审计、可事务化

三级生命周期

once / turn / session

灵活适配从一次性查询到长期任务的各种场景

LRU 淘汰策略

超限时保留最近使用的 Skill

防止 Prompt 膨胀,保障响应质量

回退机制

Tool Result 不存在时回退 System Message

对话压缩后仍保证技能内容不丢失

懒迁移策略

检测旧格式时即时迁移

平滑升级,无需全量数据变更


十三、写在最后

回到开头的问题——当 Agent 需要"十八般武艺"时,我们的解法是:

  1. 「不要一次性给」,让 Agent 先看菜单
  2. 「让 Agent 自己选」,通过工具调用主动加载
  3. 「用 State 做桥梁」,将加载状态持久化,与注入逻辑解耦
  4. 「用 Processor 做注入」,在每次请求前自动将已加载内容组装到 Prompt 中
  5. 「用生命周期管理做兜底」,确保内容不会无限累积

这套机制的核心价值在于:它把"Agent 应该知道什么"这个问题,从静态配置变成了「动态决策」——让模型自身成为知识加载的决策者,框架只负责提供高效、安全、可靠的加载管道。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-04-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 有文化的技术人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、问题:当 Agent 需要"十八般武艺"
  • 二、核心思想:渐进式披露(Progressive Disclosure)
  • 三、架构全景图
  • 四、模块一:Skill 仓库——数据源抽象
    • 4.1 接口设计
    • 4.2 基于文件系统的默认实现
  • 五、模块二:Session State Key 设计
    • 5.1 两类核心 Key
    • 5.2 为什么需要 Agent 作用域?
  • 六、模块三:Skill Tools——写入 State 的入口
    • 6.1 skill_load:加载 Skill
    • 6.2 skill_select_docs:精细化文档选择
  • 七、模块四:Request Processor——从 State 到 Prompt 的桥梁
    • 7.1 策略一:System Message 注入
    • 7.2 策略二:Tool Result 注入
    • 7.3 策略对比
  • 八、加载模式(Load Mode):Skill 内容的生命周期
  • 九、高级特性:最大加载数限制
    • 9.1 配置方式
    • 9.2 淘汰策略:最近使用优先
  • 十、Processor Pipeline:执行顺序的讲究
  • 十一、完整数据流回顾
    • 第一轮:用户提问
    • 第二轮:Agent Loop 继续
    • 下一次 Invocation(Turn 模式下)
  • 十二、设计决策总结
  • 十三、写在最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档