跳到主内容
cd ../blog
OpenCode英语学习AI 工具TUI

我把 OpenCode 的回复改成了「带中文注释的英文」——一次完整的实践记录

cooler404
2026-09-22
~10min read

背景:我英语不好,想每天多接触英文单词,但遇到不认识的词又不想切出去单独查翻译。 这篇文章记录了为了解决这个问题,我在 OpenCode 里做的一套东西:碰到了哪些问题、怎么解决的、提示词怎么评估的,以及最终成品长什么样。


一、需求是怎么来的

学英语最怕的不是难,是中断。

正常的阅读流程是这样的:

  1. 读到一个不认识的词;
  2. 停下来,切窗口,打开词典;
  3. 查完切回来,已经忘了前半句在讲什么;
  4. 于是放弃,继续用中文。

问题不在词典,在于「查」这个动作本身破坏了阅读的连贯性。

所以我想要的其实是两件事:

  • 不想查:不认识的词,意思应该直接出现在眼前,不要让我离开句子。
  • 想多接触:每天读的东西里,英文单词的密度要高,而且要能反复见到。

于是就有了这个方案:让 AI 的回复本身变成教材。


二、方案:两层阅读

核心思路一句话:把词义放在词后面,把深度查询放在一个按键之外。

具体分两层:

层级形式成本用途
第一层:行内注释**stale**(过时的) 直接跟在单词后面免费,自动保持阅读流畅,不打断
第二层:卡片查询/dict stale 弹出完整词卡免费,离线回答第一层留下的疑问

第一层负责「不中断」,第二层负责「查得深」。


三、成品截图

1. 行内注释:回复里自动带中文

每条回复里,超出基础词汇的英文词后面都会跟上中文小注。这样读的时候可以先猜,猜不出来再看括号。

行内注释

2. /dict:离线单词卡

输入 /dict ubiquitous,输入框上方立刻出现词卡:音标、中文释义、英文解释。标题右侧的 ·本地 表示这个词来自本地词典,没有联网。

单词卡

3. /zh:整句翻译卡

一句话看不懂,直接 /zh 贴进去。长文本会自动换行,不会截断。

句子翻译

4. /word:学习型词条

如果这个词你不仅要认识,还想记住,就用 /word。它会给出中文意思、词性音标、英文定义,以及一个带翻译的例句。

学习型词条

5. 卡片出现的位置

卡片渲染在输入框上方,不进入对话正文,也不占用模型上下文。

整体界面


四、四个组成部分

4.1 回复风格规则(AGENTS.md)

这是整套方案的发动机。在 OpenCode 的 ~/.config/opencode/AGENTS.md 里写清楚「回复该怎么写」:

## Language

- Reply in English. The user is a Chinese speaker practising English and wants to
  read English every day.

## Write simply and clearly

- Start with the answer in one short line. Add detail only if it helps.
- One idea per sentence. Keep sentences under about 20 words.
- Prefer plain verbs and everyday nouns over abstract wording.
- Avoid idioms, wordplay, and deeply nested clauses. They are hard to parse.
- Cut filler: no praise, no restating the question, no summary of a summary.

## Mark uncommon words

- Wrap an advanced or uncommon English word in **bold**, and put its Chinese
  meaning in parentheses right after it, like **ubiquitous**(无处不在的).
- Gloss every word a beginner textbook would not contain. If a word is ordinary
  (make, small, because, problem) skip it; otherwise mark it.
- Expect 8 to 12 marked words in a normal reply.
- Finishing with five or fewer means you were too strict — go back and add the
  missing ones before sending.
- The one exception is a very short reply, under about 80 words: 3 to 5 is enough.
- Gloss a word at most once per reply, and never again a word you already glossed
  earlier in the same conversation.

两个要点:

  • 先让英文可读。句子短、没有习语、没有废话,否则注释再多也读不下去。
  • 再让生词可见。明确写出「什么算生词」(课本里不会出现的词)和「大概多少个」(8 到 12 个)。

4.2 /dict:离线词典

数据来自 ECDICT 词库,我把它转成了一个 SQLite 快照:

  • 体积 327 MB
  • 340 万条词条(ECDICT 基础词表是 77 万,这个数字包含了词形变化等扩展条目)
  • 单次查询约 0.013 毫秒
  • 支持词形变化(running → run)

完全离线,不需要 API key,不消耗 token。这一点很重要:一个学习者每天会查几十个词,只要每次查询有成本,这个习惯就会死掉。

4.3 /zh:句子翻译

长句、段落翻译,走一条降级链:

  1. 自己的 key(Hy-MT2,腾讯云 TokenHub)
  2. 有道免费接口
  3. MyMemory
  4. 离线逐词注释(会明确标注「这是逐词翻译」)

有 key 的时候质量最好;没有 key 也能用,只是慢一点、差一点。

4.4 /word:学习型词条

这是一个斜杠命令文件 ~/.config/opencode/commands/word.md,内容就是提示词模板:

---
description: Explain one English word like a learner's dictionary
---

Explain the English word "$ARGUMENTS" for a Chinese learner. Reply with exactly
these four parts, in this order:

1. 中文意思 — the common senses only, on one short line
2. 词性 / 音标 — part of speech, then the phonetic spelling
3. Definition — one simple English sentence
4. Example — one example sentence, then its Chinese translation on the next line

Keep it short. Do not add etymology, a synonym list, or usage history.

比如查 resilient,输出长这样(对应上面的截图):

1. 中文意思 — 有韧性的;能快速恢复的;适应力强的
2. 词性 / 音标 — adjective /rɪˈzɪliənt/
3. Definition — Able to recover quickly after a shock, a failure, or a hard time.
4. Example — After losing his job, he stayed resilient and found a better one.
             失业后,他依然坚韧,找到了更好的工作。

为什么要单独做成命令?因为这段格式说明只在「查词」时才需要。放在 AGENTS.md 里,它会跟着每一个请求发送,白花 token。


五、关键设计:卡片不进入上下文

这是整套东西里最重要的一条设计,也是最容易被忽略的。

所有卡片都由一个 TUI 插件绘制,渲染在 session.composer.top 这个插槽上。卡片内容不会进入对话上下文。

实测验证:在一个独立会话里连续查了很多次词,会话的 token 计数完全没有变化:

input = 7169    output = 141     # 多次查询前后完全一致

这件事决定了方案能不能长期用下去:

  • 查 30 个词,消耗 0 token;
  • 对话记录里不会堆满词典内容;
  • 不会因为「查词」而污染上下文、影响后续对话质量。

六、过程中踩的坑和解法

这部分才是真正花时间的地方。

6.1 卡片把长文本截断了

最早实现里有一行 text.slice(0, 160),本意是防止卡片过高,结果粘贴长句子做 /zh 时,翻译卡只显示前 160 个字符,后面直接没了。

改成按终端宽度真正换行、卡片高度限制为面板的一半之后就好了。一个小细节:换行宽度比面板宽 2 列,因为中文标点是全角的,严格按宽度断行会把收尾的「。」「,」挤到下一行行首,很难看。

6.2 卡片关不掉

查完词,卡片一直挂在输入框上面,只能重启。后来加了三套关闭方式:alt+x 快捷键、卡片标题栏右侧的 ✕ 关闭 按钮、命令面板里的 Lookup: close the card。

鼠标按钮是真的调用了渲染层的 onMouseDown,用 tmux 注入真实的 SGR 鼠标事件验证过:点击后卡片确实消失。

6.3 **bold** 在这个 TUI 里不是粗体

我以为 **word** 会渲染成粗体,抓取渲染结果发现它是粉色文字(247;85;144),不是粗体单元格。视觉信号依然有效(生词是粉色的,很显眼),只是名字叫「bold」而已。

这个发现值得记下来:规则文档里的描述和实际渲染效果可能不一致,要实测确认。

6.4 想让插件自动注释,但做不到

最初的想法更激进:既然插件已经有离线词典,为什么不直接让插件扫描 AI 的回复,自动给生词加上中文?

查了插件 API 之后确认不行:

  • session.hook("context") 只能拿到输入侧的消息,改不了模型输出;
  • TUI 的插槽树里没有「对话正文」这个插槽,无法拦截渲染。

所以注释只能由模型自己生成。这引出了第七节要回答的问题:模型到底会不会乖乖加注释?

6.5 规则文档本身有成本

AGENTS.md 会跟着每一个请求发送。实测大约 461 token/请求。

解法:把只在特定场景才用的内容搬进斜杠命令。比如查词格式说明(原本 69 token)搬进 /word 之后,AGENTS.md 降到约 433 token/请求。

顺便纠正我自己一个错误:我一开始说这能省 69 token,其实我保留了一句指向命令文件的提示,所以实际只省了不到 30 token。说数字之前要真的量一遍。


七、提示词怎么评估

这是整件事里最有意思的部分,因为我一开始想当然,被打脸了。

7.1 评估方法

思路很土但有效:同一个提示词,跑很多次,数注释词的数量。

具体做法:

  1. 用 tmux 起一个全新的 OpenCode 会话(tmux new-session -d -s gm -x 120 -y 50 "opencode");
  2. 等待约 26 秒(插件在启动时加载);
  3. 发送同一个问题:Explain in 3 sentences why cache invalidation is hard in distributed systems.;
  4. tmux capture-pane -p 抓取屏幕文本;
  5. 用正则 ([A-Za-z][A-Za-z-]{2,})\(([\u4e00-\u9fff][^)]{0,6})\) 统计注释数量;
  6. 用 opencode api get /api/session 读会话的 token 消耗。

关键点:必须用全新会话,而且不能在当前会话里测。因为当前会话的 token 计数会被我自己的回复污染,测出来的数字没有意义。

7.2 各个变体的结果

同一道题,不同提示词写法的结果:

提示词变体注释词数备注
「自由标注,不要吝啬」2, 5太少,约束太软
「配额 5-10 个」5, 5稳定,但偏少
「期待 8-12 个 + 少于 5 个就重写」10, 9最好,但样本只有 2 次
「按长度缩放:每 15 词 1 个」3, 4反而不行,约束被理解成了「随便」
「期待 8-12 + 自检 + 短回复例外」0, 8, 5波动极大,甚至出现 0

把 11 次采样合在一起:

2, 5, 5, 5, 10, 9, 3, 4, 0, 8, 5
中位数 = 5,范围 = 0 ~ 10

7.3 三条结论

  1. 模型对「数量」的服从度很低。 同一句话,可能给 10 个注释,也可能给 0 个。规则能抬高平均值,但保证不了单次。
  2. 有自检句的版本明显更好。 「如果少于 5 个,说明你太保守了,回去补上」这句话,比任何「期望 8-12 个」都管用。它把统计目标变成了一个可执行的检查动作。
  3. 不要过度调参。 我试了 5 个变体,样本量都在 2-3 次,波动比差异还大。这种情况下继续改措辞是在拟合噪声。样本不够时,最该做的是增加样本,不是改提示词。

八、日常怎么用

  1. 先读英文,再看括号。 每个带注释的词,先猜一遍意思,再对答案。
  2. 卡住的词用 /dict。 免费、瞬时,所以不要犹豫,看到就查。
  3. 想记住的词用 /word。 重点读例句,读两遍。
  4. 整句卡住用 /zh。 长段落也可以直接贴。
  5. 能回英文就回英文。 输出比输入更能建立语感。

九、成本与隐私

  • 查词卡片:0 token,完全离线(/dict 全程不联网)。
  • 句子翻译:优先用自己的 key;不配 key 也能用免费链路。
  • 行内注释:这部分由模型生成,会产生正常的 token 消耗,但没有额外调用。
  • 隐私:/dict 的内容不会离开本机。句子翻译如果走免费接口,文本会发给对应服务商;走自己的 key 则发给 TokenHub。

十、已知限制和下一步

限制:

  • 行内注释的数量不可控,只能保证长期均值(见第七节)。
  • 无法让插件自动注释,因为插件 API 没有「输出侧」钩子。
  • 免费翻译接口随时可能失效。

下一步可以考虑:

  • 做一个生词本:每次 /dict 自动记录,配一个 /review 命令做间隔重复复习。
  • 让模型不要重复注释「我已经注释过的词」(目前已加同会话去重规则,跨会话还做不到)。

附录:文件清单

以下路径都在跑 OpenCode 的服务器(lsf-server)上:

路径作用
~/.config/opencode/AGENTS.md回复风格规则(英文、短句、行内注释)
~/.config/opencode/commands/word.md/word 命令的提示词模板
~/.config/opencode/plugins/lookup/插件本体:卡片渲染、翻译链路、离线查询
~/.local/share/lookup/dict.db327 MB 离线词典快照(ECDICT → SQLite)

一句话总结:不要主动去查词,让词带着意思来找你;只把那些仍然卡住你的词,交给一个免费、瞬时的词典。