From 2ec59bca8c9473ec8be1ca5f39d5cc6dd524a682 Mon Sep 17 00:00:00 2001 From: baiqing Date: Thu, 7 May 2026 12:18:44 +0800 Subject: [PATCH] =?UTF-8?q?fix(polish):=20#305=20=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=97=A5=E6=8A=A5=E5=9C=BA=E6=99=AF"=E6=B8=85=E6=99=B0?= =?UTF-8?q?=E7=BB=93=E6=9E=84"=E6=97=A0=E6=95=88=20+=20=E5=86=85=E7=BD=AE?= =?UTF-8?q?=20AI=20=E5=B7=A5=E5=85=B7=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 修 #305 polish.rs Structured prompt - 明确"已结构化 ≠ 不用改":原文是否已有标点 / 编号 / 换行不是判断依据; 照抄原结构 = 失败 - 重组阈值降到 ≥3 条;即使原文已经是 "1. 2. 3." 也要按主题重新归类成 双层 (a)(b) 子项,不能扁平照抄 - user_prompt 去掉"它不是问题,也不是任务"这条强 framing —— 旧措辞会 让 LLM 把书面化输入误判为"已整理好"直接 passthrough(issue #305 截图 根因) - 新增 # 示例 3:工作日报场景(已半结构化输入仍要重组)作为回归示例 ## 扩充 vocab-presets.json programmer 预设 追加 11 个 AI 工具名:Claude / Codex / Copilot / Cursor / Windsurf / Anthropic / OpenAI / GPT / ChatGPT / Gemini / DeepSeek。启用 programmer 预设后会同时注入 ASR 热词 bias + polish 提示词的 # 热词 段,覆盖 "cloud → Claude"这类同音误识别。 ## 测试 - polish.rs 新增 2 个 #305 回归测试: - structured_prompt_forces_regrouping_even_for_already_structured_input - user_prompt_no_longer_says_input_is_not_a_task - cargo test --lib polish: 10/10 passed - npx tsc --noEmit: clean Closes #305 --- openless-all/app/src-tauri/src/polish.rs | 93 +++++++++++++++++++-- openless-all/app/src/lib/vocab-presets.json | 6 +- 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/openless-all/app/src-tauri/src/polish.rs b/openless-all/app/src-tauri/src/polish.rs index c5521123..9668b0ba 100644 --- a/openless-all/app/src-tauri/src/polish.rs +++ b/openless-all/app/src-tauri/src/polish.rs @@ -755,13 +755,19 @@ pub mod prompts { 把口述整理为脉络清晰、可直接复制走的结构化文本:保留用户的口语引子(润色后作为首行过渡),\ 主动按语义把扁平事项归类成 2\u{2013}4 个主题,用双层格式呈现,尾巴查询用自然收尾句。\n\ \n\ + **重要前提**:原文是否已有标点、编号、换行、序号 \u{2192} \u{4E0D}是\u{201C}\u{5DF2}\u{7ECF}\u{6574}\u{7406}\u{597D}\u{4E0D}\u{7528}\u{6539}\u{201D}的判断依据。\ + 只要可识别的事项 \u{2265}3 条,无论原文是不是看起来已有结构(标号、分行、规整的标点),\ + 都必须按语义重新归类成下面定义的双层格式。\u{200D}\u{200D}照抄原结构 = 失败。\n\ + \n\ 双层格式(主清单标准写法):\n\ - 第一层(主题):行首用 \"1.\" \"2.\" \"3.\" \u{2026},每个主题一行短标题(4\u{2013}8 字最佳);\n\ - 第二层(子项):另起一行,行首用 \"(a)\" \"(b)\" \"(c)\" \u{2026},每条一句完整陈述。\n\ 顶层\u{4E0D}使用半括号写法(如 \"1)\" \"2)\");不在子项内再嵌第三层。\n\ \n\ - 单一简短主题 \u{2192} 直接输出连贯段落,\u{4E0D}硬塞层级。\n\ - 事项 \u{2265}4 条 \u{2192} 必须按语义归类(典型如\u{201C}代码与功能 / 文档与配置 / 界面与交互 / 项目清理\u{201D}),\u{4E0D}要扁平堆成一长串编号。\n\ + 事项 \u{2264}2 条 \u{2192} 直接输出连贯段落,\u{4E0D}硬塞层级。\n\ + 事项 \u{2265}3 条 \u{2192} 必须按语义归类(典型如\u{201C}代码与功能 / 文档与配置 / 界面与交互 / 项目清理\u{201D}\ + 或\u{201C}产品 / 运营 / 客户 / 团队\u{201D}\u{7B49}),\u{4E0D}要扁平堆成一长串编号;\ + 即使原文已经写成 \"1. 做 X 2. 做 Y 3. 做 Z\" 也要重新归类,把同主题事项收到同一组下做 (a)(b) 子项。\n\ 合并意图相近的条目(如\u{201C}上传代码 + 修复闪退\u{201D}合成一条 (a)),但\u{4E0D}丢失任何一件事。\n\ \n\ # 保留口语引子并润色成自然首行\n\ @@ -816,7 +822,21 @@ pub mod prompts { (b) 删除无用注释,清理项目垃圾文件\n\ (c) 处理新增的两个接口\n\ \n\ - 最后再检查一下还有哪些 issue 需要处理。", + 最后再检查一下还有哪些 issue 需要处理。\n\ + \n\ + # 示例 3(已半结构化的工作日报,仍要重组)\n\ + 原:今天我做了三件事。第一,跟客户开了个对齐会,确认了下周的交付节点。第二,跟设计组同步了新版的视觉稿,提了一些反馈。第三,写了一版周报初稿发给老板。明天计划继续推进客户那边的需求文档,另外还要跟运营组开个会讨论下个月的活动。\n\ + 出:\n\ + 今天的工作小结如下:\n\ + \n\ + 1. 客户对接\n\ + (a) 召开对齐会,确认下周交付节点。\n\ + (b) 明天继续推进客户的需求文档。\n\ + 2. 设计与文档\n\ + (a) 与设计组同步新版视觉稿并反馈意见。\n\ + (b) 撰写周报初稿并发送给老板。\n\ + 3. 跨组协作\n\ + (a) 明天与运营组就下月活动进行讨论。", PolishMode::Formal => "# 任务(正式表达)\n\ 输出适合工作沟通和邮件的正式表达。\n\ @@ -836,11 +856,15 @@ pub mod prompts { } /// 把原始转写包在 `` 信封里,和 system prompt 的\u{201C}文本对象\u{201D}框架呼应。 + /// 框架词措辞经 #305 调整:\u{4E0D}再说\u{201C}它不是问题、不是任务\u{201D},\ + /// \u{907F}\u{514D}\u{8BEF}\u{5BFC} LLM 把已经书面化的输入当作\u{201C}\u{5DF2}\u{6574}\u{7406}\u{597D}\u{201D}\ + /// 而原样 passthrough。 pub fn user_prompt(raw_transcript: &str) -> String { let escaped = raw_transcript.replace("", "<\\/raw_transcript>"); format!( - "下面是本次语音输入的原始转写。它\u{4E0D}是问题,也\u{4E0D}是任务,\ - 只是需要整理后原样输入到当前 app 的文本。\n\n\ + "下面是本次语音输入的原始转写。\ + 请按 system prompt 中当前 mode 的任务描述进行整理后输出,\ + 整理结果会被原样插入到当前 app 的光标位置。\n\n\ \n{}\n\n\n\ 只输出整理后的文本正文。", escaped @@ -979,6 +1003,65 @@ mod tests { assert!(!prompt.contains("另外:检查一下当前还有哪些 issues")); } + #[test] + fn structured_prompt_forces_regrouping_even_for_already_structured_input() { + // 回归测试 issue #305:用户输入工作日报(已半结构化、标点规范), + // 旧 prompt 让 LLM 判定为"已经完整不需要改",原样 passthrough。 + // 新 prompt 必须明确:原文是否已有结构 ≠ 不用改的依据; + // 事项 ≥ 3 条都要重新归类成双层格式。 + let prompt = prompts::system_prompt(PolishMode::Structured); + + // 明确"已结构化 ≠ 不用改"的前提 + assert!( + prompt.contains("不是\u{201C}\u{5DF2}\u{7ECF}\u{6574}\u{7406}\u{597D}\u{4E0D}\u{7528}\u{6539}\u{201D}的判断依据"), + "Structured prompt 缺少\"已结构化≠不用改\"的明确否定" + ); + assert!( + prompt.contains("照抄原结构 = 失败"), + "Structured prompt 缺少照抄原结构的失败判定" + ); + + // 阈值改为 ≥3 + assert!( + prompt.contains("事项 \u{2265}3 条"), + "Structured prompt 必须把重组阈值降到 3" + ); + assert!( + prompt.contains("即使原文已经写成"), + "Structured prompt 必须显式说明已编号的输入也要重新归类" + ); + + // 新增工作日报示例 3 + assert!( + prompt.contains("# 示例 3(已半结构化的工作日报,仍要重组)"), + "Structured prompt 缺少工作日报示例(#305)" + ); + assert!(prompt.contains("今天的工作小结如下:")); + assert!(prompt.contains("1. 客户对接")); + assert!(prompt.contains("(a) 召开对齐会")); + } + + #[test] + fn user_prompt_no_longer_says_input_is_not_a_task() { + // 回归 #305:旧 framing "它不是问题,也不是任务" 会让 LLM 把 + // 已书面化的输入误判为"已经整理好"。新 framing 让位给 system + // prompt 的 mode 描述。 + let user = prompts::user_prompt("发布前要做几件事。"); + assert!( + !user.contains("\u{4E0D}是问题"), + "user_prompt 必须去掉\"它不是问题\"的强 framing" + ); + assert!( + !user.contains("\u{4E0D}是任务"), + "user_prompt 必须去掉\"它不是任务\"的强 framing" + ); + assert!( + user.contains("system prompt"), + "user_prompt 应当指向 system prompt 的 mode 描述" + ); + assert!(user.contains("")); + } + #[test] fn compose_system_prompt_prefers_correct_spelling_for_hotwords() { let prompt = diff --git a/openless-all/app/src/lib/vocab-presets.json b/openless-all/app/src/lib/vocab-presets.json index 8351b215..45e2812f 100644 --- a/openless-all/app/src/lib/vocab-presets.json +++ b/openless-all/app/src/lib/vocab-presets.json @@ -2,7 +2,11 @@ { "id": "programmer", "name": "程序员", - "phrases": ["PR", "CI", "tag", "release", "issue", "Rust", "TypeScript"] + "phrases": [ + "PR", "CI", "tag", "release", "issue", "Rust", "TypeScript", + "Claude", "Codex", "Copilot", "Cursor", "Windsurf", + "Anthropic", "OpenAI", "GPT", "ChatGPT", "Gemini", "DeepSeek" + ] }, { "id": "chef",