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:
parent
61026cd1b6
commit
f0c29304c7
@ -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等
|
||||
|
||||
请直接输出章节内容,不要包含章节标题。
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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', '- 无明确评分要点'),
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user