前言



最近在做 Memoria 项目的 Flutter / Dart 代码时,遇到了一个非常经典但又很隐蔽的问题:

一个原本正常的 UTF-8 文件,被某个环境用 GBK 打开并保存后,文件内容看起来变成了大量乱码、转义字符,代码 diff 也变得非常恐怖。

更坑的是:

> Git 并不会认为这是“编码冲突”,它只会认为这是一次普通的文件修改。

也就是说,如果一个人把 UTF-8 文件用 GBK 保存了,然后再提交,另一个人 pull 下来之后,Git 很可能不会爆冲突,而是直接把“变了编码后的文件”合并下来。

这篇文章记录一下这个问题的原因、排查方式、修复方式和后续项目规范。


一、问题现象



原本 Dart 文件里有一些中文 prompt、中文注释、语义搜索规则等内容。

结果某次打开后,文件里出现了大量类似下面的内容:

'\u8bf7\u6839\u636e\u7528\u6237\u8f93\u5165...'

或者直接出现乱码:

璇锋牴鎹甤...

在 IDE 里看起来就是:

  • 中文变成乱码;

  • 字符串被转义成一堆 \uXXXX

  • Git diff 显示整个文件大面积变化;

  • 代码本身逻辑没怎么改,但几百行甚至上千行都变了。

这种情况在包含中文 prompt、中文配置、中文注释的项目里尤其容易出现。


二、根本原因:Git 只比较字节流,不理解文本编码

这个问题的核心原因是:

Git 本质上管理的是文件内容的字节流,而不是“人眼看到的文本”。

同一个中文字符,在 UTF-8 和 GBK 里的字节表示是不一样的。

例如中文字符:

在 UTF-8 中是:

E4 B8 AD

在 GBK 中可能是:

D6 D0

人看到的都是“中”,但底层字节完全不同。

所以当一个 UTF-8 文件被 GBK 打开、编辑、保存后,文件的底层字节流可能会发生大面积变化。

Git 看到的是:

旧字节流 != 新字节流

于是它会认为:

这个文件被正常修改了

而不是:

这个文件发生了编码冲突

三、为什么 Git 不会自动报冲突?

很多人会以为:

既然文件被弄乱了,Git 应该提示 conflict 吧?

但实际上,Git 的冲突检测主要发生在三方合并时:

base 版本
你的版本
对方版本

只有当两边修改了同一片内容,并且 Git 无法自动判断该怎么合并时,才会出现:

<<<<<<< HEAD
...
=======
...
>>>>>>> branch

但是编码变化通常有两种情况:

情况 1:只有一个人改了编码

比如:

A 提交:正常 UTF-8 文件
B 提交:把文件用 GBK 保存了
C pull:直接拿到 B 的结果

这时候 Git 认为 B 只是正常修改文件,不会产生冲突。

情况 2:两个人改了不同区域

比如:

A 改了上半部分代码
B 用 GBK 保存导致全文件变化

Git 可能仍然会尝试自动合并,不一定冲突。

情况 3:两个人改了同一行

这种情况下有可能冲突,但冲突原因仍然是“同一行被修改”,不是“UTF-8 和 GBK 冲突”。

所以结论是:

Git 没有“编码冲突”这个概念。
它只知道字节变了,不知道你是 UTF-8、GBK、Shift-JIS 还是别的编码。


四、这个问题为什么很危险?

这个坑危险在于,它不像语法错误那样立刻爆炸。

它可能表现为:

  1. 代码还能编译;

  2. 逻辑似乎没变;

  3. Git 没有冲突提示;

  4. review 的时候 diff 太大,别人不一定仔细看;

  5. 最后乱码文件被正常 merge 进主分支。

尤其是有中文 prompt 的 AI 项目,这个问题会更严重。

例如:

final prompt = '''
你是一个语义查询解析器,请把用户输入解析成 JSON。
''';

如果 prompt 编码坏了,模型收到的提示词可能就变成乱码,后续效果会非常诡异:

  • 搜索结果不稳定;

  • LLM 输出格式异常;

  • 中文查询解析失败;

  • 原本能识别“上个月”“济南”“照片”的逻辑突然失效。

这类 bug 很难第一时间想到是编码问题。


五、如何判断是不是编码问题?

1. 看 Git diff

如果你发现某个文件明明只改了一点点,但 diff 显示几百行都变了,就要警惕:

git diff path/to/file.dart

典型现象:

- 正常中文
+ \u6b63\u5e38\u4e2d\u6587

或者:

- 请根据用户输入解析
+ 璇锋牴鎹甤...

2. 看 IDE 右下角编码

在 VS Code、Android Studio、JetBrains 系 IDE 里,一般右下角会显示当前文件编码:

UTF-8
GBK
GB2312
UTF-8 with BOM

如果 Dart / Markdown / JSON 文件不是 UTF-8,就要注意。

3. 用命令检查编码

Linux / macOS 可以用:

file -bi path/to/file.dart

可能输出:

text/plain; charset=utf-8

或者:

text/plain; charset=iso-8859-1

Windows 下也可以用 Python 简单检测:

from pathlib import Path

path = Path("lib/service/semantic_query_parser_lm.dart")
data = path.read_bytes()

try:
    data.decode("utf-8")
    print("UTF-8 OK")
except UnicodeDecodeError as e:
    print("Not valid UTF-8:", e)

六、如何修复已经被 GBK 保存的文件?

情况 1:你不需要保留当前修改

这是最简单的情况,直接恢复:

git restore path/to/file.dart

或者恢复整个工作区:

git restore .

注意:这会丢弃本地未提交修改。


情况 2:你需要保留代码逻辑修改

如果里面确实有一些有效代码改动,但是编码坏了,可以这样处理:

  1. 先备份当前文件;

  2. 从 Git 恢复一份正常 UTF-8 版本;

  3. 手动把有效代码逻辑迁移过去;

  4. 不要直接整文件覆盖;

  5. 最后确认 diff 只包含真正的逻辑修改。

建议流程:

copy lib/service/semantic_query_parser_lm.dart lib/service/semantic_query_parser_lm.dart.bak
git restore lib/service/semantic_query_parser_lm.dart

然后打开 .bak,只复制真正需要的代码片段。


情况 3:只是编码错了,内容还可逆

如果确定文件是 GBK 编码,可以尝试转回 UTF-8:

iconv -f gbk -t utf-8 old.dart > new.dart

或者 Python:

from pathlib import Path

src = Path("old.dart")
dst = Path("new.dart")

text = src.read_text(encoding="gbk")
dst.write_text(text, encoding="utf-8")

但注意:如果文件已经经历过多次错误打开和保存,不一定能完全恢复。


七、如何防止再次发生?

1. 统一项目编码:全部使用 UTF-8

在项目根目录加 .editorconfig

root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.dart]
charset = utf-8
end_of_line = lf

[*.md]
charset = utf-8
end_of_line = lf

[*.json]
charset = utf-8
end_of_line = lf

这样支持 EditorConfig 的 IDE 会自动使用 UTF-8。


2. VS Code 设置

项目里可以加:

{
  "files.encoding": "utf8",
  "files.autoGuessEncoding": false,
  "files.eol": "\n"
}

建议关闭自动猜编码,因为自动猜有时反而会把 UTF-8 文件识别成 GBK。


3. JetBrains / Android Studio 设置

路径一般是:

Settings / Preferences
  -> Editor
  -> File Encodings

建议设置:

Global Encoding: UTF-8
Project Encoding: UTF-8
Default encoding for properties files: UTF-8

如果 IDE 提示:

Reload in another encoding
Convert to UTF-8

要优先选择:

Convert to UTF-8

而不是随便用 GBK 重新保存。


4. 添加 Git 提交前检查

可以写一个简单脚本,阻止非 UTF-8 文件提交。

例如 scripts/check_utf8.py

from pathlib import Path
import sys

TEXT_EXTS = {
    ".dart", ".md", ".json", ".yaml", ".yml",
    ".txt", ".py", ".js", ".ts", ".html", ".css"
}

failed = []

for path in Path(".").rglob("*"):
    if not path.is_file():
        continue

    if ".git" in path.parts:
        continue

    if path.suffix.lower() not in TEXT_EXTS:
        continue

    try:
        path.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        failed.append(str(path))

if failed:
    print("以下文件不是合法 UTF-8:")
    for f in failed:
        print(" -", f)
    sys.exit(1)

print("UTF-8 check passed.")

运行:

python scripts/check_utf8.py

也可以接到 pre-commit 里。


5. .gitattributes 统一换行

.gitattributes 不一定能完全解决编码问题,但可以规范文本文件和换行:

* text=auto

*.dart text eol=lf
*.md text eol=lf
*.json text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
*.py text eol=lf

注意:.gitattributes 主要解决文本归一化和换行问题,不能指望它自动帮你识别所有编码错误。


八、团队协作时怎么说清楚这个问题?

这类问题最好不要只说:

你代码乱码了。

而是应该明确说明:

这个文件原本是 UTF-8,但被 GBK 或其他编码方式打开并保存了。
Git 只比较字节流,不理解文本编码,所以它会把这当成普通修改,不一定产生冲突。
建议恢复原文件后,只迁移真正的逻辑修改,并统一 IDE 编码为 UTF-8。

这样对方更容易理解问题本质。


九、这次给我的教训

这次问题给我的最大提醒是:

对包含中文 prompt、中文注释、中文配置的 AI 项目来说,编码规范不是小事。

尤其是像 Memoria 这种项目,里面会有很多中文语义解析 prompt,例如:

  • 时间解析;

  • 地点解析;

  • 相册事件生成;

  • 语义检索;

  • LLM 输出 JSON 模板;

  • 中文标签和规则。

这些内容一旦被错误编码污染,表面看起来可能只是“中文显示不正常”,但实际上可能影响整个模型调用链路。

所以后续我会把 UTF-8 编码检查作为项目工程规范的一部分。


十、总结

这次问题可以总结成一句话:

Git 只管理字节流,不理解文本编码。UTF-8 文件被 GBK 打开并保存后,底层字节完全变化,Git 只会认为这是普通修改,不会自动触发所谓“编码冲突”。

解决方案:

  1. 项目统一使用 UTF-8;

  2. IDE 全局设置 UTF-8;

  3. 不要用 GBK 保存代码文件;

  4. 发现 diff 大面积异常时,优先怀疑编码问题;

  5. 对已经污染的文件,优先从 Git 恢复,再手动迁移有效逻辑;

  6. 添加 .editorconfig.gitattributes 和 UTF-8 检查脚本;

  7. 团队协作时明确说明:这是编码问题,不是普通代码风格问题。

这类 bug 不难修,但如果不重视,很容易悄悄污染主分支。

所以,尤其是中文 AI 项目,一定要记住:

代码文件统一 UTF-8。
Prompt 文件统一 UTF-8。
配置文件统一 UTF-8。
不要让 GBK 混进来。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐