feat(evals): 风格 B(两个分册、改写、自己的编号、干扰项 T01);注入的三处待修项

- build.py:风格 B 分商务资格册和技术册,表格行按 rewrite.json 改写(数字对不上的改写丢弃,规矩靠构造
  保证),用自己的编号,每 12 行技术响应挪到商务资格册(T01);干扰项写进 spec.traps。
- mutate.py:支持多个分册;表格的响应方式以档案的 mode 为准(评分要的技术规格表是 presence,不再把单元
  推成逐条响应);「暂不支持」只套在功能类要求上,数值参数写不满足的值;「提供……方案 / 证明 / 报告」
  这类文件要在的要求不注入参数类问题。
- prompts/style-B.md、prompts/profile.md:风格 B 改写说明和招标文件档案说明(开发集用)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-04 12:51:17 +08:00
parent 9d3c6e82c1
commit 2beccd5fc5
4 changed files with 200 additions and 17 deletions

View File

@@ -58,6 +58,7 @@ BIDDER = {
"agent": "陈立新",
"date": "2026年11月20日",
}
NUMS = re.compile(r"\d+(?:\.\d+)?")
DIGITS = "零壹贰叁肆伍陆柒捌玖"
UNITS = ["", "拾", "佰", "仟"]
SECTIONS = ["", "万", "亿"]
@@ -181,6 +182,7 @@ def _row(k: int, r: dict[str, Any], columns: list[str]) -> dict[str, Any]:
"responds": r["responds"],
"req": r["req"],
"iid": r["iid"],
"key": r.get("key"),
"tp": r["tp"],
}
@@ -219,10 +221,13 @@ def build(tender: str, bid: str, style: str, run: str) -> None:
answer, dev = comply(raw, positive)
else: # 功能类要求:复述型(风格 A)照常写「满足,……」
answer, dev = "满足," + re.sub(r"^[\s★▲**\d.、()()]+", "", req_text)[:120], "无偏离"
rw = rewrite.get(iid) or {}
rw = dict(rewrite.get(iid) or {})
if "answer" in rw and NUMS.findall(rw["answer"]) != NUMS.findall(answer):
rw.pop("answer") # 改写后的数字和规矩的响应对不上:丢弃改写,规矩靠构造保证
rows_by_route.setdefault(route, []).append(
{
"iid": iid,
"key": iid,
"requirement": rw.get("requirement", req_text),
"answer": rw.get("answer", answer),
"deviation": dev,
@@ -253,11 +258,13 @@ def build(tender: str, bid: str, style: str, run: str) -> None:
if not sents:
continue
text = "".join(x.text for x in sents)
rw = rewrite.get(f"u:{u.id}") or {}
rows_by_route.setdefault("tech_row", []).append(
{
"iid": None,
"requirement": text,
"answer": "满足:" + text,
"key": f"u:{u.id}",
"requirement": rw.get("requirement", text),
"answer": rw.get("answer", "满足:" + text),
"deviation": "无偏离",
"responds": [u.id],
"req": {"p": sents[0].page_no, "t": gold.norm(sents[0].text)[:24]},
@@ -298,7 +305,10 @@ def build(tender: str, bid: str, style: str, run: str) -> None:
lines = [(total * b / sum(base)).quantize(Decimal("0.01")) for b in base]
lines[-1] = total - sum(lines[:-1])
unit_prices = [(ln / w).quantize(Decimal("0.01")) for ln, w in zip(lines, weights, strict=False)]
moved: list[dict[str, Any]] = [] # 风格 B:一部分技术行写在商务资格册里(干扰项 T01)
moved_columns: list[str] = []
for n, f in enumerate(profile["forms"], start=1):
start = len(blocks)
head = {"type": "heading", "text": f"{n}.{f['title']}", "new_page": n > 1, "form": f["key"]}
kind = f["kind"]
if kind == "text":
@@ -364,6 +374,15 @@ def build(tender: str, bid: str, style: str, run: str) -> None:
else:
rows = [_row(k, r, columns) for k, r in enumerate(src, 1)]
pool = []
if style == "B" and f.get("route", "tech_row") == "tech_row" and not f.get("mode"):
keep = []
for k, r in enumerate(rows):
(moved if k % 12 == 11 else keep).append(r)
rows, moved_columns = keep, columns
if style == "B" and "no" in columns: # 风格 B 用自己的编号
prefix = "技" if f.get("route", "tech_row") == "tech_row" else "商"
for k, r in enumerate(rows, 1):
r["cells"][columns.index("no")] = f"{prefix}-{k:02d}"
blocks += [
head,
{
@@ -373,6 +392,7 @@ def build(tender: str, bid: str, style: str, run: str) -> None:
"form": f["key"],
"route": f.get("route", "tech_row"),
"only_deviations": bool(f.get("only_deviations")),
"mode": f.get("mode"),
"columns": columns,
"pool": pool,
},
@@ -399,6 +419,28 @@ def build(tender: str, bid: str, style: str, run: str) -> None:
blocks.append(b)
else:
raise ValueError(f"未知的表单类型:{kind}")
tech = kind == "response_table" and f.get("route", "tech_row") == "tech_row"
for b in blocks[start:]:
b["vol"] = "v2" if style == "B" and tech else "v1"
if moved:
for k, r in enumerate(moved, 1):
if "no" in moved_columns:
r["cells"][moved_columns.index("no")] = f"补-{k:02d}"
blocks += [
{"type": "heading", "text": "补充技术响应", "new_page": True, "vol": "v1"},
{
"type": "table",
"header": moved_columns,
"rows": moved,
"form": "moved",
"route": "tech_row",
"only_deviations": False,
"mode": None,
"columns": moved_columns,
"pool": [],
"vol": "v1",
},
]
for sec in profile.get("prose", []):
blocks.append({"type": "heading", "text": sec["title"], "new_page": True})
units = sorted(set(form_responds.get("prose:" + sec["title"], []) + form_responds.get("prose:", [])))
@@ -406,6 +448,29 @@ def build(tender: str, bid: str, style: str, run: str) -> None:
{"type": "para", "text": fill(p, ctx), "responds": units if i == 0 else [], "form": "prose"}
for i, p in enumerate(sec["paragraphs"])
]
for b in blocks[-len(sec["paragraphs"]) - 1 :]:
b["vol"] = "v2" if style == "B" else "v1"
blocks[0]["vol"] = "v1"
if style == "B":
volumes = [
{"id": "v1", "kind": "commercial", "header": None, "blocks": [b for b in blocks if b.get("vol") == "v1"]},
{
"id": "v2",
"kind": "technical",
"header": None,
"blocks": [
{"type": "title", "text": f"{proj.get('name', '')}投标文件(技术册)"},
*[b for b in blocks if b.get("vol") == "v2"],
],
},
]
else:
volumes = [{"id": "v1", "kind": "whole", "header": None, "blocks": blocks}]
traps = {u: "T01" for r in moved for u in r.get("responds") or []}
for v in rewrite: # 被改写的要求(截断、同义替换)是 T02;只写值不复述是 T03
for r in [x for xs in rows_by_route.values() for x in xs if x.get("key") == v]:
for u in r["responds"]:
traps.setdefault(u, "T03" if "answer" in rewrite[v] and "requirement" not in rewrite[v] else "T02")
spec = {
"bid": bid,
@@ -413,7 +478,8 @@ def build(tender: str, bid: str, style: str, run: str) -> None:
"style": style,
"project_name": proj.get("name", ""),
"bidder": BIDDER,
"volumes": [{"id": "v1", "kind": "whole", "header": None, "blocks": blocks}],
"volumes": volumes,
"traps": traps,
"price": {
"total": f"{total:.2f}",
"items": [