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

21 KiB
Raw Blame History

IIT / CRA Agent 最小复现项目对账结果Phase 1

执行日期2026-03-08
环境本地开发环境backend + postgres docker + local REDCap
目标:验证“报告/大盘/流水/对话”异常是否来自 REDCap 联通、规则执行、聚合链路或展示口径。


1. 本次对账对象

  • IIT 项目名称:原发性痛经0302
  • IIT 项目 ID1d80f270-6a02-4b58-9db3-6af176e91f3c
  • 项目 REDCap PID18
  • 项目 REDCap URLhttp://localhost:8080
  • 数据库:ai_clinical_researchai-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 行,且全部属于旧项目 IDtest0102-pd-study
  • 当前项目UUIDrecord_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-02total=12
  • startDate=2026-03-08&endDate=2026-03-08total=12

判定:前端传 startDate/endDate 未在后端生效,日期筛选失效可稳定复现。


4. 对话层冒烟(第一轮)

调用 /api/v1/iit/chat 的典型问答可得到答复,但存在“解释与口径不稳定”的风险:

  • “签署知情人数”问题:答复给出“表单通过数 10/12”但承认无法直接给出人数。
  • “最新质控报告”问题:引用了健康分、严重问题、警告问题、待处理 eQuery整体和 qc_report 可对齐。
  • “患者访视次数”问题返回“第2次访视”但需和 REDCap 事件表进一步逐条对账Phase 2 再做字段级真值核验)。

判定:对话层主要风险当前不是“工具不可用”,而是上游口径漂移会向下传导。


5. 根因初判(带代码入口)

根因 AP0record_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/endDate2026-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 计算
    • 回归断言:eventsCheckedqc_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=1D2 当前覆盖事件数)
    • 受试者样例 recordId=1activeEvents=5d2CoveredEvents=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 = 59100%
  • 含义:
    • 能匹配到同受试者 + 同规则名
    • 但匹配不到同字段名(历史 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
    • DBqc_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 回归脚本拆分(职责清晰)

  • 引擎机制 smokebackend/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
  • 本项目执行结果strictmissingCount=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 生效
  • 目的:支持“先迁移配置,再逐步下线兼容逻辑”的平滑策略。