# 公文知识图谱 Schema 优化 — 改造项清单

> 本文档记录各 Step 的具体代码改造内容，包括文件变更、模块改造对比、索引创建等实施细节。

---

## Step 1：配置文件外部化

### 1.1 新建文件

| 文件 | 用途 |
|------|------|
| `backend/app/config/graph_schema.yaml` | 图谱 Schema 配置中心（类型定义、归一化规则、knowledge_category 映射、Phase 控制） |
| `backend/app/core/graph_schema_loader.py` | YAML 加载器（frozen dataclass + 模块级缓存 + Phase 过滤 + 标识符校验） |
| `backend/app/config/__init__.py` | 空包文件 |
| `backend/app/utils/text_normalize.py` | `normalize_title()` 工具函数 |

### 1.2 依赖变更

| 文件 | 改动 |
|------|------|
| `backend/requirements.txt` | 新增 `PyYAML>=6.0` |

### 1.3 graph_schema_loader.py 核心接口

```python
@dataclass(frozen=True)
class GraphSchema:
    entity_types: list[dict]
    relationship_types: list[dict]
    normalization_rules: dict[str, dict]
    document_properties: list[str]
    knowledge_category_mapping: dict[str, str]
    active_phases: list[str]
```

| 方法 | 返回值 | 用途 |
|------|--------|------|
| `get_schema()` | `GraphSchema` | 获取缓存的 schema 实例 |
| `reload_schema()` | `GraphSchema` | 强制重新加载 |
| `schema.entity_type_names()` | `set[str]` | 当前激活的实体类型名集合 |
| `schema.rel_type_names()` | `set[str]` | 当前激活的关系类型名集合 |
| `schema.entity_type_map()` | `dict[str, dict]` | 实体类型名 → 完整定义 |
| `schema.get_norm_rule(name)` | `dict` | 获取指定实体的归一化规则 |
| `schema.knowledge_category_code(name)` | `str` | 中文分类名 → 编码 |
| `schema.all_node_labels()` | `set[str]` | 所有合法节点标签（含 Document） |
| `schema.all_rel_types()` | `set[str]` | 所有合法关系类型 |

### 1.4 normalize_title() 规则

按以下顺序执行归一化：

1. NFKC Unicode 正规化（全角字母数字 → 半角等）
2. 去除首尾空白
3. 统一全角/半角括号（`（` → `(`，`【` → `[` 等）
4. 统一中英文引号（`""''` → `《》`）
5. 去除首尾冗余标点
6. 折叠连续空白为单空格
7. 去除书名号前后空白

---

## Step 2：Phase 0 治理图适配

### 2.1 graph_builder.py 改造

| 改造项 | 旧实现 | 新实现 |
|--------|--------|--------|
| 合法实体类型 | 硬编码 `frozenset` | `get_schema().entity_type_names()` |
| 合法关系类型 | 硬编码 `frozenset` | `get_schema().rel_type_names()` |
| 归一化逻辑 | if/elif 逐类型硬编码 | 配置驱动：`get_norm_rule()` → `strip_punctuation`/`trim_whitespace`/`max_length` |
| 系统提示词 | 从 `_REL_DESCRIPTIONS` 字典构建 | 从 `schema.entity_types` / `schema.relationship_types` 构建 |
| 关系验证 | 从 `neo4j_client._VALID_REL_TYPES` 读 | `_get_valid_rel_types()` 读配置 |
| Document 属性 | 仅传 title/doc_number/issuing_org/doc_type/signer/publish_date | 传递 metadata 中所有可用属性 |
| normalized_title | 不存在 | 写入前自动生成（调用 `normalize_title()`） |
| 占位文档创建 | 仅 REFERENCES 扫描创建 | LLM 抽取的 BASED_ON/AMENDS/REPEALS 目标也自动创建 ref: 占位节点 |
| REFERENCES 去重 | 无 | 若 BASED_ON/AMENDS/REPEALS 已建立关系，跳过同 doc_number 的 REFERENCES |
| 占位文档属性 | 仅 doc_id/doc_number/is_placeholder | 增加 doc_code 字段（用于吸收匹配） |

### 2.2 neo4j_client.py 改造

| 改造项 | 旧实现 | 新实现 |
|--------|--------|--------|
| 默认标签 | 硬编码 `_DEFAULT_NODE_LABELS` frozenset | `_default_node_labels()` 从配置加载 |
| 默认关系 | 硬编码 `_DEFAULT_REL_TYPES` frozenset | `_default_rel_types()` 从配置加载 |
| `init_schema()` | 逐个硬编码 CREATE CONSTRAINT | 遍历 `schema.entity_types` 动态创建 |
| `merge_document_graph()` | 仅写 6 个 Document 属性 | 写 15+ 属性（含 status/effective_date/knowledge_category 等） |
| `_merge_node()` | Person 特殊分支 | 统一用 `_key_prop(label)` 获取 key_property |
| `_key_prop()` | 硬编码 doc_id / name | 从 schema 的 `key_property` 字段读取 |
| `reload_valid_types()` | 仅在传参时更新 | 传 None 时自动从配置加载默认值 |
| 占位文档吸收 | 不存在 | Document MERGE 后，若 doc_code 匹配，DETACH DELETE 旧占位节点 |

**新增 Neo4j 索引**：

```cypher
CREATE INDEX doc_status IF NOT EXISTS FOR (d:Document) ON (d.status)
CREATE INDEX doc_doc_code IF NOT EXISTS FOR (d:Document) ON (d.doc_code)
CREATE INDEX doc_knowledge_category IF NOT EXISTS FOR (d:Document) ON (d.knowledge_category_code)
CREATE INDEX doc_normalized_title IF NOT EXISTS FOR (d:Document) ON (d.normalized_title)
CREATE FULLTEXT INDEX doc_fulltext IF NOT EXISTS FOR (d:Document) ON EACH [d.title, d.subject_summary, d.doc_code]
```

### 2.3 entity_extraction.py 改造

| 改造项 | 旧实现 | 新实现 |
|--------|--------|--------|
| 静态提示词实体 | 6 种（Organization/Person/Region/Subject/DocCategory/Keyword） | 3 种（Organization/Region/PolicyTheme） |
| 静态提示词关系 | 11 种 | 7 种 |
| 关系方向 | 混合（ISSUED: Org→Doc） | 统一 Document 出发（ISSUED_BY: Doc→Org） |
| 关系判断规则 | 平铺列表 | 编号优先级序列（BASED_ON > AMENDS > REPEALS > REFERENCES） |
| PolicyTheme 约束 | 无 | 每文档最多 1-3 个核心主题 |
| REFERENCES 规则 | 无特殊说明 | 明确为兜底关系 |
| 动态提示词构建 | 基于 `_REL_DESCRIPTIONS` | 基于 `schema.entity_types` / `schema.relationship_types` |

### 2.4 graph_admin_service.py 改造

| 改造项 | 旧实现 | 新实现 |
|--------|--------|--------|
| `_DEFAULT_ENTITY_TYPES` | 硬编码列表 | `get_schema()` 动态加载 |
| `_DEFAULT_REL_TYPES` | 硬编码列表 | `get_schema()` 动态加载 |

### 2.5 ES/OpenSearch 索引改造

| 文件 | 改动 |
|------|------|
| `backend/app/infrastructure/es_client.py` | `GOV_DOC_CHUNKS_MAPPING` 新增 `knowledge_category`(keyword) 和 `knowledge_category_code`(keyword) |
| `backend/app/infrastructure/es_client.py` | `GOV_DOC_META_MAPPING` 同上 |

### 2.6 ingest_pipeline.py 改造

| 改造项 | 说明 |
|--------|------|
| 入口校验 | 自动将 knowledge_category 中文名解析为 knowledge_category_code |
| `_build_chunk_metadata()` | 返回值增加 knowledge_category / knowledge_category_code |
| `_write_doc_meta()` | body 增加 knowledge_category / knowledge_category_code |

---

## Step 3：文档推荐 / 依据链 / 修订链 API

### 3.1 graph_query_service.py 新增方法

| 方法 | 功能 | Cypher 模式 |
|------|------|-------------|
| `get_document_recommendations()` | 同机构/同主题/同地域推荐 | MATCH 共享 Organization/PolicyTheme/Region |
| `get_policy_chain()` | 政策依据链 | `[:BASED_ON*1..N]->` 路径遍历 |
| `get_revision_history()` | 修订/废止历史 | `[:AMENDS\|REPEALS*1..N]-` 双向遍历 |
| `get_same_theme_documents()` | 同主题文档列表 | MATCH 共享 PolicyTheme |

**推荐查询默认过滤规则**：

```cypher
WHERE other.doc_id <> $doc_id
  AND coalesce(other.is_placeholder, false) = false
  AND coalesce(other.status, '') <> '已废止'
```

### 3.2 graph_query_service.py 已有方法适配

| 方法 | 改动 |
|------|------|
| `search_entities()` | 标签 OR 子句从配置动态生成 |
| `get_docs_by_entity()` | 移除 Person 特殊分支 |
| `search_graph_for_docs()` | 标签过滤从配置动态生成 |
| `get_docs_overview()` | 标签白名单从配置获取 |

### 3.3 graph.py 新增 API 端点

| 端点 | 方法 | 功能 | 参数 |
|------|------|------|------|
| `/api/v1/graph/document/{doc_id}/recommendations` | GET | 文档推荐 | limit (1-50, 默认 10) |
| `/api/v1/graph/document/{doc_id}/policy-chain` | GET | 政策依据链 | max_depth (1-10, 默认 5) |
| `/api/v1/graph/document/{doc_id}/revision-history` | GET | 修订/废止历史 | max_depth (1-10, 默认 5) |
| `/api/v1/graph/document/{doc_id}/same-theme` | GET | 同主题文档 | limit (1-100, 默认 20) |

---

## 后续 Step（待实施）文件清单

| 文件 | 操作 | 涉及步骤 |
|------|------|---------|
| `backend/app/core/graph_query_planner.py` | 新建 | Step 4 |
| `backend/app/prompts/query_planning.py` | 新建 | Step 4 |
| `backend/app/core/research_engine.py` | 修改 | Step 4, 11 |
| `backend/app/core/graph_admin_service.py` | 修改 | Step 5, 8, 10 |
| `backend/app/core/graph_builder.py` | 修改 | Step 5, 8, 10 |
| `backend/app/infrastructure/neo4j_client.py` | 修改 | Step 5, 8, 10 |
| `backend/app/prompts/entity_extraction.py` | 修改 | Step 5, 8, 10 |
| `backend/app/core/graph_query_service.py` | 扩展 | Step 6, 9 |
| `backend/app/core/graph_analytics_service.py` | 新建 | Step 7 |
| `backend/app/api/v1/analytics.py` | 新建 | Step 7 |
| `backend/app/core/article_splitter.py` | 新建 | Step 8 |
| `backend/app/api/v1/topics.py` | 新建 | Step 9 |
| `backend/app/core/topic_research_engine.py` | 新建 | Step 11 |
| `backend/app/prompts/topic_research.py` | 新建 | Step 11 |
