会话历史增强
通过历史记录实现话题连续性和实体解析。
目录
背景
闻字 对每次语音输入会话进行独立处理——AI 增强步骤只能看到当前的 ASR 文本,对用户之前说过的内容一无所知。这种无状态的方式在处理孤立句子时效果尚可,但在以下场景中存在不足:
- 反复出现的专有名词 — 用户在某句话中提到"萍萍",但 ASR 将其识别为"平平"。没有先前上下文,LLM 没有依据判断哪个更正确。如果用户在之前的轮次中已确认了"萍萍",这一信号就丢失了。
- 话题连续性 — 对话天然地建立在之前的上下文之上。当用户说"她说今天很开心"时,LLM 无法在不知道之前提到过谁的情况下解析"她"的指代。
- 用户表达习惯 — 有些用户会持续使用特定的措辞、标点风格或句式结构。无状态的增强器无法适应这些偏好。
动机
核心洞察:用户近期确认的输出是反映其真实意图的最高质量信号。
与原始 ASR 文本(包含错误)或 AI 增强文本(可能过度纠正)不同,最终确认的输出代表了用户的真实意图——他们已经审阅并批准了它。将这些确认的输出反馈到增强提示词中,我们给 LLM 提供了一个滚动的上下文窗口,使其能够:
- 一致的实体解析 — 一旦"平平 → 萍萍"被确认,后续出现的"平平"就能被正确解析。
- 话题感知增强 — LLM 理解当前的对话主题,能做出符合上下文的决策。
- 风格适配 — LLM 观察用户的实际写作方式,匹配其语气和格式偏好。
工作原理
记录
每次语音输入会话都会记录到 ~/.config/WenZi/conversation_history.jsonl,不区分模式:
{
"timestamp": "2026-03-12T10:30:00+00:00",
"asr_text": "果果今天在公园里遇到了平平。",
"enhanced_text": "果果今天在公园里遇到了平平。",
"final_text": "果果今天在公园里遇到了萍萍。",
"enhance_mode": "proofread",
"preview_enabled": true,
"stt_model": "funasr-paraformer",
"llm_model": "qwen2.5:7b",
"user_corrected": true,
"audio_duration": 3.2
}
| 字段 | 类型 | 说明 |
|---|---|---|
timestamp |
string | ISO 8601 UTC 时间戳 |
asr_text |
string | 原始 ASR 识别文本 |
enhanced_text |
string | AI 增强后的文本(用户审阅前) |
final_text |
string | 最终确认的文本 |
enhance_mode |
string | 增强模式(proofread、translate 等) |
preview_enabled |
bool | 是否启用了预览模式 |
stt_model |
string | STT 模型标识 |
llm_model |
string | LLM 模型标识 |
user_corrected |
bool | 用户是否编辑了增强文本 |
audio_duration |
float | 录音时长(秒),精确到 0.1 秒 |
audio_duration 字段记录了每次会话中用户的语音时长。该值同时通过 record_recording_duration() 汇总到使用统计中。
直接模式(preview_enabled: false)和预览模式(preview_enabled: true)的会话都会被记录。这确保不会丢失任何数据,即使后续注入策略发生变化。
过滤:为何仅使用预览确认的记录
只有 preview_enabled: true 的记录会被注入到 AI 提示词中。原因如下:
- 预览模式 — 用户看到 AI 输出,可以编辑并明确确认最终文本。这个确认后的文本是可靠的。
- 直接模式 — 文本直接输入,未经审阅。用户从未验证过输出,因此可能包含未纠正的 ASR 错误或 AI 过度纠正。注入未经验证的文本会导致错误传播。
这是一个有意为之的数据质量决策:一组较小的已验证数据比一组较大的未验证数据更有价值。
按模式隔离历史
每种增强模式维护独立的对话历史。使用"纠错润色"模式时,只注入纠错润色的历史条目;使用"翻译"模式时,只出现翻译历史。这一设计:
- 保持上下文相关性 — 纠错修正示例(中文→中文)对翻译模式没有参考价值,反之亦然。
- 提高缓存命中率 — 切换模式不会使另一个模式的缓存前缀失效。
链式模式(如"纠错润色→翻译")在执行时读取各步骤模式的历史,但不写入任何历史条目。因为用户无法验证或修正中间步骤的结果,记录这些未经验证的数据可能会误导 LLM。
上下文注入
当对话历史功能启用时,历史和词库被合并到一个统一的上下文段中,共享一个指令头部。格式设计兼顾 token 效率和 API 级提示词缓存:
---
以下是辅助纠错的参考上下文:
- 对话记录(优先参考):反映用户真实的纠错偏好和话题上下文,若 ASR 识别与最终确认不同则用→分隔(识别→确认),相同则表示无需纠错。
- 词库(仅供辅助):以下专有名词 ASR 常误写为同音近音词,仅当输入中确实存在对应误写时才替换,不要强行套用。当词库与对话记录冲突时,以对话记录为准。
对话记录:
- 将是否开启对话历史注入的功能,做一个开关放在菜单栏里。
- 现在测试一下历史上下文注入的功能。
- 果果今天在公园里遇到了平平。 → 果果今天在公园里遇到了萍萍。
- 平平对果果说我今天吃了面条。 → 萍萍对果果说我今天吃了面条。
词库:
- WenZi(语音转文字工具)
- 萍萍(人名)
---
提示词格式的关键设计选择:
- 每条记录一行 — 最大程度减少 token 用量。
- 箭头标注纠正 — 仅在 ASR 文本和最终文本不同时显示,使纠正模式对 LLM 一目了然。
- 相同时不显示箭头 — 避免重复展示相同的文本。
- 仅包含 ASR + 最终文本 — AI 增强的中间文本不会被注入,以节省 token。
- 对话记录优先于词库 — 标注为"优先参考"与"仅供辅助",因为对话记录反映了用户验证过的纠正,而词库是自动化建议。
- 合并指令头部 — 历史和词库的说明文字合并到顶部一个静态块中,有利于提示词缓存(见下文)。
提示词缓存优化
系统提示词按稳定性从高到低排列,以最大化大模型 API 级缓存(OpenAI、DeepSeek 等)可复用的前缀长度:
[模式提示词] ← 每个模式固定不变
[思考简洁提示] ← 会话内固定(如开启思考模式)
[合并上下文指令头部] ← 会话内固定(说明文字)
[对话记录条目] ← 追加式增长(只增不减)
[词库条目] ← 每次请求动态变化
追加式历史构建:不再每次请求都从头重建历史,而是将新条目追加到已有列表末尾,保持提示词前缀在连续请求间完全一致。当条目总数达到 refresh_threshold 或总字符数达到 max_history_chars 时,以最近的 max_entries 条为基础重建。
前缀稳定性示例:
请求 1: ...header... + entry1 + entry2 |← 缓存前缀 →| + vocab_A
请求 2: ...header... + entry1 + entry2 + entry3 |← 缓存 →| + vocab_B
请求 3: ...header... + entry1 + entry2 + entry3 |← 缓存 →| + vocab_C
每次请求复用已缓存的 KV 状态,只为新增的 token 付费。
提示: 大多数 API 提供商要求缓存前缀至少达到 1024 tokens(约 500-700 个中文字符)。如果你使用的增强模式提示词较短,建议适当增大
max_entries(如改为 20),以确保重建后系统提示词能超过此阈值。
每个上下文源都可独立开关,并能优雅降级——如果历史记录获取失败,增强过程将在没有它的情况下继续进行。
存储与归档
自动轮转
主历史文件(conversation_history.jsonl)通过自动轮转机制保持有界。每次 log() 调用后,系统会检查是否需要轮转:
- 文件大小预检 — 如果文件小于 4 MB,则完全跳过轮转(廉价的前置检查,避免每次写入都计算行数)。
- 记录数检查 — 如果文件超过 20,000 条记录(
_MAX_RECORDS),超出限制的最旧记录将被归档。
按月归档
轮转出的记录按其 timestamp 字段中的月份分组,追加到按月命名的归档文件中:
~/.config/WenZi/
├── conversation_history.jsonl # 活跃文件(最多 20,000 条记录)
└── conversation_history_archives/
├── 2025-11.jsonl
├── 2025-12.jsonl
└── 2026-01.jsonl
每个归档文件命名为 YYYY-MM.jsonl,包含该月的所有记录。无法解析时间戳的记录会被放入 unknown.jsonl。归档文件仅追加——后续轮转会向已有的月份文件追加内容,而非覆盖。
归档完成后,主文件通过临时文件 + os.replace() 原子替换,仅保留最近的 20,000 条记录,同时所有内存缓存被清除。
浏览归档记录
历史浏览器提供了"包含归档"开关。启用后,get_all() 和 search() 会加载所有归档文件中的记录(按文件名时间排序),并将其置于活跃记录之前。这使用户能够搜索和浏览完整历史,同时保持活跃文件不会无限增长。
内存缓存
为避免重复的磁盘读取,ConversationHistory 维护两级缓存:
| 缓存 | 大小 | 用途 | 填充时机 |
|---|---|---|---|
热路径(_cache) |
最近 200 条记录 | 为 get_recent() 提供上下文注入服务 |
首次 get_recent() 调用时惰性加载 |
完整缓存(_full_cache) |
活跃文件全部记录 | 为历史浏览器的 get_all() 和 search() 服务 |
首次 get_all()/search() 调用时惰性加载 |
热路径缓存 — 存储最近 200 条原始(未过滤)记录。由 log()、update_record() 和 delete_record() 就地更新。由于上下文注入通常只需从尾部过滤出 10 条记录,200 条缓存提供了充足的余量,无需加载整个文件。
完整缓存 — 存储活跃 JSONL 文件中所有已解析的记录。通过比较文件的 mtime 检测过期——如果文件被外部修改,下次访问时会重新加载。调用方应在不再需要数据时调用 release_full_cache()(例如关闭历史浏览器窗口时)以及时释放内存。
轮转事件后,两级缓存均被完全清除。
历史浏览器分页
基于 Web 的历史浏览器(history_browser_window_web.py)以每页 100 条记录(PAGE_SIZE = 100)的方式显示。Python 后端根据过滤后的记录总数计算总页数,仅将当前页的记录发送给 WKWebView 前端。前端渲染分页器,提供上一页/下一页导航和"第 X / Y 页"指示器。
当搜索关键词、时间过滤器或归档开关发生变化时,分页会重置到第 0 页。
架构
Voice Input
│
▼
┌──────────┐ ┌───────────────────────────────────┐
│ ASR │────►│ conversation_history.jsonl │
└──────────┘ │ (最多 20,000 条记录) │
│ └────────┬──────────────┬────────────┘
│ │ │ _maybe_rotate()
│ │ ▼
│ │ ┌──────────────────────────┐
│ │ │ archives/YYYY-MM.jsonl │
│ │ │ (按月归档,仅追加) │
│ │ └──────────────────────────┘
▼ │
┌──────────┐ │ get_recent(enhance_mode=当前模式)
│ Enhancer │◄─────────────┘ filter: preview_enabled=true
│ │ 按模式过滤,经由热路径缓存
│ │
│ system_prompt = ┌── 稳定前缀(已缓存)──┐
│ mode_prompt │ │
│ + thinking_hint │ ┌─ 上下文段 ────────┐│
│ + context_header ────────────┤ │ 指令头部 ││
│ + history_entries (追加) ────┤ │ 对话记录: 条目... ││
│ + vocab_entries (动态) ──────┘ │ 词库: 条目... ││
│ └──────────────────┘│
│ │──► LLM ──► enhanced text
└──────────┘
│
▼
┌──────────────────┐
│ Preview Panel │──► 用户确认 ──► type_text
│ (预览模式) │ │
└──────────────────┘ ▼
log(enhance_mode=当前模式,
preview_enabled=true)
配置
在 config.json 的 ai_enhance 下:
{
"conversation_history": {
"enabled": false,
"max_entries": 10,
"refresh_threshold": 50,
"max_history_chars": 6000
}
}
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
enabled |
bool | false |
开关对话历史注入功能 |
max_entries |
int | 10 |
重建后的基础条目数(也是初始条目数) |
refresh_threshold |
int | 50 |
触发重建的最大条目数 |
max_history_chars |
int | 6000 |
触发重建的最大总字符数 |
该开关也可在 Settings 面板(AI 标签页)中使用。
调优指南:
- max_entries 控制每次重建后的基础大小。较大值意味着更多上下文,但"冷启动"前缀更长。如果模式提示词较短,建议设为 20+ 以便重建后立刻超过 1024 token 的 API 缓存阈值。
- refresh_threshold 控制累积多少条目后触发重建。较高值意味着更长的缓存命中区间,但每次请求的 token 用量更多。
- max_history_chars 作为 token 用量的安全上限,独立于条目数。
关键文件
| 文件 | 用途 |
|---|---|
src/wenzi/enhance/conversation_history.py |
JSONL 记录、读取、缓存、轮转、归档及提示词格式化 |
src/wenzi/enhance/enhancer.py |
将历史上下文集成到增强提示词中 |
src/wenzi/ui/history_browser_window_web.py |
基于 Web 的历史浏览器,支持分页和归档开关 |
src/wenzi/usage_stats.py |
通过 record_recording_duration() 汇总 audio_duration |
src/wenzi/app.py |
在两个输出路径中记录会话;菜单开关 |
src/wenzi/config.py |
对话历史的默认配置 |