feat(server): 参考 BidEngine 补五项:原因码、截断重跑、项目信息逐项表态、占位符、提示词规则对应校验

- 待确认记原因码(extract.FAILURES,存在 ai_values.failures),解析结果按原因计数
  (verify_failures);前端待确认条目显示具体原因
- 相邻分块重叠约 800 字;模型输出被截断或不是合法 JSON 时对半重跑(最多 3 层),
  拆到底仍不完整记 output_incomplete。修复:以前截断时整块条目被当成「没有结果」悄悄丢掉
- 项目基本信息提示词 v2:12 个字段逐项表态 found / ambiguous / not_found,跨分块合并为
  found / absent / needs_review(info_fields)
- 「____元」「××」「( )」「待填」等占位符不算取值
- 提示词 substantive v4,qualification / scoring / schedule / tech_param v3:每条禁止事项
  对应一个原因码,对照表在 prompts/README.md
- 顾问(Fable)深读参考项目的结论补进 ref-bid-engine-analysis.md §5;架构 §13 SK12
- 待确认:国动 4 → 2,S02 13 → 3,S06 57 → 55

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-03 21:59:12 +08:00
parent e3f234ca0b
commit f99bb8b358
22 changed files with 884 additions and 81 deletions

View File

@@ -12,6 +12,8 @@ class Completion:
tokens_in: int | None = None
tokens_out: int | None = None
output_mode: str = "json_mode"
# 输出不完整:被截断(finish_reason=length)或不是合法 JSON。调用方要拆小输入重跑,不能当成「没有结果」
incomplete: bool = False
class TransientError(Exception):

View File

@@ -135,15 +135,25 @@ def _tech_param(lines: list[tuple[str, str, str, str]]) -> dict[str, Any]:
return {"items": items}
INFO_KEYS = (
"name", "code", "purchaser", "agency", "procurement_method", "legal_regime", "budget_total", "ceiling_total",
"eval_method", "accept_consortium", "accept_imported", "sme_only",
) # fmt: skip
def _project_info(lines: list[tuple[str, str, str, str]]) -> dict[str, Any]:
"""v2 的格式:每个字段都表态 found / not_found。"""
fields: dict[str, Any] = {}
for sid, _, p, _ in lines:
for key, label in (("name", "项目名称"), ("code", "项目编号"), ("purchaser", "采购人")):
if key not in fields and (m := re.search(label + r"[::]\s*([^,。;]+)", p)):
fields[key] = {"value": m.group(1).strip(), "sid": sid, "quote": p[:30]}
fields[key] = {"status": "found", "value": m.group(1).strip(), "sid": sid, "quote": p[:30]}
if "budget_total" not in fields and (m := re.search(r"预算[^0-9]{0,8}(\d+(?:\.\d+)?)\s*(万元|元)", p)):
fields["budget_total"] = {"value": m.group(1), "unit": m.group(2), "sid": sid, "quote": p[:30]}
return {"fields": fields, "lots": []}
fields["budget_total"] = {
"status": "found", "value": m.group(1), "unit": m.group(2), "sid": sid, "quote": p[:30],
} # fmt: skip
none = {"status": "not_found", "value": None, "unit": None, "sid": None, "quote": None}
return {"fields": {k: fields.get(k, none) for k in INFO_KEYS}, "lots": []}
TASKS: dict[str, Callable[[list[tuple[str, str, str, str]]], dict[str, Any]]] = {

View File

@@ -41,16 +41,20 @@ class OpenAICompatProvider:
raise TransientError("server", f"模型接口 {r.status_code}")
r.raise_for_status()
payload = r.json()
raw = payload["choices"][0]["message"]["content"]
choice = payload["choices"][0]
raw = choice["message"]["content"] or ""
usage = payload.get("usage") or {}
incomplete = choice.get("finish_reason") == "length"
try:
data = json.loads(raw)
except json.JSONDecodeError:
data = {"items": []} # 内容质量问题:不算任务失败,相关条目由规则兜底进待确认区(架构 §3.6)
# 以前当成「没有结果」:输出被截断时,这一整块的条目会悄悄丢掉(2026-11-03 审计发现,输出正好 8192 token)
data, incomplete = {}, True
return Completion(
data=data,
raw=raw,
model=payload.get("model", model),
tokens_in=usage.get("prompt_tokens"),
tokens_out=usage.get("completion_tokens"),
incomplete=incomplete,
)