diff --git a/app/services/ai_rewriter.py b/app/services/ai_rewriter.py index 066ecfd..d90e949 100644 --- a/app/services/ai_rewriter.py +++ b/app/services/ai_rewriter.py @@ -34,9 +34,9 @@ def _is_likely_timeout_error(exc: BaseException) -> bool: return "timed out" in s or "timeout" in s -# 短文洗稿:5 个自然段、正文总字数上限(含标点) -MAX_BODY_CHARS = 500 +# 短文洗稿:正文目标约 500 字,优先完整性(软约束,不硬截断) MIN_BODY_CHARS = 80 +TARGET_BODY_CHARS = 500 def _preview_for_log(text: str, limit: int = 400) -> str: @@ -54,7 +54,7 @@ SYSTEM_PROMPT = """ 1) **忠实原意**:只概括、转述原文已有信息,不编造事实,不偷换主题; 2) 语气通俗、干脆,避免套话堆砌; 3) 只输出合法 JSON:title, summary, body_markdown; -4) **body_markdown 约束**:恰好 **5 个自然段**;段与段之间用一个空行分隔;**不要**使用 # / ## 标题符号;全文(正文)总字数 **不超过 500 字**(含标点); +4) **body_markdown 约束**:按内容密度使用 **4~6 个自然段**;段与段之间用一个空行分隔;**不要**使用 # / ## 标题符号;正文以 **约 500 字**为目标,优先完整表达并避免冗长重复; 5) title、summary 也要短:标题约 8~18 字;摘要约 40~80 字; 6) 正文每段需首行缩进(建议段首使用两个全角空格「  」),避免顶格; 7) 关键观点需要加粗:请用 Markdown `**加粗**` 标出 2~4 个重点短语; @@ -71,19 +71,19 @@ REWRITE_SCHEMA_HINT = """ } body_markdown 写法: -- 必须且只能有 **5 段**:每段若干完整句子,段之间 **\\n\\n**(空一行); +- 使用 **4~6 段**:每段若干完整句子,段之间 **\\n\\n**(空一行); - **禁止** markdown 标题(不要用 #); -- 正文总长 **≤500 字**,宁可短而清楚,不要写满废话; +- 正文目标约 **500 字**(可上下浮动),以信息完整为先,避免冗长和重复; - 每段段首请保留首行缩进(两个全角空格「  」); - 请用 `**...**` 加粗 2~4 个关键观点词; -- 内容顺序建议:第 1 段交代在说什么;中间 3 段展开关键信息;最后 1 段收束或提醒(均须紧扣原文,勿乱发挥)。 +- 内容顺序建议:首段交代在说什么;中间段展开关键信息;末段收束或提醒(均须紧扣原文,勿乱发挥)。 """.strip() # 通义等模型若首次过短/结构不对,再要一次 _JSON_BODY_TOO_SHORT_RETRY = """ 【系统复检】上一次 body_markdown 不符合要求。请重输出**完整** JSON: -- 正文必须 **恰好 5 个自然段**(仅 \\n\\n 分段),无 # 标题,总字数 **≤500 字**; +- 正文必须使用 **4~6 个自然段**(仅 \\n\\n 分段),无 # 标题;篇幅尽量收敛到约 500 字,同时保持信息完整; - 忠实原稿、简短高效; - 引号只用「」『』; - 只输出 JSON。 @@ -347,7 +347,8 @@ class AIRewriter: self, req: RewriteRequest, cleaned_source: str, reason: str, trace: dict[str, Any] | None = None ) -> RewriteResponse: sentences = self._extract_sentences(cleaned_source) - points = self._pick_key_points(sentences, limit=5) + para_count = self._fallback_para_count(cleaned_source) + points = self._pick_key_points(sentences, limit=max(5, para_count)) title = req.title_hint.strip() or self._build_fallback_title(sentences) summary = self._build_fallback_summary(points, cleaned_source) @@ -358,17 +359,14 @@ class AIRewriter: t = re.sub(r"\s+", " ", (s or "").strip()) return t if len(t) <= n else t[: n - 1] + "…" - paras = [ - _one_line(self._build_intro(points, cleaned_source), 105), - _one_line(analysis["cause"], 105), - _one_line(analysis["impact"], 105), - _one_line(analysis["risk"], 105), - _one_line(conclusion, 105), - ] + paras = [_one_line(self._build_intro(points, cleaned_source), 105)] + if para_count >= 4: + paras.append(_one_line(analysis["cause"], 105)) + paras.append(_one_line(analysis["impact"], 105)) + if para_count >= 5: + paras.append(_one_line(analysis["risk"], 105)) + paras.append(_one_line(conclusion, 105)) body = "\n\n".join(paras) - if len(body) > MAX_BODY_CHARS: - body = body[: MAX_BODY_CHARS - 1] + "…" - normalized = { "title": title, "summary": summary, @@ -429,6 +427,14 @@ class AIRewriter: ), } + def _fallback_para_count(self, source: str) -> int: + length = len((source or "").strip()) + if length < 240: + return 4 + if length > 1200: + return 6 + return 5 + def _clean_source(self, text: str) -> str: src = (text or "").replace("\r\n", "\n").strip() src = re.sub(r"https?://\S+", "", src) @@ -725,8 +731,6 @@ class AIRewriter: text = (body or "").strip() if not text: text = "(正文生成失败,请重试。)" - if len(text) > MAX_BODY_CHARS: - text = text[: MAX_BODY_CHARS - 1] + "…" return text def _quality_issues( @@ -749,16 +753,18 @@ class AIRewriter: paragraphs = [p.strip() for p in re.split(r"\n\s*\n", body) if p.strip()] pc = len(paragraphs) - need_p = 4 if lenient else 5 - if pc < need_p: - issues.append(f"正文需约 5 个自然段、空行分隔(当前 {pc} 段)") - elif not lenient and pc > 6: - issues.append(f"正文段落过多(当前 {pc} 段),请合并为 5 段左右") + min_p, max_p = (3, 6) if lenient else (4, 6) + if pc < min_p: + issues.append(f"正文段落偏少(当前 {pc} 段),建议 {min_p}-{max_p} 段") + elif pc > max_p: + issues.append(f"正文段落偏多(当前 {pc} 段),建议控制在 {min_p}-{max_p} 段") - if len(body) > MAX_BODY_CHARS: - issues.append(f"正文超过 {MAX_BODY_CHARS} 字(当前 {len(body)} 字),请压缩") - elif len(body) < MIN_BODY_CHARS: + if len(body) < MIN_BODY_CHARS: issues.append(f"正文过短(当前阈值 ≥{MIN_BODY_CHARS} 字)") + elif len(body) > 900: + issues.append( + f"正文偏长(当前 {len(body)} 字),建议收敛到约 {TARGET_BODY_CHARS} 字(可上下浮动)" + ) if re.search(r"(?m)^#+\s", body): issues.append("正文请勿使用 # 标题符号,只用自然段") @@ -805,8 +811,6 @@ class AIRewriter: title = (normalized.get("title") or "").strip() if len(title) < 4 or len(body) < 40: return False - if len(body) > MAX_BODY_CHARS + 80: - return False return True def _format_markdown(self, text: str) -> str: