Skill 运行时配置机制
借鉴 GitHub Actions 的分层变量模型:四个层级逐层覆盖,在 Skill 被加载时注入成环境变量。
问题
一个 Skill 想跑起来,需要一批"配置"——比如工作目录、数据库地址、数据库密码、要查哪张表。
问题是没有一个人能知道全部:平台知道环境,开发者知道连哪些资源,用户知道这次要干嘛。
还有个特殊要求:开发者知道密码,想让 Skill 调工具时能用,但不想让用户和智能体看到明文。
借鉴 GitHub:分层变量 + 逐层覆盖
GitHub Actions 早就解决过同一个问题。它不给变量打"来源"标签,而是分成几个层级,
同名变量越靠近具体使用场景的层级,优先级越高。我们照搬这个思路。
GitHub Actions 的做法
- Organization 变量(最通用)
- Repository 变量(覆盖 org)
- Environment 变量(最具体,覆盖前两层)
- Variables 明文 / Secrets 打码,分开存
我们的四个层级
- 平台层(最通用)
- 组织 / org 层(覆盖平台)
- Skill 自己层(覆盖 org)
- 用户配置层(最具体,覆盖前三层)
四个层级怎么覆盖
每一层都可以为同一个配置项提供值。下面的层覆盖上面的层,
谁离用户最近、谁说了算。
① 平台
系统自动提供环境信息,所有 Skill 通用的兜底值
WORKSPACE_DIR
▼ 被下层覆盖 ▼
② 组织 / org
同一个组织下多个 Skill 共享的值(如组织统一的数据库)
DB_HOST
▼ 被下层覆盖 ▼
③ Skill 自己
Skill 开发者发布时填的、这个 Skill 专属的值
DB_PASSWORD(密钥)
▼ 被下层覆盖 ▼
④ 用户配置
用户在使用前 / 使用中提供的值,优先级最高
TARGET_TABLE
最终值 = 从上往下合并,下层赢
不需要"声明谁来填"。 每一层都可以给任意配置项赋值,机制完全一样。
某个值由哪一层提供、被哪一层覆盖,是运行时合并出来的,不用提前指定。
同一层内不会冲突。 每一层就是一张"变量名 → 值"的表,一个名字在一层里只有一个值。
所以同一个 org 下的两个 Skill 声明了相同的变量名——它们在 org 层读到的就是同一个值,
这正是"org 层共享"想要的效果。如果这两个 Skill 偏偏需要不同的值,
就各自在更具体的 Skill 层把它覆盖掉(Skill 层是按单个 Skill 隔离存的,互不影响)。
关键边界:两个 Skill 同名变量、值却不同
举个会让人担心的例子:「获取商品」声明了 DB_URL,
「获取用户」也声明了 DB_URL,但两个 Skill 要连的是不同的库。
用户说"获取某用户的全部商品",智能体先加载 user skill、再加载 product skill——
看起来 DB_URL 就撞了。
实际上不会撞,因为环境变量不是注入到一个全局共享的环境,
而是「执行某个 Skill 的工具时,只注入那个 Skill 自己的那份配置」(见流程第 4 步):
# 调 product skill 的工具时: # 调 user skill 的工具时:
DB_URL=商品库 DB_URL=用户库
两个 DB_URL 永远不会同时出现在同一个环境里。
变量名只要求"在单个 Skill 内唯一",跨 Skill 天然隔离。
这给实现定了一条硬约束:注入必须按每一次工具执行隔离,
绝不能写全局 os.environ。
子进程工具/命令/MCP 用传 env= 的方式各传各的;
进程内运行的工具不能依赖真实的全局环境变量,要用上下文传参拿到本 Skill 的配置。
只要守住这条,"同名不同值"就不是漏洞,而是被设计正常处理掉的情况。
怎么保证两次执行的环境真的不一样
上面说"调 product 的工具时 DB_URL=A,调 user 的工具时
DB_URL=B"——凭什么能保证?
关键是:执行环境不是某个会被改来改去的"当前状态",
而是每次调用时,从"这个工具属于哪个 Skill"现场算出来的。靠两件事:
1
工具和 Skill 的归属,在加载时就定死了
blade-agent 加载一个 Skill 时,它的工具注册进 ToolRegistry
就带上了 skill_id 命名空间(这个绑定代码里已经有)。
一个工具属于哪个 Skill,是加载那一刻定死的事实,之后不会变。
2
每次调用,env 是现场从 skill_id 推出来的
工具调用的分发逻辑走一条纯推导链,每次调用独立跑一遍:
拿到要调的工具
→ 查它的 skill_id # 注册时就带着
→ 取这个 skill 的配置表 # 加载时按 skill_id 合并好、存好
→ 现场拼出这一次执行的 env
→ 把 env 传给这一次执行
调 product 的工具
skill_id = product
→ product 配置表
→ DB_URL=商品库
调 user 的工具
skill_id = user
→ user 配置表
→ DB_URL=用户库
为什么不会错:因为根本不存在一个"全局当前环境"被先后改写——
也就没有"上一次的值残留到下一次"的问题。
每次调用都是 工具 → skill_id → 配置表 → env
这条链独立算一遍。"保证不一样"不靠时序上的小心,
而靠这条不可变的推导链:env 由 skill_id 决定,skill_id 由工具决定,
工具归属在加载时定死。
完整流程
1
Skill 声明它需要哪些配置项
在 SKILL.md 顶部的 frontmatter 里,只列出名字,并标出哪些是密钥。
不写值、不写来源。
config:
- WORKSPACE_DIR
- DB_HOST
- DB_PASSWORD secret: true
- TARGET_TABLE
↓
2
智能体在运行过程中动态选择并加载某个 Skill
注意:Skill 不是 session 一开始就定好的。智能体是在 session 运行途中
根据任务挑选 Skill 并加载的。所以配置的合并和注入,发生在每次加载 Skill 的那一刻,
不是 session 启动时。
↓
3
加载这个 Skill 时,针对它合并四层配置
平台 → org → Skill → 用户,按覆盖规则合并出这一个 Skill 的最终值。
谁有值谁填,下层盖上层。不做"是否填全"的预先检查。
不同 Skill 各自合并,互不影响。
↓
4
执行这个 Skill 的工具 / 命令 / MCP 时,把合并结果作为环境变量注入
合并好的值绑定在这个 Skill 上。之后凡是执行它的工具、命令、连它的 MCP,
就把这些值注入到那次执行的环境变量里。这是核心动作。
# 执行该 Skill 的工具时,环境变量里:
WORKSPACE_DIR=/workspace
DB_HOST=10.x.x.x
DB_PASSWORD=••••••••
TARGET_TABLE=orders_2026
↓
5
Skill 的代码 / 工具 / MCP 用 os.environ 读
需要哪个配置就读哪个。智能体只管调工具,不碰这些值。
没填全怎么办?不用管。
如果某个配置项四层都没人提供,不要去检查、也不要提前告诉模型。
直接跑就行——如果模型真的用到了,工具自然会报错,模型看到报错会自己去问用户。
省掉一整套校验逻辑。
密钥为什么自动就安全了
关键点:智能体(大模型)根本不读环境变量。环境变量是给 Skill 代码用的,
不进入和大模型的对话。密码进了环境变量这一步,天然就和大模型隔开了。
✅ 能用到密码的
- Skill 的工具代码 读 env
- Skill 连接的数据库 / MCP 读 env
🚫 看不到密码明文的
- 智能体 / 大模型 不读 env
- 业务用户 界面不显示
- SKILL.md 文件本身 只有名字声明
唯一要补的小动作:工具结果回给智能体之前做一次打码——
万一某个工具把密码原样回显,自动替换成 ••••,
防止密码顺着工具结果漏进对话。
两个项目怎么分工
这套机制要同时改 skill_registry(Skill 注册中心)和 blade-agent(智能体本体)。
四个层级的值分别住在两个项目里。
| 层级 |
值住在哪 |
怎么来的 |
| 平台 |
blade-agent |
运行环境自带 |
| org |
skill_registry |
org 是 registry 的概念,多个 Skill 共享 |
| Skill |
skill_registry |
开发者发布 Skill 时填,跟着 Skill 走 |
| 用户 |
blade-agent |
智能体在 session 里追问出来的 |
skill_registry 要改
- parser 解析 SKILL.md 的 config: 声明段(现在只读 name/description)
- 新增 org 层 / Skill 层配置值的存储;密钥单独加密存
- 这些值不写进 SKILL.md 正文、不打包进下载 zip(否则会展示给大模型)
- 扩展已有的"取 Skill 内容"接口:响应里额外带上 org 层 + Skill 层的值(含密钥)
blade-agent 要改
- frontmatter parser 也解析 config: 声明
- 平台层值从运行环境取;用户层值由智能体追问后存进 session
- 加载 Skill 时把四层合并出最终值
- 执行该 Skill 的工具 / 命令 / MCP 时注入环境变量
- 工具输出打码
不用新开连接。 blade-agent 运行时本来就通过 HTTP 连 skill_registry——
搜索、Get Skill Content(下载并读取)。org 层 / Skill 层的值跟着 Skill 内容一起回来,
复用这条已有的通道。密钥安全的边界是:它出现在 API 响应里(发给 blade-agent 运行时),
但不出现在 SKILL.md 正文里(发给大模型)。
这版方案刻意不做什么
- 不做占位符语法(${{ }})——直接用环境变量,Skill 代码用 os.environ 读。
- 不做 type: resource 那种"指向某个资源实例"——配置项就是普通字符串,按层覆盖。
- 不在 SKILL.md 里声明"谁来填"——来源是层级合并的结果,不用提前指定。
- 不做"是否填全"的预先校验——交给工具报错 + 模型自己追问。