会话历史增强

通过历史记录实现话题连续性和实体解析。

目录

背景

闻字 对每次语音输入会话进行独立处理——AI 增强步骤只能看到当前的 ASR 文本,对用户之前说过的内容一无所知。这种无状态的方式在处理孤立句子时效果尚可,但在以下场景中存在不足:

  1. 反复出现的专有名词 — 用户在某句话中提到"萍萍",但 ASR 将其识别为"平平"。没有先前上下文,LLM 没有依据判断哪个更正确。如果用户在之前的轮次中已确认了"萍萍",这一信号就丢失了。
  2. 话题连续性 — 对话天然地建立在之前的上下文之上。当用户说"她说今天很开心"时,LLM 无法在不知道之前提到过谁的情况下解析"她"的指代。
  3. 用户表达习惯 — 有些用户会持续使用特定的措辞、标点风格或句式结构。无状态的增强器无法适应这些偏好。

动机

核心洞察:用户近期确认的输出是反映其真实意图的最高质量信号

与原始 ASR 文本(包含错误)或 AI 增强文本(可能过度纠正)不同,最终确认的输出代表了用户的真实意图——他们已经审阅并批准了它。将这些确认的输出反馈到增强提示词中,我们给 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 增强模式(proofreadtranslate 等)
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 提示词中。原因如下:

这是一个有意为之的数据质量决策:一组较小的已验证数据比一组较大的未验证数据更有价值

按模式隔离历史

每种增强模式维护独立的对话历史。使用"纠错润色"模式时,只注入纠错润色的历史条目;使用"翻译"模式时,只出现翻译历史。这一设计:

链式模式(如"纠错润色→翻译")在执行时读取各步骤模式的历史,但不写入任何历史条目。因为用户无法验证或修正中间步骤的结果,记录这些未经验证的数据可能会误导 LLM。

上下文注入

当对话历史功能启用时,历史和词库被合并到一个统一的上下文段中,共享一个指令头部。格式设计兼顾 token 效率和 API 级提示词缓存:

---
以下是辅助纠错的参考上下文:
- 对话记录(优先参考):反映用户真实的纠错偏好和话题上下文,若 ASR 识别与最终确认不同则用→分隔(识别→确认),相同则表示无需纠错。
- 词库(仅供辅助):以下专有名词 ASR 常误写为同音近音词,仅当输入中确实存在对应误写时才替换,不要强行套用。当词库与对话记录冲突时,以对话记录为准。

对话记录:
- 将是否开启对话历史注入的功能,做一个开关放在菜单栏里。
- 现在测试一下历史上下文注入的功能。
- 果果今天在公园里遇到了平平。 → 果果今天在公园里遇到了萍萍。
- 平平对果果说我今天吃了面条。 → 萍萍对果果说我今天吃了面条。

词库:
- WenZi(语音转文字工具)
- 萍萍(人名)
---

提示词格式的关键设计选择:

提示词缓存优化

系统提示词按稳定性从高到低排列,以最大化大模型 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() 调用后,系统会检查是否需要轮转:

  1. 文件大小预检 — 如果文件小于 4 MB,则完全跳过轮转(廉价的前置检查,避免每次写入都计算行数)。
  2. 记录数检查 — 如果文件超过 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.jsonai_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 对话历史的默认配置

脚本系统 →

使用 Python 脚本自动化 macOS 任务——Leader 键、快捷键、提示、剪贴板等。