Agent Skill 按需加载:架构设计与实现解析
❝当 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 加载机制可以拆解为「四个核心模块」和「两条数据流」┌──────────────────────────────────────────────────────────────┐ │ Agent Runtime │ │ │ │ ┌──────────┐ ┌────────────────────┐ ┌─────────────┐ │ │ │ Skill │───▶│ Request Processor │───▶│ Model │ │ │ │Repository│ │ Pipeline │ │ Request │ │ │ └──────────┘ └────────────────────┘ └─────────────┘ │ │ │ │ │ │ │ ┌───────┴────────┐ │ │ │ │ Session State │ │ │ │ │ (loaded,docs) │ │ │ │ └───────┬────────┘ │ │ │ │ │ │ ┌────┴─────┐ ┌────────┴───────┐ │ │ │ Skill │───▶│ Tool Execution │ │ │ │ Tools │ │ (StateDelta) │ │ │ └──────────┘ └────────────────┘ │ └──────────────────────────────────────────────────────────────┘「数据流一模型决策 → 工具调用 → State 写入」模型判断需要某技能 → 调用 skill_load 工具 → 工具执行产生 StateDelta → 写入 Session State「数据流二Processor 读取 State → 注入 Prompt」新一轮请求 → Processor 从 State 读取已加载技能 → 组装完整内容 → 注入到模型 Prompt 中两条数据流通过「Session State」这个中间介质解耦。写入和读取可以发生在不同的请求轮次中天然支持异步和跨轮次的状态传递。四、模块一Skill 仓库——数据源抽象4.1 接口设计Skill 仓库是整个系统的数据源层。它需要回答三个问题「有哪些技能可用」概览「某个技能的完整内容是什么」详情「某个技能有哪些关联文档」辅助资料抽象为接口type Repository interface { // 返回所有 Skill 的摘要名称 一句话描述 Summaries() []Summary // 根据名称获取完整的 Skill 定义 Get(name string) (*Skill, error) // 获取 Skill 关联文档的存储路径 Path(name string) (string, error) }设计要点Summaries()返回的是「轻量摘要」成本极低适合每次请求都调用Get()返回「完整内容」开销大只在确定需要时调用接口而非具体实现方便扩展文件系统、数据库、远程服务等多种后端4.2 基于文件系统的默认实现一种直观的组织方式是用文件夹来表示 Skillskills/ ├── code-review/ │ ├── SKILL.md # 核心定义YAML front matter Markdown body │ ├── guide.md # 辅助文档 │ └── reference.txt # 辅助文档 └──>--- name: code-review description: 代码审查技能涵盖代码规范、安全检查和性能优化 --- ## 审查指南 当你进行代码审查时请遵循以下步骤...文件系统实现在初始化时一次性扫描目录、解析 front matter、建立名称索引。如果name字段未指定回退到文件夹名称——简单约定优于复杂配置。五、模块二Session State Key 设计Session State 是 Skill 加载状态的持久化存储。Key 的设计直接影响多 Agent 隔离和查询效率。5.1 两类核心 KeyKey 模式含义典型值temp:skill:loaded:{agent}/{skill}标记某 Skill 已被某 Agent 加载1temp:skill:docs:{agent}/{skill}记录 Agent 对某 Skill 选择的文档*或 JSON 数组temp:前缀表示这些是临时状态不参与 Session 的持久化归档。5.2 为什么需要 Agent 作用域在多 Agent 协作场景中父 Agent 可能编排多个子 Agent 在同一个 Session 里工作。如果所有 Agent 的 Skill 状态都混在一起就会出现串台场景Agent-A 加载了 code-reviewAgent-B 加载了>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.1skill_load加载 Skill这是最核心的 Tool。当模型决定需要某个技能时会调用它。「关键架构决策」Tool 的执行函数Call和 State 写入StateDelta是「分离的」。// Call 只负责验证和返回确认 func (t *LoadTool) Call(ctx context.Context, args LoadInput) string { if _, err : t.repo.Get(args.Skill); err ! nil { returnerror: skill not found } returnloaded: 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.2skill_select_docs精细化文档选择提供更精细的文档管理能力支持三种操作模式模式行为场景add在已选文档基础上追加再帮我看看这个文件replace替换整个文档选择默认换一组参考资料clear清除所有已选文档不需要参考资料了同样通过 StateDelta 机制更新DocsKey的值保持写入逻辑的一致性。七、模块四Request Processor——从 State 到 Prompt 的桥梁Processor 是整个机制的核心调度中心。我们设计了「两种注入策略」各有优劣。7.1 策略一System Message 注入「思路」将 Skill 内容追加到 System Message 的尾部。「处理流程」ProcessRequest() │ ├─ 1. 旧版 State 迁移如需要 │ ├─ 2. 清理过期的 Skill StateTurn 模式 │ ├─ 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 StateOnce 模式注入后的 System Message 结构示例原始 System Prompt 内容 Available skills: - code-review: 代码审查技能 ->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 ModeSkill 内容的生命周期不同任务场景对 Skill 内容的存活时间有不同需求。设计三种加载模式来覆盖模式行为适用场景「once」注入一次后立即清除 State一次性查询防止 Prompt 无限膨胀「turn」默认当前调用内有效下次调用时清除多轮工具调用同一轮任务内保持上下文「session」跨调用持续有效直到 Session 结束整个会话都需要某技能的长期任务「Turn 模式的实现细节」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 配置方式agent : NewAgent(my-agent, WithSkills(repo), WithMaxLoadedSkills(3), // 最多同时保留 3 个已加载 Skill )9.2 淘汰策略最近使用优先当已加载数量超过上限时按以下逻辑淘汰「逆序扫描事件历史」找到最近被skill_load/skill_select_docs调用过的 Skill按调用时间从近到远排列保留最新的 N 个同时间的按字母序排列保证确定性避免随机性导致的不稳定行为未被保留的 Skill 的 State Key 被清除「为什么选择最近使用而非最常使用」「时效性更强」最近加载的 Skill 最可能与当前任务相关「实现更简单」不需要维护频率计数器「符合直觉」类似 LRULeast Recently Used缓存策略已被广泛验证十、Processor Pipeline执行顺序的讲究所有 Processor 按特定顺序排列成 Pipeline执行顺序直接影响正确性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 被最终冻结之前完成概览和内容的注入。十一、完整数据流回顾用一个端到端的例子把所有模块串起来第一轮用户提问用户: 帮我做一下这段代码的审查 → Skills Request Processor 注入概览: System Message Available skills:\n- code-review: 代码审查\n- ... → 模型看到概览判断需要 code-review 技能 → 模型调用 skill_load(skillcode-review, include_all_docstrue) → 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 继续→ 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... → 模型现在拥有完整的审查指导开始执行代码审查任务下一次 InvocationTurn 模式下→ 新的 Invocation 开始 → maybeClearSkillStateForTurn() 清除上轮的 Skill State → code-review 的内容不再出现在 Prompt 中 → 如果新任务又需要模型会重新加载十二、设计决策总结设计点决策价值渐进式披露概览常驻 详情按需加载节省 90% 的冗余 Token 消耗State 作为中间介质工具写入 StateProcessor 读取 State写入和读取完全解耦支持跨轮次传递Agent 作用域隔离Key 中嵌入 Agent 名称多 Agent 共享 Session 时互不干扰双注入策略System Message 注入 Tool Result 注入简单场景与缓存优化场景各取所需StateDelta 机制Tool 声明 Delta框架统一提交可测试、可审计、可事务化三级生命周期once / turn / session灵活适配从一次性查询到长期任务的各种场景LRU 淘汰策略超限时保留最近使用的 Skill防止 Prompt 膨胀保障响应质量回退机制Tool Result 不存在时回退 System Message对话压缩后仍保证技能内容不丢失懒迁移策略检测旧格式时即时迁移平滑升级无需全量数据变更十三、写在最后回到开头的问题——当 Agent 需要十八般武艺时我们的解法是「不要一次性给」让 Agent 先看菜单「让 Agent 自己选」通过工具调用主动加载「用 State 做桥梁」将加载状态持久化与注入逻辑解耦「用 Processor 做注入」在每次请求前自动将已加载内容组装到 Prompt 中「用生命周期管理做兜底」确保内容不会无限累积这套机制的核心价值在于它把Agent 应该知道什么这个问题从静态配置变成了「动态决策」——让模型自身成为知识加载的决策者框架只负责提供高效、安全、可靠的加载管道。