默认UTF-8解码让Agent读到二十万乱码
OpenClaw的链接解析环节直接使用默认UTF-8解码器,导致Shift_JIS日文页面被转换为208,953个替换字符,且未显式报错。文章指出,其通用网页工具已有按字节序标记、响应头及页面声明识别编码的处理逻辑。
AI 深度解读
OpenClaw 抓取夏目漱石《心》时,交给模型的不是日文,而是 208,953 个替换字符 U+FFFD。问题出在网页字节转成文本的那一步:抓取器默认使用 UTF-8,却碰上了采用 Shift_JIS 的青空文库。
这次修复值得看,因为它展示了一种容易被误判为「模型不行」的故障:工具没有报错,流程照常完成,输入却已经损坏。作者的处理也很直接:复用项目已有的解码逻辑,再沿真实消息链路验证。
模型读到乱码,问题可能早在输入阶段
OpenClaw 是开源 AI agent。消息包含链接时,它会抓取页面,把文本交给模型理解。出问题的代码只有一行:
const content = new TextDecoder().decode(buffer).trim();
它负责把响应字节转成字符串。要看的是 new TextDecoder():不传编码参数时,它使用 UTF-8,不会根据网页声明自动切换。
青空文库使用 Shift_JIS,其中大量字节无法按 UTF-8 正确解码,于是被替换成 �。原文指出,EUC-JP、GBK、Big5 等旧编码页面也会遇到同类问题。
抓取成功,不等于模型拿到了正确内容。
这里没有明显异常,模型只是收到一堵替换字符墙,回复也随之失真。排查 agent 的回答质量时,如果只盯着提示词和模型,就可能错过真正损坏信息的环节。
修复为什么只需要复用一个函数
项目的通用网页工具 web_fetch 已经有可用的解码器,会依次检查:
- 字节顺序标记 BOM,支持 UTF-8、UTF-16LE、UTF-16BE。
Content-Type响应头中的charset参数。- 对没有响应头字符集声明的 HTML、XML,检查
<?xml encoding="...">、<meta charset>和<meta http-equiv="Content-Type">。
遇到 TextDecoder 不认识的编码标签,它会回退到 UTF-8,而不是抛出异常。
作者把 src/agents/tools/web-shared.ts 中的函数导出,签名为 export function decodeResponseBytes(res: Response, bytes: Uint8Array, truncated = false): string,再让链接理解流程调用它:
// src/link-understanding/apply.ts
import { decodeResponseBytes } from "../agents/tools/web-shared.js";
const content = decodeResponseBytes(response, buffer).trim();
这段修改让链接抓取使用已有的解码策略。关键是第二行调用:编码判断交给共享函数,调用方继续处理得到的文本。
此前另一个修复 PR 使用了严格解码方案,解决了 Shift_JIS 页面,却破坏了没有声明字符集的页面。原文没有展开那份实现的细节,但明确指出,复用现有解码器可以让这些页面继续按 UTF-8 处理。
这里的取舍很实际:补上旧编码支持,同时保留未声明编码页面的原有行为,也避免维护两套解码规则。
六个字节的测试,如何验证到真实会话
回归测试通过本地 HTTP 服务返回 content-type: text/plain; charset=Shift_JIS,响应体是 [147, 250, 150, 123, 140, 234],对应「日本語」。测试要求 agent 看到正确文本:在 main 分支失败,应用修复后通过,其余测试结果不变。
作者随后开启严格 SSRF 防护,通过生产使用的链接理解路径访问真实公网页面。原文没有列出完整页面清单或这一轮的详细结果,不能据此推导覆盖范围。
维护者 obviyus 又通过构建后的 Gateway 验证:建立真实会话,发送真实聊天消息,用模型测试替身记录实际收到的内容。main 分支的 Shift_JIS 内容仍然乱码;修复后,模型收到「阿部寛のホームページ」,UTF-8 对照页保持不变。修复最终合入 openclaw/openclaw#188766。
这种验证比「函数返回正确字符串」更进一步:它确认正确文本确实走到了模型输入端。
我的判断
我认同这次修复的两个选择:优先复用已有能力,以及检查模型最终收到什么。前者减少规则分叉,后者避免局部测试通过、实际链路仍然出错。
但这不是完整的网页编码识别方案。文章证明了指定 Shift_JIS 用例和所述真实会话的结果,没有给出不同编码、错误声明、缺失声明页面的大规模对照数据。按声明解码,也无法保证声明本身正确。
与统一强制 UTF-8 相比,这套策略能处理更多明确声明旧编码的页面;与直接改成严格解码相比,它保留了原有回退行为。对这次缺陷,证据足够具体;对所有网页的兼容性,还不能背书。
能拿来做什么
- 检查 Node 抓取服务和 AI 链接工具中的
new TextDecoder()、Response.text()。原文指出,两者都按 UTF-8 解码,不会自动遵循服务端字符集声明。 - 如果项目已有网页解码工具,让链接预览、正文提取和 agent 抓取共用它,并明确 BOM、响应头、文档声明的处理顺序。
- 为旧编码加入可控的本地 HTTP 用例,再通过真实消息入口记录模型收到的文本;同时保留 UTF-8 对照页,确认修复没有改变已有行为。