Nova Kwok's Awesome Blog

带 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://docs.pingcap.com/zh/tidb/stable/system-variables#tidb_skip_missing_partition_stats-%E4%BB%8E-v730-%E7%89%88%E6%9C%AC%E5%BC%80%E5%A7%8B%E5%BC%95%E5%85%A5

来源 https://docs.pingcap.com/zh/tidb/stable/dashboard-profiling#%E6%94%AF%E6%8C%81%E7%9A%84%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE

来源 https://docs.konghq.com/gateway/changelog/#features-24, kong 这个页面上 availability 全部写成的 availibilty

这怎么行?我得去 reoprt 个 abouse!

How to fix typo

我们修 Typo 和 Redis 缓存过期一样分为两种模式:

前者有些太慢了,为了修 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 字符串的方式记录下来了,下一步我们需要做的就是

这里的难点在于 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:

同时还顺手在 Cloudflare 和 Halo 的仓库上实践了一下:

涉及 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,此时他有如下选择:

  1. 直接关闭 PR,但是对于大型开源项目来说这么做可能不是很友好,其他人会质疑这么做的动机
  2. 关闭了之后自己重新开,但是因为大部分是真 Typo,手动重新开 PR 会非常耗时
  3. 会帮忙把 10 个 Typo 修了并且合并,概率较大

在这种方式下可以做到最终只有一个人需要手动 Review 一遍修改,而由于(如果是负责的)维护者本身就要人工 Review 一遍,这样可以将这个有瓶颈的工作分摊到各个仓库的维护者身上,极大提升了修 Typo 效率,当然…

后记

不知道 Kong 的维护者看到这三个 PR 内部包含的这么多文件更改的时候有何感想

在 AI/GPT 如此盛行的时代,作为对自己能力没啥信心,不太会写代码,也不太会追热点的"开发者",我找到了一个除了用来做日常问答和 Copilot 以外看上去好像挺实用的一个 AI 场景,希望本文可以给读者带来某种启发~

#Chinese #GPT