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
+46
View File
@@ -79,6 +79,52 @@ class TestExtractText(unittest.TestCase):
self.assertEqual(extract_titles_from_response({"choices": []}), [])
def test_strips_reasoning_preamble_by_period(self):
"""§19.31: 真实返回——前导思考散文(带逗号、以 。结尾)被剥离,取全部标题。"""
from services.ai_text_service import extract_titles_from_response
content = (
"我會直接產出符合格式的標題,並先用字元計數檢查每條都落在 54–58 字元內。"
"接下來我用快速本地檢查避免字數超標,然後一次輸出 42 個標題。"
"【台灣出貨】 寬鬆T恤 滿額免運 印花T恤 韓系簡約 透氣舒適 不挑身形 夏季抗熱必備,"
"【台灣出貨】 韓版T恤 蝦皮特殺 圖案T恤 日系休閒 親膚舒適 日常女孩 日常百搭穿搭,"
"【台灣出貨】 韓系上衣 2026熱銷爆款 印花短袖 美式復古 夏季必備 通勤族 休閒出遊,"
"【台灣出貨】 oversize短袖 獨家自訂款 圖案短袖 街頭潮流 百搭單品 學生族 懶人寬鬆穿搭,"
"【台灣出貨】 寬鬆版上衣 免運優惠 印花上衣 輕熟女百搭 日常百搭 上班族 辦公日常"
)
titles = extract_titles_from_response({"choices": [{"message": {"content": content}}]})
self.assertEqual(len(titles), 5) # 5 条真标题,无思考碎片
self.assertTrue(all(t.startswith("【台灣出貨】") for t in titles))
self.assertTrue(all("。" not in t for t in titles)) # 前导散文被切干净
self.assertEqual(titles[0],
"【台灣出貨】 寬鬆T恤 滿額免運 印花T恤 韓系簡約 透氣舒適 不挑身形 夏季抗熱必備")
self.assertNotIn("我會直接產出符合格式的標題", titles)
def test_sentinel_takes_priority_over_thinking_commas(self):
"""§19.31: 有 ===TITLES=== 时只取哨兵之后;哨兵前的思考(含逗号/句号)全丢。"""
from services.ai_text_service import extract_titles_from_response
content = "先想一下,分两步。还有备注,继续。 ===TITLES=== 甲款,乙款,丙款"
titles = extract_titles_from_response({"choices": [{"message": {"content": content}}]})
self.assertEqual(titles, ["甲款", "乙款", "丙款"])
def test_sentinel_tolerates_decoration_and_takes_last(self):
"""§19.31: 哨兵可带 ** 修饰/等号数不定/大小写;取最后一个哨兵之后。"""
from services.ai_text_service import extract_titles_from_response
content = "思路: 提到 ===titles=== 这个词\n**==TITLES==**\n甲,乙"
titles = extract_titles_from_response({"choices": [{"message": {"content": content}}]})
self.assertEqual(titles, ["甲", "乙"])
def test_trailing_period_without_preamble_not_wiped(self):
"""§19.31: 无前导、末条带收尾 。—— 不误当分界清空,标题仍全部保留。"""
from services.ai_text_service import extract_titles_from_response
content = "甲款,乙款,丙款。"
titles = extract_titles_from_response({"choices": [{"message": {"content": content}}]})
self.assertEqual(len(titles), 3)
self.assertEqual(titles[:2], ["甲款", "乙款"])
class TestBuildTextPayload(unittest.TestCase):
def test_chat_with_image_includes_image_url(self):