面试知识库

14. Skill 体系#

一句话结论:skill 是放在 system prompt 外的按需知识块——目录常驻(name + description),正文由模型自己判断要不要调 Skill(skill=…) 读进来;代价是漏触发看不见,所以用真跑模型的触发率验收兜住(13/13 正例、0/5 误触发)。

速背卡#

#hint(≤15 字)展开一句
1目录常驻、正文按需每轮只付 7 行 description,正文只在触发那轮进上下文
27 份内置 skillsearch-discovery / purchase-research / bundle-planning / order-care / memory-personalization / image-shopping / cross-border-duty
3预注入 → 模型自觉D1 按 planner 预注入,S2 整条删掉,改模型读 description 自己判
4同轮发出,不等往返Skill(...) 和 item_search(...) 同一次回复里一起发
513/13 触发率S4 首轮 11/13,收紧「单品直搜」边界后 13/13,负例误触发 0/5
6description 是唯一触发面正文写得再好,description 不准就永远读不到
7present_guide 是终结工具「怎么挑」类答案从 chat_fallback 拆出来,分节 + 来源,调完即收尾

30 秒版#

skill 就是把「某一类活的完整打法」从 system prompt 里搬出来,写成 skills/<name>/SKILL.md。框架只把每份的 name + description 拼成 <agent-skills> 常驻在 system prompt 后面,正文由模型自己调内置 Skill 工具按需读。好处是每轮固定成本只有 7 行描述,坏处是可能漏读——所以我做了一套真跑模型的触发验收集,18 条 query 看模型第一批工具调用里有没有那份 skill,改了一句分流表措辞就从 11/13 到 13/13。

3 分钟版#

  1. 问题:system prompt 一路涨到 144 行,里面大半规则(套装怎么拆、订单怎么答、关税口径)一轮只有一类用得上,却每轮都付 token、每轮都要模型读一遍。
  2. 做法:按意图切成 7 份 SKILL.md,框架 LocalSkillLoader 扫 skills/,只把 name + description + dir 进 system prompt(app/agent/skills.py)。
  3. 加载:模型看 description 判断,调 Skill(skill="search-discovery") 拿正文。prompt 里一张 7 行意图分流表告诉它「这轮像哪行读哪份」。
  4. 中间走过弯路:D1(42311aa)按 planner 的 bundle_slots 确定性预注入套装 skill 省一次往返;S2(8b851f4)把这条整条删了,理由见下节。
  5. 同轮发出:prompt 明写「Skill(…) 和本轮第一个检索/读取工具同一次回复里一起发」,不等 skill 回来再搜。
  6. 验收:scripts/eval/run_skill_trigger.py 在 post_reflect 挂探针,拿到第一批 tool_call 就抛 BaseException 掐断整轮,只判「读没读对那份」,一轮实付约 $0.007。
  7. 用户侧:用户可以写自己的 skill(user_skills 表,my/ 前缀),走同一条框架通路;也可在输入框 / 显式选一份,正文以 <selected-skill authority="reference_only"> 拼进本轮用户消息。

它解决什么问题#

① 每轮都为用不上的规则付钱

  • 坏法:所有打法写进 system prompt。144 行里套装 17 行、订单细则、research 口径每轮都在,而一轮只用得上一类。坏了看不见——只表现为输入 token 偏高。
  • 修法:S1(53a48ea)切出 3 份新 skill,system prompt 144 → 126 行,只留每轮都成立的东西(自动 price_compare + item_picker、ask_user 两种形态、<termination> / <constraints> / <security_boundary>、两条红线)。
  • 代价:多了一次模型判断,判错就等于那份打法没生效。

② 预注入省往返 vs 前缀稳定

  • 坏法(D1,42311aa):planner 拆出 len(bundle_slots) >= 2 就由控制面预取 bundle-planning 正文注入,省掉模型「读 description → 调 Skill」那次往返。
  • 修法(S2,8b851f4):整条删掉(_prefetch_skill / _skill_prefetch_due / BUNDLE_SKILL_NAME / SKILL_PREFETCH 全删),改模型自觉。三条理由:往返本来就能靠同轮发出省掉;判据不硬(除 bundle_slots 外只能退回关键词正则,而 bundle_slots 本身也是 LLM 输出);HarnessSession 每轮新建 + state.context 跨轮累积,预注入会让同一份正文在多轮会话里躺 N 份(实测 prefill_planner 在 3 轮会话里出现 6 次)。
  • 代价:漏触发从「机制保证不会发生」变成「靠验收发现」。所以才有 S4 那套触发率脚本。
  • 分界线(写进 app/harness/prefill.py 模块 docstring 防回潮):机制只管事实接地(KB 预取、图理解),模型自觉管打法加载。

③ 「怎么挑」类答案没有合适出口

  • 坏法:选购标准答案和「你好」「谢谢」共用 chat_fallback,整段塞进 message,来源混在正文末尾、分节靠模型自己打标题。
  • 修法:S3(3943ecf)加 present_guide 终结工具,把结构变成字段(GuideSection 标题 + 要点、明说的假设、来源表),前端出指南卡(frontend/src/components/GuideCard.tsx)。
  • 代价:多一个终结出口,模型要多判一次走哪个;靠 <termination> 一句话分(讲标准走 present_guide,其余走 chat_fallback)。

④ 文案被工具内部 LLM 换掉

  • 坏法:终结工具在内部再调一次 fast 模型「归纳一下」。2026-09-16 的教训:1500+ 字符的电动牙刷选购指南被归纳成一句客套话。
  • 修法:present_guide 与 shopping_summary 同口径——文案由主模型在入参里给,工具只排版(app/tools/present_guide.py 模块 docstring)。
  • 代价:入参变长,主模型要一次把结构写对。

⑤ 用户想让 Agent 按自己的套路选购

  • 坏法:给用户开「自定义 prompt」,等于让用户改系统指令。
  • 修法:用户个人 skill(app/db/user_skills.py)走同一条框架通路——name 加 my/ 前缀进 <agent-skills> 目录、正文由内置 Skill 工具按需读,不加新工具、不改 harness。显式选用那条路明标 authority="reference_only":不能新增工具、不能扩权、不能代替下单确认、不能改本轮硬约束。
  • 代价:多一次 SELECT(每次模型调用都会 list_skills),单机 SQLite 几百微秒。

7 份内置 skill 各管什么#

skill管什么分流表上的触发行
search-discovery描述性需求 → 一份清单:哪些约束进 filter、哪些进 query、怎么核「都在预算内」多约束叠着 / 送礼 / intent_grounding=web / 召回空
purchase-research先讲判断依据再对标准;澄清最多一轮问「这品类怎么挑」/ 评价某款值不值(evaluate)
bundle-planning槽位规划:bundle(一套齐,MCKP)vs parallel(多类并列)两种形态plan 的 bundle_slots ≥ 2
order-care下单 / 查单 / 取消 / 售后:确认卡机制、取消的强制顺序、办不了的怎么说这轮沾到订单
memory-personalization长期事实在哪、怎么写、被问起时怎么答记忆本身成为话题
image-shopping图搜措辞与边界:找到的是同品类相似款、不是同款用户传了参考图
cross-border-duty到手价口径:关税、de minimis、运费分档、收货国四层解析关税 / 运费 / 到手价

前 3 份与 order-care 出自 S1/S3(对照 ../commerce-agents/shopping-agent/skills/ 同名 skill,本仓数据支撑不了的规则如缺货替代、退货窗口、物流事件按计划删掉);memory-personalization 出自 M4;image-shopping、cross-border-duty、bundle-planning 是批 4-3(e11e9f1,2026-09-07)就有的三份。

内置 skill vs 用户个人 skill#

内置个人
存哪skills/<name>/SKILL.md,随仓库走user_skills 表,按 user_id 归属
谁能改只有我(前端只读,可「复制为我的」当初稿)用户自己,CRUD 在 app/api/skills.py
名字空间目录名my/ 前缀(USER_SKILL_PREFIX),撞不上内置
怎么进上下文LocalSkillLoader(scan_subdir=True) → <agent-skills>UserSkillLoader.list_skills() → 同一个 <agent-skills>
上限无(但每份多一行常驻 description)每人 20 份 / description ≤400 字 / 正文 ≤8000 字
额外入口—输入框 / 显式选中 → 正文拼进本轮用户消息

机制怎么跑#

  1. skill_loaders()(app/agent/skills.py)返回两个 loader:LocalSkillLoader(SKILLS_DIR, scan_subdir=True) + UserSkillLoader()。SKILLS_ENABLED=0 时返回空表。
  2. scan_subdir=True 是必须的:默认只在给定目录自身找 SKILL.md,本仓布局是 skills/<name>/SKILL.md,不开就静默加载 0 个(框架只打一行 info)。
  3. 框架在 _get_system_prompt 里现算 <agent-skills> 块,拼在本仓定稿 prompt 之后:[基线正文 + on_system_prompt 钩子策略块] + [<agent-skills> 目录块]。顺序是框架定的,不去抢。
  4. 框架自动挂内置只读工具 Skill(SKILL_VIEWER_TOOL_NAME = "Skill"),白名单 app/security/tool_whitelist.py 要认得它。
  5. 模型读 prompt 里的 7 行意图分流表(prompt/prompts.yml,<workflow> 段)判这轮像哪行,把 Skill(skill="…") 和本轮第一个检索工具同一次回复发出去。
  6. 正文回来后模型照着那份打法走剩下的轮次;Skill 是只读工具,权限永远 ALLOW。
  7. 用户显式选用时走另一条路:orchestrator.py:401 resolve_selected_skill(skill) 按目录名找正文,orchestrator.py:446 把 render_selected_skill 的 <selected-skill> 块拼到本轮 turn_query 末尾(不是 system prompt),模型不必再调 Skill。
  8. 讲标准类答案收尾:模型调 present_guide(在 TERMINAL_TOOLS 里,app/agent/constants.py:34),入参就是最终答案,工具只做形状校验(≤6 节 / 每节 ≤5 点 / ≤8 条来源)与排版,前端渲染指南卡。

触发率怎么验收#

为什么必须真跑模型:S2 之后 skill 加载是模型的一次自由判断,没有任何确定性判据可断言,单测测不出来。

  1. 标注集内联在脚本里(scripts/eval/build_skill_trigger.py 的 CASES)——data/* 被 gitignore,人工策划的 query 没法复现,所以数据跟代码走,main() 生成 data/eval/skill_trigger.jsonl。
  2. 18 条:6 份 skill 各出正例共 13 条 + 5 条负例(不该读 skill 的单品直搜等)。image-shopping 登记在 UNCOVERED(判据是 image_paths,uploaded/ 被 gitignore,无可复现图源),理由原样写进产物头部,不让「没测」看起来像「测过没事」。
  3. 覆盖自检:skills/ 下每份要么有正例、要么在 UNCOVERED 里,否则 SystemExit。
  4. 截停点选 post_reflect(app/harness/adapter.py 的 _run_post_reflect):跑在 on_reasoning 之后、工具执行之前,拿到第一批 tool_call 就判定。
  5. 用 BaseException 掐断(_StopAfterFirstCall):HarnessMiddleware.run 的最后一层是 except Exception,普通异常会被吞掉只留一行日志。探针不用模块级装饰器注册——否则「import 一下」就往生产 pipeline 里插了个会掐断别人用例的 hook,装的动作留给 main。
  6. 四种判法分开记:PASS / MISS(压根没读)/ WRONG_SKILL(读错份)/ 负例 FALSE_FIRE——前两者要修的东西不一样。
  7. 结果:首轮 11/13、误触发 0/5(451b6d5)。两条 MISS(多约束、送礼)的第一批都只发了 item_search,而 search-discovery 的 description 明写了这两种情形——不是描述没覆盖,是「我已经知道该搜什么」被当成了不读的理由。
  8. 修的是分流表措辞(c922059):收紧「单品直搜」那条例外——只有点名具体东西(型号/书名/明确款式)才算;叠着的约束与送礼哪怕检索词一眼能写出来也要先读,并点明那份 skill 讲的是检索词之外的事。结果 13/13,负例仍 0/5(触发率不是拿多读换来的)。
  9. 没解决的:同轮发出那条也给了具体形状(同一次回复两条工具调用 + 例子),没起作用——13 条正例里只有 2 条同轮带了业务工具,报告里记为 lone_skill_round(11 条)。留作下一个靶子。

演进时间线#

日期提交改了什么为什么
2026-09-07e11e9f1批 4-3:接框架 LocalSkillLoader,3 份 skill(cross-border-duty / bundle-planning / image-shopping)只发主 Agent不自建 loader;目录常驻正文按需
2026-09-1455f1981个人 Skill:按用户存库 + 进 <agent-skills> + 输入框 / 显式选用用户要自己的选购套路,又不能让他改系统指令
2026-09-19c300282M4:新增 memory-personalization,删 skills/memory-forget/记忆改成只经模型上下文生效
2026-09-1942311aaD1:套装 skill 按 bundle_slots 预注入 + 新增 memory-forget省掉「读 description → 调 Skill」那次往返
2026-09-1953a48eaS1:新增 search-discovery / purchase-research / order-care,bundle-planning 并入 planning-goals 四条,system prompt 144 → 126 行对照 Anthropic commerce-agents 的 skill 切法
2026-09-198b851f4S2:删除全部 skill 预注入(含 D1),改模型自觉 + 同轮发出判据不硬、正文跨轮重复躺 N 份、往返本可同轮省掉
2026-09-193943ecfS3:present_guide 终结工具 + 前端指南卡「怎么挑」的答案形状固定,不该和闲聊共用出口
2026-09-19451b6d5S4:触发验收集(18 条)+ 真跑模型的触发率脚本模型自觉之后漏触发只能靠验收发现
2026-09-19c922059S4 靶子一:分流表收紧「单品直搜」边界正例 11/13 → 13/13
2026-09-220d50ff4Skill 页分「我的 / 系统内置」两组 + 内置只读详情 + GET /api/skills/builtin/{name}内置 7 份原先只在输入框菜单里看得到

数字与证据#

数字指什么来源状态
7内置 skill 份数ls skills/(HEAD 634f4ab)✓
144 → 126 行S1 后 system prompt 行数53a48ea 提交正文✓(提交正文)
13/13、0/5S4 正例触发 / 负例误触发data/eval/skill_trigger_report_r2.json 的 summary✓
11/13S4 首轮正例触发451b6d5 提交正文 + skill_trigger_report.json✓
约 $0.007跑一轮 18 条的实付451b6d5 提交正文仅口径
6 次 / 3 轮预注入导致 prefill_planner 在 3 轮会话里重复躺的次数8b851f4 提交正文✓(提交正文,未复跑)
20 / 400 / 8000个人 skill 每人份数、description 字数、正文字数上限app/db/user_skills.py:21-23✓
线上个人 skill 数据量用户在 gcjp 上有没有写过个人 skill—未验证

追问 10 题#

  1. 问:skill 和把知识写进 system prompt 有什么区别? 答:成本模型不同——skill 是目录常驻(每份一行 description)、正文按需;写进 prompt 是每轮都付。证据:app/agent/skills.py 模块 docstring。
  2. ⚠ 问:skill 会不会破坏前缀缓存? 答:<agent-skills> 块是静态的(只随 SKILL.md 文件变)且排在策略块之后,缓存本来就断在策略块;但跑任务期间改 SKILL.md 会让这一轮后续所有请求的 system prompt 变掉,从头失效(框架每次调用都重扫目录 + getmtime)。证据:同上,推论 2、3。
  3. ⚠ 问:为什么不按 planner 结果确定性预注入? 答:试过(D1)又删了(S2)。三条:同轮发出就能省掉那次往返;除 bundle_slots 外没有硬判据,而它本身也是 LLM 输出;state.context 跨轮累积会让正文重复躺 N 份。
  4. 问:模型漏读 skill 怎么办? 答:目前只能靠触发率验收发现,然后改 description 或分流表措辞——S4 那次改的是 prompt 里「单品直搜」的例外边界,不是 skill 正文。
  5. 问:触发率脚本为什么要抛 BaseException? 答:HarnessMiddleware.run 的最后一层是 except Exception,普通异常会被吞掉,只留一行日志,整轮照跑照收费。
  6. 问:负例是干什么的? 答:防「靠让模型多读换触发率」。13/13 那次负例仍是 0/5,说明提升不是拿误触发换的。
  7. 问:用户个人 skill 会不会成为提权口子? 答:两层——进目录那条路和内置走同一个只读 Skill 工具,不加新工具不改 harness;显式选用那条路明标 authority="reference_only",正文里写「不能新增工具、不能扩权、不能代替下单确认、不能改本轮硬约束」。
  8. ⚠ 问:present_guide 和 shopping_summary 什么关系? 答:同一条口径(文案由主模型在入参给、工具只排版),但服务两类答案:前者是判断依据、没有商品卡垫着,答案被换掉就什么都不剩;两者都在 TERMINAL_TOOLS。
  9. 问:Skill 工具算读还是写? 答:只读,权限永远 ALLOW,它是框架内置的 SkillViewer,名字就叫 Skill。
  10. 问:为什么 EXPECTED_SKILLS 是白名单不是自动扫描? 答:每份 SKILL.md 给每轮 system prompt 常驻多一行 description,这笔开销要有人看见——加一份就得改一次测试。证据:42311aa 提交正文、tests/test_skills.py。

坑与易混点#

  1. LocalSkillLoader 不开 scan_subdir=True 会静默加载 0 个 skill(只有一行 info 日志),表现为「写了 skill 但模型从来不知道」。
  2. 跑任务期间改 SKILL.md 会让那一轮的前缀缓存从头失效——改完重开一轮。
  3. app/agent/skills.py 的模块 docstring 还写着「三个 skill」「三条推论」,是批 4-3 的原文;现在内置是 7 份,docstring 未同步(不改代码,记在这里)。
  4. skills/memory-forget/ 被 D1 加过又被 M4 删掉——看提交历史容易以为它还在,当前 skills/ 下没有。
  5. 「模型自觉」只保证读到,不保证同轮发出:13 条正例里 11 条是单独一轮只发 Skill(报告字段 lone_skill_round),prompt 写了形状也没扳回来。

本章和别章的接口#

  • 主循环、TERMINAL_TOOLS 与终结判定在第 2 章。
  • 长期记忆的写入与注入(memory-personalization 讲的那套事实)在第 5 章。
  • Rubric 评测与其他离线评测脚本在评测那章;本章只讲触发率这一条验收。