带 GPT 辅助判定的快速大规模修文档 Typo——以 Kong 文档站的实践
This post is also available in English, at Fast, Large-Scale Document Typo Correction with GPT-Assisted Judgement: A Case Study of Kong’s Documentation Site
曾经有人戏称——参与开源软件最快的方式就是给开源仓库修 Typo,这其实没问题。
不过也有人看不起修 Typo 的人,认为只是为了刷工作量,代码中的 Typo 有的时候并不需要修,毕竟,代码在能跑的情况下有点 Typo 在代码/注释中其实也无伤大雅,毕竟,汉字的序顺并不定一能影阅响读。
当年我刚刚加入 PingCAP 的时候提交的第一个给 @pingcap 的 PR 是 https://github.com/pingcap/docs/pull/2058 ,将文档中的
http
改成https
除了代码本身以外,有一类文档个人感觉还是值得修修 Typo 的,那就是各大文档仓库,比如:
- https://github.com/pingcap/docs
- https://github.com/pingcap/docs-cn
- https://github.com/Kong/docs.konghq.com
- https://github.com/github/docs
- …
文档本身作为产品的门面,如果你是一个用户,看到这种文档你会有如何想法:
来源 https://docs.konghq.com/gateway/changelog/#features-24, kong 这个页面上
availability
全部写成的availibilty
这怎么行?我得去 reoprt 个 abouse!
How to fix typo
我们修 Typo 和 Redis 缓存过期一样分为两种模式:
- passive way
- 当我们阅读文档到某个部分发现有个 Typo,这个时候正义感爆棚的我们找到「Edit this docs」按钮,登录自己的 GitHub 去提交 PR
- active way
- 想办法主动发现 Typo 并提交
前者有些太慢了,为了修 Typo 需要仔阅细读(注意,因为汉字的序顺并不定一能影阅响读,例如上面 Kong 的文档中你不仔细看是不是不容易发现 availibilty
其实是 Typo)所有文档。
你发现上面我写的是「仔阅细读」而不是「仔细阅读」了么?
所以本文的主旨在于介绍后者,提供一个「静态检查 -> GPT 判断 -> 快速人工处理」的方式。
Der aktive Weg(The active way)
为了尽可能加速我们修 Typo 的能力,本文提出一个「静态检查 -> GPT 判断 -> 快速人工处理」的方式,我们按照这个顺序依次介绍。
由于 https://github.com/Kong/docs.konghq.com 中的 Typo 挺多,加上我住的地方离 Kong 上海办公室很近,所以本文我们以这个仓库作为示例。
拍摄于 Kong 上海办公室楼下
静态检查+初筛选
本来这一步是想手搓一个工具的,但是发现 GitHub 上有个很好用的库叫做 typos , https://github.com/crate-ci/typos,在安装之后只要到项目目录下 typos
即可标记出所有的潜在 Typo,例如,我们在 docs.konghq.com/app
目录下:
find . -name "*.md" | xargs -I {} typos {}
就可以看到不少 Typos 标记:
error: `hexidecimal` should be `hexadecimal`
--> ./_src/gateway/plugin-development/pdk/kong.request.md:335:61
|
335 | * Percent-encoded values of reserved characters have their hexidecimal
| ^^^^^^^^^^^
|
error: `Hashi` should be `Hash`
--> ./_src/gateway/reference/configuration/configuration-3.4.x.md:2223:58
|
2223 | resurrected for when they cannot be refreshed (e.g., the HashiCorp vault is
| ^^^^^
|
error: `mis` should be `miss`, `mist`
--> ./_src/gateway/reference/configuration/configuration-3.4.x.md:4138:11
|
4138 | note that mis-management of keyring data may result in irrecoverable data loss.
| ^^^
但是如你所见,这个里面有不少的 False positive 的案例,比如 HashiCorp 他认为 Hashi 应该改为 Hash,比如 mis-management 他认为应该改为 miss-management。
对于这种情况我们可以编写一个 typos.toml
进行简单的初筛,内容如下:
[default.extend-words]
Hashi = "Hashi"
mis = "mis"
然后将命令换为:
find . -name "*.md" | xargs -I {} typos {} --config /path/to/typos.toml
但是通过这种方式标记出来的 Typo 我们需要人工判断,并手动找到对应的文件做修改,依然是一个比较费时费力的操作,所以我们需要让 typos
输出程序可以理解的方式交由下一步处理,好在 typos
支持 --format json
参数,加上这个参数之后输出的内容就变成了类似如下:
{"type":"typo","path":"/path/to/workspace/docs.konghq.com/app/_src/gateway/how-kong-works/routing-traffic.md","line_num":685,"byte_offset":81,"typo":"fo","corrections":["of","for","do","go","to"]}
{"type":"typo","path":"/path/to/workspace/docs.konghq.com/app/_src/gateway/breaking-changes/30x.md","line_num":124,"byte_offset":6,"typo":"fuction","corrections":["function"]}
{"type":"typo","path":"/path/to/workspace/docs.konghq.com/app/_src/gateway/production/tracing/api.md","line_num":2,"byte_offset":19,"typo":"Referenece","corrections":["Reference"]}
我们暂时称这个文件为——脏 Typo JSON。
GPT 标记
在上文中我们已经可以让潜在 Typo以一行一个 JSON 字符串的方式记录下来了,下一步我们需要做的就是
- 读取「脏 Typo JSON」的每一行内容
- 找到对应行的内容
- 找到潜在的 Typo
- 替换掉潜在的 Typo
- 并问问 GPT 这个替换它认为是否合理
这里的难点在于 Prompt,我的 Prompt 如下:
{
"messages": [
{
"role": "system",
"content": """
你是一个熟悉互联网公司名称和词语和拼写的判官,我会给你一个句子和句子中需要替换的单词,你需要以0-100之间的数字告诉我这个单词是否应该被替换,无论什么情况,你都只能回答 0到100 之间的数字,数字越大表示越有概率需要更换,如果你不能确定,请回答概率数字,不能有任何额外的解释或者注释,只回答数字,\n
同时你需要判断是否是特定公司名(例如 Hashicorp 是个公司名字,不应该被替换),或者是否是无意义字符串来决定,如果是特定公司名或者无意义字符串,你需要回答 0,如果是一个普通的单词,你需要回答 100,\n
请首先判断需要被改写的句子是否是一个有意义的句子,如果不是有意义的句子,你需要回答 0\n
例如 Time-to-live (in seconds) of a HashiCorp vault miss (no secret). 中 Hashi 是 HashiCorp 的一部分,并不是一个 Typo,所以不应该被替换,需要回答 0\n
例如 02:21:00:86:ce:d0:fc:ba:92:e9:59:16:1c:c3:b2:11:11:ed: 中的 ba 由于是一个示例字符串的一部分而不是任何有意义的句子,所以不应该被替换,需要回答 0\n
例如 X-Kong-Admin-Request-ID: ZuUfPfnxNn7D2OTU6Xi4zCnQkavzMUNM 中的 OTU 由于是一个示例字符串的一部分而不是任何有意义的句子,所以不应该被替换,需要回答 0\n
"""
},
{
"role": "user",
"content": "{} \n {} 改为 {}"
}
],
"stream": False,
"model": "gpt-4",
"temperature": 0.5,
"presence_penalty": 0,
"frequency_penalty": 0,
"top_p": 1
}
等 Prompt 写好之后就是花钱找 OpenAI 的 API Key 然后对接的事情了,这里由于主要是个 PoC ,所以就用 Python 完成,关键代码如下:
gpt_rate_response = client.chat.completions.create(
messages=formatted_message['messages'],
model=formatted_message['model'],
stream=formatted_message['stream'],
top_p=formatted_message['top_p'],
temperature=formatted_message['temperature'],
presence_penalty=formatted_message['presence_penalty'],
frequency_penalty=formatted_message['frequency_penalty']
)
gpt_rate = gpt_rate_response.choices[0].message.content
上面代码中的 gpt_rate
就是一个 0~100 之间的数字,不过可能由于我的 Prompt 没有写的很好,只会输出 0 或者 100,这个时候我们只要丢掉打分是 0 的就行了。
在这里我想感谢 @BennyThinks 的「头顶冒火」服务,地址是 https://burn.hair/ ,在这个项目中 @BennyThinks 的服务给我提供了非常多的支持。
目前新用户注册送 $1 邀请好友送 $1 加频道(https://t.me/mikuri520)送 $2
同时进一步感受到了——「没有钱,就没法做科研」的道理
这里 GPT 标记完成之后我们其实只是清理掉了「脏 Typo JSON」文件中大概率误判的内容并写回,我们称这个 GPT 清理过的文件为「干净 Typo JSON」
快速标记替换
现在我们终于有了「干净 Typo JSON」了,我们需要有个方式来对文件快速完成替换,为了人机工程考虑,我们引入的操作模式为:「屏幕上每次出现两行,第一行是原始行 ,第二行是替换了之后的行,用户只需要按 Y 即可确认替换,按 N 表示放弃替换」,界面如下:
部分代码实现如下:
def do_replace(file_path,line_num,typo,correction):
lines = []
with open(file_path, 'r', encoding='utf-8') as file:
lines = file.readlines()
lines[line_num-1] = lines[line_num-1].replace(typo, correction)
with open(file_path, 'w', encoding='utf-8') as file:
file.writelines(lines)
def get_ch():
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return ch
...
# Display the original and corrected lines, and give color to the corrected word
typo_word_index = orignal_line.find(typo_word)
print("Path: ", json_obj['path'])
print(f"{orignal_line[:typo_word_index]}{Back.RED}{orignal_line[typo_word_index:typo_word_index + len(typo_word)]}{Style.RESET_ALL}{orignal_line[typo_word_index + len(typo_word):]}")
print(f"{corrected_line.replace(json_obj['corrections'][0], Back.GREEN + json_obj['corrections'][0] + Style.RESET_ALL)}")
print("Do you want to continue? [y/n]: ")
ch = get_ch()
if ch == 'y':
do_replace(json_obj['path'], json_obj['line_num'], typo_word, json_obj['corrections'][0])
print("Replaced!")
elif ch == 'n':
continue
else:
print("Exiting...")
exit(0)
当然,如果 Typo 足够多的话,即使能这样快速按 Y/N 来替换,对着电脑按 10+ 分钟也和做狗推没啥区别
进一步提升 ROI
使用本文提出的「静态检查 -> GPT 判断 -> 快速人工处理」方式,我完成了对 https://github.com/Kong/docs.konghq.com 仓库中绝大部分(GPT 可能会有少量 false negative 反向误判)Typo 的修复,总共提交了 3 个 PR:
- https://github.com/Kong/docs.konghq.com/pull/7311 (Merged)
- https://github.com/Kong/docs.konghq.com/pull/7312 (Merged)
- https://github.com/Kong/docs.konghq.com/pull/7317 (Merged)
- https://github.com/Kong/docs.konghq.com/pull/7706
同时还顺手在 Cloudflare 和 Halo 的仓库上实践了一下:
- https://github.com/cloudflare/cloudflare-docs/pull/14263 (Merged)
- https://github.com/halo-dev/docs/pull/343 (Merged)
涉及 68 个分散在仓库各处的文件的改动,总用时(Clone 仓库+运行脚本+手动提交)大约 30 分钟,简称「Typo 仙人」。
目前这个流程中最耗时的时间还是莫过于「快速人工处理」,即使已经有了上面可以快速按 Y/N 的 TUI 程序,人肉手动做最终的判定还是会受到操作者(也就是我自己)的瓶颈限制,所以这里可能有如下思路:
假设「静态检查 -> GPT 判断」之后如总共有 100 个 Typo,但由于「GPT 判断」还是会有些 False Positive 的部分导致其中 10 个是假的但是没判断出来,这样我们的修改中就有 90 个真 Typo 和 10 个假 Typo,这里如果我们按照上面的「快速人工处理」的话还是得手动按 100 次 Y/N,比较费力,不如这个时候我们直接本地在「GPT 判断」完成之后自动完成对文件的修改然后把 PR 提交上去,那么:
由于库的维护者应该是要手动看一遍的,他应该看到有 10 个假 Typo,此时他有如下选择:
- 直接关闭 PR,但是对于大型开源项目来说这么做可能不是很友好,其他人会质疑这么做的动机
- 关闭了之后自己重新开,但是因为大部分是真 Typo,手动重新开 PR 会非常耗时
- 会帮忙把 10 个 Typo 修了并且合并,概率较大
在这种方式下可以做到最终只有一个人需要手动 Review 一遍修改,而由于(如果是负责的)维护者本身就要人工 Review 一遍,这样可以将这个有瓶颈的工作分摊到各个仓库的维护者身上,极大提升了修 Typo 效率,当然…
后记
不知道 Kong 的维护者看到这三个 PR 内部包含的这么多文件更改的时候有何感想
在 AI/GPT 如此盛行的时代,作为对自己能力没啥信心,不太会写代码,也不太会追热点的"开发者",我找到了一个除了用来做日常问答和 Copilot 以外看上去好像挺实用的一个 AI 场景,希望本文可以给读者带来某种启发~