【日记】AI自己烧烤半小时修好了编码问题

TOM 发布于 2026-04-15 103 次阅读







一次跨越字符编码的探险 —— PowerShell 管道与 UTF-8 的相爱相杀


一次跨越字符编码的探险

PowerShell 管道与 UTF-8 的相爱相杀
PowerShell
UTF-8 编码
调试技巧
opencode
JSON

那天,我随手给 AI 发了一句话:"帮我找找这个汉字乱码问题出现的原因。"本以为最多几分钟就能得到答案,没想到 AI 竟然足足折腾了半个多小时。

它没有直接给我答案,而是像一位执着的侦探,不断翻阅资料、反复做实验、一步步排除可能性。最终,AI 结合微软文档和 PowerShell 源码,硬是把这个藏在管道深处的编码陷阱挖了出来。

以下是我和 AI 这次合作的完整记录。这个故事告诉我们:有时候,一个好问题比答案更有价值;而 AI 的潜力,往往在我们给它足够空间去探索时才会真正爆发。

故事背景:一个 AI 编程助手(opencode)的会话备份脚本,本来功能正常,某天突然开始"撒谎"——脚本显示备份成功,但备份出来的文件根本无法打开。工程师花费了大量时间追踪问题,最终发现罪魁祸首既不是脚本逻辑错误,也不是 opencode 有 bug,而是一个藏在 PowerShell 管道处理机制深处的字符编码陷阱

🤔 问题初现

事情是这样的。我的朋友(以下简称"用户")在使用 opencode 时,有一个会话备份脚本,功能是把 AI 对话历史导出为 JSON 文件保存。某天用户发现,脚本运行时明明显示"Export: success",但用 Python 打开文件却报错了:

json.decoder.JSONDecodeError: Invalid \escape: line 6 column 28 (char 140)

这是一个 JSON 解析错误,意味着文件内容在某个地方被破坏了。

🕵️ 初步调查

我首先想到的是 BOM(Byte Order Mark)问题。BOM 是 UTF-8 文件开头的 3 个特殊字节,告诉程序"这是一个 UTF-8 文件"。某些情况下,有 BOM 的 JSON 会导致解析失败。

我检查了备份文件的开头:

# PowerShell 查看文件头字节
Get-Content "backup.json" -Encoding Byte -TotalCount 3

# 结果:239, 187, 191 —— 这就是 UTF-8 BOM!

239, 187, 191 正好是 0xEF 0xBB 0xBF,标准的 UTF-8 BOM。

🔧 第一轮修复:向 BOM 宣战

我开始尝试去掉 BOM,尝试了多种方法:

# 方法1:使用 UTF8NoBomEncoding
$Utf8NoBom = New-Object System.Text.UTF8Encoding($False)
[System.IO.File]::WriteAllText($outputFile, $result, $Utf8NoBom)

# 方法2:手动去掉 FEFF 字符
$result = $result.TrimStart([char]0xFEFF)

文件头确实干净了(不再是 239,187,191),但 JSON 解析仍然失败。

💡
BOM 只是表面问题,真正的"内鬼"还藏在更深的地方……

🔍 深挖:十六进制下的真相

我决定直接看文件内容,特别是报错提到的"第 6 行第 28 列"。我把文件拆开,一行一行地检查:

# 提取 opencode export 的原始输出
$raw = opencode export ses_xxx 2>&1 | Select-Object -Skip 1
$bytes = [System.Text.Encoding]::UTF8.GetBytes(($raw -join "`n"))

然后用 Python 检查原始字节:

# 查看第6行的原始字节
lines = content.split(b'\n')
print('Line 6:', lines[5][:80])

结果让我愣住了:

# 正常应该是:
"directory": "D:\\资料库\\文档\\THU\\活动\\ALS Program"

# 实际保存的是(编码被打乱了):
"directory": "D:\\\\\\xe7\\x92\\xa7\\xe5\\x8b\\xac\\xe6\\x9e\\xa1..."

路径中的中文字符完全变样了!这些 \xe7\x92\xa7 看起来像是被错误解读后的 UTF-8 字节序列。

🎯 关键突破:opencode 本身是清白的

我开始怀疑是不是 opencode 本身输出就有问题。于是我换了一种方式捕获输出——用 Start-Process 的重定向功能,直接把标准输出写到文件,不经过 PowerShell 管道:

# 直接用进程重定向,不经过管道
Start-Process -FilePath "opencode" -ArgumentList "export","ses_xxx" `
-NoNewWindow -Wait -PassThru `
-RedirectStandardOutput "$env:TEMP\opencode_out.txt"

然后验证这个文件:

✅ Valid JSON! —— 文件完全正常!

这证明了 opencode 输出的 JSON 数据本身是完整正确的,问题出在 PowerShell 处理输出的过程中!

🕳️ 罪魁祸首:PowerShell 管道的编码陷阱

🔬 还原作案现场

我终于理解了真正发生了什么:

opencode export 这个命令实际上会往两个地方写东西:

  • stdout(标准输出):纯净的 UTF-8 JSON 数据
  • stderr(标准错误):一条状态信息 "Exporting session: ses_xxx"

原脚本用 2>&1 把两者合并,然后通过 PowerShell 管道传递:

$rawOutput = opencode export $sessionId 2>&1 # 合并 stdout 和 stderr
$jsonLines = $rawOutput | Select-Object -Skip 1 # 管道处理
[System.IO.File]::WriteAllLines($outputFile, $jsonLines) # 写入文件

问题就出在管道的每一步:PowerShell 在处理这个外部命令的输出时,会把字节流当成"字符串"来处理。而中文、日文等非 ASCII 字符在 UTF-8 中是多个字节(通常 3-4 字节),PowerShell 的字符串系统在处理过程中会错误地拆分/合并这些字节,导致编码彻底破坏。

✅ 修复方案

既然管道会破坏编码,那就不用管道。改用进程重定向,让 stdout 的字节流直接落盘:

# 之前(会损坏编码)
$rawOutput = opencode export $sessionId 2>&1
$jsonLines = $rawOutput | Select-Object -Skip 1
[System.IO.File]::WriteAllLines($outputFile, $jsonLines)

# 之后(完美保留编码)
$proc = Start-Process -FilePath "opencode" -ArgumentList "export", $sessionId `
-NoNewWindow -Wait -PassThru `
-RedirectStandardOutput $tempFile
if ($proc.ExitCode -eq 0) {
Copy-Item $tempFile $outputFile -Force
}

关键在于:Start-Process -RedirectStandardOutput 直接把进程的 stdout 字节流写入文件,完全不经过 PowerShell 的字符串系统,所以 UTF-8 编码得以完整保留。

📊 问题总结

做法 结果
PowerShell 管道捕获外部命令输出 ❌ 编码会被破坏
Start-Process -RedirectStandardOutput ✅ 字节流直接落盘
管道 + -join "`n" + WriteAllLines ❌ 仍会损坏 UTF-8
直接重定向后 Copy-Item ✅ 保持原始编码

🌟 经验教训

Step 1: 不要假设

一开始我以为"BOM 导致的",花了大量时间研究如何去 BOM,结果走了弯路。正确的做法是先验证"原始数据是否正确"。

Step 2: 用正确的工具

十六进制查看是排查编码问题的利器。Python 的 bytes 类型可以精确看到每个字节,而不会被字符串解释干扰。

Step 3: 隔离变量

当怀疑某个环节有问题时,想办法绕过它。我通过 Start-Process 绕过了整个管道系统,一下就定位到了问题。

Step 4: 理解底层

PowerShell 的管道是基于 .NET 的字符串对象工作的,而不是字节流。这在处理二进制数据或非 ASCII 文本时容易出问题。

🎓
这个故事告诉我们:工具越强大,隐藏的陷阱可能越深。一个看似简单的"管道"操作,背后可能涉及到字符编码的复杂转换。

📚 附录:什么是 BOM?

UTF-8 的 BOM 是文件开头的 3 个字节 0xEF 0xBB 0xBF,作用是告诉阅读者"这个文件是 UTF-8 编码的"。但很多 JSON 解析器不认可 BOM,认为它不是合法的 JSON 内容。

有 BOM
EF BB BF
← BOM (3 bytes)
{ "info": { ... } }
❌ 解析器可能报错

无 BOM
EF BB BF
EF BB BF
← 已被删除
{ "info": { ... } }
✅ JSON 解析正常

📚 附录:stdout 和 stderr 是什么?

每个程序运行时都可以向两个"通道"输出信息:

  • stdout(标准输出):正常输出,如程序的计算结果、数据等
  • stderr(标准错误):错误信息、状态提示等

默认情况下,两者都显示在终端上,但它们是独立的。2>&1 表示"把 stderr 合并到 stdout"。

opencode export

stdout
纯净的 UTF-8 JSON 数据
← 本应直接写入文件

stderr
"Exporting session: ..."
← 状态信息

PowerShell 管道
2>&1 合并 + 管道处理
⚠️ UTF-8 编码在这里被破坏

❌ 编码损坏的字符串
中文路径变成 \xe7\x92\xa7...
JSON 解析失败


此作者没有提供个人介绍
最后更新于 2026-04-15