Skip to content

[Feature] Skills 显示模式:仅列名称即可省下约 12k tokens/请求,所有技能仍可加载 / Skills display mode (full | names | off): ~12k fewer prompt tokens per request, all skills stay loadable #404

Description

@fkhb90

Problem / 问题

中文

  • PI-Desktop 每一轮请求都会把所有可见 skill 的完整 id — name: description 塞进 system prompt 的 # Skills 区块。
  • 实测:561 个已安装 skill、331 个可见时,该区块 73,130 bytes;395 个可见时 85,457 bytes,占固定前缀约 69%
  • 同一 session 实测固定前缀 token 长度:req1 = 29,479 tokens(占该轮 89,944 峰值上下文的 32.8%)。这部分成本每一轮都付一次(后续以 cacheRead 计费,但仍是同一个前缀在反复上传)。
  • 这段内容是模型极少完整阅读的目录元数据:基准 session 中共 31 次工具调用,Skill 工具一次都没被用过
  • 目前没有任何开关可以调整。我已确认 app 设置模型只暴露 theme / mode / permissions / command shell / context usage display / large-paste threshold / font / language / proxy / contextCompaction没有任何 skill 相关项;在 app.asar(main + renderer,49 个文件)中搜索 compact_allskillsCompactnamesOnlyshowDescriptionshideDescriptionsdescriptionModeskillVisibility 等候选键,0 命中

English

Every request injects the full id — name: description line for every visible skill into the # Skills block of the system prompt. Measured on a real install: 73,130 bytes with 331 visible skills; 85,457 bytes with 395 visible (≈69% of the fixed prefix). Measured fixed-prefix token length: 29,479 tokens at req1, i.e. 32.8% of that session's 89,944-token peak context — paid on every single request. In the baseline session, 31 tool calls were made and Skill was never used once.

There is no switch for this today. Verified: the settings model exposes theme, mode, permissions, command shell, context-usage display, large-paste threshold, font, language, proxy and contextCompaction — nothing skill-related; and a targeted search of app.asar (main + renderer, 49 files) for compact_all, skillsCompact, namesOnly, showDescriptions, hideDescriptions, descriptionMode, skillVisibility returns zero hits.

Relevant code, resources/agent-runtime/sidecar.js:

// src/plugin-skills-prompt.ts
function pluginSkillsPrompt(skills) {
  if (!skills.length) return void 0;
  return [
    "# Skills",
    "",
    `Plugins have taught you the following skills. ...`,
    "",
    ...skills.map((skill) => {
      const description = skill.description?.trim();
      return `- \`${skill.id}\` \u2014 ${skill.name}${description ? `: ${description}` : ""}`;
    })
  ].join("\n");
}

No mode switch, no truncation. Note the formatter already emits the names-only shape when description is absent.

Proposed change / 期望改动

新增一个 skills 显示模式设置(三个值),并可选加一个描述长度上限:

mode 输出 用途
full(默认,= 现状) - `id` — Name: Description 现有用户行为不变
names - `id` — Name 大型 skill 库推荐默认
off 省略整个 # Skills 区块 完全在别处管理 skill 的场景

可选:描述上限(字符数),给想要折衷的用户。

为什么 names 是最佳平衡点:每个 skill 的名称仍留在 prompt 里,可发现性不变——模型依然能看到全部 id,并可用 Skill 工具载入任何一个(该工具本来就会重新读取完整正文,且 pluginSkillsDigest 已把正文排除在快取摘要之外)。消失的只是那一行简介,而同一区块本来就在告诉模型「先读完整 skill 文件再动手」。

实测(真实 331 个可见 skill):

变体 区块 bytes 相对现状 每请求省下的 tokens(@4.05–4.59 B/tok)
full(现状) 73,130
names only 19,190 −73.8% −11,752 … −13,319
full,描述截到 120 字符 50,862 −30.4% −4,851 … −5,498

最小改动

function pluginSkillsPrompt(skills, mode = "full") {
  if (!skills.length || mode === "off") return void 0;
  return [
    "# Skills",
    "",
    `Plugins have taught you the following skills. ...`,
    "",
    ...skills.map((skill) => {
      const description = mode === "names" ? "" : skill.description?.trim();
      return `- \`${skill.id}\` \u2014 ${skill.name}${description ? `: ${description}` : ""}`;
    })
  ].join("\n");
}

两处调用点都要传入解析后的 mode(目前都写作 pluginSkillsPrompt(this.pluginSkills)):

  • RuntimeSession 构造 → 喂养 defaultSystemPrompt
  • composeSystemPrompt()if (tools.has(SKILL_TOOL_NAME)) { ... } 分支

快取安全要求(重要)

本 runtime 的 prompt 快取依赖前缀稳定,因此:

  1. mode 必须在 session 建立时解析一次并保持不变,与目前的 baseSystemPrompt 相同;不要每轮重新读取设置。
  2. mode 必须纳入 catalog digest(目前为 pluginSkillsDigest(this.pluginSkills) === pluginSkillsDigest(requestedPluginSkills),用于让 runtime 退休并重建 prompt)。否则切换设置要么不生效,要么更糟——在 session 中途改变前缀、把快取整个打坏(我在实测中见过一次同类事件造成约 29k tokens 的重新前算)。

验收标准

  1. names 模式下 # Skills 区块对每个 skill 都符合现有的「无描述」格式。
  2. 切换模式在新 session 生效,且反映在 pluginSkillsDigest(runtime 会退休)。
  3. 331 个 skill 的库,固定前缀下降 ≥ 11,000 tokens(可用下方方法验证)。
  4. 所有 skill 名称仍然存在,且 Skill(id=…) 仍能载入每一个。
  5. 稳态 cache hit 维持 ≥ 98%(基准 session 实测中位数 98.16%)。
  6. Skill 工具描述仍说明如何载入,区块的引导句仍要求「先读完整文件再动手」。

Alternatives / 其他方案

  • 只靠卸载/停用 skill — 已经可行,我也确实用这个方法砍掉了 64 个 skill。但它是钝器:用能力换 token,且每个 skill 都要人来判断。显示模式是用简介文字换 token,所有能力仍可触达。
  • 把描述截断到 N 字符 — 可行的折衷(120 字符省 30.4%),但描述会读起来像被切断的片段,且仍留下约 50 KB。
  • 直接缩短 SKILL.md 里的 description — 会伤害同一批文件的其他消费者(其他 agent、skill 列表 UI、人类阅读)。该修的是 prompt 的格式层,不是内容层。

另外:本提议与 #203(暴露 compaction 阈值)方向一致——都是把上下文预算交还给用户;与 #287 / #150(skill 市场、GitHub 链接)不冲突,可并存。

Additional context / 补充信息

数字怎么量出来的(可复现)

PI-Desktop 的序列化顺序是 [固定前缀][tools][对话],供应商做前缀快取。当 tools 数组在 session 中途变动时,快取会刚好在固定前缀结尾断开,此时 cacheRead 就等于固定前缀 token 长度。 这个事件可以刻意触发:调用一次 ToolSearch 启用任一 on-demand 工具,然后读下一个请求的 usage。数据源:~/.pi-desktop/sessions/<id>.jsonl 中每条 assistant 消息的 meta.usage

以此法取得的读数:

session 时点 断点处 cacheRead 触发
8d58db77 变更前 27,392 启用 Grep
3eac5178 停用 64 个 skill 后 24,704 启用 BrowserPreview

区块 bytes 的来源:skills 根目录下的 SKILL.md frontmatter(namedescription),用 pluginSkillsPrompt() 的格式字符串重组后量 UTF-8 bytes。

环境:Windows 11、PI-Desktop 0.14.8、561 个已安装 skill(停用后 331 个可见)、provider opencode-gohttps://opencode.ai/zen/go/v1)、model deepseek-v4.1-flash、上下文窗口 1M。

先例:Hermes Agent 已有等价开关 skills.compact_all: true,其文档描述为「system prompt 中的 skill 索引只显示 skill 名称(不含描述)……每个 skill 名称仍可见,且仍可透过 skill_view() 载入——只是丢掉描述」,并自估该区块省约 67%,与本次实测的 −73.8% 同量级。

建议标签area: skillsenhancement(我没有 triage 权限,无法在提交时自行设置)。

提交前已检索全部 224 个 issue,未发现重复

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions