fix(polish): 上下文感知润色限最近 4 轮,消除历史堆积导致的首字延迟 (#678)#682
Conversation
1.3.6 起非 Raw 润色默认把最近 5 分钟内「全部」历史轮前置进 LLM 请求 (types.rs 默认窗口 5 分钟,eligible_polish_context_turns 不限条数)。历史 越多、请求越长、首 token 越慢——流式插入下这正是「出第一个字」的时刻, 于是面向全体用户表现为转写变慢。 给 eligible_polish_context_turns 增加硬上限 MAX_POLISH_CONTEXT_TURNS=4: sessions 为 newest-first,take 保留最近 4 轮,把无界的上下文拼接收敛为常数 量级,同时保留多轮连续性。附单测验证超量历史只回看最近 N 轮。
|
@claude 请审核:给上下文感知润色加最近 4 轮硬上限以消除历史堆积导致的首字延迟。重点看 (1) take(4) 作用在 newest-first 序列上是否确实保留「最近」N 轮、与 build_polish_history_messages 的 .rev() 配合后时序是否正确;(2) 取值 4 是否合理,是否该改成可配置(prefs)。另注:仅 Windows 本地 sherpa 在线模型的冷加载延迟是另一条路径,会单独提 PR。 |
|
Claude finished @appergb's task in 2m 37s —— View job 代码审核结论
问题一:
|
| 阶段 | 代码位置 | 顺序 |
|---|---|---|
recent_within_minutes 返回结果 |
persistence.rs:1220-1227 |
newest-first(注释明确 + take_while 依赖此约定) |
eligible_polish_context_turns 过滤 + take |
dictation.rs:2519-2544 |
维持 newest-first,.take(4) 在 filter_map 之后,保留最近 4 条符合条件的 |
| LLM 消息构建(OpenAI 路径) | polish.rs:1227 |
.iter().rev() → oldest-first → 时序正确 |
| LLM 消息构建(Gemini 路径) | llm_gemini.rs:439 |
.iter().rev() → oldest-first → 时序正确 |
.take(MAX_POLISH_CONTEXT_TURNS) 作用在 filter_map 之后,语义是「最多保留 4 条通过所有过滤器的轮次」,而非「扫描最多 4 个原始 session 再过滤」——这更符合直觉,结果也正确。
一个隐性合约值得记录:recent_within_minutes 内部用 take_while 而非 filter,依赖底层存储严格 newest-first。一旦该契约被打破(如并发写入或数据库修复后插入顺序乱掉),take_while 会静默截断,take(4) 随之保留到错误的轮次——但这是预存在风险,不是本 PR 引入的。
问题二:常量值 4 是否合理,是否应改为可配置?
值 4 本次可接受;可配置性建议作后续 PR。
合理性分析:
- 4 轮 = 8 条 LLM message(user+assistant 各一条),转写场景每条通常一到两句,累计额外 token 约 100–300,但扩大到重度用户每天数百次触发时,削减效果显著。
- 时间窗口(5 分钟)与轮数上限(4 轮)是两个正交旋钮:前者已作为 pref 暴露,后者此处硬编码——对延迟优先的场景是合理起点。
关于可配置性:
- 项目里
prefs.polish_context_window_minutes已经是用户 pref,风格一致的话max_polish_context_turns也可以加进去,让重度用户自行权衡延迟与连贯性。 - 但作为修复 regression 的单一职责 PR,当前硬编码的选择是务实的——避免同时扩展 prefs schema。建议在后续 polish 体验迭代时一并加入。
测试覆盖的一个小盲区
现有新测用例 polish_context_is_capped_to_most_recent_turns 验证了「堆积 > 上限时截断为 4」的场景,是对的。但存在一个未覆盖的边界:
若部分 session 因
error_code非空或style_pack_id不匹配被filter/filter_map淘汰,且淘汰后剩余数量 < 4,结果应该是 < 4(不是 4)。
这个行为实际上是正确的(.take() 在 filter 之后),但没有测试断言它。如果将来有人误将 .take() 移至 filter 之前,测试不会抓住这个 bug。可考虑补一个:
// 7 条中 3 条因 error_code 过滤,有效 4 条 —— 不应再被 take 截断
assert_eq!(turns.len(), 4); // 恰好等于上限,确认 take 在 filter 之后生效当然,这是锦上添花,不影响本 PR 正确性。
整体结论
可以合入。 修改逻辑正确、作用位置正确、时序正确、测试通过。唯一的后续行动项:
- (低优)补充「filter 减少 eligible 数量时 take 不会过度截断」的测试用例。
- (中优)后续版本将
max_polish_context_turns作为用户 pref 暴露,与polish_context_window_minutes对齐。
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
PR 合并冲突提示(听写模块)本 PR 与 #692、#688、#666 均修改了 openless-all/app/src-tauri/src/coordinator/dictation.rs。 实测结论:
建议: 合并前与 #666 作者确认顺序,避免一方已合入后另一方 rebase 成本突增。 |
User description
关联 issue
Closes #678
问题
1.3.6 起非 Raw 润色默认把最近 5 分钟内的全部历史轮前置进 LLM 请求:
types.rs:828默认窗口 5 分钟(开箱即开);coordinator/dictation.rs::eligible_polish_context_turns末尾直接.collect(),不限条数。历史越多 → 输入 token 越多 → 首 token 时间越长。在默认流式插入下,TTFT 就是「光标出现第一个字」的时刻 → 面向全体用户表现为转写变慢(约 +1s 量级,越重度越明显)。
改动(单一职责)
MAX_POLISH_CONTEXT_TURNS = 4。eligible_polish_context_turns在.collect()前加.take(MAX_POLISH_CONTEXT_TURNS)。sessions为 newest-first(recent_within_minutes保证),take 保留最近 4 轮;build_polish_history_messages再.rev()成时序——顺序正确。取值 4 偏向延迟优先、可调;未改时间窗口、未动其他逻辑。
测试
新测断言:堆积 7 条历史时只返回最近 4 轮(newest-first 的前 4 条)。
PR Type
Bug fix
Description
Limit polish context turns to 4 (MAX_POLISH_CONTEXT_TURNS).
Add unit test to verify capping behavior.
Diagram Walkthrough
File Walkthrough
dictation.rs
Cap polish context turns to 4, add testopenless-all/app/src-tauri/src/coordinator/dictation.rs
MAX_POLISH_CONTEXT_TURNS = 4..take(MAX_POLISH_CONTEXT_TURNS)beforecollect()ineligible_polish_context_turns.polish_context_is_capped_to_most_recent_turnstoverify capping.