fix(server): 按 S01–S10 评测修复读标抽取(F1–F8、口径 Q1–Q4)

- 合并时待确认条目不再借正式条目进清单,没通过的类别记在 pending_categories
- 结构识别:章(按章跳过不相干的部分)、表后「上述……否则无效」说明、★ 节标题
- INVALID 补写法;输出用到上限 92% 按截断处理,拆到底时采用已有输出
- 格式范本正文、整表概括句不收;项目信息多看评标方法所在分块
- WPS 旧版 DOC 直接修正字体表字符集;表格按单元格切句(s3-v4)
- 口径:★ 节继承实质性、★ 定义为计分项时降为重要、「必须/应」声明记 keyword 规则、
  政府采购资格条件按 87 号令第六十三条带无效类别
- 提示词 substantive v5,qualification / schedule / tech_param v4

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-03 23:19:30 +08:00
parent f99bb8b358
commit f98fd51128
17 changed files with 912 additions and 62 deletions

View File

@@ -3,6 +3,10 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Protocol
from bid_doctor.core.config import get_settings
NEAR_LIMIT = 0.92
@dataclass(frozen=True, slots=True)
class Completion:
@@ -12,10 +16,17 @@ class Completion:
tokens_in: int | None = None
tokens_out: int | None = None
output_mode: str = "json_mode"
# 输出不完整:被截断(finish_reason=length)或不是合法 JSON。调用方要拆小输入重跑,不能当成「没有结果」
# 输出不完整:被截断(finish_reason=length)、用到了上限的 92% 以上,或不是合法 JSON。
# 调用方要拆小输入重跑,不能当成「没有结果」
incomplete: bool = False
def near_limit(tokens_out: int | None) -> bool:
"""输出用到了上限的 92% 以上:模型常在快到上限时自己把 JSON 收尾,后面的条目整段不输出,
finish_reason 却是正常结束(评测 §3.5:S03 一块 7757 / 8192 token,后两页参数全丢)。按截断处理。"""
return tokens_out is not None and tokens_out >= NEAR_LIMIT * get_settings().ai_max_output_tokens
class TransientError(Exception):
"""限流、5xx、网络抖动:可以重试;重试后仍失败按基础设施错误处理(退回额度)。"""

View File

@@ -11,7 +11,7 @@ import json
import httpx
from bid_doctor.ai.providers.base import Completion, TransientError
from bid_doctor.ai.providers.base import Completion, TransientError, near_limit
from bid_doctor.core.config import get_settings
@@ -26,6 +26,7 @@ class OpenAICompatProvider:
"model": model,
"temperature": 0,
"response_format": {"type": "json_object"},
"max_tokens": s.ai_max_output_tokens,
"messages": [{"role": "system", "content": system}, {"role": "user", "content": user}],
}
try:
@@ -44,7 +45,7 @@ class OpenAICompatProvider:
choice = payload["choices"][0]
raw = choice["message"]["content"] or ""
usage = payload.get("usage") or {}
incomplete = choice.get("finish_reason") == "length"
incomplete = choice.get("finish_reason") == "length" or near_limit(usage.get("completion_tokens"))
try:
data = json.loads(raw)
except json.JSONDecodeError: