Skills 系统三触发模式
Skills 系统三触发模式
学习目标
本章聚焦 LibreChat 的 Skills 系统。你将了解:
- 三种触发模式(手动、模型、always-apply)的工作原理与优先级
- 技能目录的预算感知注入策略
- ACL 权限与用户级活跃状态管理
- 同名技能碰撞处理
前置知识
下文假设你已理解上述概念,直接聚焦 LibreChat 的具体实现。
项目实践
三种触发模式
LibreChat 的 Skills 系统支持三种触发模式,每种有不同的激活时机:
| 模式 | 触发方式 | 注入位置 | 适用场景 |
|---|---|---|---|
| 手动调用 | 用户通过 $skill-name popover 选择 | 紧贴用户消息之前 | 用户明确需要某技能 |
| 模型调用 | 模型通过 skill tool 自主选择 | 模型工具调用轮次 | 模型判断需要某技能 |
| always-apply | frontmatter 标记 alwaysApply: true | 比手动调用更远的位置 | 持续生效的领域知识 |
注入优先级:always-apply 先注入(更远),手动调用后注入(更近)。LLM 倾向于关注最近的上下文,所以用户明确选择的技能获得更高的注意力权重。
技能目录注入
injectSkillCatalog 函数负责将技能列表格式化为 SKILL.md 目录,注入到 Agent 的 additional_instructions 中:
1. 分页查询 accessibleSkills(每页 100 条,最多 10 页)2. 过滤活跃技能(resolveSkillActive)3. 格式化目录(formatSkillCatalog),控制上下文预算4. 注入到 additional_instructions5. 注册 skill tool 定义上下文预算控制:
SKILL_CATALOG_LIMIT = 100:最多注入 100 个技能MAX_CATALOG_PAGES = 10:最多扫描 10 页formatSkillCatalog根据contextWindowTokens自动截缩描述
ACL 权限与活跃状态
技能访问控制通过 resolveAgentScopedSkillIds 实现三层过滤:
1. 管理员能力开关(skillsCapabilityEnabled) └─ false → 所有 Agent 无权使用技能
2. Agent 级开关(skills_enabled + skills 列表) ├─ 临时 Agent → 由对话级 toggle 控制 ├─ 持久 Agent → 由 builder 的 skills_enabled 控制 └─ skills = [] → 完整目录回退
3. 用户级活跃状态(resolveSkillActive) ├─ skillStates 显式覆盖 → 最高优先级 ├─ 所有者技能 → 默认活跃 └─ 共享技能 → 由 defaultActiveOnShare 控制同名技能碰撞处理
当同一租户内不同作者创建了同名的技能时:
getSkillByName按updatedAt降序返回最新的一个injectSkillCatalog对disableModelInvocation: true的技能进行去重:如果存在同名的可调用技能,丢弃不可调用的版本- 对目录中可见的重复名技能发出 warn 日志
disableModelInvocation 与 userInvocable
每个技能有两个独立控制字段:
| 字段 | 含义 | 影响路径 |
|---|---|---|
disableModelInvocation: true | 模型不能通过 skill tool 调用 | 不影响手动调用 |
userInvocable: false | 用户不能通过 $ popover 调用 | 不影响模型调用 |
这使得技能作者可以精细控制:“这个技能只能由模型自动使用,用户不需要看到”或”这个技能只允许用户手动触发”。
手动调用的粘性跨 Turn 保持
手动选择的技能通过 buildSkillPrimeContentParts 构造 tool_call 格式的 content parts,写入响应消息。这导致 extractInvokedSkillsFromPayload 在后续 turn 中将其识别为”已调用的技能”,从而自动重新注入——技能在当前对话中持续生效,直到用户开始新对话或编辑原始消息。
问题与规避
问题 1:技能目录为空但仍然注册了 skill tool
规避:catalogVisibleSkills.length > 0 条件控制——当所有技能都是 disableModelInvocation: true 时,不注册 skill tool(避免浪费 prompt 空间)。但 read_file 和 bash_tool 仍然注册,因为手动调用的技能可能需要读取文件。
问题 2:手动调用和 always-apply 重复注入同一技能
规避:injectSkillPrimes 执行交叉列表去重——如果同一技能名同时出现在手动和 always-apply 列表中,丢弃 always-apply 版本(manual 优先级更高)。去重发生在 cap 限制之前,确保 cap 反映真实数量。
问题 3:技能数量过多导致上下文溢出
规避:三级防御:
SKILL_CATALOG_LIMIT = 100硬性上限formatSkillCatalog根据contextWindowTokens截缩描述MAX_PRIMED_SKILLS_PER_TURN = 30限制每 turn 实际注入的技能体数量
设计取舍
三触发模式 vs 单一触发
三触发模式增加了系统复杂度,但提供了灵活性:
| 场景 | 单一触发的问题 | 三触发的优势 |
|---|---|---|
| 临时需要 | 模型可能遗漏 | 用户手动触发,确定性高 |
| 持续领域知识 | 每个 turn 都要模型判断 | always-apply 自动注入,零延迟 |
| 模型自主判断 | 用户不知道有哪些可用 | 技能目录让模型自行选择 |
LibreChat 的选择适合其多用户、多 Agent 场景:不同用户、不同对话需要不同的技能组合。