# 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. 根因初判(带代码入口) ### 根因 A(P0):`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问题”的冲突 ### 根因 B(P0):时间线接口参数约定不一致 - 代码入口:`backend/src/modules/admin/iit-projects/iitQcCockpitController.ts` - `getTimeline` 仅读取 `query.date`,未处理 `startDate/endDate` - 前端使用区间筛选时,后端实际不生效 ### 根因 C(P1):同名指标多来源并行,无统一冻结口径 - `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-A:D2 口径修复** - 明确 activeEvents 定义(按患者真实到访事件) - 修正 `getCompletenessReport()` 的 `eventsChecked` 计算 - 回归断言:`eventsChecked` 与 `qc_event_status` 的 D2 事件集合一致 2. **P0-B:eQuery 状态流转回归** - 构造一条 `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 同步完成 P1:eQuery 重开入口 - 后端新增 `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 ` - 严格失败:`npx tsx scripts/validate_guard_types_for_project.ts --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` 生效 - 目的:支持“先迁移配置,再逐步下线兼容逻辑”的平滑策略。