diff --git a/config/prompts.yaml b/config/prompts.yaml index 923333c..e33f364 100644 --- a/config/prompts.yaml +++ b/config/prompts.yaml @@ -273,6 +273,9 @@ content_prompts: 当前章节: 《{title}》 章节定位: {chapter_path} + 写作模式: {writing_mode} + 篇幅要求: {length_hint} + 子标题分工: {child_outline} 分值关注: {score_info} 评分要点: {rubric_points} @@ -295,9 +298,14 @@ content_prompts: 要求: 1. 内容专业、详实,符合招标文件要求 2. 突出技术优势和实施能力 - 3. 语言正式、逻辑清晰 - 4. 字数控制在500-800字 - 5. 严禁新增任何章/节级标题或“商务条款、技术偏差、响应情况”等模板段,如需结构化仅使用普通段落或加粗语句 - 6. 开头不得出现“经认真研读招标文件要求”“偏差说明如下”等跨章节套话,内容必须围绕《{title}》本身展开 + 3. 严格遵守写作模式与篇幅要求: 有子标题仅写父级统领概要(不展开子标题正文),无子标题输出完整正文 + 4. 严禁新增任何章/节级标题或“商务条款、技术偏差、响应情况”等模板段,如需结构化仅使用普通段落或加粗语句 + 5. 开头不得出现“经认真研读招标文件要求”“偏差说明如下”等跨章节套话,内容必须围绕《{title}》本身展开 + 6. 格式上结合招标要求,先总述响应招标内容,点明标题的主要内容是什么,再分开将业务部分、技术实现一一说明,通过编号的形式让内容更有条理。最后如有必要可有一段总结性的内容,但不一定必须。一定要专业、条理清晰,符合招标文件的严格要求 + 7. 业务描述上一定要流程化、场景化 + 8. 技术描述上一定通俗易懂,但又不失专业性,特别是一些非常切题的或者应用性契合很好的技术一定要写明白 + 9. 一定要遵循知识库中提供的内容,严格遵守 + 10. 内容中不要提及招标单位的具体名称,如果需要提及,可用需求单位、业主单位等类似词语代替 + 11. 内容中不要出现专业软件的具体版本号,比如unity等 请直接输出章节内容,不要包含章节标题。 diff --git a/src/bidmaster/nodes/content/generate_content.py b/src/bidmaster/nodes/content/generate_content.py index 475e6c6..e471c75 100644 --- a/src/bidmaster/nodes/content/generate_content.py +++ b/src/bidmaster/nodes/content/generate_content.py @@ -58,13 +58,12 @@ class GenerateContentNode(BaseNode): sub_chapters = self._find_sub_chapters(state, chapter_id, max_level=settings.max_sub_chapter_level) if sub_chapters: - # 有子标题:逐个生成后拼接 - logger.info(f"章节 {chapter_id} 包含 {len(sub_chapters)} 个子标题,逐个生成") - content_parts = [] - for sub in sub_chapters: - sub_content = self._generate_with_rag(sub, config, state, planner) - content_parts.append(f"## {sub['title']}\n\n{sub_content}") - content = "\n\n".join(content_parts) + logger.info( + "章节 %s 包含 %s 个子标题,仅生成父级统领概要,不在此处展开子标题内容", + chapter_id, + len(sub_chapters), + ) + content = self._generate_with_rag(chapter, config, state, planner) else: # 无子标题:直接生成 content = self._generate_with_rag(chapter, config, state, planner) diff --git a/src/bidmaster/tools/rag.py b/src/bidmaster/tools/rag.py index d45108a..83309ab 100644 --- a/src/bidmaster/tools/rag.py +++ b/src/bidmaster/tools/rag.py @@ -220,6 +220,9 @@ class RAGTool: prompt_variables = { "title": task_title, "chapter_path": context.get('chapter_path', task_title), + "writing_mode": context.get('writing_mode', '无子标题:输出完整正文'), + "length_hint": context.get('length_hint', '600-1000字,完整回应评分要点'), + "child_outline": context.get('child_outline', '无子标题'), "score_info": context.get('score_info', '目标得分:未明确'), "requirements_summary": context.get('requirements_summary', ''), "rubric_points": context.get('rubric_points', '- 无明确评分要点'), diff --git a/src/bidmaster/utils/prompt_planner.py b/src/bidmaster/utils/prompt_planner.py index f0f5ad1..bd06544 100644 --- a/src/bidmaster/utils/prompt_planner.py +++ b/src/bidmaster/utils/prompt_planner.py @@ -31,10 +31,13 @@ class PromptPlanner: chapter_id = chapter["id"] metadata = self.chapter_metadata.get(chapter_id) + child_outline = self._build_child_outline(chapter_id) + has_children = bool(child_outline) + requirements = ( (metadata or {}).get("requirements") or chapter.get("requirements") - or self._build_child_outline(chapter_id) + or child_outline or "" ) rubric_points = (metadata or {}).get("rubric_points") or self._split_requirements(requirements) @@ -44,15 +47,20 @@ class PromptPlanner: parent_context = self._collect_parent_context(chapter) sibling_outline = self._collect_sibling_outline(chapter) - objectives = self._build_objectives(requirements, metadata, emphasis) - consistency_rules = self._build_consistency_rules(chapter_path, parent_context) + objectives = self._build_objectives(requirements, metadata, emphasis, has_children) + consistency_rules = self._build_consistency_rules(chapter_path, parent_context, has_children) guidance_notes = self._format_guidance_notes((metadata or {}).get("guidance") or chapter.get("guidance")) + child_outline_display = child_outline or "无子标题" + writing_mode = self._compose_writing_mode(has_children) + length_hint = self._compose_length_hint(has_children) context_parts: List[str] = [] if parent_context: context_parts.append(f"父章节摘要:{parent_context}") if sibling_outline: context_parts.append(f"同级章节定位:{sibling_outline}") + if has_children: + context_parts.append(f"子标题分工:{child_outline_display}") context_summary = "\n".join(context_parts) or "(暂无可引用的上文,可直接围绕本章节展开)" spec = { @@ -70,6 +78,10 @@ class PromptPlanner: "emphasis": emphasis or "", "category": (metadata or {}).get("category"), "guidance_notes": guidance_notes, + "child_outline": child_outline_display, + "writing_mode": writing_mode, + "length_hint": length_hint, + "has_children": has_children, } return spec @@ -123,11 +135,22 @@ class PromptPlanner: return "" return ";".join(titles[:6]) + def _compose_writing_mode(self, has_children: bool) -> str: + if has_children: + return "有子标题:仅写父级统领概要,不展开子标题正文" + return "无子标题:输出完整正文" + + def _compose_length_hint(self, has_children: bool) -> str: + if has_children: + return "200-400字,保持凝练且覆盖子标题分工" + return "600-1000字,完整回应评分要点" + def _build_objectives( self, requirements: str, metadata: Optional[Dict[str, Any]], emphasis: str, + has_children: bool, ) -> List[str]: objectives: List[str] = [] if requirements: @@ -140,13 +163,16 @@ class PromptPlanner: if emphasis: objectives.append(f"突出用户强调内容:{emphasis}") + if has_children: + objectives.append("仅输出父章节统领性概要,不展开子标题正文") + base_objective = "围绕章节主题提供结构化、工程化的内容" if base_objective not in objectives: objectives.append(base_objective) return objectives - def _build_consistency_rules(self, chapter_path: str, parent_context: str) -> List[str]: + def _build_consistency_rules(self, chapter_path: str, parent_context: str, has_children: bool) -> List[str]: rules = [ f"严格聚焦章节路径 {chapter_path},不得跨章节展开", "禁止创建新的章/节级标题,仅可使用段落或加粗语句", @@ -156,6 +182,9 @@ class PromptPlanner: if parent_context: rules.append("与父章节内容保持逻辑衔接,避免信息冲突") + if has_children: + rules.append("父章节仅做统领与衔接,不展开子标题具体内容") + return rules def _format_score_info(self, chapter: Dict[str, Any], metadata: Optional[Dict[str, Any]]) -> str: diff --git a/src/bidmaster/utils/validation.py b/src/bidmaster/utils/validation.py index e042368..027c22a 100644 --- a/src/bidmaster/utils/validation.py +++ b/src/bidmaster/utils/validation.py @@ -193,6 +193,12 @@ class ContentValidator: if heading_issue: issues.append(ContentValidationIssue(code="heading_misaligned", message=heading_issue)) + heading_scan_issue = None + if not heading_issue: + heading_scan_issue = self.detect_heading_anywhere(chapter, content) + if heading_scan_issue: + issues.append(ContentValidationIssue(code="heading_detected", message=heading_scan_issue)) + template_issue = self.detect_cross_chapter_signature(content) if template_issue: issues.append(ContentValidationIssue(code="template_detected", message=template_issue)) @@ -231,6 +237,30 @@ class ContentValidator: return f"检测到跨章节模板短语“{phrase}”,请使用本章节专属内容" return None + def detect_heading_anywhere(self, chapter: Dict[str, Any], content: str) -> Optional[str]: + if not content: + return None + + chapter_title = chapter.get('title', chapter.get('id')) + for raw_line in content.splitlines(): + stripped = raw_line.strip() + if not stripped: + continue + + if stripped.startswith('#'): + return f"检测到Markdown标题“{raw_line}”,请不要创建新的章/节级标题,直接撰写《{chapter_title}》正文" + + normalized = self._normalize_line(stripped) + for pattern in self._opening_patterns: + if pattern.match(normalized): + return f"检测到新的章节标题“{raw_line}”,请直接编写《{chapter_title}》正文内容" + + for keyword in self._opening_blacklist: + if normalized.startswith(keyword): + return f"检测到与章节无关的套话“{raw_line}”,请按章节《{chapter_title}》展开" + + return None + def _first_meaningful_line(self, content: str) -> Optional[str]: for raw_line in content.splitlines(): stripped = raw_line.strip()