feat: separate parent summaries from child content

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
sladro 2025-12-05 14:57:34 +08:00
parent 61026cd1b6
commit f0c29304c7
5 changed files with 84 additions and 15 deletions

View File

@ -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等
请直接输出章节内容,不要包含章节标题。

View File

@ -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)

View File

@ -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', '- 无明确评分要点'),

View File

@ -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:

View File

@ -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()