151 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""pipeline_service.substance_gate — 标书「实质性响应」硬门禁(2026-09-16 用户报障根治)。
用户原话级问题:「目前产出的投标书像是编写指南,而不是标书本身」「标书质量太低,
检核还得了满分,这本身就是问题」。
实测根因(甘肃交通项目 bid_v1):写者交付的正文是**编写指南体**——
- 1.1 投标函开头「一、编制依据与响应说明」「按封面原样抄录,见第六节《人工填写清单》」
+17 处「【待填-投标总价】」「【待填-投标人名称】」+《自查表》《人工填写清单》;
- 2.2.2 实施方案开头「按第三章格式编制,完整覆盖评分标准四个子项①②③④」。
这些是「教人怎么写标书」的元内容(meta-content),不是投标文件正文本身。
而 review_chapter / finalize_bid_score 只查篇幅+伪图+得分率,**没有任何「这是不是
标书正文」的门禁**,评分器(LLM)按「写作层是否提到评分子维度」打分,指南体表面
把每个子维度都「提到」了 → 满分放行 → 评分报告自己列了「无效标」却给 100/100。
判定是**确定性的**(质量闸门不靠 LLM 自觉,对齐 diagram_gate 的分层原则):
- 元内容标题(## 编制说明/编制依据/填写口径/自查表/人工填写清单/…)→ 强信号
- 元内容句(「按…格式编制」「完整覆盖…子项」「见第X节《…清单》」「提示性括注」)→ 中信号
- 非规范待填占位(【待填-xxx】/[待填],区别于合法的【材料待补:xxx】)→ 强信号
综合打分超阈值 → 判为「指南体/未实质填写」,机制层拒绝。
豁免边界(避免误伤真实标书):
- 【材料待补:xxx】是合法占位符规范(实体材料缺失),不计入待填违规;
- 签字栏下划线(___)、日期「年 月 日」是真实标书正常留白,不计入;
- 阈值适中:主体合格、仅开头一段啰嗦的章节(如 2.2.2)不触发整章拒绝,
通篇待填+编制说明的空壳(如 1.1)必触发。
消费方:
- pipeline-bidding bid_write_capability.write_chapter(写入时拦指南体)
- pipeline-bidding bid_review_capability.review_chapter(评审兜底)
- pipeline-bidding bid_score_capability.finalize_bid_score(整书放行前硬门禁)
"""
import re
# 合法占位符(实体材料缺失,2026-09-16 占位符规范)——不计入待填违规
_LEGIT_PLACEHOLDER_RE = re.compile(r"【材料待补[::]")
# 元内容标题:## 编制说明 / 编制依据 / 填写口径 / 自查表 / 人工填写清单 / …
_META_HEADING_RE = re.compile(
r"^#{1,6}\s*[^#\n]*?"
r"(编制说明|编制依据|编制要求|编制口径|编制环节|编写说明|编写依据|编写要求|"
r"填写口径|填写说明|填报口径|填报说明|填报要求|"
r"自查表|自检表|核对表|人工填写清单|待人工签章清单|待签章清单|"
r"应答口径|认定口径|落稿|回填规则|页码回填|响应说明|编制与.{0,6}说明)",
re.M)
# 元内容句:正文里「讲怎么写」而非「写内容」的句子
_META_SENTENCE_PATTERNS = (
r"按[^。\n]{0,30}格式编制",
r"按[^。\n]{0,30}(规定|要求)填报",
r"本(?:章|节|部分|表)[^。\n]{0,15}按[^。\n]{0,25}编制",
r"完整覆盖[^。\n]{0,20}(?:子项|维度|评分|要求)",
r"逐(?:项|条|字)(?:照录|抄录|填报)[^。\n]{0,15}(?:原文|格式|栏目)",
r"见[^。\n]{0,12}《[^》\n]{0,20}(?:清单|说明|表)》",
r"(?:提示性|操作性)[^。\n]{0,8}括注",
r"不占用[^。\n]{0,10}栏目",
r"一律置于[^。\n]{0,12}第[^。\n]{0,4}节",
r"(?:未|不得|禁止)(?:删改|增设|改变)[^。\n]{0,10}栏目",
r"原样(?:抄录|填报|照录)",
r"本(?:清单|表|节)[^。\n]{0,10}(?:集中|统一)(?:标注|管理|列明)",
)
_META_SENTENCE_RE = re.compile("|".join("(?:%s)" % p for p in _META_SENTENCE_PATTERNS))
# 非规范待填占位:【待填-xxx】/[待填]/(待填)/ 待填写。区别于合法的【材料待补:】
_FILL_PLACEHOLDER_RE = re.compile(
r"[【\[((]\s*待填[^\]】))\n]{0,40}[\]】))]" # 【待填-投标总价】
r"|待填[写补]?\s*[-—::]" # 待填-xxx / 待填写:
r"|[【\[((]\s*待(?:补(?![::])|定|确认)[^\]】))\n]{0,30}[\]】))]" # 【待定】【待确认】(非材料待补)
)
# 阈值:综合分 >= 此值判为指南体/未实质填写
# 元内容标题 +2 / 元内容句 +1 / 待填占位 +1
GUIDE_SCORE_THRESHOLD = 4
def analyze_substance(text):
"""分析正文实质性。返回结构化结果(不抛异常)。
分层判定(2026-09-16 走查校正:阈值4曾误伤2.2.1/2.2.2等有实质内容仅带前言的章):
- 待填占位(【待填-xxx】)= 无歧义「表单没填」铁证 → 强信号,权重高;
- 元内容标题/句 = 「讲怎么写」→ 弱信号,真实标书也常带「编制依据」前言,
单独不足以判指南体,需与待填或高密度叠加。
is_guide(硬门禁拒绝)只在高精度组合下成立,避免误伤有实质内容的章节。
{
'is_guide': bool, # 硬门禁:判为指南体/未实质填写(机制拒绝)
'score': int, # 综合违规分(软信号,喂评分器)
'meta_density': float, # 元内容密度(每千字违规命中数)
'meta_headings': [str],
'meta_sentences': [str],
'fill_placeholders': [str],
}
"""
text = text or ""
headings = [m.group(0).strip()[:60] for m in _META_HEADING_RE.finditer(text)]
sentences = [m.group(0).strip()[:50] for m in _META_SENTENCE_RE.finditer(text)]
# 待填占位:排除合法【材料待补:】——先挖掉合法占位再统计
scrubbed = _LEGIT_PLACEHOLDER_RE.sub("", text)
fills = [m.group(0).strip()[:40] for m in _FILL_PLACEHOLDER_RE.finditer(scrubbed)]
n_h, n_s, n_f = len(headings), len(sentences), len(fills)
score = n_h * 2 + n_s + n_f
kchars = max(1.0, len(text) / 1000.0)
meta_density = round((n_h + n_s) / kchars, 2)
# 硬门禁(高精度,宁缺毋滥——误伤真实内容会逼无谓重写):
# ① 待填占位 >= 2:表单大面积没填(1.1=17、2.1.4=31 铁证);或
# ② 综合分 >= 15:元内容压倒性(通篇讲怎么写);或
# ③ 元内容标题 >= 3 且 待填 >= 1:既是指南框架又没填实。
is_guide = (n_f >= 2) or (score >= 15) or (n_h >= 3 and n_f >= 1)
return {
"is_guide": is_guide,
"score": score,
"meta_density": meta_density,
"meta_headings": headings,
"meta_sentences": sentences,
"fill_placeholders": fills,
}
def find_substance_problems(text):
"""返回问题描述列表(空 = 通过)。供门禁调用方判定 + 拼提示。"""
a = analyze_substance(text)
if not a["is_guide"]:
return []
problems = []
if a["meta_headings"]:
problems.append("元内容标题 %d 处:%s"
% (len(a["meta_headings"]), "、".join(a["meta_headings"][:5])))
if a["meta_sentences"]:
problems.append("编写说明句 %d 处:%s"
% (len(a["meta_sentences"]), "、".join(a["meta_sentences"][:5])))
if a["fill_placeholders"]:
problems.append("非规范待填占位 %d 处:%s"
% (len(a["fill_placeholders"]), "、".join(a["fill_placeholders"][:8])))
return problems
def gate_message(problems, scene="标书章节"):
"""门禁拒绝提示(带改法,对齐 diagram_gate.gate_message 风格)。"""
return ("FAIL: %s 是「编写指南/作业指导书」,不是投标文件正文本身(2026-09-16 硬门禁)。\n"
"检出问题:%s\n"
"改法:① 删除所有「编制说明/编制依据/填写口径/自查表/人工填写清单/应答口径」"
"等讲『怎么写』的元内容段落——正文只写『写什么』;"
"② 投标函/承诺函/报价表等按招标格式**直接填真实内容**(投标人名称、报价、"
"日期等能从项目/知识库取到的就填实,取不到的实体材料用规范占位符"
"【材料待补:具体材料名】,禁止用【待填-xxx】这类自造占位);"
"③ 技术方案/实施方案写实质措施(架构/流程/指标/责任人/工期),"
"不写「本章覆盖评分子项①②③」这类编写说明。"
% (scene, ";".join(problems)))