返回文章列表

让 AI 给博客做体检:10 个坏链接、1.7MB 封面图和一个被推翻的假设

网站能打开、CI 一直是绿的,当成待测系统体检后却找到了 10 个点不动的目录链接、两张超大封面图和一段多余的 174KB 代码。记录一次由 AI 编码 Agent 执行、按测试标准验收的网站体检:被推翻的两个判断,和一条拦住 AI 改动的校验规则。

网站能打开、页面能点、CI 一直是绿的。可当我真的把它当成一个待测系统来体检,第一天就找到了 10 个点不动的目录链接、两张 1.7MB 和 1.4MB 的封面图,以及一段几乎每个知识库页面都要多下载的 174KB 代码。

博客从 9 月中旬之后停更了一个多月。重新开始写之前,我决定先做一件测试工程师最熟悉、却很少对自己的东西做的事:把它当成一个待测系统,完整体检一遍。

这次体检由 AI 编码 Agent 执行:读代码、跑构建、写脚本爬全站、提修复方案、改代码、跑回归。我负责三件事:定规则、审方案、决定合不合并。

这也正好是上一篇《AI 写完了,也说修好了:测试人员如何验收编码 Agent?》的一次实战。那篇讲的是“怎么验收 Agent 的交付”,这一篇是我把同一套标准用在自己的网站上,看看会发生什么。

先说结论:问题不少,但最有价值的不是修掉了哪些 Bug,而是体检过程中两个被推翻的判断,和一条两个多月前写下、这次拦住了 AI 改动的校验规则。


一、先定规则,再动手

让 Agent 直接“优化一下网站”,得到的往往是一堆看起来合理、却无法验收的改动。所以开始前,我先把规则定下来:

  • 先诊断,后修改。 第一轮只读不改,输出问题清单和可优化点,由我决定做哪些。
  • 每个改动都要有前后数字。 “应该更快了”不算,要给出改动前后的体积、数量或耗时。
  • 每个修复都要能证明没有副作用。 尤其是改动公共逻辑时,要证明其他页面的输出没有变。
  • 不能只看 CI 是否变绿。 CI 只能证明“没有破坏已有断言”,证明不了“新改动是对的”。

对应到具体手段,这次用了四类检查:

检查方式用来回答的问题
lint + 生产构建 + 站点校验脚本有没有破坏已有约定
关闭 JavaScript 后爬取全站搜索引擎和 AI 爬虫实际看到了什么
真实浏览器自动化搜索、目录、图表、站内跳转这些交互还正常吗
改动前后输出逐字节对比公共逻辑的改动有没有误伤其他页面

其中最后一项最容易被省略,却是这次让我最放心的一项,后面会讲到。


二、跑通了,不等于对:10 个点不动的目录链接

体检里最典型的一个问题,藏在一篇五月份的文章里。

《用 AI 维护十年老项目》这篇文章的右侧目录有 21 项,看起来很完整。但其中 10 项点了没有任何反应,比如“技术栈”“核心模块”“高风险区域”“回归测试”。

原因是:文章里有几段示例,演示 AI 输出的项目摘要格式,这些示例放在代码块里,内容本身就带着 Markdown 的二级标题标记:

## 技术栈
## 核心模块
## 高风险区域

页面渲染时,代码块里的内容被正确地当成代码展示;但目录组件用的是另一套提取逻辑,它直接从原文里找以 ## 开头的行,没有跳过代码块。于是代码块里的 10 行被当成了标题,生成了 10 个指向不存在锚点的目录项。

为什么一直没人发现?因为这类缺陷在视觉上完全正常:目录看起来很专业,项目数量还“更丰富”了,只有真的去点,才知道跳不过去。

再往下查,问题的根源更值得警惕:“标题的锚点 id 怎么生成”这条规则,在渲染器和目录组件里各写了一遍。 两份实现今天碰巧一致,明天任何一边改了,目录就会悄悄失效。

修复做了三件事:

  • 把标题提取和锚点生成合并到同一个模块,渲染器和目录共用;
  • 提取标题时跳过代码块;
  • 同名标题自动追加序号,避免两个标题生成同一个锚点。

验证是这次我最想强调的部分。改的是全站共用的 Markdown 渲染逻辑,所以只看这一篇文章修好了是不够的。Agent 写了一个对比脚本,把全部 59 份 Markdown 内容(文章、RAG 专题章节、项目页)分别用新旧两版渲染器跑一遍:

  • 渲染出的 HTML 逐字节一致,59 份里 0 份变化;
  • 新的目录里,每一项都能在页面中找到对应的锚点,0 个断链。

这就是“改了公共逻辑”时最朴素也最可靠的回归手段:新旧版本对同一批输入跑一遍,比较输出。


三、性能问题藏在依赖链里,而不是在源码表面

1. 每个知识库页面都在下载整份 RAG 教程

知识库页面共用一个布局组件,负责搜索、章节导航和“上一篇 / 下一篇”。它需要知道各个页面的标题和更新时间,于是引用了知识库的页面注册表。

问题出在注册表的下一层:为了列出 RAG 专题的 14 个章节,注册表又引用了 RAG 专题的数据文件。而这个文件里,除了章节标题,还有 14 章的完整正文。

布局组件(在浏览器端运行)
  → 知识库页面注册表
    → RAG 专题数据(含 14 章全文,约 150KB)

布局组件要在浏览器端运行,所以这条链上的东西都会被打包进前端代码。结果是:57 个知识库页面里,有 56 个都要多下载一个 174KB(压缩后 67KB)的文件,而这些页面根本用不到 RAG 正文。

只看源码很难发现这个问题,每一行引用都“合理”。它是在检查构建产物时才暴露的:Agent 在打包后的前端文件里搜索 RAG 正文中的一句话,发现它出现在几乎所有知识库页面加载的文件里。

修复方式是把章节的元数据(标题、更新时间)和正文拆成两个文件,注册表只引用元数据。改完后,这个共享文件从 174KB 降到 44KB(压缩后从 67KB 降到 14KB),并再次确认:知识库页面加载的文件里,已经找不到任何 RAG 正文。

2. 两张没有压缩的封面图

博客文章里的图片,是由 Markdown 解析器直接输出成普通图片标签的,不经过框架自带的图片优化。也就是说,放进去多大的图,读者就下载多大的图。

两张 PNG 封面分别是 1.7MB 和 1.4MB。转成同尺寸的 WebP 后,分别是 70KB 和 75KB,肉眼看不出区别。

3. 本可以提前生成的页面

博客列表页、文章分享图、RSS 和图片站点地图,原本都是“有人访问时服务器现做”。原因各不相同:列表页在服务端读取了搜索参数;分享图没有声明要预先生成哪些文章……

改成构建时生成后,访问这些地址只需要直接返回现成文件。这里有一个约束必须守住:列表页之前专门做过“全部文章在服务端渲染”,方便搜索引擎抓取。所以改造后,Agent 专门验证了:静态 HTML 里仍然包含当时全部 43 篇文章的链接,带搜索参数访问时,浏览器端也能正确筛选。

这一节的经验是:性能测试要看构建产物和网络请求,而不是只读源码。 源码层面每一处都说得通,问题出在它们组合起来的结果上。


四、一个被推翻的假设

这是整次体检里我最想记录的一段。

文章里的流程图用 Mermaid 渲染。Agent 读代码时发现,渲染组件在监听整个页面的所有 DOM 变化:只要页面上任何地方插入或删除了节点,就重新检查一遍有没有需要渲染的图表。

它据此提出了一个听起来很严谨的推断:

如果某张图的语法有误,渲染失败时会把源码写回页面;这次写回本身是一次 DOM 变化,会再次触发渲染,再次失败,再次写回……形成无限循环。

推理链条每一步都说得通,修复方案也写好了。按很多人的习惯,这时候就可以在提交说明里写上“修复了渲染失败时的无限循环”。

但它在提交前多做了一步:用改动前的旧代码复现这个问题。

具体做法是,在一篇有流程图的文章页面里,用浏览器自动化插入一张故意写错语法的图,然后统计 4 秒内这张图被重新渲染了几次。

结果:1 次。 没有循环。

回头再看代码才发现,渲染组件里本来就有一个“正在渲染中”的标记。失败时写回源码发生在渲染过程中,由此触发的那次检查会被这个标记直接拦下。推断里漏掉了这一层。

flowchart LR A["读代码,形成假设"] --> B["写好修复"] B --> C{"用旧代码复现"} C -->|"复现成功"| D["确认缺陷,带证据提交"] C -->|"复现失败"| E["推翻假设,降级改动的定位"]

最终这个改动还是保留了,因为它确实让组件少做了很多无用功:只在出现新图表时才处理,不再对评论区加载、图片预览这类无关变化做全量扫描。但它的定位从“修复严重缺陷”降成了“小幅减少无用开销”,PR 说明也照实改写。

同一次体检里还有第二次类似的修正。讨论“教程目录页要不要改成静态生成”时,Agent 一开始说“会影响 SEO,因为搜索引擎抓不到分页内容”;后来核对页面元数据才发现,所有带筛选参数的教程页,规范地址本来就指向目录页本身,搜索引擎原本就不会单独收录分页。于是它主动更正:改成静态的 SEO 代价其实很小,真正要放弃的主要是“关闭 JavaScript 也能翻页”这一点。

这两件事让我更确定一件事:AI 的推理写得再像样,在被复现之前,也只是一个假设。 这和验收人写的结论没有区别:“我认为是这样”和“我复现过是这样”,是两个不同等级的证据。


五、自己写的校验规则,拦住了 AI 的改动

体检的最后一部分是 SEO。Agent 写了一个爬虫,在本地生产构建上关闭 JavaScript,从站点地图出发,顺着站内链接爬完了 134 个地址,模拟搜索引擎和 AI 爬虫看到的原始页面。

先说好消息:死链、重复标题、重复描述、规范地址错误、缺少主标题、图片缺少替代文本,全部是 0。

需要改进的主要有四项,改动前后的对比如下:

项目改动前改动后
站点地图里缺少更新日期的地址76 / 1224 / 122
有面包屑结构化数据的页面0116
有文章类结构化数据的页面43113
描述过短的页面236
图片站点地图收录的图片43 张(仅分享图)62 张(新增 19 张正文配图)

这里有一个插曲。

Agent 最初的方案里,除了给每个知识页面补上更新日期,还给知识库首页取了“所有子页面中最新的那个日期”。改完一跑站点校验,失败了,报错是:

Knowledge sitemap entry must not claim an unverified lastModified date.

这条断言是 8 月加进站点校验脚本的。当时站点地图里所有页面的更新日期都是构建时间,等于每次部署都对搜索引擎说“所有页面今天都更新了”。那次提交删掉了这些日期,同时加了这条断言,防止以后再有人写回去。

Agent 去翻了这条断言的提交历史,理解了它要防的是什么,然后调整了方案:

  • 知识库首页仍然不声明日期,遵守原有约定;
  • 各个知识页面只使用数据里人工维护的“最近更新”日期,也就是页面底部展示给读者的那个日期,不做任何推断;
  • 再进一步,把日期的取值顺序改成与页面底部完全一致,并验证站点地图、结构化数据和页面底部三处的日期 0 处不一致。

这件事让我意识到:把判断写成自动化断言,它的寿命比记忆长得多。 两个多月前的一个决定,在今天准确地拦住了一个“看起来合理”的改动,而且它对人和 AI 一视同仁。

如果你的项目里有“不能这样做”的经验,别只写在文档里,尽量把它变成一条会失败的检查。


六、还没做的,和决定不做的

体检不是为了把所有问题一次清零。这次有几项我看过评估后,决定暂缓或不做:

  • 教程目录页保持服务端渲染。 改成静态生成能省掉一点服务器开销,但会失去“关闭 JavaScript 也能筛选翻页”的能力;个人站的访问量下,这点开销读者基本感觉不到。
  • 24 篇文章的标题在搜索结果里会被截断。 加上站名后缀后超出了显示宽度。搜索引擎本来就常改写标题,收益有限,暂不处理。
  • 旧文章的站内链接偏少。 有 5 篇文章,全站只有文章列表页链向它们。这是内容层面的问题,需要我自己慢慢补,而不是让程序自动生成链接。
  • 线上环境的检查。 这次的检查都在本地生产构建上完成;域名跳转、真实的页面速度和搜索引擎的收录情况,需要我在线上和站长平台上自己确认。

七、给个人网站做体检,可以照着这张清单走

维度检查什么怎么查
功能目录、锚点、站内链接能不能跳对每个目录项断言页面中存在对应锚点;爬全站找非 200 的地址
功能搜索、筛选、图表在站内跳转后是否正常用浏览器自动化走一遍真实交互
性能浏览器端到底下载了什么在构建产物里搜索“不该出现的内容”,比如正文、全量数据
性能图片是否经过压缩列出静态资源里最大的文件,逐个确认是否必要
性能哪些页面每次访问都在现做看构建输出里每条路由的渲染方式
SEO爬虫看到的原始页面关闭 JavaScript 爬全站,检查标题、描述、规范地址、结构化数据
SEO站点地图的日期是否真实日期必须来自可核实的数据,不要用构建时间
工程公共逻辑改动是否误伤新旧版本对同一批输入跑一遍,逐字节比较输出
工程重要的约定会不会被遗忘把“不能这样做”写成会失败的校验

写在最后

这次体检前后一共开了两个 PR,改了十几处问题。但回头看,真正值得记下来的是三个习惯:

  • 改公共逻辑,就用新旧输出对比来证明没有误伤;
  • 任何“我认为”,都先复现成“我看到”,再写进结论;
  • 把踩过的坑写成会失败的断言,让它替你记住。

这三条对人适用,对 AI 也一样适用。AI 编码 Agent 让体检的执行成本降得很低,这次从读代码、爬全站到逐字节对比,几乎都是它完成的;但哪个问题值得修、哪个结论可以相信,仍然要靠证据来定,而不是靠它说得多有道理。

博客恢复更新了。下一篇,我想接着聊聊另一类“说法和证据对不上”的情况:AI 自己报告的效果,和独立复核出来的结果,到底能差多少。


参考资料与延伸阅读

版权与声明

本站所有内容仅代表作者个人观点,与作者供职的公司、客户或其他关联机构无关。

本文除特别声明外,采用 CC BY-NC-SA 4.0 许可协议。转载或改编请署名、附原文与许可证链接,并标明改动;不得用于商业用途,演绎作品须以相同许可发布。

评论