给 Agent 的空结果也要附带诊断
一次批量网页转换暴露出登录拦截、扫描 PDF 缺少文本层和正文选择器失效等不同空结果来源。作者建议 API 返回结构化原因,避免下游 Agent 将空白成功响应缓存并当作有效数据。
AI 深度解读
400 个 URL 全部返回了正常的 Markdown 响应,抽查后却发现,一半是空字符串。作者这次批处理暴露的问题是:接口调用成功,不等于内容提取成功。如果下游是 AI Agent,空结果还可能被当成可信材料,继续生成摘要。
作者的改法很小:把空结果变成带原因的明确输出。这篇短文值得读的地方,就在于它把一个常被忽略的接口细节,连到了缓存、重试和 AI 幻觉问题上。
为什么 400 次正常响应,仍然不能算成功
作者最初看到的是一次顺利完成的批处理:400 个 URL,400 份看起来正常的 Markdown 响应。没有报错,也没有 HTTP 500。直到抽查内容,才发现其中一半是空字符串。
问题出在「成功」的判断标准上。请求完成、服务返回响应,只能证明调用链路走通了,不能证明拿到了可用正文。
对于后面还要做摘要、检索或其他处理的程序,这个区别很关键。空字符串既没有提供内容,也没有解释内容为什么缺失。下游无法判断应该重新请求、换一种提取方式,还是停止处理。
空结果可以是有效答案,但必须让调用方知道它为什么为空。
同样是空字符串,背后可能是三类问题
作者逐个排查空结果,发现原因分成三类:
- 登录限制:服务器返回 HTTP 200,但拿到的是登录引导页,没有目标正文。
- 扫描版 PDF:文件只有图片,没有可提取的文本层。
- 页面布局变化:原先用于定位正文的选择器,已经匹配不到内容。
这三种情况的处理方式完全不同。登录页需要解决访问条件;扫描版 PDF 要考虑图像文字识别;选择器失效则需要修复提取逻辑。反复执行同一套转换流程,未必能改变结果。
作者认为,其中只有一类属于自己的提取逻辑需要修复的问题。不过,这个责任划分有前提:如果转换器承诺支持扫描件,是否提供文字识别就也属于产品能力范围。原文没有交代这样的支持边界。
作者给出的改法,是返回 empty_body、no_text_layer、selector_miss 这样的诊断原因,取代裸露的空字符串。它们分别描述空内容、缺少文本层、选择器未命中等状态。原文没有提供完整响应结构,也没有明确说明登录限制如何单独编码,不能把这几个名称当成一套已经完备的错误协议。
空的 200,为什么可能比 500 更麻烦
作者的判断是:500 至少可能触发重试,空的 200 却容易被缓存和信任。
这里真正危险的,是下游把 HTTP 成功直接当成业务成功。假如摘要流程只检查请求有没有抛错,空正文也可能继续进入模型;假如缓存只按状态码判断,暂时没提取到内容的结果就可能被保留下来。
但这些后果取决于具体实现。并非所有 500 都会重试,也并非所有 200 都会进入缓存。作者描述的是他们流水线暴露出的风险,不能当成 HTTP 的固定行为。
作者随后把更严格的处理放进自己的 document converter,让空提取结果携带诊断信息。据作者描述,下游当天就不再对空白页面编造摘要。原文没有展示诊断响应样例,也没有解释下游如何处理诊断,因此我们能确认的是作者报告了改善,无法从文章还原全部实现。
我的判断
我赞同把空结果作为正式输出。它让调用方获得了做下一步决策所需的信息,也比只返回「转换失败」更具体:没有文本层和选择器失效,显然不该走同一种恢复流程。
相比单纯增加重试次数,这种做法先解释失败原因,能减少无效重复。相比把所有异常都改成 500,它又保留了访问条件、输入文件特征和提取逻辑之间的区别。
局限也很明确:这是一篇经验记录。作者给出了 400 个 URL、一半为空的观察,却没有提供修复后的成功率、对照实验或各类问题的数量分布。诊断是否准确、下游是否稳定停止生成,仍缺少进一步证据。
诊断信息只有被下游用于分支判断,才真正完成了这次修复。
能拿来做什么
- 在 Node 内容采集或 Markdown 转换流程中,把「请求成功」和「正文可用」分别检查;正文为空时保留原因,避免只返回
""。 - 给 AI 摘要入口增加内容校验;收到空正文或提取诊断时,先停止摘要生成,再按原因选择后续处理。
- 按问题类型安排恢复动作:登录限制检查访问条件,扫描版 PDF 评估文字识别,
selector_miss检查页面结构和选择器。 - 检查现有缓存规则,确认空提取结果是否会被直接当作成功内容保存,并为这类结果制定明确的缓存策略。