Files
AIclinicalresearch/docs/03-业务模块/IIT Manager Agent/09-技术评审报告/2026-03-08-IIT-CRA-最小复现项目对账结果-Phase1-原发性痛经0302.md
HaHafeng a666649fd4 feat(iit): harden QC pipeline consistency and release artifacts
Implement IIT quality workflow hardening across eQuery deduplication, guard metadata validation, timeline/readability improvements, and chat evidence fallbacks, then synchronize release and development documentation for deployment handoff.

Includes migration/scripts for open eQuery dedupe guards, orchestration/status semantics, report/tool readability fixes, and updated module status plus deployment checklist.

Made-with: Cursor
2026-03-08 21:54:35 +08:00

487 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# IIT / CRA Agent 最小复现项目对账结果Phase 1
> 执行日期2026-03-08
> 环境本地开发环境backend + postgres docker + local REDCap
> 目标:验证“报告/大盘/流水/对话”异常是否来自 REDCap 联通、规则执行、聚合链路或展示口径。
---
## 1. 本次对账对象
- IIT 项目名称:`原发性痛经0302`
- IIT 项目 ID`1d80f270-6a02-4b58-9db3-6af176e91f3c`
- 项目 REDCap PID`18`
- 项目 REDCap URL`http://localhost:8080`
- 数据库:`ai_clinical_research``ai-clinical-postgres`
项目映射校验结果:
- `iit_schema.projects` 中存在且唯一:
- `id=1d80f270-6a02-4b58-9db3-6af176e91f3c`
- `name=原发性痛经0302`
- `redcap_project_id=18`
- `status=active`
---
## 2. 第一轮 SQL 证据(核心)
### 2.1 聚合结果与原始质控事实明显不一致
- `iit_schema.qc_project_stats`(该项目):
- `total_records=0`
- `passed_records=0`
- `failed_records=0`
- `warning_records=0`
- `health_score=82.3`
- `d1_pass_rate=41.2`, `d2=100`, `d3=100`, `d5=100`, `d6=69.6`, `d7=100`
- `iit_schema.record_summary`(该项目):
- `COUNT(DISTINCT record_id)=0`
- `iit_schema.qc_field_status`(该项目):
- `COUNT(*)=713`
- `COUNT(DISTINCT record_id)=12`
- 存在 FAIL/WARNING/PASS 混合状态,且 D1/D2/D3/D5/D6 均有数据
- `iit_schema.qc_event_status`(该项目):
- `COUNT(*)=25`
- `iit_schema.equery`(该项目):
- `pending=262`
判定:`qc_field_status/qc_event_status/equery` 已有大量有效数据,但 `record_summary` 为空,导致项目级统计口径断裂。
### 2.2 历史项目残留与项目隔离异常迹象
- `record_summary` 总表仅有 14 行,且全部属于旧项目 ID`test0102-pd-study`
- 当前项目UUID`record_summary` 无行
判定:当前项目未建立或未插入 `record_summary` 基础行,后续“仅 UPDATE 的聚合语句”无法生效。
---
## 3. 第一轮 API 证据(核心)
使用管理员账号登录后,调用 `/api/v1/admin/iit-projects/:projectId/...`
### 3.1 Dashboard / Report / Trend 三口径冲突
- `qc-cockpit` 返回:
- `qualityScore=82`
- `totalRecords=0`
- `passRate=100`
- `criticalCount=137`
- `queryCount=262`
- `qc-cockpit/report` 返回:
- `totalRecords=0`
- `criticalIssues=156`
- `warningIssues=284`
- `passRate=0`
- `qc-cockpit/trend` 返回:
- 最新点:`total=148`, `passed=48`, `passRate=32`
判定:同一项目同一时间,`passRate` 至少出现 `100 / 0 / 32` 三个版本,属于 P0 级口径漂移。
### 3.2 时间线日期筛选无效(复现)
调用 `qc-cockpit/timeline`
- 不带日期:`total=12`
- `startDate=2026-03-01&endDate=2026-03-02``total=12`
- `startDate=2026-03-08&endDate=2026-03-08``total=12`
判定:前端传 `startDate/endDate` 未在后端生效,日期筛选失效可稳定复现。
---
## 4. 对话层冒烟(第一轮)
调用 `/api/v1/iit/chat` 的典型问答可得到答复,但存在“解释与口径不稳定”的风险:
- “签署知情人数”问题:答复给出“表单通过数 10/12”但承认无法直接给出人数。
- “最新质控报告”问题:引用了健康分、严重问题、警告问题、待处理 eQuery整体和 `qc_report` 可对齐。
- “患者访视次数”问题返回“第2次访视”但需和 REDCap 事件表进一步逐条对账Phase 2 再做字段级真值核验)。
判定:对话层主要风险当前不是“工具不可用”,而是上游口径漂移会向下传导。
---
## 5. 根因初判(带代码入口)
### 根因 AP0`record_summary` 聚合是“仅 UPDATE”无 UPSERT
- 代码入口:`backend/src/modules/iit-manager/engines/QcAggregator.ts`
- `aggregateRecordSummary()` 当前逻辑:
- 只执行 `UPDATE iit_schema.record_summary rs ... FROM agg`
- 依赖 `rs` 预先存在
- 当项目没有预写 `record_summary` 行时:
- 聚合更新行数为 0
- `HealthScoreEngine.queryRecordStats()` 基于 `record_summary` 得到 `totalRecords=0`
- Dashboard/Report 中基于该数据的统计出现“0记录 + 非0问题”的冲突
### 根因 BP0时间线接口参数约定不一致
- 代码入口:`backend/src/modules/admin/iit-projects/iitQcCockpitController.ts`
- `getTimeline` 仅读取 `query.date`,未处理 `startDate/endDate`
- 前端使用区间筛选时,后端实际不生效
### 根因 CP1同名指标多来源并行无统一冻结口径
- `passRate` 在 cockpit/report/trend 来源不一致
- 导致用户看到“同一页不同值”的信任问题
---
## 6. 与三大类问题的对应关系(第一轮)
- 第一大类(报告/关键事件):
- 已确认强相关于根因 A + 根因 C
- 第二大类AI 实时工作流水):
- 已确认日期筛选问题由根因 B 直接导致
- 总问题数差异与根因 C 强相关
- 第三大类AI 对话助手):
- 目前更像“上游口径漂移传导”而非对话工具不可用
- 仍需在 Phase 2 做患者级真值核验
---
## 7. 下一步(建议立即执行)
1. P0 修复 `QcAggregator.aggregateRecordSummary`:改为 UPSERT或先补齐 record_summary 基础行再 UPDATE
2. P0 修复 `getTimeline`:支持 `startDate/endDate`,并与 `date` 兼容。
3. P0 统一 `passRate` 口径(按 Phase0 SSOT至少先让 cockpit/report/trend 展示同一主口径。
4. 修复后回归本项目 3 个关键断言:
- `record_summary` 记录数应为 `12`
- Dashboard/Report/Trend 的主通过率在同一时间窗内一致
- 时间线区间筛选对 `2026-03-08` 返回 `0`(当前数据下)
---
## 8. 第二轮回归看板18 问题闭环状态)
> 执行日期2026-03-08第二轮
> 结论口径:
> - `已闭环`:问题已修复并复测通过
> - `部分闭环`:核心问题缓解,但仍有体验/口径尾项
> - `未闭环`:仍存在明确缺陷或未完成验证
### 8.1 第一大类报告与关键事件11 项)
| # | 问题 | 当前状态 | 证据 / 说明 |
|---|---|---|---|
| 1 | D1/D2/D3D4/D6 与维度评分不一致 | **部分闭环** | 主通过率口径已统一;但 D2 的 `eventsChecked=1` 仍需继续核验业务定义。 |
| 2 | 质控 0 个受试者 | **已闭环** | `record_summary` 已恢复 `12` 条,`qc_project_stats.total_records=12`。 |
| 3 | 健康分与问题规模冲突(如健康分高但问题多) | **部分闭环** | 评分与问题计数可同时存在(不同公式);需补“评分解释文案”避免误读。 |
| 4 | 上方通过率 100 / 下方趋势 33 | **已闭环** | 当前 cockpit/report/trend 均为 `passRate=0`(同一快照)。 |
| 5 | 质控完成无热力图 | **已闭环** | cockpit 返回 heatmap `rows/columns` 非空。 |
| 6 | 执行摘要无信息 | **已闭环** | report summary 可稳定返回 `totalRecords/criticalIssues/warningIssues/pendingQueries`。 |
| 7 | 报告分数 vs 大盘分数冲突 | **部分闭环** | 主指标已一致;健康分展示仍建议增加“更新时间+公式说明”。 |
| 8 | D1 无数据 | **已闭环** | D1 报表已恢复:`eligible=0, ineligible=11, incomplete=1`。 |
| 9 | D2 事件数=1、明细异常 | **部分闭环** | 已修复 `eventsChecked=5`(按活跃事件);同时新增 `d2EventsChecked=1` 标识 D2 覆盖事件数,避免误读。 |
| 10 | 已回复仍显示 0 | **已闭环** | 已执行 `pending -> responded -> closed(ai_review)` 实流回归,`equeries/stats` 与 D3D4 报表最终一致。 |
| 11 | 无“重开质疑”操作 | **未闭环** | 前端仍缺 reopen 操作入口(后端状态机支持)。 |
### 8.2 第二大类AI 实时工作流水3 项)
| # | 问题 | 当前状态 | 证据 / 说明 |
|---|---|---|---|
| 12 | 流水问题总数 vs 待处理总数不一致 | **部分闭环** | 已在口径上解释为“字段问题数 vs eQuery 数”;需在 UI 文案继续强化区分。 |
| 13 | 事件编码生成逻辑不明 | **部分闭环** | 已补齐后端 `event_id -> event_label` 统一映射(报表/对话可显示“筛选期”等);仍保留 event_id 作为证据辅助。 |
| 14 | 日期筛选按钮无效 | **已闭环** | timeline 支持 `startDate/endDate``2026-03-01~03-02` 返回 `0`。 |
### 8.3 第三大类AI 对话助手5 项)
| # | 问题 | 当前状态 | 证据 / 说明 |
|---|---|---|---|
| 15 | 已签署知情显示 0 | **已闭环** | 回归问答返回 `11/12`(签署率 91.7%)。 |
| 16 | 3号患者被误判为无问题 | **已闭环** | 回归问答已返回“不符合”并列出 rule 证据。 |
| 17 | 总体通过率答复异常 | **已闭环** | 回归问答返回 `0%`,并给出公式 `passedRecords/totalRecords`。 |
| 18 | 患者2信息不全 / 患者4访视描述偏差 | **已闭环** | 患者2信息完整度提升患者4访视描述已优先输出业务事件名保留 event_id 仅作证据)。 |
---
## 9. 严重未闭环问题(当前 P0/P1
### P0必须优先清零
1. ~~**D2 统计口径残留问题**`eventsChecked=1` 与多事件现实不一致。~~(已完成:`eventsChecked` 修复 + `d2EventsChecked` 解释字段)
2. ~~**“已回复仍为0”缺少场景化回归**:需要构造 responded/reviewing/closed 数据流转验证。~~(已完成:状态流转回归通过)
### P1应在下一轮完成
1. ~~**事件标签可读性**technical event_id 需稳定映射到 eventLabel报表 + 对话统一)。~~(已完成第一阶段:后端统一映射,前端显示可读事件名)
2. **EQuery 重开操作入口**:前端补 `reopen` 按钮与状态回流。
3. **评分解释文案**:健康分与问题数量并存时给出公式与更新时间提示。
---
## 10. 下一步执行计划(建议)
1. **P0-AD2 口径修复**
- 明确 activeEvents 定义(按患者真实到访事件)
- 修正 `getCompletenessReport()``eventsChecked` 计算
- 回归断言:`eventsChecked``qc_event_status` 的 D2 事件集合一致
2. **P0-BeQuery 状态流转回归**
- 构造一条 `pending -> responded -> reviewing/closed` 流程
- 验证eQuery 列表、stats、D3D4 报表、对话回答四处一致
3. **P1事件标签统一**
- 补全 `event_id -> event_label` 映射策略(优先 metadata其次兜底格式化
- 前端与 AI 对话统一使用 `eventLabel`,避免技术 ID 直出
---
## 11. 本轮执行证据(新增)
### 11.1 D2 口径修复结果
- 接口:`GET /api/v1/admin/iit-projects/:projectId/qc-cockpit/report/completeness`
- 修复后结果:
- `eventsChecked=5`(项目活跃事件数)
- `d2EventsChecked=1`D2 当前覆盖事件数)
- 受试者样例 `recordId=1``activeEvents=5``d2CoveredEvents=1`
### 11.2 eQuery 状态流转回归结果
- 操作:选取一条 pending eQuery调用 `POST /equeries/:id/respond`
- 实际状态流:
- `pending -> responded -> closed (ai_review)`(异步作业触发)
- 最终一致性(等待异步稳定后):
- `GET /equeries/stats`: `pending=261, closed=1, responded=0`
- `GET /qc-cockpit/report/equery-log`: 同步为 `pending=261, closed=1, responded=0`
---
## 12. eQuery 专项 P0 修复2026-03-08 夜间增量)
### 12.1 已落地代码改动
1. **eQuery 生成去重策略升级(后端)**
- 文件:`backend/src/modules/iit-manager/services/DailyQcOrchestrator.ts`
- 从原来的 `recordId + fieldName` 去重,升级为 `recordId + eventId + ruleName` 去重。
- 目的:减少“同一业务问题因不同字段重复建单”(对应问题 9
2. **高噪音规则抑制(后端)**
- 文件:`backend/src/modules/iit-manager/services/DailyQcOrchestrator.ts`
- 新增 `shouldSuppressIssue()`,当前先抑制三类已确认噪音:
- `不良事件记录与知情同意状态一致性检查`(缺上下文时高频误报)
- `所有纳入标准完整性检查` 且实际值为全 `1,1,...` 的错误告警
- `访视日期早于知情同意书签署日期` 但两日期相同的误报文本
3. **eQuery 文案可读性增强(后端)**
- 文件:`backend/src/modules/iit-manager/services/DailyQcOrchestrator.ts`
- 新增 `normalizeQueryText()`
- 清理 `(标准: [object Object])`
- 去掉 markdown `**` 噪音
- 对“入组状态与排除标准冲突检查”改写为业务可读提示语
4. **eQuery 上下文字段补齐(后端)**
- 文件:`backend/src/modules/iit-manager/services/DailyQcOrchestrator.ts`
- 创建 eQuery 时补写 `eventId/formName`,并在 `expectedAction` 中带出事件上下文。
- 目的:缓解“详情中表单/访视点显示不全(-)”。
5. **规则消息层通用序列化修复(后端)**
- 文件:
- `backend/src/modules/iit-manager/engines/HardRuleEngine.ts`
- `backend/src/modules/iit-manager/engines/SkillRunner.ts`
- 新增逻辑字面量与实际值格式化,避免 `expectedValue`/`actualValue` 落成 `[object Object]` 或不可读数组。
6. **eQuery 列表可用性增强(前端)**
- 文件:`frontend-v2/src/modules/iit/pages/EQueryPage.tsx`
- 新增:
- 质疑序号列
- 访视点列eventId
- 按严重程度过滤
- 按受试者号过滤
- 前端默认排序:按受试者号升序、同受试者按创建时间降序
### 12.2 状态评估(对应 14 条中的关键项)
- **部分闭环(需复测)**
- #4 排序混乱 / 建议增加序号:已实现前端排序+序号
- #9 同一目的重复记录:已增强去重键
- #10/#14 事件/表单显示不足:新单已补上下文,历史单仍需数据回填
- #12 告警文案与全 1 误报:已在派单前抑制
- **仍需继续(下一轮 P0**
- #3/#7/#11 的“规则本体逻辑”仍建议在规则配置层做根因修正(当前先做了派单层防噪)
- #5 纳入标准检查覆盖不全:仍需补齐规则集合与事件适用范围
- #13 缺少访视定位的业务解释:需在报表与详情页增加 eventLabel 映射显示
### 12.3 第二批 P0规则执行层护栏已落地
1. **规则执行层新增业务护栏HardRuleEngine / SkillRunner 双端一致)**
- 文件:
- `backend/src/modules/iit-manager/engines/HardRuleEngine.ts`
- `backend/src/modules/iit-manager/engines/SkillRunner.ts`
- 新增 `forcePassByBusinessGuard()`,在规则命中前做语义纠偏:
- `访视日期早于知情同意书签署日期`:当访视日期 >= 签署日期(含同日)直接判通过
- `SF-MPQ和CMSS评估日期与访视日期不一致`:存在缺失值时不判“不一致”
- `所有纳入标准完整性检查`:实际值全 1 时直接判通过
- `入组状态与排除标准冲突检查`:实际值全 0 时直接判通过
2. **D2 缺失告警事件名可读化**
- 文件:`backend/src/modules/iit-manager/engines/CompletenessEngine.ts`
- 告警文案从技术事件码切换为 `eventLabel`(如“筛选期”),减少 `65a64dbbd9_arm_1` 直接暴露。
> 以上为执行层兜底能立即压误报下一步仍建议在规则配置skill.config.rules层做同名规则逻辑重构避免长期依赖护栏。
### 12.4 同步完成 P1eQuery 重开入口
- 后端新增 `POST /api/v1/admin/iit-projects/:projectId/equeries/:equeryId/reopen`
- 文件:
- `backend/src/modules/admin/iit-projects/iitEqueryService.ts`
- `backend/src/modules/admin/iit-projects/iitEqueryController.ts`
- `backend/src/modules/admin/iit-projects/iitEqueryRoutes.ts`
- 状态流:`closed -> reopened`
- 前端已接入“重开”按钮
- 文件:
- `frontend-v2/src/modules/iit/api/iitProjectApi.ts`
- `frontend-v2/src/modules/iit/pages/EQueryPage.tsx`
- 仅在 `closed` 状态显示“重开”操作,执行后刷新列表。
---
## 13. 执行验证与卡点结论2026-03-08 晚)
### 13.1 复跑验证结论
- 脚本:`backend/scripts/run_iit_qc_once.ts`
- 项目:`1d80f270-6a02-4b58-9db3-6af176e91f3c`原发性痛经0302
- 结果(成功):
- `batch.totalRecords=12`
- `batch.totalEvents=37`
- `batch.fieldStatusWrites=1015`
- `orchestrate.pushSent=true`
- `orchestrate.equeriesCreated=0`
### 13.2 “卡在哪里”的根因
- 本次并非主流程卡死,`QcExecutor -> DailyQcOrchestrator` 已完整跑通并返回结果。
- 之前观察到的告警根因是:
- 审计日志 SQL 使用了旧列 `action`
- 当前表 `iit_schema.audit_logs` 实际列为 `action_type`
- 已修复后复跑,相关 `audit_logs.action` 报错未再出现。
### 13.3 历史 eQuery 上下文回填结果
- 脚本:`backend/scripts/backfill_equery_context.ts`
- 回填结果:
- `missingBefore=262`
- `updatedRows=203`
- `missingAfter=59`
### 13.4 剩余 59 条未回填原因
- 脚本:`backend/scripts/analyze_missing_equery_context.ts`
- 统计:`B_RULE_MATCH_FIELD_MISMATCH = 59`100%
- 含义:
- 能匹配到同受试者 + 同规则名
- 但匹配不到同字段名(历史 eQuery 以 `exclusion_criteria2/3/4/5` 多字段拆条,现有 `qc_field_status` 粒度/字段记录不一致)
- 代表样例:`category=入组状态与排除标准冲突检查``event_id/form_name` 为空。
### 13.5 容错回填(二阶段)执行结果
- 已升级脚本:`backend/scripts/backfill_equery_context.ts`
- Phase 1严格匹配`record + rule + field`
- Phase 2容错匹配`record + rule`,取最近 QC 事件/表单)
- 本次执行结果:
- `missingBefore=59`
- `strictUpdatedRows=0`
- `relaxedUpdatedRows=59`
- `missingAfter=0`
- 复核脚本:`backend/scripts/analyze_missing_equery_context.ts`
- `reasonStats=[]`
- `sample=[]`
- 结论:历史 eQuery 的 `event_id/form_name` 缺失已清零。
---
## 14. 新增端到端脚本(四流程一体)
- 脚本:`backend/scripts/e2e_iit_full_flow.ts`
- 运行方式:
- `npx tsx scripts/e2e_iit_full_flow.ts 1d80f270-6a02-4b58-9db3-6af176e91f3c`
- 可选:`--with-chat`(增加 LLM 问答链路)
### 14.1 覆盖的 4 个流程
1. REDCap 结构与真实数据拉取metadata/events/form-event/records-by-event
2. 规则配置加载与覆盖校验(`qc_process` 活跃规则)
3. 执行质控与报告编排(`QcExecutor` + `DailyQcOrchestrator`
4. 多消费端一致性校验Cockpit / Report / Tools 的通过率一致)
### 14.2 本次执行结果(通过)
- Stage1_REDCap通过
- `metadata=74`, `events=5`, `formEventMapping=19`, `recordEventRows=37`, `uniqueRecords=12`
- Stage2_Rules通过
- `ruleCount=79`, `multiFieldRules=29`, `categories={D1,D3,D5,D6}`
- Stage3_Execution通过
- `totalRecords=12`, `totalEvents=37`, `fieldStatusWrites=1015`
- DB`qc_field_status=713`, `qc_event_status=25`, `record_summary=12`
- Stage4_Consumption通过
- `reportPassRate=0`, `cockpitPassRate=0`, `toolPassRate=0`(一致)
---
## 15. 元数据驱动护栏落地(去硬编码)
### 15.1 执行内核收敛
- 已完成:`SkillRunner` 的 HARD_RULE 执行改为单路径复用 `HardRuleEngine.executeWithRules()`
- 价值:删除重复实现,避免“同一规则两套行为”。
### 15.2 guardType 元数据写回(项目级)
- 脚本:`backend/scripts/suggest_guard_types_for_project.ts`
- 运行:`npx tsx scripts/suggest_guard_types_for_project.ts 1d80f270-6a02-4b58-9db3-6af176e91f3c --apply`
- 结果:`updated=5`
- `date_not_before_or_equal` ×1
- `pass_if_exclusion_all_zero` ×1
- `pass_if_all_ones` ×1
- `skip_if_any_missing` ×2
### 15.3 回归脚本拆分(职责清晰)
- 引擎机制 smoke`backend/scripts/regression_hardrule_guards.ts`(可写死样例)
- 项目动态回归:`backend/scripts/regression_hardrule_guards_by_project.ts`(从 `qc_process` 读取规则)
- 项目动态回归结果:`skipped=[]`,核心 guard 断言全部通过。
### 15.4 复跑端到端验证
- 脚本:`backend/scripts/e2e_iit_full_flow.ts`
- 结果Stage1~Stage4 全部通过(口径一致性维持)。
- 备注:本次编排阶段 `pushSent=false`(非主流程阻断项),不影响质控执行与报告一致性断言。
---
## 16. 守护门禁与兼容开关(新增)
### 16.1 guardType 门禁脚本
- 新增:`backend/scripts/validate_guard_types_for_project.ts`
- 用法:
- 检查:`npx tsx scripts/validate_guard_types_for_project.ts <projectId>`
- 严格失败:`npx tsx scripts/validate_guard_types_for_project.ts <projectId> --strict`
- 本项目执行结果strict`missingCount=0`, `mismatchCount=0`
### 16.2 E2E 增加严格 guard 覆盖断言
- 脚本:`backend/scripts/e2e_iit_full_flow.ts`
- 新增参数:
- `--strict-guards`
- 或环境变量 `E2E_REQUIRE_GUARD_TYPES=1`
- 严格模式下Stage2 会校验 guardType 候选规则的覆盖率(未覆盖即 fail
### 16.3 兼容兜底可控下线
- 文件:`backend/src/modules/iit-manager/engines/HardRuleEngine.ts`
- 新增开关:`IIT_GUARD_LEGACY_NAME_FALLBACK`
- 默认开启(兼容历史规则名)
- 设为 `0` 可关闭规则名兜底,仅按 `metadata.guardType` 生效
- 目的:支持“先迁移配置,再逐步下线兼容逻辑”的平滑策略。