🐎 第3章 授人以渔:Skills 工程实践
📄 一句话概括

描述写得好不好是可以测的:备一批带标准答案的请求,改一版跑一遍,看错的是哪一类。 实测三版:命中八、十二、十二;三条描述的常驻成本八十六、一百八十六、一百七十三。 先做加法把命中率拉满,再做减法把成本压下来。

📝 本章和前面几章的关系

上一节定形状,这一节定描述。形状决定「用上之后干得好不好」,描述决定「用不用得上」。 这一节是全章唯一一个「能自己验证」的环节,值得认真跑一遍。

一、先备一批带标准答案的请求

测试的第一步不是改描述,是准备数据。要一批真实说法,每个都标好「该用哪个技能」。 几个要点:条数十几条就够;要覆盖每个技能至少两条;最容易混的那几类要多放几条。 演示里十二个请求、三个技能,其中四个是「解释代码」——因为这一组最容易被误判成审查。

请求要写成用户真实会说的话,别写成产品需求。 「这行代码什么意思」是真的,「请对该代码段进行语义解释」不是。 标答案这一步不能省。没有标准答案的测试只能看「感觉对不对」,改完之后也不知道是变好还是变坏。

二、第一版:欠区分

第一版三条描述都是「做什么」型:审查代码看边界条件、解释代码说明函数作用、提交代码写提交信息。 十二个请求对了八个。四个错里两个是「解释」被当成「审查」——因为两边都在说「代码」和「看」。 这不是过触发,是【欠区分】:两个技能的描述太像,模型分不开。

欠区分和过触发的区别在于:前者是「分不开」,后者是「谁都想要」。修法也不同。 欠区分要加区分度(场景词),过触发要加排除条件。第一版的毛病是前者。

三、第二版:补场景词(做加法)

修法很直接:每条描述后面补一句「用户说……时使用」,把用户可能用的词列进去。 命中率从八提到十二,全对了。代价是三条描述从八十六 token 涨到一百八十六,翻了一倍多。 这个代价在只有三个技能的时候不算什么。 但 3.3 节算过:技能数量乘描述长度就是常驻预算,二十个技能的时候,每条多三十 token 就是六百。

所以「命中率拉满」不是终点,它只是第一步。下一步是在不掉命中率的前提下把字减回去。

四、第三版:删枚举、加排除(做减法)

第三版做的是反过来的事:把枚举的「干嘛的」「为什么这么写」删掉,换成一句「要改代码、 要审查时不要用这个技能」。 命中率还是十二,三条描述降到一百七十三——比第二版少了十三,比第一版只多八十七。 为什么一句排除能顶掉几个场景词?因为场景词是「列举」,排除句是「划界」。 列举永远列不全,而划界一次就把整个邻域切掉了。

这就是「正向描述告诉模型什么时候用它,反向描述告诉它什么时候别用」的实际价值——不只是防过触发, 还能让正向那半写短。 所以迭代的正确顺序是:先做加法把命中率拉满,再做减法把成本压下来。 顺序反了会分不清「省了字」和「丢了场景」哪个起的作用。

五、每次只改一处

两轮改动,每轮只动一个方向:第一轮补正向,第二轮删枚举加反向。 如果一次全写上,跑出十二分,你不知道是哪句起的作用——下次遇到新场景,也就不知道该怎么改。 这条规矩和调参是一样的道理:一次改一个变量,结果才可归因。

还有一个细节:改完要看「错的是哪一类」,不是只看总分。 总分十二说明全对,但如果是十一,那两个错是不是同一类,决定了下一步该加正向还是加反向。

六、这套办法的边界

演示里用字面重合度代替模型的判断,因为本沙箱没有模型。 所以它能验证的是排序对不对,不能验证分值准不准。 真实描述在字面上可能跟请求一个词都不重合,但模型照样能判对——这一点这里测不出来。 因此这套办法的用法是:把字面重合当成一个下界。字面都对不上,模型多半也判不出来; 字面对上了,不代表模型一定判得对。

真机上跑这套流程,判断那一步由模型做,你需要的是把「该用哪个」的答案标好,然后看它选了什么。 流程一模一样。 最后一条:这套测试要在技能集合变了之后重跑。加了新邻居,原来的边界就可能被挤动。

七、动手 Demo:同一批请求,描述改三版:命中率与常驻成本

十二个请求、三个技能,每个请求标好该用哪个。 三版描述:干巴巴的、补了场景词的、删了枚举并加了排除句的。 每版都算两个数:首名命中几个、三条描述常驻多少 token。用字面重合度代替模型的判断。

def _run(version, name):
    lib = [(n, d, u"x/SKILL.md", 100) for n, d in version]
    for q, want in REQS:
        got = pick(q, lib, top=1)[0][1]     # 按字面重合度取第一名
        ok = (got == want)
    return name, ok, rows, sum(tokens(d) for _n, d in version)

实跑输出:

--------------------------------------------------------------------
同一批请求,描述改三版:命中率与常驻成本
--------------------------------------------------------------------

  十二个请求,三个技能,其中有四个请求是「解释代码」——它们最容易被误判成审查。
| 版本 | 首名命中 | 三条描述常驻 |
|---|---:|---:|
| 第一版 | 8/12 | 86 |
| 第二版 | 12/12 | 186 |
| 第三版 | 12/12 | 173 |


  ① 第一版错在哪
  --------------------------------------------------------------------
  这次改动帮我看看有没有问题 → git-commit(该是 code-review)
  这段代码为什么这么写 → code-review(该是 explain)
  这行代码什么意思 → code-review(该是 explain)
  改完了,存一下 → code-review(该是 git-commit)
  十二个里错 4 个,其中 2 个是「解释」被当成「审查」——因为两边的描述都在说「代码」和「看」。
  这是典型的【欠区分】,不是过触发。

  ② 第二版加了场景词
  --------------------------------------------------------------------
  每个描述后面补一句「用户说……时使用」,把用户可能用的词写进去。
  命中率从 8/12 提到 12/12。

  ③ 第三版:删枚举,换一句排除
  --------------------------------------------------------------------
  第二版十二个全对了,但它是靠往描述里堆场景词换来的:三条描述加起来 186 token,
  是第一版 86 的两倍多。
  第三版做的是反过来的事:把枚举的「干嘛的」「为什么这么写」删掉,换成一句「要改代码、
  要审查时不要用这个技能」。
  命中率还是 12/12,三条描述反而降到 173 token。
  ! 这一句是整段描述里最值钱的。正向描述告诉模型什么时候用它,反向描述告诉它什么时候别用。
  只写前者,等于只给了一半信息;写全了,正向那半就能写短。

  ④ 迭代的手感
  --------------------------------------------------------------------
| 改了什么 | 动作 | 命中 | 常驻 token |
|---|---|---:|---:|
| 第一版 → 第二版 | 补场景词 | 8 → 12 | 86 → 186 |
| 第二版 → 第三版 | 删枚举、加排除句 | 12 → 12 | 186 → 173 |

  两次改动都不大,但方向不同:第一次做加法,第二次做减法。
  加法把命中率拉满,减法把成本压下来——顺序不能反。
  先做加法是为了一次只改一处,看得出是哪句生效;
  先做减法会分不清「省了字」和「丢了场景」哪个起的作用。
  所以迭代的办法是固定的:先备一批带标准答案的请求,改一版跑一遍,看错的是哪几类,
  再针对那一类补一句。每次只改一处,才知道是哪句生效。
  ! 这套办法的成本很低:十二个请求,人工判对错,两分钟跑完。
  而它换来的是一条能被验证的描述——不测的描述永远只是「看起来还行」。

①第一版八分,错四个:两个是「解释」被当成「审查」,一个是「看看有没有问题」被当成提交, 一个是「改完了存一下」被当成审查。 根子是欠区分——两条描述都在说「代码」和「看」,模型分不开。 这不是过触发,过触发是「谁都想要」,欠区分是「分不开」。 ②第二版补了场景词,十二分全对。代价是三条描述从八十六 token 涨到一百八十六,翻一倍多。 这个代价在三个技能时不算什么,但 3.3 节算过:技能个数乘描述长度就是常驻预算。 二十个技能的时候,每条多三十 token 就是六百。 ③第三版是减法:把枚举的词删掉,换成一句「要改代码、要审查时不要用这个技能」。 命中率还是十二,常驻降到一百七十三,比第二版少十三。一句排除顶掉了几个场景词。 原因是场景词靠列举,永远列不全;排除句靠划界,一次把整个邻域切掉。 ④两次改动方向不同:第一次做加法,第二次做减法。顺序不能反——加法把命中率拉满,减法把成本压下来。 先做加法是为了一次只改一处,看得出哪句生效;先做减法会分不清「省了字」和「丢了场景」哪个起的作用。 最后一句要说清口径:这里用字面重合度代替模型判断,所以只能比排序对不对,不能比分值准不准。 真机上流程一样,只是判断那一步由模型做。

30 秒实验

  1. 把第三版里那句排除删掉,重跑一遍。命中率会掉吗?常驻降到多少?这说明那句排除句在买什么?
  2. 给请求集加四条「都该用 code-review」的说法,重跑三版。哪一版的稳定性最差?为什么?
  3. 把三条描述各加一句「功能强大、适用多种场景」,重跑三版。命中有变化吗?常驻涨到多少? 这说明废话词的成本是什么?

常见误区

  1. 不准备标准答案就开始改描述。改完只能看「感觉对不对」,错了也不知道是哪句造成的。
  2. 一次改三处再跑。分数变了也归不了因,下次遇到新场景还是不知道该怎么改。
  3. 只看总分不看错的是哪一类。十一分和十二分差的那一条,决定了下一步加正向还是加反向。
  4. 把命中率拉满就收工。描述是常驻的,字多一个就多交一份税,减法还没做。
  5. 用这套字面测试的分数当成真机准确率。它只能验排序方向,验不了模型的语义判断。
自测三问(点开对答案) 1. 欠区分和过触发的修法有什么不同? —— 欠区分是「分不开」,要加场景词提高区分度;过触发是「谁都想要」,要加排除条件划边界。 2. 为什么第三版比第二版短,命中率还一样? —— 因为把枚举的场景词换成了一句排除句。列举永远列不全,划界一次就把整个邻域切掉,所以更省。 3. 为什么迭代要先做加法再做减法? —— 加法先一次只补一处,能看出哪句生效;乘法阶段(减法)如果先做,会分不清是省了字还是丢了场景。 顺序反了归不了因。

测试办法有了,下一节换个视角:把技能当成软件工程里的模块,用那几条老原则过一遍。

相关笔记