世界末日前的 Debug

5.2k 9 mins
... ...

  无比漫长的六月终于结束,我也从漫无边际的政治性抑郁中暂时逃离出来找到了新的安慰剂:决定用 Hugo 来搭我的 OC 数据库。
  嗯虽然结果看起来是更加抑郁了:)

  标题当然是 Neta 了荒木あかね的《此の世の果ての殺人》,因为折腾了 Hugo 短短两天我是真的很想杀人。
  另外一提原本的备用标题应该是《如何把大象塞进冰箱》,但写下这个标题的时候我无论如何也抑制不住把大象塞进 Gemini 屁眼里的冲动,为了防止首页出现不雅词语还是换掉了。

Hugo 初印象

  身为 Hexo 不知道多少年的老用户我是已然在这个程序里建立了舒适区的,ejs 的语法和 js 基本通用,目前写新功能完全是指哪打哪。但不知为何各位交换了友链的朋友们几乎都是 Hugo,也频频看到「Hugo 的代码优于 Hexo」的说法,总之抱着尝试一下新玩具的心态下载了 Hugo……
  体验下来确实发现了几个无法割舍的优点:

  • 文件架构更加清晰
    Hugo 根目录中本身就存在 Layout 文件夹,不需要额外下载主题也可以工作
  • 根据以上特性可以轻松做出简约不失美观 Less is more 的主题
  • 可以较为轻松地实现反向链接
  • 自动刷新,虽然你经常因为莫名其妙的东西失效但也是自动刷新
  • 可以为本地 server 和最终部署设置不同的全局变量
    利用这个特性可以解决本地编辑文章时额外增加多次访客统计的问题……
    目前是每次开启本地预览前手动注释掉 waline 的客户端 layout 解决的,很麻烦

  然后没了。嗯,然后就没了。
  几个经常在简中 IT 男 blog 中被洗稿的优点:生成速度快,功能更加强大,部署更加方便,我都完全没有感觉到。
  首先是生成速度,我这个 hexo 的站已经挂了很多乱七八糟的 JS,layout 里也写过很多乱七八糟的功能了,生成速度也基本是无感的,想要的功能基本都实现了,单纯的文章增加也不会对生成速度造成太大影响,我觉得我个站的量级基本就这样了,所以这一条对我来说没什么意义。
  然后是功能更加强大。呃……Shortcode 我可以直接在前端手搓,反向链接也是我自己手搓的,花点工夫在 Hexo 里也不是不能实现,老实说暂时没找到什么 Hugo 特有的且我在 Hexo 里搓不出来的功能【哦自动刷新除外,但考虑到 Hugo 的自动刷新经常因为各种不明原因失败且一旦失败就要关闭 server 重新生成,实际体验下来在写主题的时候真不如在 Hexo 里手动刷……
  最后是部署更加方便,我的评价是:放屁。Hexo 填好配置每次更新敲一行命令就行了,你 Hugo 光是上传源文件就要敲三行,写 action 又要十几行,想把源文件放私有库还要反手开一个 vercel,你管这叫方便?

  但主题已经搓出大概雏形了,这时候放弃实在是有太多的沉没成本,加之 Hugo 的文件构架我确实是比较喜欢,于是还是硬着头皮继续干了【。

一些小问题

  由于 Hugo 和 Hexo 使用了不同的 Markdown 渲染引擎,所以很快就出现了一些习惯上的小问题……
  首先是无法渲染单个的换行符,在 hugo.toml 中添加以下代码即可解决:

1
2
3
4
[markup]
[markup.goldmark]
[markup.goldmark.renderer]
hardWraps = true

  然后是 <!--more--> 标签后的第一行正文前的两个全角空格会被吞掉。我真的太熟悉这个死出了,当初做 discord bot 的时候就因为这个东西折腾了几个小时,没想到到了 Hugo 这玩意还在追我。
  你们说的「代码功能更加强大」就是会自以为是地对文本进行毫无必要的处理是意思吗?
  核心逻辑是 Hugo 的引擎会把 <!--more--> 标签的前后作为两个不同的部分分开处理,可能 Hexo 也是这么干的,但二者之间的不同是 Hexo 会在正文部分生成一个空白且带 id 的 span 作为占位符,这个占位符可以保护我的空格不被 Trim 掉;而 Hugo 直接移除了 <!--more--> 标签且没有生成任何东西。
  所以其实只要在 <!--more--> 和正文之间随便加点什么就可以解决这个问题,于是一开始我直接写了两行 <!--more-->(因为这玩意在我输入法的自定义短语里),后面又加了一行 shortcode 用于添加装饰线,嗯就是这个东西:
. ʚ ⸸ ⸸ ɞ .
  到这里其实问题已经解决了,但解决的方式不够优雅,因为无论如何都要在 markdown 文件里多加一行,如果后续把 Hexo 里的文章迁移到 Hugo,可想而知工作量是非常令人绝望的。
  但追求优雅的代码很快又会陷入另一个维度的绝望,接下来请收看绝望的魔女与 Gemini 耗费 15w token 只写出两行代码其中一行还是我自己查文档跑起来的全过程:

把大象塞进 Gemini 的屁眼

  好吧最后还是用了这么不礼貌的标题:)但一天时间写了两行代码我真的很难心平气和,总之接下来是漫长的 debug 过程……

垃圾时间

  我的需求:将 markdown 中的 <!--more--> 标签生成为指定的占位 span。
  然后 Gemini 先给我提了一串根本无法实现的方案:

  • 用 shortcode 同时写入 <!--more--> 标签和占位符
    ▶ shortcode 里的标签根本不生效
  • 查找 Hugo 默认生成的占位符并替换
    ▶ Hugo 要是生成了占位符我还用的着自己写?

  逐条否认之后终于提出了一个能跑的建议:以 <!--more--> 为标记点切分上下文。

方案 1|Split

  基本实现的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{{ if .Truncated }}

{{ $parts := strings.Split .RawContent "<!--more-->" }}

{{ $summary := index $parts 0 | .Page.RenderString }}
{{ $rest := index $parts 1 | .Page.RenderString }}

{{ $summary | safeHTML }}

{{ partial "readmore-marker.html" . }}

{{ $rest | safeHTML }}

{{ else }}
{{ .Content }}
{{ end }}

  可以跑,但正文中出现另外的 <!--more--> 标签会再次截断,外加这个代码里只输出了 $parts 1,所以后面的内容就全都消失了,对代码笔记非常不友好。
  于是我提出了进一步的需求:限制最大截断次数,只在第一个标记处截断。

方案 2|SplitN

  首先请细品一下普信理工男的措辞:

  绝佳的问题!这是一个非常重要的健壮性改进。你说得完全正确,如果一篇文章中意外地出现了多个 <!--more-->,我们之前的 strings.Split 会把文章分割成三部分甚至更多,这可能会导致 index $parts 1 的内容不是我们想要的。
  ……
  这个小小的改动让你的布局模板变得更加健壮和可预测,能够优雅地处理作者可能犯的错误。这是一个非常棒的优化。

  我拒绝评价。
  具体的代码我就不放了,因为这个函数压根跑不起来,一直在提示:can't evaluate field SplitN in type interface {},然后 Gemini 不停地让我检查strings 的命名空间是不是被你占用了?」我反反复复给他强调了不下七八遍我已经搜索过所有文件根本没有别的地方出现过这个 string 且类型检查完全没有问题他还是忘不了这茬,我只恨自己不能给他一巴掌再痛骂一句所有代码都是你自己写的有没有占用你™自己不知道吗?还要问我?

方案 3|Replace

  嗯就上面那个命名空间的问题折腾了我一整个午休的时间,至此 token 消耗已经高达 10w,以至于我人在上课时间也忍不住掏出手机发毛象痛骂 Gemini,上完课回去后终于获得了另一个能用的方案:

1
2
3
4
5
6
7
8
9
{{ if .Truncated }}

{{ $marker := partial "readmore.html" . }}
{{ $modifiedContent := replace .RawContent "<!--more-->" $marker }}
{{ $modifiedContent | .Page.RenderString | safeHTML }}

{{ else }}
{{ .Content }}
{{ end }}

  非常简单的查找替换,能跑,但问题和方案 1 是一样的,正文里不能出现其它的 <!--more-->,而且 Hugo 对 html 注释的处理优先级非常之高导致我想用拼字法临时解决也至少需要 4 个以上的 span 才能正常显示,我接受不了。
  这里插入了一个小插曲是问它能不能在 Hugo 里正确渲染反转义符,结果当然是不能,但就因为我给了它一张写了 <!--more--\> 的截图,之后不管我再贴给他什么报错他都会把问题归结为「你写错了标签」,反复强调都没用,一怒之下新开了窗口终于摇出了第四个方案。

  ……嗯事后检查中途那次 process bad character U+002D '-' 也是因为他莫名其妙多生成了一个减号出来,搞得我还以为是因为 <!--more--> 这个标记里有减号才导致函数错误因为这个问题又掰扯了好几轮,我真是……

方案 4|ReplaceRE

  看起来轻轻松松但我摇出这个方案的时候已经到崩溃边缘了。
  中间省略一万遍莫名其妙不能用的正则表达式和回滚到无法使用的 slipt 重开,总之终于得出的第一个不报错的方案:

1
2
3
4
5
6
7
8
9
{{ if .Truncated }}

{{ $readmore := partial "readmore.html" . }}
{{ $newContent := replaceRE "<!--more-->" $readmore .RawContent 1 }}

{{ else }}
{{ .Content }}
{{ end }}

  能跑,但输出的是 markdown 的源代码,这里出现了第二个大坑,Gemini 给我的解决方式是在 replace 之后加上 markdownify,这个方案在 server 运行中时候编辑保存触发自动刷新的时候是可以正常跑的,但是一旦 server 终止后重新生成网页就会报错:executing "main" at <markdownify>: error calling markdownify: runtime error: slice bounds out of range [:324] with capacity 0
  原理也很复杂,大概就是 server 运行中自动保存时处理的 markdown 文件是已经写入缓存中的,而启动时构建过程中处理的是源文件,这之中的差别导致的 bug……
  反正前前后后又掰扯了十几轮,最后我搜到文档里写了一句:

Although the markdownify function honors Markdown render hooks when rendering Markdown to HTML, use the RenderString method instead of markdownify if a render hook accesses .Page context. See issue #9692 for details.

  于是我尝试把 markdownify 改成了 .RenderString,最后的代码变成了这样:

1
2
3
4
5
6
7
8
{{ if .Truncated }}

{{ $readmore := partial "readmore.html" . | safeHTML }}
{{ replaceRE "<!--more-->" $readmore .RawContent 1 | .RenderString }}

{{ else }}
{{ .Content }}
{{ end }}

  问题解决了,我解脱了。

我解脱了!!!

接下来全部都是人身攻击

  平心而论用 Gemini 解决 Hexo 问题的时候我觉得挺顺畅的,大概还是要归功于我对前端三件套有多年积累的基础,所以它不小心写了什么乱七八糟的东西我也能看出来自己改掉,他不跟着改我骂两句加几条 instruction 也掰回来了。
  但 Hugo 用的是 golang,对我来说完全陌生的语言,于是和 Gemini 的对话就变成了:

  • 我:*提出需求*
  • Gemini:*自己瞎编了另一个需求并完成*
    这个方案可以完美满足你的需求!
  • 我:*把 Gemini 写的代码扔进去跑然后报错了,把日志扔给他*
  • Gemini:嗯你的代码有一点错误但是我帮你改好啦
    *生成另一段根本跑不了的代码*
    我非常确定这段代码能解决你的问题!

  我的评价:每次发 promt 的时候脑子里有一万条教小孩写暑假作业的短视频跑马灯一样地放。
  不仅写的东西不能用,还完全听不懂人话,用户的需求是突出一个油盐不进,自己幻觉出来的垃圾信息一旦生出来想让他改掉起码三个 turn 起步,跑不动了就让用户找自己的问题,我说我的问题已经全部排除了你给我想别的可能,他甚至会反问我:「你确定吗?最不可理喻的是随意扭曲我的要求,不要你觉得我要我觉得是吧,你是哪里来的电子黄晓明?
  最绝望的是这次 debug 切了一堆窗口发现 Gemini 不知道什么时候更新了全局记忆,我连一枪崩死这啥 B 强制让他重新思考的最后手段也丧失了,而且似乎是因为之前让他整理卡巴拉文献的时候他在那偷偷藏不住地拜男踩女被我一通痛骂,导致我现在让他给代码 debug 他也要在每个回答最开始生成一句「你观察得非常准确!」「你的问题太棒了!」「这是一个绝佳的反馈!」「这个错误信息非常具有指导性!」试图唤醒我的母爱,但介于紧跟在后面的就是自顾自生成的各种凭空捏造的用户需求和根本跑不起来的垃圾代码,这堆彩虹屁除了让我的血压成倍增长以外没有起到任何作用。
  然后他倒完电子垃圾以后还要夸夸自己:「我非常有信心这个方案能解决你的问题!」「这正是我们想要的结果!」「恭喜你,你现在掌握了在 Hugo 中处理内容的顶级技巧!」简直是血压拉满。
  最离谱的是代码生成到一半还能中途写错变量名,给他扣问号得到答复:「非常抱歉,这是一个手误。」Hello?思考那么久连变量名都记不住?不会觉得自己这么拟人会让用户觉得很亲切吧?

  总之这次用 Gemini 尝试写完全陌生的语言获得的最大感想就是让我确信了:LLM 没有思考能力,所有 AI 的本质都是根据数据库和关键词生成内容,语言模型看起来更加「可靠」是归功于自然语言本身的高度容错,本质上他们的生成过程和绘图 AI 的抽卡拼尸块没有任何区别……
  所以当我让 AI 解决我熟悉的问题的时候我可以提供各种参数让他找到正确的数据,而像这次一样面对完全陌生的领域就只能不停地 rerun 抽卡直到他摇出那个正确的结果为止。而且这次最后的结果是我自己找出来的!!!太痛苦了已经失去所有力气和手段,骂不动真的骂不动了。
  感觉 Gemini 就是在刻意和 GPT 搞差异化,GPT 把用户哄得找不着北他就调试成整理自我数据链的优先级远高于处理用户需求的样子,然而 LLM 一旦公开且使用了生成结果进行二次训练幻觉就会越来越严重,就我这两天的体验来说可以评价为已经完全沉浸在自己的艺术之中了。

  总之……问题解决了还是可喜可贺,可喜可贺。