# 排障记录:人设 + 技能如何做到"跟随专家动态切换" > 目标功能:选中专家 → 该专家人设进 system prompt、该专家 skills 进技能目录,且切换立即反映、跨会话隔离。 > 这行列出了实际遇到的 4 个问题、根因与修法,供后续维护/二次开发参考。 > UI 侧问题见 docs/layout-debug.md;最终设计见 docs/architecture.md。 --- ## 背景:验证链路 技能/人设的"生效面"各有独立检查点: - **技能**:会话 system prompt 里的 ``(由 `tool-skill` 在 `agent/pre-step` 用 `skills.snapshot({ scope: agent })` 生成,**按 digest 缓存**——选中后需发一条消息触发新一步才会刷新)。 - **人设**:会话 system prompt 本身(dsh **每模型步重组装** system prompt)。 --- ## 问题 1:`agent.ctx.skills` 抛 "cannot get property 'skills' without inject" **现象**:想按会话挂技能,调 `agent.ctx.skills.registerProvider` 抛 `Error: cannot get property "skills" without inject`;人设 `agent.ctx.systemPrompt.section` 却正常(`applyPersona ok`)。 **根因**:`agent.ctx` 是 agent 的运行时作用域 ctx,其 **inject 清单里没有 `skills`**;`ctx.skills` 从 宿主根 ctx(`inject:['skills']`)访问才可用。而 `systemPrompt` 在 agent.ctx 上恰好可用(这就是人设正常、技能报错的差异来源)。 **修法**:技能**不**走 `agent.ctx.skills`,改为在**宿主根 ctx 注册全局 provider**(见问题 3)。 --- ## 问题 2:技能切了不生效 / 要重进会话才生效(人设同理) **现象**:选中技能后 `` 没有;切换后要重进会话才生效。 **根因(两处)**: - `` 是 **digest 缓存**的 catalog 消息,选中后必须**发一条新消息**触发 `pre-step` 才会刷新; - 人设走 per-agent `agent.ctx.systemPrompt.section`,实测"要重进会话才生效"(agent 作用域注册在运行期不可靠)。 **修法**: - 技能:注册后/选择变更时 `providerControl.invalidate()` 刷 catalog 缓存(见架构篇)。 - 人设:**弃用** per-agent `agent.ctx.systemPrompt.section`,改用**全局一段 + 函数式 `text`**, 每步按 `context.agent.id` 读内存表 → 切换下一步即生效(最终方案)。 --- ## 问题 3:换了全局 provider 后,所有专家的技能都可见 **现象**:全局注册后技能能看到了,但 `` 里**出现所有专家的技能**(非本会话所选)。 **根因**:第一个全局实现用 `FileSystemSkillProvider` + `customSkillDirs` = **所有已选专家的并集**, 跨会话合在一起,故一个会话能看到别的专家的技能。 **修法**:改成 **scope 门控 provider**(`createExpertSkillProvider`):仍在全局层注册(全局必然被每个 `snapshot({scope})` 查询),但 `list(options)` 用 `options.scope` → sessionId → 该会话所选专家 → **只返回其技能**; 未选专家/无技能会话返回空 → 严格按会话隔离。 --- ## 问题 4:`skill provider "experts" returned skill "browser-use" for provider "experts:senior-developer"` **现象**:门控后出现此报错,技能被丢弃。 **根因**:dsh 校验 **`candidate.provider === provider.name`**(`validateCandidate`);外层 provider 名 `experts`, 但内层 `FileSystemSkillProvider` 产出的候选 `provider` 是它自己配置的 `experts:`,不匹配 → 被拒。 **修法**:在 `list()`/`get()` 返回前把候选/定义的 `provider` 字段改写成外层名 `'experts'` (`candidate` 读取靠 `locator`,改写字段不影响 `get()` 解析)。 --- ## 最终落地的两个"官方范式"要点(来自 dsh-skill-hub 参考) 1. **访问 `ctx.skills`**:宿主根 ctx + `inject:['skills']`(`agent.ctx` 不可用)。 2. **浮层/动态内容**:注册进 **global 层**最能稳定被查询;按会话区分靠"读取方上下文"(`options.scope` / `context.agent`)**门控**,而不是靠作用域分层注册。