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

@@ -109,7 +109,7 @@ async def _stage(ctx: TenantContext, job_id: UUID, stage: str) -> dict[str, Any]
return checkpoints
async def _stages(ctx: TenantContext, job_id: UUID) -> dict[str, int]:
async def _stages(ctx: TenantContext, job_id: UUID) -> dict[str, Any]:
async with tenant_tx(ctx) as s:
job = (await s.execute(select(ParseJob).where(ParseJob.id == job_id))).scalar_one()
doc = (await s.execute(select(SourceDocument).where(SourceDocument.id == job.document_id))).scalar_one()
@@ -189,7 +189,7 @@ async def _stages(ctx: TenantContext, job_id: UUID) -> dict[str, int]:
last[0] = pct
await _progress(ctx, job_id, "extract", pct)
items, info = await extract.run(ctx, job_id, doc_info, progress)
items, info, run_stats = await extract.run(ctx, job_id, doc_info, progress)
await _stage(ctx, job_id, "verify") # 三道校验在抽取时逐条做完;这里只是进度节点
await _stage(ctx, job_id, "merge")
@@ -197,7 +197,15 @@ async def _stages(ctx: TenantContext, job_id: UUID) -> dict[str, int]:
d = (await s.execute(select(SourceDocument).where(SourceDocument.id == doc.id))).scalar_one()
lots = await project_extracted.apply(s, ctx, d, info, extract.legal_regime_by_rule(body), items)
stats = await checklist.merge(s, ctx, d, job_id, items, notes, lots)
return {**stats, "lots_found": len([k for k in lots if k != "默认"])}
return {
**stats,
"lots_found": len([k for k in lots if k != "默认"]),
# 质量统计(评测和排查用):没通过校验的原因码条数、对半重跑次数、拆到底仍不完整的分块、项目信息各字段的结论
"verify_failures": {k.removeprefix("fail:"): n for k, n in run_stats.items() if k.startswith("fail:")},
"chunks_split": run_stats.get("chunks_split", 0),
"output_incomplete": run_stats.get("output_incomplete", 0),
"info_fields": {k: v["status"] for k, v in info.status.items()},
}
async def _finish(
@@ -206,7 +214,7 @@ async def _finish(
status: str,
error_class: str | None,
code: str | None,
result: dict[str, int] | None = None,
result: dict[str, Any] | None = None,
) -> None:
async with tenant_tx(ctx) as s:
job = (await s.execute(select(ParseJob).where(ParseJob.id == job_id))).scalar_one()