feat(原型设计): 关联资料包与对话记录

This commit is contained in:
2026-06-03 23:48:53 +08:00
parent aa5d4d77f8
commit 7836690303
2 changed files with 113 additions and 345 deletions

View File

@@ -643,6 +643,10 @@
color: var(--ink); color: var(--ink);
} }
.search-input::placeholder {
color: #9aa8bf;
}
.system-nav { .system-nav {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
@@ -837,7 +841,9 @@
conversations: [ conversations: [
{ {
id: "conv-001", id: "conv-001",
title: "注册批次审核主线", packageId: "pkg-001",
productName: "新型冠状病毒 2019-nCoV 核酸检测试剂盒",
title: "新型冠状病毒 2019-nCoV 核酸检测试剂盒",
time: "今天 10:05", time: "今天 10:05",
status: "高风险", status: "高风险",
summary: "已完成目录汇总、完整性检查、字段抽取与一致性核查。", summary: "已完成目录汇总、完整性检查、字段抽取与一致性核查。",
@@ -948,7 +954,9 @@
}, },
{ {
id: "conv-002", id: "conv-002",
title: "仅做目录汇总", packageId: "pkg-002",
productName: "乙型肝炎病毒核酸检测试剂盒",
title: "乙型肝炎病毒核酸检测试剂盒",
time: "昨天 16:40", time: "昨天 16:40",
status: "已完成", status: "已完成",
summary: "生成目录、页数和章节点结果,未继续完整性核查。", summary: "生成目录、页数和章节点结果,未继续完整性核查。",
@@ -981,6 +989,7 @@
], ],
activeConversationId: "conv-001", activeConversationId: "conv-001",
activeAbilityKey: "import", activeAbilityKey: "import",
materialSearchQuery: "",
knowledge: { knowledge: {
metrics: [ metrics: [
{ label: "知识库资料总数", value: "27", note: "法规、模板、示例资料、内部 SOP" }, { label: "知识库资料总数", value: "27", note: "法规、模板、示例资料、内部 SOP" },
@@ -1076,6 +1085,28 @@
{ label: "章节点命中", value: "14", note: "覆盖 CH1 至 CH6 关键章节" }, { label: "章节点命中", value: "14", note: "覆盖 CH1 至 CH6 关键章节" },
{ label: "目录异常", value: "2", note: "1 份页数待复核1 份疑似目录错放" } { label: "目录异常", value: "2", note: "1 份页数待复核1 份疑似目录错放" }
], ],
packages: [
{
id: "pkg-001",
conversationId: "conv-001",
productName: "新型冠状病毒 2019-nCoV 核酸检测试剂盒",
batchId: "SUB-20260603-001",
fileCount: "18",
pages: "236",
status: "审核中",
note: "已完成目录汇总并进入完整性检查。"
},
{
id: "pkg-002",
conversationId: "conv-002",
productName: "乙型肝炎病毒核酸检测试剂盒",
batchId: "SUB-20260602-004",
fileCount: "12",
pages: "148",
status: "已完成",
note: "仅执行目录汇总与页数识别。"
}
],
items: [ items: [
{ path: "第1章 监管信息/CH1.2 监管信息目录.docx", chapter: "CH1.2", pages: "3", status: "已识别", note: "命中监管信息目录" }, { path: "第1章 监管信息/CH1.2 监管信息目录.docx", chapter: "CH1.2", pages: "3", status: "已识别", note: "命中监管信息目录" },
{ path: "第1章 监管信息/CH1.4 申请表.docx", chapter: "CH1.4", pages: "5", status: "已识别", note: "用于字段抽取与一致性核查" }, { path: "第1章 监管信息/CH1.4 申请表.docx", chapter: "CH1.4", pages: "5", status: "已识别", note: "用于字段抽取与一致性核查" },
@@ -1094,6 +1125,18 @@
return data.conversations.find(item => item.id === data.activeConversationId) || data.conversations[0]; return data.conversations.find(item => item.id === data.activeConversationId) || data.conversations[0];
} }
function getFilteredMaterialPackages() {
const query = data.materialSearchQuery.trim().toLowerCase();
if (!query) {
return data.materials.packages;
}
return data.materials.packages.filter(item =>
item.productName.toLowerCase().includes(query)
|| item.batchId.toLowerCase().includes(query)
);
}
function statusClass(text) { function statusClass(text) {
if (text.includes("高风险") || text.includes("阻断") || text.includes("失败")) return "danger"; if (text.includes("高风险") || text.includes("阻断") || text.includes("失败")) return "danger";
if (text.includes("待复核") || text.includes("处理中") || text.includes("解析中")) return "warning"; if (text.includes("待复核") || text.includes("处理中") || text.includes("解析中")) return "warning";
@@ -1307,6 +1350,9 @@
} }
function renderMaterialsPage() { function renderMaterialsPage() {
const filteredPackages = getFilteredMaterialPackages();
const activeConversation = getActiveConversation();
return ` return `
<section class="page ${activeTabId === "materials" ? "active" : ""}" data-page="materials"> <section class="page ${activeTabId === "materials" ? "active" : ""}" data-page="materials">
${renderMetrics(data.materials.metrics)} ${renderMetrics(data.materials.metrics)}
@@ -1314,7 +1360,7 @@
<div class="panel-head"> <div class="panel-head">
<div> <div>
<h2>资料包总览</h2> <h2>资料包总览</h2>
<p>直接承接原始材料目录,用于上传后快速看到文件结构、章节点和异常。</p> <p>资料包与对话记录一一关联,对话名称直接采用解析后的产品名称。</p>
</div> </div>
<div class="table-actions"> <div class="table-actions">
<button class="mini-btn">导入资料包</button> <button class="mini-btn">导入资料包</button>
@@ -1323,6 +1369,49 @@
</div> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div class="search-bar">
<input
class="search-input"
id="materialSearchInput"
value="${data.materialSearchQuery}"
placeholder="根据产品名称或批次号搜索资料包"
/>
<button class="secondary-btn">搜索资料包</button>
<button class="secondary-btn">清空搜索</button>
</div>
<div class="table-shell" style="margin-bottom:16px;">
<table>
<thead>
<tr>
<th>产品名称</th>
<th>关联对话</th>
<th>批次号</th>
<th>文件数</th>
<th>页数</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${filteredPackages.map(item => `
<tr>
<td>${item.productName}</td>
<td>${item.productName}</td>
<td>${item.batchId}</td>
<td>${item.fileCount}</td>
<td>${item.pages}</td>
<td><span class="status-tag ${statusClass(item.status)}">${item.status}</span></td>
<td><button class="mini-btn" data-open-conversation="${item.conversationId}">查看对话</button></td>
</tr>
`).join("")}
${filteredPackages.length === 0 ? `
<tr>
<td colspan="7">未找到匹配的产品名称资料包。</td>
</tr>
` : ""}
</tbody>
</table>
</div>
<div class="table-shell"> <div class="table-shell">
<table> <table>
<thead> <thead>
@@ -1341,7 +1430,7 @@
<td>${item.chapter}</td> <td>${item.chapter}</td>
<td>${item.pages}</td> <td>${item.pages}</td>
<td><span class="status-tag ${statusClass(item.status)}">${item.status}</span></td> <td><span class="status-tag ${statusClass(item.status)}">${item.status}</span></td>
<td>${item.note}</td> <td>${item.note}${activeConversation.productName ? `<br /><span class="muted">所属产品:${activeConversation.productName}</span>` : ""}</td>
</tr> </tr>
`).join("")} `).join("")}
</tbody> </tbody>
@@ -1672,6 +1761,17 @@
return; return;
} }
const openConversation = event.target.closest("[data-open-conversation]");
if (openConversation) {
data.activeConversationId = openConversation.dataset.openConversation;
activeTabId = "chat";
const currentConversation = getActiveConversation();
activeNodeId = currentConversation.nodes[0]?.id || "";
data.activeAbilityKey = Object.keys(currentConversation.abilities)[0] || "";
rerender();
return;
}
const node = event.target.closest("[data-node-id]"); const node = event.target.closest("[data-node-id]");
if (node) { if (node) {
activeNodeId = node.dataset.nodeId; activeNodeId = node.dataset.nodeId;
@@ -1689,6 +1789,15 @@
rerender(); rerender();
} }
}); });
document.addEventListener("input", event => {
if (event.target.id === "materialSearchInput") {
data.materialSearchQuery = event.target.value;
if (activeTabId === "materials") {
renderPages();
}
}
});
</script> </script>
</body> </body>
</html> </html>

View File

@@ -1,341 +0,0 @@
# 注册资料审核 Agent 需求分析与设计思路
## 1. 题目理解
本题要求建设的不是普通文档问答机器人,而是面向 NMPA 体外诊断试剂注册申报资料的“资料准备与审核 Agent”。
系统需要围绕一个注册资料包完成以下闭环:
1. 接收并解析一批申报资料。
2. 自动汇总文件目录与页数。
3. 对照 NMPA 法规和本地公告附件包检查资料完整性。
4. 从产品资料中抽取核心字段。
5. 将抽取结果填入申报表格、对照清单或目标模板。
6. 检查文档结构、章节规范性和跨文档字段一致性。
7. 输出合规风险预警、责任人通知建议和整改动作。
因此,系统设计的重点应从“单文档对话”转向“资料包级审核编排”。
## 2. 输入资料口径
### 2.1 待审核资料
题目第一项要求“自动汇总文件夹文件目录与页数”,这意味着首版输入不应只支持单文件上传,还要覆盖资料包导入。
V1 建议支持以下导入方式:
1. 多文件批量上传。
2. 文件夹拖拽或前端批量选择文件。
3. 压缩包上传并自动解压。
压缩包格式覆盖:
1. `zip`
2. `rar`
3. `7z`
系统解包后应保留原始相对路径,用于还原资料目录、识别章节点和发现文件夹结构异常。压缩包内多层目录按原目录作为章节点识别依据。
`rar``7z` 解压必须采用纯 Python 实现,允许新增第三方 Python 依赖包,避免服务器部署时依赖系统级解压工具。
### 2.2 法规依据资料
法规核查不应只依赖在线网页或大模型记忆。当前本地材料中已经提供了主规则依据:
```text
docs/原始材料/关于公布体外诊断试剂注册申报资料要求和批准证明文件格式的公告/
```
该目录下资料应作为 V1 的主规则包,包括注册申报资料要求、格式要求、安全和性能基本原则清单、注册证格式、变更和延续注册资料要求等。
`docs/原始材料/附件 4 体外诊断试剂注册申报资料要求及说明.doc` 可作为同源补充材料。
外部网站 `cmde.org.cn``nmpa.gov.cn` 在 V1 中主要作为法规来源说明和后续在线更新依据,不作为演示时的唯一实时依赖。
## 3. 核心需求重整
### 3.1 自动汇总文件目录与页数
系统应扫描当前资料包中的所有文件,形成目录汇总表。
目录汇总表至少包含:
| 字段 | 说明 |
|---|---|
| 原始路径 | 解压或上传后的相对路径 |
| 文件名 | 原始文件名 |
| 文件类型 | PDF、DOCX、DOC、TXT、MD 等 |
| 页数 | 精确页数或待复核状态 |
| 章节点 | 如 CH1.2、CH1.4、CH1.11.5 |
| 资料名称 | 识别出的注册资料名称 |
| 处理状态 | 已解析、解析失败、待人工确认 |
| 是否命中法规目录 | 用于后续完整性核查 |
页数统计策略:
1. PDF 优先用 `pdfplumber``PyMuPDF` 精确统计。
2. DOCX 页数必须精确统计,不能以估算页数作为 V1 验收结果。
3. DOC 可通过兼容解析工具或转换后处理,无法精确统计时标记为待人工复核。
4. 扫描件或无法提取正文的文件标记为“需 OCR / 待人工复核”。
### 3.2 按 NMPA 法规要求核查完整性
完整性核查的规则来源应优先来自本地公告附件包。
V1 规则建议拆成三层:
1. 资料齐套性:是否具备注册申报资料要求中的必交文件。
2. 章节点结构:文件是否落在正确章节,目录与实际文件是否一致。
3. 格式模板:申请表、注册证、清单、声明类文件是否符合对应格式要求。
核查结果需要区分:
| 状态 | 说明 |
|---|---|
| 已提供 | 文件和章节点均匹配 |
| 缺失 | 必交资料未找到 |
| 疑似提供 | 文件名或内容疑似匹配,但命名/归类不规范 |
| 错放 | 文件存在但章节点或目录位置不合理 |
| 待复核 | 规则无法直接判定 |
自动通知责任人属于完整性核查后的动作。V1 责任人先通过后台或配置文件手动维护并按资料章节配置。系统可先输出责任角色和通知载荷例如“CH4 临床评价资料缺失 -> 临床注册负责人”,后续再接飞书机器人执行 `@` 通知。
### 3.3 产品关键信息抽取与回填
题目中明确要求抽取:
1. 产品名称
2. 检测靶标
3. 适用范围
4. 储存条件
5. 性能指标
结合样例材料,首版建议以 `docs/原始材料/目标产品说明书.docx` 作为核心抽取样本,同时从申请表、产品列表、声明类文件中抽取可比对字段。
抽取结果先进入“统一字段池”,再用于回填。
统一字段池字段建议:
| 字段 | 说明 |
|---|---|
| field_key | 标准字段编码 |
| field_label | 中文字段名 |
| value | 标准化字段值 |
| raw_value | 原文值 |
| source_document | 来源文件 |
| source_location | 来源章节、表格或页码 |
| confidence | 置信度 |
| conflict_status | 一致、冲突、待确认 |
| fillable | 是否可回填 |
回填目标已确认。V1 应按两步走:
1. 先输出结构化回填表,并自动填入注册申报表格或法规对照清单字段。
2. 基于 Word 模板库生成可导出的目标文件。
注册申报表格和对照清单都应在模板库中建立字段映射,而不是把回填逻辑写死在 Prompt 中。
### 3.4 文档结构、信息一致性与章节规范性核查
结构核查应覆盖两类对象:
1. 单文档内部章节是否完整。
2. 资料包整体章节点是否符合 NMPA 目录结构。
以说明书或性能研究资料为例,需要检查是否包含分析灵敏度、特异性、重复性等必检项目。
一致性核查应基于统一字段池执行。首版按当前项目口径采用严格一致规则,即同一审核范围内同一字段只要文本不一致,就标记为冲突或待人工复核。
强一致字段建议包括:
1. 产品名称
2. 规格型号 / 包装规格
3. 申请人名称
4. 检测靶标
5. 适用范围 / 预期用途
6. 储存条件
当前样例中存在“目标产品说明书”和第 1 章监管资料产品名称不一致的情况。系统不应在需求阶段强行解释为同一产品,而应设计为:
1. 用户先选择审核范围或项目批次。
2. 系统对选定范围内的文件做一致性核查。
3. 若发现跨产品资料混入,输出混档风险。
### 3.5 合规风险预警与处理建议
风险清单应把完整性、结构、字段一致性和抽取失败结果统一汇总。
风险项字段建议:
| 字段 | 说明 |
|---|---|
| risk_level | 高 / 中 / 低 |
| risk_type | 缺失、错放、冲突、格式不规范、混档、待复核 |
| related_document | 涉及文件 |
| requirement_source | 法规或规则来源 |
| description | 问题描述 |
| suggestion | 处理建议 |
| owner_role | 建议责任角色 |
| notification_message | 可发送给责任人的通知摘要 |
示例:
```text
文件 CH4 临床评价资料:未在当前资料包中识别到临床评估报告。建议补充临床评价资料,并由临床注册负责人复核。
```
```text
产品名称冲突:说明书中的产品名称与申请表中的产品名称不一致。建议先确认当前审核范围是否混入其他产品资料。
```
## 4. Agent 编排设计
### 4.1 总体链路
建议 Agent Core 采用固定编排链路:
```text
资料导入
-> 解压与文件扫描
-> 页数统计与目录汇总
-> 文本 / 表格 / 章节解析
-> 章节点归类
-> 法规规则匹配
-> 字段抽取与统一字段池
-> 一致性核查
-> 风险汇总
-> 申报表格 / 对照清单自动回填
-> Word 生成
-> 审计记录与责任人通知
```
### 4.2 规则优先,模型辅助
法规完整性、必交项、章节点和强一致字段不应交给大模型自由判断。
推荐分工:
| 能力 | 处理方式 |
|---|---|
| 文件扫描、解压、页数统计 | 工具 / 服务层 |
| 法规目录匹配 | 结构化规则 |
| 必交项缺失判断 | 结构化规则 |
| 固定字段抽取 | 正则、标题、表格规则 |
| 长文本字段归纳 | LLM 辅助 |
| 法规条款引用 | RAG 辅助 |
| 风险文案与建议 | 规则结论 + LLM 组织语言 |
### 4.3 RAG 的职责
RAG 不作为最终合规判断引擎,只负责:
1. 从法规原文中找证据。
2. 从业务资料中找来源片段。
3. 为审计记录保留可追溯依据。
4. 支持用户追问“为什么这么判”。
### 4.4 Tool Registry 建议工具
Agent Core 后续应注册以下工具:
1. `scan_submission_package`:扫描资料包并保留目录结构。
2. `extract_archive`:解压 zip、rar、7z其中 rar、7z 必须使用纯 Python 依赖实现。
3. `count_document_pages`统计页数DOCX 必须精确统计。
4. `classify_chapter_node`:识别章节点和资料名称。
5. `check_nmpa_completeness`:按结构化规则检查齐套性。
6. `extract_product_fields`:抽取产品核心字段。
7. `compare_field_consistency`:执行字段一致性比对。
8. `check_document_structure`:检查章节和必检项目。
9. `build_risk_alerts`:汇总风险和处理建议。
10. `build_fill_outputs`:生成注册申报表格或对照清单回填结果。
11. `render_word_template`:按模板生成 Word 文件。
12. `build_owner_notification`:生成责任人通知载荷。
## 5. Django 模块落地思路
### 5.1 Documents
Documents 是资料治理中心,应从“单文件上传记录”升级为“资料包 + 文件记录 + 解析结果”。
需要新增或强化:
1. 资料包批次。
2. 压缩包解压记录。
3. 原始相对路径。
4. 页数和页数可信度。
5. 章节点识别状态。
6. 文本、表格和标题解析状态。
7. 业务资料与法规资料隔离。
### 5.2 Agent Core
Agent Core 是审核编排引擎,应沉淀:
1. 注册资料审核专用输出 schema。
2. NMPA 规则包加载器。
3. 统一字段池。
4. 风险映射规则。
5. RAG 证据检索。
6. Word 模板回填接口。
### 5.3 Chat
Chat 页面应升级为注册审核工作台。
建议保留自然语言输入,同时提供快捷任务按钮:
1. 汇总当前资料目录及页数。
2. 检查资料完整性。
3. 抽取产品核心信息。
4. 检查一致性。
5. 生成风险预警报告。
### 5.4 Audit
Audit 需要记录:
1. 审核范围。
2. 使用的规则版本。
3. 参与文件清单。
4. 工具调用结果。
5. 风险结论。
6. 回填输出文件。
7. 通知责任人结果。
## 6. V1 实现优先级
建议按以下顺序落地,保证演示先闭环:
1. 批量上传 / 压缩包解压 / 文件目录汇总。
2. PDF、DOCX 页数统计和解析状态展示,其中 DOCX 必须精确统计。
3. 基于公告附件包整理结构化必交项规则,第 2 至第 6 章先做规则级初步确认。
4. 完整性核查与缺失项风险输出。
5. 从目标产品说明书抽取产品名称、靶标、适用范围、储存条件、性能指标。
6. 对说明书、申请表、产品列表做字段一致性核查。
7. 输出综合风险报告和处理建议。
8. 注册申报表格 / 对照清单自动回填和 Word 模板导出。
9. 责任人通知载荷与飞书机器人演示。
## 7. 已确认约束与剩余待确认事项
以下约束已经确认,应作为后续实现依据:
1. DOCX 页数必须精确统计。
2. 压缩包内多层目录按原目录作为章节点依据。
3. `rar``7z` 解压必须纯 Python 实现,允许新增第三方依赖包。
4. 责任人先手动配置,按资料章节维护。
5. 第 2 至第 6 章不补充企业真实样本,先按公告附件包进行规则级初步确认。
剩余待确认事项:
1. 后续如业务方另行提供专用 Word 模板,需要确认模板版本、生效范围和字段映射审批机制。
## 8. 结论
本题最稳的设计表达是:
```text
资料包治理 + 本地法规规则包 + 统一字段池 + 规则优先的 Agent 编排 + RAG 证据解释 + 风险与回填输出
```
这样既能覆盖题面五个要求,也能解释为什么系统需要 Django、Documents、Agent Core、RAG、Tool Registry 和 Audit 这些边界。