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:
@@ -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):
|
||||
|
||||
@@ -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]]] = {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -20,6 +20,10 @@ from bid_doctor.core.ids import uuid7
|
||||
RETRIES = 2
|
||||
|
||||
|
||||
class OutputIncomplete(Exception):
|
||||
"""模型输出被截断或不是合法 JSON:调用方把输入拆小重跑(extract.run),不能当成「没有结果」。"""
|
||||
|
||||
|
||||
def provider() -> Provider:
|
||||
return OpenAICompatProvider() if get_settings().ai_provider == "openai_compat" else FakeProvider()
|
||||
|
||||
@@ -61,10 +65,12 @@ async def run_json(
|
||||
tokens_out=result.tokens_out if result else None,
|
||||
latency_ms=int((time.monotonic() - started) * 1000),
|
||||
output_mode=result.output_mode if result else None,
|
||||
error_class=error.error_class if error else None,
|
||||
error_class=error.error_class if error else "bad_output" if result and result.incomplete else None,
|
||||
)
|
||||
)
|
||||
if error:
|
||||
raise error
|
||||
assert result is not None
|
||||
if result.incomplete:
|
||||
raise OutputIncomplete(task)
|
||||
return result.data
|
||||
|
||||
Reference in New Issue
Block a user