我最近开始构造自己的 LLM Wiki,在这个过程中我需要编写自己的 LLM skills。(一方面我的文档架构跟别人不一样,只能参考别人的 skills 写自己的;另一方面,我就喜欢折腾。)在这个过程中踩了几个坑,在此记录下来。将来不一定有用,因为 LLM 发展太快了,今时今日的局限,6 个月之后可能就不是问题。
抄写无语义文本会抄错
我需要跟踪我的哪些博客文章更新了,然后重新生成对应的总结文档,为此我用 JavaScript 计算了每一篇博客文章的 SHA256 hash,然后写到总结文档的头部,只有 hash 对不上时才重新生成总结文档。在 skill 里面我把算好的 hash 提供给 LLM,让它在编写总结文档时把这写到头部,它有时候竟然会抄错整个 hash 中的一个字符。
我问 Claude Code 为什么会发生这样的事情,它说 LLM 擅长顺着 token 吐字符,而 hash 是没有任何语义的随机字符串,LLM 不擅长输出这样的 token,所以会偶尔输错。Claude Code 帮我改了一下 skill,在里面强调了 hash 必须一字不差地抄写到总结文档里。将来我会改用 JavaScript 生成好带 hash 头部的总结文档,再让 LLM 去填充内容,这样才能保证不出错。
在时间轴上刻舟求剑
我在 skill 里让 LLM 把当前时间的 ISO UTC 时间戳写到文档头部。我跑了一次这个 skill,过了一个小时又跑了一次同一个 skill,发现它写入的时间戳还是一小时前的。我问 Claude Code 为什么会这样子,它说 LLM 会记住「当前时间」这个概念,在同一个对话中不再尝试查时间。
原来 LLM 对时间流逝完全没有概念!这简直就是在时间河流中的刻舟求剑,一旦记下了当前时间就认定它永远不变。为了解决这个问题,我让 skill 调用脚本获取当前的时间戳,不再是自行输出时间戳。因为我已经在用 JavaScript,所以我就让 LLM 调用 Node 执行一行 JavaScript 获取时间戳:
node -e 'console.log(new Date().toISOString())'
前后自我矛盾和自作聪明
我写了一个需要跟我交互的 skill。这个 skill 让 LLM 审阅 Wiki 当中已有概念文档,然后找出可以合并的概念(因为它们是同义词、相关概念或者从属关系),接着问我是否要合并,以及按照哪个方向合并(把哪个概念合并进另一个概念)。我进行是否合并的最终决定。
这个 skill 写着,在 LLM 建议我合并概念 A 和概念 B 时,它应该提供一个有 4 个选项的菜单:
- 把 A 合并进 B
- 把 B 合并进 A
- 不合并(以后也不要再提出合并 A 和 B)
- 跳过(再次使用这个 skill 时可以再提出合并 A 和 B)
此外我还让 LLM 帮我判断应该往哪个方向进行合并,如果它能决定哪个方向更好的话它就应该把哪个选项放在首位,旁边注明「推荐」,例如说:
- 把 B 合并进 A(推荐)
- 把 A 合并进 B
- 不合并
- 跳过
实际执行这个 skill 时,它竟然给我出了这样的选项:
- 不合并(推荐)
- 把 B 合并进 A
- 把 A 合并进 B
- 跳过
这就很自我矛盾了。因为 skill 里面写着,LLM 必须找出值得合并的概念。如果 LLM 推荐我不要合并这两个概念,那就违背了它上文「值得合并」的筛选条件。
更搞笑的是,有时候 LLM 会自作聪明地把两对概念同时拿出来让我审阅,于是给出这样的选项:
- 都不合并(既不合并 A 和 B,也不合并 C 和 D)
- 分别审阅这两对概念
遇到这样的问题,我只好跑回去让 Claude Code 帮我改 skill。它往 skill 里面加入了更多约束的 prompt,上述现象确实消失了。但为什么多加约束就能用,约束增加到多少就会影响 skill 执行的质量,这一切我都不知道。
总结
上述问题全部出现在 Sonnet 4.6,换成 Opus 4.7 有没有同样的问题,我不知道。这就是 LLM 踩坑的局限性,你永远不知道这些坑是不是特定模型特定版本导致的。今时今日需要在 harness 层面进行修复的问题,或许 6 个月之后的 LLM 就不再依赖这类 harness 操作。这篇文章 6 个月之后再看,是否还有价值,还真的很难说。
没有评论:
发表评论