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

@@ -23,6 +23,7 @@ from typing import Any
from uuid import UUID
from bid_doctor.ai import prompts
from bid_doctor.ai import service as ai_service
from bid_doctor.ai.service import provider
from bid_doctor.checklist import extract
from bid_doctor.core.db import TenantContext
@@ -72,9 +73,12 @@ async def cached_run_json(ctx: Any, *, job_type: str, job_id: Any, task: str, pr
raise
print("retry", task, e, file=sys.stderr)
await asyncio.sleep(3)
data = {"data": r.data, "model": r.model, "tokens_in": r.tokens_in, "tokens_out": r.tokens_out}
data = {"data": r.data, "model": r.model, "tokens_in": r.tokens_in, "tokens_out": r.tokens_out,
"incomplete": r.incomplete}
f.write_text(json.dumps(data, ensure_ascii=False))
calls.append({"task": task, "version": version, "user": user, **data})
if data.get("incomplete"):
raise ai_service.OutputIncomplete(task) # 和线上一样:输出被截断时由 extract.run 对半重跑
return data["data"]
@@ -97,7 +101,7 @@ async def main() -> None:
declared={m for n in notes for m in n.markers},
)
ctx = TenantContext(tenant_id=UUID(int=1), user_id=UUID(int=1))
items, info = await extract.run(ctx, UUID(int=2), doc)
items, info, run_stats = await extract.run(ctx, UUID(int=2), doc)
by_sid = {f"p{s.page_no}s{s.seq}": s for s in body}
# ---------- 结果 ----------
@@ -190,6 +194,9 @@ async def main() -> None:
f"条目 {len(items)}:" + ",".join(f"{z}/{r or '-'} {n}" for (z, r), n in zone.most_common()),
"按类别:" + ",".join(f"{c}/{z}:{n}" for (c, z), n in sorted(cats.items())),
"项目信息:" + ",".join(f"{k}={getattr(v, 'value', v)}" for k, v in info.fields.items())[:600],
"项目信息结论:" + ",".join(f"{k}:{v['status']}{v.get('codes') or ''}" for k, v in info.status.items()),
"对半重跑 {} 次,拆到底仍不完整 {} 块".format(run_stats.get("chunks_split", 0), run_stats.get("output_incomplete", 0)),
"没通过校验的原因:" + ",".join(f"{k.removeprefix('fail:')} {n}" for k, n in sorted(run_stats.items()) if k.startswith("fail:")),
"",
"## 模型输出审计",
]