fix(ai-title): 过滤模型「前导思考」,只取标题 (§19.31)

推理型文本模型即便提示词要求「只返回逗号连接的标题」,仍常在标题前先输出
一段思考散文;该散文自身带逗号,直接逗号切分会切出假标题混进结果。切分前
先剥离前导思考(_strip_title_preamble),两层:
- ① 哨兵优先:文本含 ===TITLES===(正则 =+\s*TITLES\s*=+,忽略大小写、
  吞相邻 * / # 装饰)时只取最后一个哨兵之后,思考连同哨兵丢弃
- ② 句号兜底(无需改提示词):无哨兵时切最后一个 。!? 及之前;真标题不含
  句末标点,故最后一个句号即前导散文↔标题列表的分界;仅当其后仍含逗号/换行
  分隔符时才剥离,避免末条标题的收尾句号被误当分界清空

- ai_text_service.py:加 _TITLE_SENTINEL / _SENTENCE_END / _strip_title_preamble,
  _clean_titles 切分前调用
- config_service.py:DEFAULT_TITLE_PROMPT 补哨兵约定(思考写最前、单独一行
  ===TITLES===、其后只放逗号标题)
- docs/11 §17.2/§17.3 更新 + 新增 §17.9 决策;tasks.md §19.31
- 不碰 packaging/default_config/title_prompt.txt(用户精细模板,并行未提交)

验证:test_ai_text_service(22)+ 全套 py37(236 测试)通过,唯一失败为
无关的 test_config_service packaging 模板;离屏冒烟用真实返回样本 → 前导思考
被剥离、取全部干净标题,默认提示词含哨兵。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 15:50:48 +08:00
co-authored by Claude Opus 4.8
parent bc40905b38
commit a413aa6f15
5 changed files with 128 additions and 6 deletions
+23
View File
@@ -1563,3 +1563,26 @@
- [x] 不动 `generate_texts`/`generate_text` 默认 `resolution="1K"`;模型条目 `timeout_seconds>0` 仍优先覆盖(`_post` 逻辑不变)
- [x] 测试:`tests/test_ai_title.py` 断言 `generate_titles` 以 `resolution="4K"` 调 `generate_texts`(mock 客户端捕获 kwargs);既有用例保持绿
- [x] 验证:`test_ai_title.py`、`test_ai_text_service.py`、全套 py37 通过(`test_config_service` 的 packaging 模板失败属并行历史遗留,无关)
### 19.31 标题解析:过滤模型「前导思考」,只取标题 — docs/11 §17.2 / §17.3 / §17.9
前置阅读:
- `docs/11-ai-outfit.md`(§17.1 / §17.2 / §17.3 / §17.9)
- `src/services/ai_text_service.py`(`_TITLE_SPLIT` / `_clean_titles` / `extract_titles_from_response`)
- `src/services/config_service.py`(`DEFAULT_TITLE_PROMPT`)
- `tests/test_ai_text_service.py`
背景:
推理型文本模型即便提示词写明「只返回逗号连接的标题」,仍常在标题**前面**先输出一段思考散文(实测:「我會直接產出符合格式的標題,並先用字元計數檢查…然後一次輸出 42 個標題。【台灣現貨】…」)。这段散文自身带逗号,直接逗号切分会切出假标题混进结果。加一步剥离前导思考:哨兵优先(`===TITLES===`)+ 句号兜底(切最后一个 `。!?` 及之前)。发布包 `title_prompt.txt` 是用户并行改的精细模板(未提交),本任务**不碰**它,只改代码内置默认 + 解析。
任务:
- [x] 文档更新:`docs/11` §17.2 补「剥离前导思考」、§17.3 补哨兵约定、新增 §17.9 决策
- [x] `ai_text_service.py`:加 `_TITLE_SENTINEL`(`=+\s*TITLES\s*=+`,忽略大小写)+ `_strip_title_preamble(text)`;`_clean_titles` 切分前先调用
- [x] `_strip_title_preamble`:① 有哨兵取最后一个哨兵之后;② 无哨兵切最后一个 `。!?` 及之前,仅当其后仍含 `[,,\r\n]` 分隔符时才剥离(防末条句号清空)
- [x] `config_service.py`:`DEFAULT_TITLE_PROMPT` 补哨兵约定(思考写最前、单独一行 `===TITLES===`、其后只放逗号标题)
- [x] 不碰 `packaging/default_config/title_prompt.txt`(用户精细模板,并行未提交工作)
- [x] 测试:真实前导思考样本(带 `。`+逗号列表)被剥离、取全部标题;哨兵优先于句号且忽略思考内逗号;无前导的纯逗号列表不变;末条带收尾 `。`、无前导时不误切清空
- [x] 验证:`test_ai_text_service.py`、`test_ai_title.py`、全套 py37 通过;离屏冒烟用真实返回样本走 `extract_titles_from_response`