Hanzi Flow 是一款围绕一个想法打造的汉字练习应用:自适应句子选择。它会根据你当前的掌握程度,从 79,000 多条真实例句中挑选含 2 到 5 个生字的句子,让你始终处于 90% 到 95% 的理解区间。不用管理卡组,只需输入拼音开始练习。

源码: github.com/brianhliou/hanzi-flow

在新页面打开 →

现有工具的问题

中文学习应用基本分为三类,而它们都有各自的短板。

像 Anki 这样的闪卡应用让你死记孤立的汉字。整理卡组花的时间比真正学习还多。等你终于在真实文本里见到某个字时,却认不出来,因为你是脱离语境学的。

像 Pleco 这样的词典应用查词很好用,但完全是被动的。你读了,你懂了,什么也没留下。没有主动回忆,没有打字练习。

阅读应用和家教给你的要么是儿童读物(无聊),要么是母语者内容(难度过高)。你要么无聊,要么招架不住,两者都无法培养流利度。

缺失的一环是保持心流状态。也就是 90% 到 95% 的理解区间,让你有挑战但不至于沮丧。你需要难度恰当、自动挑选的句子,并通过打字进行主动回忆。

我做了什么

Hanzi Flow 是带自适应难度的句子练习。你读中文、输入拼音,应用根据你对每个字的掌握程度挑选下一句。它有 Anki 的间隔重复,却不用管理卡组;有 Duolingo 的单次练习流程,但用的是真实句子而非会话手册里的例句。

练习界面:逐字输入句子

这款应用:

  • 来自 Tatoeba 的 79,000 多条句子(真实用法,而非教科书例句)
  • 4,000 多个汉字:HSK 1 至 9 级,外加约 1,000 个超纲字
  • 自适应算法:根据你的掌握程度挑选含 2 到 5 个生字的句子
  • 掌握度追踪:字级得分配合间隔重复(EWMA + SRS)
  • 对齐 HSK 3.0:可按官方大纲等级筛选(1 至 9 级或超纲)
  • 简繁支持:简体、繁体或混合
  • 实时反馈:彩色视觉提示,外加 1,598 个音频文件(覆盖全部拼音音节)
  • 100% 本地优先:无需账号,不做追踪,可离线使用

你练习与自己水平相符的地道句子,通过拼音输入培养打字流利度,系统会随你的进步自动调整。无需手动管理卡组。

使用体验

首次运行:设置偏好

第一次打开应用时,你要选两项:

字形:简体、繁体或混合。这会筛选语料库以匹配你的目标。

HSK 等级:筛选是累积的。选「HSK 1-3」,你得到的句子只包含 1、2、3 级的字。选「HSK 超纲」则纳入官方大纲之外约 1,000 个进阶字。

这些设置随时可在设置页更改。更改后,应用会清空当前的句子队列,并按新的筛选条件重新生成。

设置页:字形与 HSK 等级偏好

练习循环

练习流程很直白:

  1. 读句子:汉字大字号显示
  2. 输入拼音:逐字输入,声调可选(wo3wo 都行)
  3. 获得反馈:当前字高亮为蓝色,错误则红色闪烁并可重试(最多 4 次)
  4. 查看拼音:每个字下方显示带声调的正确答案
  5. 显示译文:完成后可选择在底部查看英文
  6. 下一句:按空格键或点击按钮

反馈即时且可视化。你的注意力始终落在当前字上(蓝色高亮)。答错时红色闪烁并播放正确读音的音频。你有 4 次机会,之后系统会继续往下并把该字标为紫色(放弃)。系统接受异读(地 既接受 de 也接受 di),但会显示符合语境的那个。

练习中句子的彩色反馈

关键机制在于反馈回路。现实中,你可能在书里看到一个字,猜错了,却永远不知道。或者看电视时完全漏掉读音。这类学习循环很慢。随意学习的人可能要花好几个月,才能积累足够的接触量来真正学会一个字。

这款应用压缩了这个循环。你看到一个字,输入它,立刻得到带音频的纠正,马上重试,然后继续。全程十秒。一次练习做 100 个字,就跑完了 100 次学习循环。主动回忆(打字)、即时纠正(音频加视觉)和高频次(句子源源不断)叠加起来,产生的效果是被动接触给不了的。

进度追踪

统计页展示四项主要指标:

  • 练习过的汉字总数:你见过的不重复汉字
  • 已掌握汉字:掌握度 ≥ 0.8 的字
  • 练习过的句子:完成的不重复句子
  • 整体正确率:首次尝试即正确的百分比

掌握度分布把汉字分为几组:

  • 学习中(s < 0.6):仍在建立熟悉度
  • 熟练(0.6 ≤ s < 0.8):快了
  • 已掌握(s ≥ 0.8):牢固

每个字都会按间隔重复记录下次复习时间。

统计页:进度总览

掌握度分布:汉字分布情况

自适应算法(NSS)

随机选句效果很差。你会反复看到相同的句子,在已经掌握的内容上浪费时间,或者被含 10 个以上生字的句子压垮。

下一句选择(NSS)就是挑选下一句的算法。它在新知识、间隔重复、新鲜度和难度之间取得平衡。

核心想法

最佳区间:每句 2 到 5 个生字

  • 0 到 1 个生字:太简单,只是复习
  • 2 到 5 个生字:恰到好处,挑战足够,语境也足够
  • 6 个以上生字:太难,你只能瞎猜

算法还要平衡:

  • 新知识(你从未见过的字)
  • 间隔重复(复习到期的字)
  • 新鲜度(不要反复刷同样的句子)
  • 多样性(混合不同难度)

工作原理

NSS 按批次运行。你开始练习时,它生成 10 个句子。做完 8 个后,它在后台预取下一批。你永远不用等待。

四个步骤:

1. 筛选符合条件的句子

从完整语料库(79K)开始,应用你的筛选条件:

  • 字形(简体/繁体/混合)
  • HSK 等级(你选择的范围)
  • 冷却(排除最近 20 分钟内见过的句子)

这样得到候选池,通常是 20K 到 40K 条句子。

2. 随机抽取 300 个候选

从池中随机取 300 条。数量足够保证多样性,又小到能快速打分(100 毫秒以内)。随机抽样确保你每次练习看到的句子都不同。

3. 为每个候选打分

对这 300 条中的每一条,按四个因素计算得分:

  • 基础收益:每个字的 (1 - 掌握度) 之和。掌握度低的字得分更高。若已过复习期,收益翻倍。
  • 新鲜度:按距上次见到的时间给加分。鼓励多样性,避免反复刷。
  • 通过惩罚:如果这句你已多次全对,就降低得分。避免过度练习。
  • k 惩罚:惩罚生字数落在 2 到 5 区间之外的句子。强制控制难度。

加总起来:score = base_gain + novelty - pass_penalty - k_penalty

4. 取前 10,打乱,入队

按得分排序,取前 10 条,打乱顺序以混合难度,然后加入队列。

你练这 10 条。剩下 2 条时,算法会在后台再跑一次,预取下一批。

掌握度追踪

每个字都有一个 0.0 到 1.0 的掌握度得分(s)。

  • s = 0.3(新字的默认值)
  • s → 1.0(稳定答对)
  • s → 0.0(一直答错)

更新采用 α=0.15 的指数加权移动平均(EWMA)。近期表现权重更大,但变化是渐进的。一次失误不会让你跌到底,一次答对也不会虚高。

答对一个字后,它的稳定度(SRS 间隔)会增长为 1.2 倍。起点是 1 小时,然后 1.2 小时,再 1.44 小时。相比 Anki 激进的跳跃(1 天 → 3 天 → 7 天),这让汉字在轮换中停留更久。

为什么更慢?当天复习很重要。你需要在一次练习中见到某个字 3 到 5 次,才能把它从短期记忆转入长期记忆。间隔跳得太快会错过这个窗口。

当一个字到期(超过它的 next_review_ts)时,它在打分中获得双倍权重。算法会优先选择含过期字的句子。这就是经典的间隔重复。

数据管线

做这个需要完备的数据:带读音的汉字、带译文的句子、难度标签、质量过滤。下面是我构建语料库的过程。

字集

从 Unicode CJK 统一汉字区(U+4E00 至 U+9FFF)入手,共 20,992 个字。涵盖现代汉语的所有汉字,外加罕见异体字和历史字形。

然后用三个来源的数据为每个字补充信息:

  • Unihan 数据库:拼音读音、异体字、部首拆分
  • CC-CEDICT:英文释义与例词(学 → “study; learn”,学生 → “student”)
  • OpenCC:简繁对应关系,用于字形分类

结果:99.7% 的字有拼音,67% 有英文释义,35% 有异体对应。我知道哪些字真的出现在 Tatoeba 里(5,000 多个),哪些只是理论存在(罕见的文言字)。

汉字覆盖曲线:累计字数

这条曲线显示汉字在各 HSK 等级上的累积情况。HSK 1 约 300 字,HSK 2 再加约 300 字,以此类推。等级越高曲线越平缓,逐渐接近约 3,000 字的识字门槛。再往上,边际收益递减。

频率分布遵循齐普夫定律:少数汉字占主导,大多数都很罕见。前 500 个字覆盖 75% 的文本,前 2,000 个覆盖 95%。这对自适应算法很重要,因为先学高频内容能立刻带来阅读能力。

频率分布:幂律

HSK 分级

HSK(汉语水平考试)是中国的官方水平测试。2021 年修订版(HSK 3.0)定义了 9 个等级,总计 3,000 个汉字。我整合了来自 elkmovie/hsk30 的官方字表(由政府 PDF 经 OCR 得到)。

每个句子按其中最难的字分级:

  • 全是 HSK 1 的字?标为「1」
  • 最难的字是 HSK 3?标为「3」
  • 含 HSK 9 以上的字?标为「beyond-hsk」

这驱动了筛选功能。当你选择「HSK 3」时,你只会看到每个字都属于 1 至 3 级的句子。不会有意外。

词汇量增长:各 HSK 等级的累计字数

句子在各 HSK 等级上的分布

句子处理

从众包翻译数据库 Tatoeba 抓取了 79,000 多条中文句子。每条一边是英文,一边是中文。

难点:语境感知的拼音。汉字有多音,读法取决于语境。我先用 jieba + pypinyin 跑第一遍,再用 GPT-4o-mini 校验(79K 句约花 2 美元)。在 地(de 对 di4)、著(zhe 对 zhu4)、谁(shei2 对 shui2)等常见多音字上修正了 10,336 处错误。

多音字示例:

  • 了:le(助词)对 liao3(完结)
  • 行:xing2(可以)对 hang2(行列)
  • 长:zhang3(生长)对 chang2(长)

光靠查表是不行的。你得理解词语所处的语境。

第 1 步:jieba + pypinyin。jieba 把「我们试试看」切分为 [“我们”, “试试”, “看”],然后 pypinyin 为每个词生成拼音:我们试试看 → wo3 men shi4 shi4 kan4。多数情况有效,但在助词和口语读音上会出错。

第 2 步:GPT-4o-mini 校验。把整个语料库过了一遍 OpenAI 的 API(79K 句约 2 美元,每次调用批量处理 10 条)。提示词要求逐字给出带声调的拼音。在 79,603 条句子中发现 10,336 处错误。主要修正:

  • (120 处修改):pypinyin 给出 jue2(感觉),OpenAI 给出 jiao4(睡觉)
    • 你應該去睡覺了吧:「你该去睡觉了」
  • (696 处修改):pypinyin 给出 zhu4(穿着),OpenAI 给出 zhe(助词)
    • 生活就是當你忙著進行你的計劃時…:「生活就是当你忙于推进自己的计划时发生的事」
  • (404 处修改):pypinyin 给出 shui2(书面),OpenAI 给出 shei2(口语)
    • 你知不知道他们是谁?:「你知道他们是谁吗?」
  • (136 处修改):pypinyin 给出 zhang3(生长),OpenAI 给出 chang2(长)
    • 我看见一个长头发的女生:「我看见一个长头发的女生」

我手动审阅了高频字,并应用了高置信度的修正。OpenAI 的语境理解能力捕捉到了基于规则的方法漏掉的错误。

英文译文:同样用 GPT-4o-mini 生成(总计约 1 美元)。Tatoeba 的数据集只有约 2,000 条句子带译文。若只用这些,语料库会从 79,000 多条缩到 2,000 条,自适应算法的效果就毁了。所以我用 GPT-4o-mini 翻译了全部 79,000 多条句子。大语言模型在翻译上很擅长,因为它们会利用语境,而这正是中译英准确性所需要的。示例:

  • 我們試試看! → Let’s give it a try!
  • 你在干什麼啊? → What are you doing?
  • 今天是6月18号,也是Muiriel的生日! → Today is June 18th, and it’s Muiriel’s birthday!
  • 生日快乐,Muiriel! → Happy birthday, Muiriel!
  • 密码是”Muiriel”。 → The password is “Muiriel.”

还应用了一些质量过滤:

  • 字形分类:简体、繁体、中性或混合
  • 粗俗内容移除:过滤脏话(约 10 条句子)
  • 长度上限:移除超过 50 字的句子(约 270 条),以免出现滚动
  • 元句子:移除 Tatoeba 自我指称的句子(约 80 条)

最终语料库:79,333 条干净句子

字形分布:简体/繁体/混合的构成

架构与性能

使用现代 Web 技术构建,针对速度与隐私做了优化。

为什么本地优先?

  • 隐私:无账号、无追踪、无遥测。你的进度永不离开设备。我甚至没接分析工具。数据归你所有。
  • 性能:没有网络延迟。句子选择在 100 毫秒内完成。打字反馈即时。统计页 50 毫秒内加载。
  • 简单:无后端要维护,无数据库要扩容,无鉴权。应用就是静态文件加客户端 JS。我免费托管在 Vercel 上。
  • 代价:没有跨设备同步。笔记本和手机上的练习是分开的。对 MVP 来说可以接受。多数人只有一台主力设备。以后可以加导出/导入或可选的云同步。

技术栈

前端:

  • Next.js 15,使用 App Router
  • React 19 + TypeScript
  • Tailwind CSS
  • Dexie.js 操作 IndexedDB

数据管线:

  • Python 3.9+ 编写处理脚本
  • jieba 做分词
  • pypinyin 生成语境感知的拼音
  • pandas + matplotlib 做分析
  • GPT-4o-mini 做翻译与拼音校验

音频:

  • AWS Polly(Zhiyu 音色)做语音合成
  • 1,598 个音节,覆盖全部声调
  • 采用 OGG 格式以兼容网页浏览器

部署:

  • Vercel(零配置)
  • 静态站点,无服务器
  • 全球 CDN

数据加载与缓存

应用在首次访问时加载大数据集,随后积极缓存。

首次加载:

  • 79,333 条句子(约 41MB JSON):只取一次,存入内存
  • 汉字映射 CSV(约 2MB):ID、拼音、释义
  • 音频文件:按需获取(每个 1 到 2KB)

内存缓存:

  • 句子存在 JavaScript 数组里
  • 汉字到 ID 的映射存在 Map 里(O(1) 查找)
  • 会话期间不重新请求

IndexedDB 持久化:

  • WordMastery 表:每个练习过的字一行(掌握度得分、SRS 间隔、下次复习时间、次数统计)
  • SentenceProgress 表:每条完成的句子一行(通过率、尝试次数、上次见到时间)
  • SentenceQueue 表:存放接下来的 10 个句子(更改偏好时重新生成)

所有数据都留在你的设备上。除首次加载外没有网络请求。

性能技巧:

  • 音频预加载:NSS 挑出下一句时,会在后台预加载其拼音音频(3 到 5 个文件,不到 10KB)。等你看到它时,音频已缓存好。
  • 队列预取:当你做完 10 句中的 8 句时,NSS 在后台为下一批运行。你永远不用等待。
  • 惰性加载:统计页只在你访问时才计算分布,而不是每次操作都算。

离线: 首次加载之后,可以完全离线使用。在飞机上、地铁里,随处都能练。进度会立刻保存到 IndexedDB(无需保存按钮)。

关键设计决策

声调数字还是声调符号

应用接受声调数字(wo3shi4)而不是声调符号(wǒ、shì)。为什么?

  • 更好输入(不需要特殊键盘)
  • 是中文输入法的标准做法(为真实打字做准备)
  • 无歧义(vü 等价,声调符号需要 Unicode 组合字符)
  • 迫使你明确思考声调(wo3 = 第三声),而不是靠视觉记忆

基于句子的学习

为什么用句子而不是孤立的词?

  • 语境:在真实用法中学汉字。「我们试试看」教的是 我们、试试、看,同时还有语法结构。
  • 阅读练习:贴近真实阅读。培养解析汉字序列的能力。
  • 打字流利度:训练完整句子的肌肉记忆,跟你实际使用的一样。
  • 难度校准:句子天然具有难度分布(生字数量)。词级做不到这一点。
  • 动力:完成一个句子让人觉得有进展。你在读真正的中文,而不是刷「的 的 的」闪卡。

接下来

就日常练习而言,这款应用的功能已经完整。如果我回头继续做,可能会加上:

  • 移动端 PWA(可安装)
  • HSK 统计细分
  • 复习模式(以 SRS 为核心的练习)
  • 整句音频(完整发音,而非仅字级)
  • 汉字卡片(字源、笔顺、组词)
  • 声调分析(追踪你容易混淆的声调)
  • 自定义语料库(导入你自己的句子)
  • 导出/导入(跨设备迁移)

这是开源项目。有想法?欢迎提 issue 或 PR。

结语

做这个是为了解决自己的痛点。我想练中文阅读,又不想承担闪卡的额外负担。真实句子、自适应难度、干净的打字界面。

结果变成了对数据管线(79K 多条带语境感知拼音的句子)、自适应算法(EWMA + SRS)和本地优先架构(IndexedDB + 缓存)的深入钻研。我在多音字、齐普夫定律,以及为当天复习调优间隔重复方面学到了很多。

如果你在学中文,试试看。如果你是开发者,看看仓库。句子选择算法、数据处理和本地优先方案里都有些有意思的东西。

鸣谢:Tatoeba 提供句子,MDBG 提供 CC-CEDICT,elkmovie 提供 HSK 3.0 字表,Unicode 联盟提供 Unihan。

自己试试

访问 hanzi.brianhliou.com 开始。

  1. 选择你的字形和 HSK 等级
  2. 用声调数字输入拼音(wo3ni3ta1
  3. 到统计页查看进度
  4. 每天练习,看掌握度上升

源码在 GitHub,采用 MIT 许可证。克隆后本地运行:

git clone https://github.com/brianhliou/hanzi-flow.git
cd hanzi-flow/app
npm install
npm run dev

打开 http://localhost:3000