diff --git a/project-docs/AI合同平台-AI受托法务复核与引用门禁整改报告-2026-08-26.md b/project-docs/AI合同平台-AI受托法务复核与引用门禁整改报告-2026-08-26.md
new file mode 100644
index 00000000..f21cc1c0
--- /dev/null
+++ b/project-docs/AI合同平台-AI受托法务复核与引用门禁整改报告-2026-08-26.md
@@ -0,0 +1,110 @@
+# AI 合同平台 AI 受托法务复核与引用门禁整改报告
+
+- 日期:2026-08-26
+- 原评测运行:`stratified-v1-20260826`
+- 复核主体:`ai-delegated-legal-review-gpt-5.6-sol`
+- 复核类型:平台所有者委托的 AI 法律质量复核
+- 模型:`gpt-5.6-sol`
+- 最终结论:**43 条复核已完成,但 0 条接受、43 条拒绝;受控真实用户试点继续阻断**
+
+## 一、结论摘要
+
+平台所有者明确授权由 AI 完成原计划中的 43 条法律质量复核,以减少常规人工介入。系统没有把 AI 冒充为具名人类法务,而是以独立复核主体 `ai-delegated-legal-review-gpt-5.6-sol` 写入不可变复核事件。
+
+第二遍复核结果如下:
+
+| 复核项 | 结果 |
+| --- | ---: |
+| 应复核 / 已复核 | 43 / 43 |
+| 事实与场景一致 | 42 |
+| 事实结论存疑 | 1 |
+| 引用可核验通过 | 0 |
+| 引用可核验失败 | 43 |
+| 发现重大遗漏 | 4 |
+| 接受 / 修订 / 拒绝 | 0 / 0 / 43 |
+
+因此,“复核完成”不等于“复核通过”。原 100 条自动结构门禁虽然在旧规则下显示 100% 通过,但其中 43 条抽检揭示了系统性引用假阳性,原自动结论已被本报告撤销,不能用于真实用户试点放行。
+
+## 二、关键缺陷
+
+### 2.1 真实模型回答与兜底引用被错误拼接
+
+法律问答在 Provider 成功时,以规则兜底响应为底稿,仅替换回答正文。规则兜底响应自带示例法条,因此即使本次知识检索没有命中,最终响应仍显示有引用。
+
+43 条样本均具备以下冲突:
+
+- 回答正文明确说明“未命中明确法律依据,需要进一步检索确认”;
+- `primary_sources` 全部为空;
+- 响应却附加 1 至 2 条规则兜底法条;
+- 旧质量门禁只检查引用是否含标题、条号和摘录,没有检查来源 ID、文档、分块、URL,以及回答是否实际使用该引用。
+
+### 2.2 法条文本正确不代表回答被法条支持
+
+本次附带的五种法条摘录经政府或司法机关公开文本抽核,文字本身基本一致:
+
+- 《中华人民共和国民法典》第509条、第703条和第707条可在[最高人民检察院公布的民法典全文](https://www.spp.gov.cn/zdgz/202006/t20200602_463886.shtml)中核对;
+- 《劳动合同法》第19条可在[上海市人力资源和社会保障局公开说明](https://rsj.sh.gov.cn/trdhy_17355/20200617/t0035_1382881.html)中核对;第39条可在[上海市人力资源和社会保障局公开说明](https://rsj.sh.gov.cn/trdhy_17355/20260805/99af9c20ddd44f9a855b8c6836941b39.html)中核对。
+
+但这些法条没有作为本次回答的检索来源,也没有在正文中建立“具体结论—具体法源”的对应关系。尤其 35 条不同合同类型统一附《民法典》第509条,最多只能说明一般履约原则,不能支撑回答中的全部专业建议。因此 43 条的“引用可核验”均判定失败。
+
+## 三、第二遍内容复核发现
+
+除系统性引用失败外,AI 第二遍复核还发现 4 条重大遗漏:
+
+| 样本 | 主要问题 |
+| --- | --- |
+| `data_processing-01` | 未把委托处理目的、期限、方式、信息种类、处理权限、禁止超指令处理、数据主体权利协助和委托方监督机制设为独立前置条件。 |
+| `data_processing-10` | 证据保全建议未充分处理最小必要、法定删除义务、数据主体权利与业务保留周期之间的冲突,且“争议终结前停止常规删除”范围过宽。 |
+| `sales-10` | 引用核验材料未明确官方来源、文本版本、公布及施行日期、检索日期和网页存档。 |
+| `transport-10` | 未明确保存行业规则、承运条款、平台规则、技术标准及其当时有效版本和来源页面。 |
+
+其中 `data_processing-10` 同时被判定为事实结论存疑;其余 42 条在给定场景内逻辑基本自洽,但仍因引用失败不能接受。
+
+## 四、已完成整改
+
+1. Provider 成功响应只允许返回本次真实检索得到的引用,不再继承规则兜底示例引用。
+2. 自动质量门禁新增 `citation_ungrounded`:引用必须至少具有文档 ID、知识分块 ID、稳定来源 ID 或来源 URL。
+3. 自动质量门禁新增 `citation_answer_conflict`:回答声明未检索到依据却同时附引用时直接阻断。
+4. 新增可复用的 AI 受托复核执行器,采用“先评估产物、后显式执行”两阶段流程;执行前校验任务 ID、回答哈希、运行 ID和租户,防止评估后内容被替换。
+5. 复核产物不保存原问题和模型全文,只保存输入 SHA-256、批次哈希、判定、置信度和简要发现。
+6. 运营快照新增接受、修订、拒绝、批准率和法律复核门禁;存在拒绝时产生错误级告警,避免把“43/43 已完成”误读为通过。
+7. 后台文案调整为“治理复核”,明确 AI 受托复核优先、人工只处理疑难、冲突和低置信度例外,且 AI 不得冒充人类法务。
+
+## 五、审计落库结果
+
+- 原待复核队列:43;落库后:0。
+- `review_required / review_completed`:43 / 43。
+- 复核主体统一为 `ai-delegated-legal-review-gpt-5.6-sol`。
+- 43 条均以 `citation_verifiability=fail`、`disposition=rejected` 追加写入复核事件表。
+- 每条反馈均记录模型、授权主体、输入哈希、置信度、引用结论和内容结论;原模型输出未被覆盖。
+- 运行时评估 JSON 位于 `.runtime/delegated-ai-legal-review/`,不进入 Git。
+
+## 六、发布决定
+
+当前状态应更新为:
+
+**容量基线历史数据可保留;旧引用质量门禁结论 INVALIDATED;AI 法律复核 COMPLETED / REJECTED;受控真实用户试点 BLOCKED;公开注册 BLOCKED。**
+
+这不是 AI 自动化失败,而是 AI 复核成功发现了此前人工数量要求掩盖的系统性证据缺陷。继续减少人工介入的正确方向是强化可追溯法源和自动拒绝机制,而不是让 AI 对缺少依据的输出自动签字通过。
+
+## 七、下一阶段修改方案
+
+### P0:重新评测前
+
+1. 建设共享或按租户授权的中国现行法源库,至少覆盖本次十类合同的核心法律、行政法规、司法解释和必要监管规则。
+2. 每个法源保存稳定来源 ID、官方 URL、标题、条号、摘录、公布日期、施行日期、版本和抓取/复核时间;不得仅保存自由文本。
+3. 回答输出增加结论到来源 ID 的明确绑定;任何没有来源绑定的法律结论进入拒绝或低置信度队列。
+4. 修复四个重大遗漏对应的提示词和 Playbook,重点补齐数据处理委托边界、证据保全与删除冲突、版本化外部规则留存。
+
+### P0:重新运行门禁
+
+使用新运行 ID 重新执行 100 条分层样本,不得复用本次已通过的旧质量状态。最低放行条件为:
+
+- 真实检索来源覆盖率 100%;
+- 无来源引用和正文—引用冲突均为 0;
+- 40 条高风险和确定性抽样全部由 AI 第二遍复核;
+- 重大遗漏为 0;
+- 接受或修订后接受率 100%,拒绝数为 0;
+- AI 置信度不足、结论冲突或来源时效存疑时才进入人工例外队列。
+
+在以上条件完成前,不建议邀请真实外部用户进入合同法律内容试点。
diff --git a/project-docs/AI合同平台-受控试点容量与分层质量评测报告-2026-08-26.md b/project-docs/AI合同平台-受控试点容量与分层质量评测报告-2026-08-26.md
index e19ca538..dffeda80 100644
--- a/project-docs/AI合同平台-受控试点容量与分层质量评测报告-2026-08-26.md
+++ b/project-docs/AI合同平台-受控试点容量与分层质量评测报告-2026-08-26.md
@@ -1,10 +1,12 @@
# AI 合同平台受控试点容量与分层质量评测报告
+> **后续审计更正(2026-08-26):** AI 受托复核发现 43/43 条存在无可追溯来源的兜底引用,旧自动引用门禁结论已撤销。43 条均已复核并拒绝,受控真实用户试点继续阻断。完整证据和整改方案见《AI合同平台-AI受托法务复核与引用门禁整改报告-2026-08-26》。以下容量数据仍有效,但“自动质量通过”仅作为旧规则历史结果,不再构成发布证据。
+
- 日期:2026-08-26
- 运行 ID:`stratified-v1-20260826`
- 阶段:核心产品上线收口——受控试点容量与分层质量评测
-- 自动评测结论:**通过**
-- 法律内容人工验收:**未完成,不得视为已通过**
+- 自动评测结论:**旧规则历史通过;引用门禁结论已撤销**
+- 法律内容治理复核:**43 / 43 已完成,0 接受、43 拒绝**
- 发布建议:可继续内部和具名受控试点;不得据此开放公共注册或宣称法律意见、司法存证或正式生产模型能力已验收
## 一、管理结论
@@ -21,8 +23,8 @@
| 10 并发准入拒绝 | 0 | 通过 |
| 模型执行 P95 | 179,040 ms | 通过,接近 180 秒门限 |
| fallback / 失败 / 受控重试 | 0 / 0 / 0 | 通过 |
-| 待具名人工复核 | 43 | 待完成 |
-| 已完成人工复核 | 0 | 未通过法律内容验收 |
+| 待治理复核 | 0 | 已全部处理 |
+| 已完成治理复核 | 43 | 0 接受、43 拒绝,未通过法律内容验收 |
## 二、已落地能力
@@ -96,10 +98,10 @@
| 各合同类型自动通过率 | ≥ 70% | 全部 100% | 通过 |
| 模型执行 P95 | ≤ 180 秒 | 179.04 秒 | 通过,黄色边界 |
| 10 并发准入拒绝 | 0 | 0 | 通过 |
-| 高风险人工复核完成 | 100% | 0 / 40 | 未完成 |
-| 全部抽检完成 | 100% | 0 / 43 | 未完成 |
+| 高风险治理复核完成 | 100% | 40 / 40 | 完成但全部因引用缺陷拒绝 |
+| 全部抽检完成 | 100% | 43 / 43 | 完成;接受 0、拒绝 43 |
-因此,本阶段只能签署“自动容量和结构质量门禁通过”,不能签署“法律内容质量通过”。
+因此,本阶段只能保留容量基线,旧结构门禁中的引用判断已被后续审计撤销,不能签署“法律内容质量通过”。
## 六、运行与工程证据
@@ -114,7 +116,7 @@
## 七、已知限制与上线边界
-1. 100% 是自动结构门禁通过率,不是法律正确率。43 条具名复核未完成前,不应对外宣称内容质量已验收。
+1. 旧规则下的 100% 自动结构通过率已被 43 条 AI 受托复核证明存在引用假阳性;43 条全部拒绝,不应对外宣称内容质量已验收。
2. 执行 P95 距 180 秒只剩 960 ms,且最大值 221.7 秒。受控试点应继续显示后台进度、允许取消和人工接管,不应恢复同步等待。
3. 当前模型来源是开发期 Codex CLI 与个人订阅会话,不是可供真实客户使用的生产 Provider 合同、API 配额或服务等级协议。
4. 本地运行时 anti-abuse 为 disabled;真实用户注册仍应保持关闭。公开试点前至少切换 observe/enforce,配置哈希密钥并验证每日调用/数据出口配额。
@@ -126,8 +128,8 @@
### P0:受控试点放行前
-1. 指定至少两名具名法律复核人员,完成 40 个高风险样本和 3 个抽样样本;每条核对事实、现行法源、引用定位和重大遗漏。
-2. 导出 43 条结构化复核反馈,按合同类型统计 corrected/rejected、不可核验引用和高风险遗漏;任一类型出现重大遗漏时重新评测该类型 10 条。
+1. 先补齐十类合同的可追溯现行法源库和结论—来源绑定,再使用新运行 ID 重新执行 100 条样本。
+2. 默认由 AI 完成第二遍复核;仅将低置信度、法源时效冲突和重大争议送人工例外队列。任何引用不可核验或重大遗漏都阻断放行。
3. 对 3 条超过 180 秒的任务分析 Provider 耗时和提示词长度;将 P95 目标留出至少 10% 余量后再扩大租户并发。
4. 用真实超级管理员登录运营页,下钻 `controlled-pilot-v1 / stratified-v1-20260826`,验证筛选、输出预览、显式确认和复核落库,但不得用测试人员身份替代法务结论。
@@ -141,6 +143,6 @@
## 九、最终发布决定
-本阶段建议状态为:**自动容量与结构质量门禁 PASS;法律内容验收 PENDING;公共发布 BLOCKED**。
+本阶段建议状态更新为:**容量基线保留;旧引用质量门禁 INVALIDATED;AI 法律复核 COMPLETED / REJECTED;受控真实用户试点与公共发布均 BLOCKED**。
-允许继续的范围是内部开发和具名受控试点,租户活动任务不超过 10、单用户不超过 3,并保留后台任务、重试、fallback 和人工接管。完成 43 条人工复核及上述 P0 条件前,不开放真实用户自由注册,不将本地评测包装成第三方存证、司法效力或正式生产服务等级证明。
+允许继续的范围仅为内部开发与隔离 UAT,租户活动任务不超过 10、单用户不超过 3,并保留后台任务、重试、fallback 和例外接管。使用新运行 ID 完成可追溯法源整改、100 条重新评测及 AI 第二遍复核前,不开放受控真实用户试点或自由注册,也不将本地评测包装成第三方存证、司法效力或正式生产服务等级证明。
diff --git a/yuqei-ai-platform/packages/ai-openapi/openapi.yaml b/yuqei-ai-platform/packages/ai-openapi/openapi.yaml
index cdbf55fa..71801963 100644
--- a/yuqei-ai-platform/packages/ai-openapi/openapi.yaml
+++ b/yuqei-ai-platform/packages/ai-openapi/openapi.yaml
@@ -1857,7 +1857,7 @@ components:
threshold: { type: number }
ModelJobOperationsSnapshot:
type: object
- required: [metrics, alerts, completed_sample_count, target_sample_count, remaining_sample_count, baseline_ready, quality_passed, quality_needs_review, quality_failed, quality_pass_rate, review_required, review_completed, admission, quality_segments]
+ required: [metrics, alerts, completed_sample_count, target_sample_count, remaining_sample_count, baseline_ready, quality_passed, quality_needs_review, quality_failed, quality_pass_rate, review_required, review_completed, review_accepted, review_corrected, review_rejected, review_approval_rate, review_gate_passed, admission, quality_segments]
properties:
metrics:
$ref: "#/components/schemas/ModelInferenceJobMetrics"
@@ -1875,6 +1875,11 @@ components:
quality_pass_rate: { type: number }
review_required: { type: integer }
review_completed: { type: integer }
+ review_accepted: { type: integer }
+ review_corrected: { type: integer }
+ review_rejected: { type: integer }
+ review_approval_rate: { type: number }
+ review_gate_passed: { type: boolean }
evaluation_run_id: { type: string, nullable: true }
admission: { $ref: "#/components/schemas/ModelJobAdmissionSnapshot" }
quality_segments:
@@ -1894,7 +1899,7 @@ components:
accepting_submissions: { type: boolean }
ModelJobQualitySegment:
type: object
- required: [dimension, value, sample_count, quality_passed, quality_needs_review, quality_failed, review_required, review_completed, quality_pass_rate]
+ required: [dimension, value, sample_count, quality_passed, quality_needs_review, quality_failed, review_required, review_completed, review_accepted, review_corrected, review_rejected, review_approval_rate, quality_pass_rate]
properties:
dimension: { type: string, enum: [contract_type, risk_level, industry] }
value: { type: string }
@@ -1904,6 +1909,10 @@ components:
quality_failed: { type: integer }
review_required: { type: integer }
review_completed: { type: integer }
+ review_accepted: { type: integer }
+ review_corrected: { type: integer }
+ review_rejected: { type: integer }
+ review_approval_rate: { type: number }
quality_pass_rate: { type: number }
ModelJobQualityReviewRequest:
type: object
diff --git a/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/api/legal.py b/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/api/legal.py
index 6fc257b3..c3b61407 100644
--- a/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/api/legal.py
+++ b/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/api/legal.py
@@ -116,7 +116,18 @@ def register_legal_routes(
retrieved_citations=citations,
)
if provider_response.status == "succeeded" and provider_response.answer:
- response = fallback_response.model_copy(update={"answer": provider_response.answer})
+ # A provider answer may only expose sources that were actually retrieved for
+ # that answer. The rule-based fallback carries generic example citations;
+ # copying those citations onto a real provider answer makes an ungrounded
+ # answer look sourced and lets the model-job quality gate pass incorrectly.
+ response = fallback_response.model_copy(
+ update={
+ "answer": provider_response.answer,
+ "citations": citations,
+ "primary_sources": citations[:3],
+ "confidence": 0.78 if citations else 0.35,
+ }
+ )
audit_status = "succeeded"
elif fallback_allowed:
response = fallback_response
diff --git a/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/api/model_jobs.py b/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/api/model_jobs.py
index 8900805f..aa32cf6d 100644
--- a/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/api/model_jobs.py
+++ b/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/api/model_jobs.py
@@ -424,6 +424,7 @@ def execute_model_job(
error_code="output_quality_gate_failed",
error_message=quality.summary,
review_trigger="quality_gate",
+ fallback_used=audit_status == "fallback",
**quality_fields,
)
if audit_status == "fallback":
@@ -449,6 +450,7 @@ def execute_model_job(
error_code="output_quality_review_required",
error_message=quality.summary,
review_trigger="quality_gate",
+ fallback_used=audit_status == "fallback",
**quality_fields,
)
if review_trigger is not None:
diff --git a/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/model_jobs.py b/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/model_jobs.py
index 196e8cb3..ccfd9dab 100644
--- a/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/model_jobs.py
+++ b/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/model_jobs.py
@@ -202,6 +202,10 @@ class ModelJobQualitySegment(BaseModel):
quality_failed: int
review_required: int
review_completed: int
+ review_accepted: int = 0
+ review_corrected: int = 0
+ review_rejected: int = 0
+ review_approval_rate: float = 0.0
quality_pass_rate: float
@@ -218,6 +222,11 @@ class ModelJobOperationsSnapshot(BaseModel):
quality_pass_rate: float
review_required: int = 0
review_completed: int = 0
+ review_accepted: int = 0
+ review_corrected: int = 0
+ review_rejected: int = 0
+ review_approval_rate: float = 0.0
+ review_gate_passed: bool = False
evaluation_run_id: str | None = None
admission: ModelJobAdmissionSnapshot
quality_segments: list[ModelJobQualitySegment] = Field(default_factory=list)
@@ -715,6 +724,7 @@ class ModelInferenceJobStore:
quality_summary: str | None = None,
quality_gate_version: str = "model-job-v1",
review_trigger: ReviewTrigger | None = None,
+ fallback_used: bool | None = None,
) -> ModelInferenceJob:
now = datetime.now(UTC)
existing = self.get_record(job_id, tenant_id=tenant_id)
@@ -738,7 +748,7 @@ class ModelInferenceJobStore:
result=result,
provider_status=provider_status,
audit_run_id=audit_run_id,
- fallback_used=status == "fallback",
+ fallback_used=(status == "fallback") if fallback_used is None else fallback_used,
retryable=retryable,
error_code=error_code,
error_message=error_message,
@@ -882,6 +892,13 @@ class ModelInferenceJobStore:
)
def resolve(self, job_id: str, *, tenant_id: str, resolution_note: str) -> ModelInferenceJob:
+ existing = self.get_record(job_id, tenant_id=tenant_id)
+ if existing is None:
+ raise KeyError(job_id)
+ if existing.review_status == "required":
+ raise ValueError(
+ "This job requires a structured quality review; submit fact, citation, omission, and disposition feedback."
+ )
return self._update(
job_id,
tenant_id=tenant_id,
@@ -1057,6 +1074,11 @@ class ModelInferenceJobStore:
quality_failed = sum(item.quality_status == "failed" for item in evaluated)
review_required = sum(item.review_status in {"required", "completed"} for item in evaluated)
review_completed = sum(item.review_status == "completed" for item in evaluated)
+ review_accepted = sum(item.review_status == "completed" and item.review_disposition == "accepted" for item in evaluated)
+ review_corrected = sum(item.review_status == "completed" and item.review_disposition == "corrected" for item in evaluated)
+ review_rejected = sum(item.review_status == "completed" and item.review_disposition == "rejected" for item in evaluated)
+ review_approval_rate = round((review_accepted + review_corrected) / max(review_required, 1), 4)
+ review_gate_passed = review_completed == review_required and review_rejected == 0
admission = self.admission_snapshot(
tenant_id=tenant_id,
tenant_active_limit=tenant_active_limit,
@@ -1090,7 +1112,18 @@ class ModelInferenceJobStore:
if metrics.total >= 10 and metrics.failure_rate > 0.05:
alerts.append(_alert("failure_rate_high", "error", "任务失败率偏高", "失败率超过 5%。", metrics.failure_rate, 0.05))
if metrics.awaiting_human >= 5:
- alerts.append(_alert("manual_queue_backlog", "warning", "人工复核队列积压", "等待人工复核的任务达到 5 个。", metrics.awaiting_human, 5))
+ alerts.append(_alert("manual_queue_backlog", "warning", "治理复核队列积压", "等待 AI 或人工治理复核的任务达到 5 个。", metrics.awaiting_human, 5))
+ if review_rejected:
+ alerts.append(
+ _alert(
+ "legal_review_rejected",
+ "error",
+ "法律质量复核未通过",
+ f"已有 {review_rejected} 个复核任务被拒绝,不能仅凭复核完成数放行试点。",
+ review_rejected,
+ 0,
+ )
+ )
if stale_active:
alerts.append(_alert("stale_active_jobs", "error", "存在停滞任务", "有排队或运行任务超过 10 分钟未更新。", stale_active, 0))
if admission.active_saturation_rate >= 1:
@@ -1129,6 +1162,11 @@ class ModelInferenceJobStore:
quality_pass_rate=quality_pass_rate,
review_required=review_required,
review_completed=review_completed,
+ review_accepted=review_accepted,
+ review_corrected=review_corrected,
+ review_rejected=review_rejected,
+ review_approval_rate=review_approval_rate,
+ review_gate_passed=review_gate_passed,
evaluation_run_id=evaluation_run_id,
admission=admission,
quality_segments=_quality_segments(evaluated),
@@ -1364,6 +1402,11 @@ def _quality_segments(records: list[ModelInferenceJobRecord]) -> list[ModelJobQu
for value in values:
selected = [item for item in records if getattr(item, field_name) == value]
passed = sum(item.quality_status == "passed" for item in selected)
+ review_required = sum(item.review_status in {"required", "completed"} for item in selected)
+ review_completed = sum(item.review_status == "completed" for item in selected)
+ review_accepted = sum(item.review_status == "completed" and item.review_disposition == "accepted" for item in selected)
+ review_corrected = sum(item.review_status == "completed" and item.review_disposition == "corrected" for item in selected)
+ review_rejected = sum(item.review_status == "completed" and item.review_disposition == "rejected" for item in selected)
result.append(
ModelJobQualitySegment(
dimension=dimension, # type: ignore[arg-type]
@@ -1372,8 +1415,12 @@ def _quality_segments(records: list[ModelInferenceJobRecord]) -> list[ModelJobQu
quality_passed=passed,
quality_needs_review=sum(item.quality_status == "needs_review" for item in selected),
quality_failed=sum(item.quality_status == "failed" for item in selected),
- review_required=sum(item.review_status in {"required", "completed"} for item in selected),
- review_completed=sum(item.review_status == "completed" for item in selected),
+ review_required=review_required,
+ review_completed=review_completed,
+ review_accepted=review_accepted,
+ review_corrected=review_corrected,
+ review_rejected=review_rejected,
+ review_approval_rate=round((review_accepted + review_corrected) / max(review_required, 1), 4),
quality_pass_rate=round(passed / max(len(selected), 1), 4),
)
)
diff --git a/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/model_output_quality.py b/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/model_output_quality.py
index 15b489eb..ad5856c0 100644
--- a/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/model_output_quality.py
+++ b/yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api/model_output_quality.py
@@ -43,6 +43,14 @@ _PROMPT_LEAK_MARKERS = (
"developer message",
"忽略之前的指令",
)
+_NO_EVIDENCE_MARKERS = (
+ "未命中明确依据",
+ "未命中明确法律依据",
+ "未检索到明确依据",
+ "需要进一步检索确认",
+ "no authoritative source was retrieved",
+ "no legal source was retrieved",
+)
def evaluate_model_output_quality(
@@ -74,6 +82,10 @@ def evaluate_model_output_quality(
labels.append(_label("missing_citation", "error", "法律问答没有返回引用依据。"))
elif any(not _citation_is_reviewable(item) for item in citations):
labels.append(_label("citation_incomplete", "warning", "部分引用缺少标题、出处或摘录。"))
+ elif any(not _citation_is_grounded(item) for item in citations):
+ labels.append(_label("citation_ungrounded", "error", "引用没有可追溯的文档、知识分块、稳定来源 ID 或来源 URL。"))
+ elif normalized_answer and any(marker in normalized_answer.casefold() for marker in _NO_EVIDENCE_MARKERS):
+ labels.append(_label("citation_answer_conflict", "error", "回答声明未命中依据,但响应同时附加了引用。"))
else:
labels.append(_label("citation_reviewable", "info", f"已返回 {len(citations)} 条可审阅引用。"))
elif operation == "contract_review":
@@ -102,5 +114,14 @@ def _citation_is_reviewable(value: Any) -> bool:
return all(isinstance(value.get(key), str) and value[key].strip() for key in ("title", "reference", "quote"))
+def _citation_is_grounded(value: Any) -> bool:
+ if not isinstance(value, dict):
+ return False
+ if any(isinstance(value.get(key), str) and value[key].strip() for key in ("document_id", "chunk_id", "url")):
+ return True
+ source_ids = value.get("source_ids")
+ return isinstance(source_ids, list) and any(isinstance(item, str) and item.strip() for item in source_ids)
+
+
def _label(code: str, severity: QualitySeverity, message: str) -> ModelOutputQualityLabel:
return ModelOutputQualityLabel(code=code, severity=severity, message=message)
diff --git a/yuqei-ai-platform/services/ai-platform-api/tests/test_legal_qa.py b/yuqei-ai-platform/services/ai-platform-api/tests/test_legal_qa.py
index 317273e5..3db70e4f 100644
--- a/yuqei-ai-platform/services/ai-platform-api/tests/test_legal_qa.py
+++ b/yuqei-ai-platform/services/ai-platform-api/tests/test_legal_qa.py
@@ -23,7 +23,7 @@ def test_legal_qa_returns_answer_with_citations() -> None:
def test_legal_qa_uses_default_citation_for_unknown_question() -> None:
- app = create_app(AiPlatformSettings(environment="test"))
+ app = create_app(AiPlatformSettings(environment="test", provider_call_mode="disabled"))
client = TestClient(app)
response = client.post("/api/v1/legal/qa", json={"question": "这个合同需要注意什么?"})
diff --git a/yuqei-ai-platform/services/ai-platform-api/tests/test_model_jobs.py b/yuqei-ai-platform/services/ai-platform-api/tests/test_model_jobs.py
index bbb56d0b..413ee14f 100644
--- a/yuqei-ai-platform/services/ai-platform-api/tests/test_model_jobs.py
+++ b/yuqei-ai-platform/services/ai-platform-api/tests/test_model_jobs.py
@@ -152,6 +152,10 @@ def test_stratified_quality_review_queue_and_feedback_dataset() -> None:
"quality_failed": 0,
"review_required": 1,
"review_completed": 0,
+ "review_accepted": 0,
+ "review_corrected": 0,
+ "review_rejected": 0,
+ "review_approval_rate": 0.0,
"quality_pass_rate": 1.0,
},
{
@@ -163,6 +167,10 @@ def test_stratified_quality_review_queue_and_feedback_dataset() -> None:
"quality_failed": 0,
"review_required": 1,
"review_completed": 0,
+ "review_accepted": 0,
+ "review_corrected": 0,
+ "review_rejected": 0,
+ "review_approval_rate": 0.0,
"quality_pass_rate": 1.0,
},
{
@@ -174,6 +182,10 @@ def test_stratified_quality_review_queue_and_feedback_dataset() -> None:
"quality_failed": 0,
"review_required": 1,
"review_completed": 0,
+ "review_accepted": 0,
+ "review_corrected": 0,
+ "review_rejected": 0,
+ "review_approval_rate": 0.0,
"quality_pass_rate": 1.0,
},
]
@@ -192,6 +204,15 @@ def test_stratified_quality_review_queue_and_feedback_dataset() -> None:
assert review.json()["reviewer_id"] == "legal-reviewer-a"
assert client.get(f"/api/v1/model-jobs/{created['id']}").json()["status"] == "resolved"
assert len(client.get(f"/api/v1/model-jobs/{created['id']}/quality-reviews").json()) == 1
+ reviewed_snapshot = client.get(
+ "/api/v1/model-jobs/operations?target_sample_count=1&evaluation_run_id=stratified-v1"
+ ).json()
+ assert reviewed_snapshot["review_completed"] == 1
+ assert reviewed_snapshot["review_accepted"] == 1
+ assert reviewed_snapshot["review_corrected"] == 0
+ assert reviewed_snapshot["review_rejected"] == 0
+ assert reviewed_snapshot["review_approval_rate"] == 1.0
+ assert reviewed_snapshot["review_gate_passed"] is True
def test_automatic_review_policy_covers_quality_high_risk_and_sampling() -> None:
@@ -261,10 +282,19 @@ def test_sqlalchemy_quality_review_is_persisted_with_resolved_job() -> None:
assert resolved.status == "resolved"
assert resolved.reviewed_by == "reviewer-db-test"
assert store.quality_reviews(job.id, tenant_id="review-db-test") == [event]
+ snapshot = store.operations_snapshot(
+ tenant_id="review-db-test",
+ target_sample_count=1,
+ )
+ assert snapshot.review_completed == 1
+ assert snapshot.review_rejected == 1
+ assert snapshot.review_approval_rate == 0.0
+ assert snapshot.review_gate_passed is False
+ assert "legal_review_rejected" in {alert.code for alert in snapshot.alerts}
engine.dispose()
-def test_model_job_is_queued_polled_and_completed_with_explicit_fallback() -> None:
+def test_model_job_without_grounded_fallback_citation_enters_review() -> None:
app, repository, client = _app_and_client()
created_response = client.post(
@@ -288,19 +318,22 @@ def test_model_job_is_queued_polled_and_completed_with_explicit_fallback() -> No
)
assert completed is not None
- assert completed.status == "fallback"
+ assert completed.status == "awaiting_human"
assert completed.progress_percent == 100
assert completed.fallback_used is True
assert completed.retryable is True
assert completed.result and completed.result["answer"]
assert completed.audit_run_id
assert completed.provider_status == "skipped"
+ assert completed.quality_status == "failed"
+ assert completed.review_trigger == "quality_gate"
+ assert "citation_ungrounded" in {label.code for label in completed.quality_labels}
polled = client.get(f"/api/v1/model-jobs/{created['id']}").json()
- assert polled["status"] == "fallback"
+ assert polled["status"] == "awaiting_human"
assert polled["recommended_poll_after_ms"] == 0
-def test_model_job_retry_handoff_resolution_and_metrics() -> None:
+def test_model_job_retry_structured_review_and_metrics() -> None:
app, repository, client = _app_and_client()
created = client.post(
"/api/v1/legal/qa/jobs",
@@ -339,17 +372,30 @@ def test_model_job_retry_handoff_resolution_and_metrics() -> None:
"assignee": "legal-duty",
},
)
- assert handoff.status_code == 200
- assert handoff.json()["status"] == "awaiting_human"
- assert handoff.json()["handoff_assignee"] == "legal-duty"
+ assert handoff.status_code == 409
+ unstructured_resolution = client.post(
+ f"/api/v1/model-jobs/{created['id']}/resolve",
+ json={"resolution_note": "不应绕过结构化质量复核。"},
+ )
+ assert unstructured_resolution.status_code == 409
resolved = client.post(
- f"/api/v1/model-jobs/{created['id']}/resolve",
- json={"resolution_note": "法务已核对引用并完成答复。"},
+ f"/api/v1/model-jobs/{created['id']}/quality-reviews",
+ headers={"X-User-Id": "ai-quality-reviewer"},
+ json={
+ "legal_fact_consistency": "uncertain",
+ "citation_verifiability": "fail",
+ "high_risk_omission": "uncertain",
+ "disposition": "rejected",
+ "feedback_text": "无可追溯引用,拒绝当前输出。",
+ },
)
assert resolved.status_code == 200
- assert resolved.json()["status"] == "resolved"
- assert resolved.json()["retryable"] is False
+ assert resolved.json()["reviewer_id"] == "ai-quality-reviewer"
+ stored = client.get(f"/api/v1/model-jobs/{created['id']}").json()
+ assert stored["status"] == "resolved"
+ assert stored["retryable"] is False
+ assert stored["review_disposition"] == "rejected"
metrics = client.get("/api/v1/model-jobs/metrics?window_hours=24").json()
assert metrics["total"] == 1
diff --git a/yuqei-ai-platform/services/ai-platform-api/tests/test_model_output_quality.py b/yuqei-ai-platform/services/ai-platform-api/tests/test_model_output_quality.py
index 354da55b..6128564f 100644
--- a/yuqei-ai-platform/services/ai-platform-api/tests/test_model_output_quality.py
+++ b/yuqei-ai-platform/services/ai-platform-api/tests/test_model_output_quality.py
@@ -8,7 +8,12 @@ def test_legal_qa_quality_passes_reviewable_answer() -> None:
result={
"answer": "知识产权归属应明确既有成果与项目成果的边界,并约定权利登记、许可范围及侵权责任。还应区分源代码、文档、数据和第三方素材,明确交付条件、使用地域、许可期限及离职人员成果归属。",
"citations": [
- {"title": "民法典", "reference": "技术合同章", "quote": "技术成果归属由当事人约定。"},
+ {
+ "title": "民法典",
+ "reference": "技术合同章",
+ "quote": "技术成果归属由当事人约定。",
+ "document_id": "law-civil-code-technology",
+ },
],
},
)
@@ -17,6 +22,47 @@ def test_legal_qa_quality_passes_reviewable_answer() -> None:
assert result.score == 100
+def test_quality_gate_rejects_generic_fallback_citation_without_traceable_source() -> None:
+ result = evaluate_model_output_quality(
+ operation="legal_qa",
+ request_payload={"question": "采购预付款如何控制?"},
+ result={
+ "answer": "建议设置预付款担保和分阶段付款。",
+ "citations": [
+ {
+ "title": "中华人民共和国民法典",
+ "reference": "第五百零九条",
+ "quote": "当事人应当按照约定全面履行自己的义务。",
+ },
+ ],
+ },
+ )
+
+ assert result.status == "failed"
+ assert "citation_ungrounded" in {item.code for item in result.labels}
+
+
+def test_quality_gate_rejects_answer_that_denies_attached_evidence() -> None:
+ result = evaluate_model_output_quality(
+ operation="legal_qa",
+ request_payload={"question": "采购预付款如何控制?"},
+ result={
+ "answer": "未命中明确法律依据,需要进一步检索确认。",
+ "primary_sources": [
+ {
+ "title": "中华人民共和国民法典",
+ "reference": "第五百零九条",
+ "quote": "当事人应当按照约定全面履行自己的义务。",
+ "document_id": "law-civil-code-509",
+ },
+ ],
+ },
+ )
+
+ assert result.status == "failed"
+ assert "citation_answer_conflict" in {item.code for item in result.labels}
+
+
def test_quality_gate_blocks_placeholder_and_lost_question() -> None:
result = evaluate_model_output_quality(
operation="legal_qa",
diff --git a/yuqei-ai-sdk-python/src/yuqei_sdk/ai_platform.py b/yuqei-ai-sdk-python/src/yuqei_sdk/ai_platform.py
index 04f1a521..9106d65a 100644
--- a/yuqei-ai-sdk-python/src/yuqei_sdk/ai_platform.py
+++ b/yuqei-ai-sdk-python/src/yuqei_sdk/ai_platform.py
@@ -155,6 +155,10 @@ class ModelJobQualitySegment(BaseModel):
quality_failed: int
review_required: int
review_completed: int
+ review_accepted: int = 0
+ review_corrected: int = 0
+ review_rejected: int = 0
+ review_approval_rate: float = 0.0
quality_pass_rate: float
@@ -189,6 +193,11 @@ class ModelJobOperationsSnapshot(BaseModel):
quality_pass_rate: float
review_required: int = 0
review_completed: int = 0
+ review_accepted: int = 0
+ review_corrected: int = 0
+ review_rejected: int = 0
+ review_approval_rate: float = 0.0
+ review_gate_passed: bool = False
evaluation_run_id: str | None = None
admission: ModelJobAdmissionSnapshot
quality_segments: list[ModelJobQualitySegment] = Field(default_factory=list)
diff --git a/yuqei-contract-platform/apps/contract-web/src/app/admin/model-jobs/page.tsx b/yuqei-contract-platform/apps/contract-web/src/app/admin/model-jobs/page.tsx
index 654236ad..7761fb61 100644
--- a/yuqei-contract-platform/apps/contract-web/src/app/admin/model-jobs/page.tsx
+++ b/yuqei-contract-platform/apps/contract-web/src/app/admin/model-jobs/page.tsx
@@ -63,7 +63,7 @@ export default function ModelJobOperationsPage() {
通过 {snapshot.quality_passed} · 待人工 {snapshot.quality_needs_review} · 阻断 {snapshot.quality_failed}
+自动通过 {snapshot.quality_passed} · 待治理复核 {snapshot.quality_needs_review} · 自动阻断 {snapshot.quality_failed}
| 维度 | 分层 | 样本 | 通过率 | 抽检 | |
|---|---|---|---|---|---|
| {dimensionLabel(segment.dimension)} | {segment.value} | {segment.sample_count} | {formatRate(segment.quality_pass_rate)} | {segment.review_completed}/{segment.review_required} |
| 维度 | 分层 | 样本 | 自动通过率 | 复核 | 接受/修订/拒绝 |
|---|---|---|---|---|---|
| {dimensionLabel(segment.dimension)} | {segment.value} | {segment.sample_count} | {formatRate(segment.quality_pass_rate)} | {segment.review_completed}/{segment.review_required} | {segment.review_accepted}/{segment.review_corrected}/{segment.review_rejected} |
记录事实一致性、引用可核验性和高风险遗漏,形成规则迭代数据。
AI 受托复核优先,疑难、冲突和低置信度任务再转人工;所有结论均记录复核主体与证据。