# service_guide_extractor 抽取契约、提示词与 API 草案

## 1. 文档目标

本文档定义标准办事指南抽取器 `service_guide_extractor` 的：

- 模块职责
- 入参与出参契约
- 抽取流程
- 提示词拆分方式
- API 草案
- 入库链路接入方式

本文档与 [prd/standard-service-guide-structured-schema-draft.md](prd/standard-service-guide-structured-schema-draft.md) 和 [prd/gov-service-guides-opensearch-mapping-design.md](prd/gov-service-guides-opensearch-mapping-design.md) 配套使用。

---

## 2. 设计目标

`service_guide_extractor` 的目标不是做一个“大而全单 prompt”，而是构建一个可控、可调试、可降级的结构化抽取组件。

需要满足：

1. 能识别文档是否属于标准办事指南
2. 能按栏目抽取，而不是整篇一锅端
3. 能保留材料行、收费行、窗口行等行级语义
4. 能输出质量信息和回链信息
5. 失败时不阻塞文档主入库

---

## 3. 与现有入库链路的关系

### 3.1 推荐接入位置

推荐将 `service_guide_extractor` 放在以下位置：

1. Docling 解析完成后
2. 元数据抽取完成后
3. GraphBuilder 之前或之后都可

推荐顺序：

```text
文件解析 -> 元数据抽取 -> 标准指南识别 -> 标准指南抽取 -> Guide 索引写入 -> Matter/Graph 同步 -> 其余流程
```

### 3.2 原因

1. 标准指南抽取需要 Docling 结构文本与元数据
2. 它可以为后续 Matter 图同步提供更稳定输入
3. 即便失败，也不应影响主文档索引写入

### 3.3 非阻塞策略

Guide 抽取建议作为“非阻塞增强阶段”：

- 成功：文档状态 `completed` 或 `completed + guide completed`
- 失败：文档主入库成功，但 `service_guide_status = failed`
- 总体状态可标记为 `partial_failed`

补充要求：

- Guide profile 写入时同步复制文档级 `acl_ids`
- 权限真值源仍然是 `gov_doc_meta`
- 权限更新任务需要按 `doc_id` 覆盖刷新对应 Guide 文档的 `acl_ids`
- 事项/Guide 默认按公共服务场景公开；仅当存在显式 `acl_ids` 时才进入受限访问控制

---

## 4. 模块职责与文件建议

### 4.1 建议新增文件

建议新增：

- `backend/app/core/service_guide_extractor.py`
- `backend/app/prompts/service_guide_extraction.py`
- `backend/app/api/schemas/service_guide.py`
- `backend/app/api/v1/service_guide.py`

### 4.2 模块职责划分

#### `service_guide_extractor.py`

负责：

- 场景识别
- section 切分
- LLM 抽取编排
- 结构归一化
- ACL 透传与根字段镜像生成
- 绑定 Matter / Material / Organization / Region
- 质量评估
- 输出最终 `StandardServiceGuideProfile`

#### `service_guide_extraction.py`

负责：

- 检测 prompt
- section 抽取 prompt
- profile 汇总 prompt
- 规则与模板函数

#### `service_guide.py`

负责：

- 查询 API
- Admin 预览/回填 API
- 错误语义与权限控制
- 公开查询优先，复用现有 PermissionService / `acl_ids` 过滤逻辑处理受限 Guide

---

## 5. 抽取流程草案

推荐采用“规则优先 + 分阶段 LLM”的流程。

### 5.1 Stage 0：场景识别

输入：

- 标题
- doc_type
- knowledge_category
- 正文前 2000~4000 字
- Docling 标题结构

输出：

- 是否为 `standard_service_guide`
- 识别置信度
- 识别依据

识别优先级建议：

1. 人工标记或上传元数据命中
2. 标题规则命中
3. section 规则命中
4. LLM 判定兜底

### 5.2 Stage 1：Section 切分

优先规则切分，必要时 LLM 修正。

目标 section：

- `basic_info`
- `cross_region_service`
- `review_info`
- `result_info`
- `acceptance_info`
- `process_info`
- `materials`
- `fees`
- `legal_basis`
- `rights_and_obligations`
- `remedies`
- `consultation_and_supervision`
- `service_windows`

输出：

- 每个 section 的文本块
- 起止锚点
- 切分置信度
- 未识别 section 列表

### 5.3 Stage 2：Section 结构化抽取

对每个 section 单独抽取，不对整篇一次输出所有字段。

原因：

1. 标准指南文本很长
2. 表格/多列文本混乱时，按 section 处理更稳定
3. 材料清单和窗口列表需要专门规则

### 5.4 Stage 3：Profile 汇总与规范化

将各 section 抽取结果合并为统一 `StandardServiceGuideProfile`。

需要做的事情：

- 统一字段名
- 统一布尔值和枚举值
- 时间字段结构化
- 生成根级镜像字段
- 生成 `guide_search_text`

补充约束：

- `matter_identity.colloquial_names` 是别名唯一事实源，不再生成 `basic_info.daily_terms`
- `cross_region_service` 长地域列表拆为 `regions_summary` / `regions_detail`
- `process_info` 首版只保留 `summary`、`step_titles`、`raw_text` 等轻量字段

### 5.5 Stage 4：绑定与质量评估

绑定包括：

- Matter 候选匹配
- Material 候选匹配
- Organization 候选匹配
- Region 候选匹配

质量评估包括：

- completeness_score
- confidence_score
- needs_review
- missing_fields
- warnings

### 5.6 Stage 5：写索引与产物记录

产出：

- `gov_service_guides` 结构化详情
- `gov_doc_meta` 镜像字段更新
- `gov_service_guides.acl_ids` 与文档元数据保持一致
- trace artifact：section 切分结果、抽取结果、warnings

---

## 6. 抽取契约草案

### 6.1 输入契约

```python
class ServiceGuideExtractionInput(BaseModel):
    doc_id: str
    content_hash: str
    title: str = ""
    doc_type: str = ""
    knowledge_category: str = ""
    source_url: str = ""
  acl_ids: list[str] = Field(default_factory=list)
    metadata: dict[str, Any] = Field(default_factory=dict)
    plain_text: str = ""
    markdown_text: str = ""
    structured_blocks: list[dict[str, Any]] = Field(default_factory=list)
```

说明：

1. `plain_text` 用于规则识别与 prompt 输入
2. `markdown_text` 用于保留表格和层级结构
3. `structured_blocks` 用于更细粒度的 section 切分

### 6.2 输出契约

```python
class ServiceGuideExtractionOutput(BaseModel):
    detected: bool
    scene_type: str = ""
    detection_confidence: float = 0.0
    detection_reasons: list[str] = Field(default_factory=list)
    profile: dict[str, Any] | None = None
    root_fields: dict[str, Any] = Field(default_factory=dict)
    bindings: dict[str, Any] = Field(default_factory=dict)
    quality: dict[str, Any] = Field(default_factory=dict)
    raw_sections: dict[str, str] = Field(default_factory=dict)
    artifacts: dict[str, Any] = Field(default_factory=dict)
    warnings: list[str] = Field(default_factory=list)
```

### 6.3 运行时契约

推荐主方法：

```python
class ServiceGuideExtractor:
    async def extract(self, payload: ServiceGuideExtractionInput) -> ServiceGuideExtractionOutput:
        ...
```

### 6.4 错误语义

提取器自身不建议抛出未处理异常给上游业务层。

推荐返回：

- `detected = false`：不是标准办事指南
- `detected = true, profile = None`：识别为指南但抽取失败
- `warnings`：抽取过程中的非致命问题

---

## 7. Section 切分契约

### 7.1 推荐数据结构

```python
class GuideSectionChunk(BaseModel):
    section_name: str
    raw_text: str
    start_anchor: str = ""
    end_anchor: str = ""
    confidence: float = 0.0
    block_indices: list[int] = Field(default_factory=list)
```

### 7.2 section_name 枚举建议

```python
GuideSectionName = Literal[
    "basic_info",
    "cross_region_service",
    "review_info",
    "result_info",
    "acceptance_info",
    "process_info",
    "materials",
    "fees",
    "legal_basis",
    "rights_and_obligations",
    "remedies",
    "consultation_and_supervision",
    "service_windows"
]
```

### 7.3 切分规则建议

优先使用标题关键词：

- 基础信息
- 跨域通办
- 审批信息
- 审批结果
- 受理范围
- 受理条件
- 办理流程
- 材料清单
- 收费项目信息
- 设定依据
- 权利与义务
- 法律救济
- 咨询方式与监督方式
- 办理窗口

必要时允许将“受理范围 + 受理条件”合并为一个 section，再由抽取阶段拆细。

---

## 8. 提示词设计草案

不建议用单个 prompt 从整篇文档直接输出完整 profile。推荐拆为三类 prompt。

### 8.1 Prompt A：场景识别

#### 系统提示词

```text
你是政务服务事项文档识别专家。请判断给定文档是否属于“标准办事指南”类文件。

标准办事指南通常具有稳定栏目，例如：基础信息、审批信息、材料清单、办理流程、收费项、法律救济、咨询监督、办理窗口。

请仅基于输入内容判断，不要凭常识补全。
输出 JSON：
{
  "scene_type": "standard_service_guide | other",
  "confidence": 0.0,
  "reasons": ["..."]
}
```

#### 用户提示词模板

```text
请识别以下文档场景类型。

标题：{title}
文种：{doc_type}
知识分类：{knowledge_category}

正文摘要：
{content_head}
```

### 8.2 Prompt B：Section 结构化抽取

每个 section 使用同一个系统 prompt，不同 `section_name` 走不同 user prompt。

#### 系统提示词

```text
你是标准办事指南结构化抽取专家。

请只处理当前 section 的文本，不要补全其他栏目，不要推断不存在的字段。

严格要求：
1. 一行材料对应一个对象
2. 一行收费对应一个对象
3. 一条窗口对应一个对象
4. 字段缺失时返回空字符串、空数组或 null
5. 无法稳定拆列时，只保留能直接从原文确认的字段，其余返回 null/空数组
6. 表格破损、跨行粘连或地域列表过长时，优先保留 `raw_text` 并输出 warning，不要猜测
7. `colloquial_names` 只写入 `matter_identity.colloquial_names`
8. `process_info` 首版只输出 `summary`、`step_titles`、`raw_text`、`notes`、`needs_review`
9. 只输出 JSON，不要输出解释
```

#### 用户提示词模板

```text
当前 section：{section_name}
事项名称：{matter_name}
文档标题：{title}

请根据以下 section 原文抽取结构化结果。

section 原文：
{section_text}

输出字段要求：
{section_schema_hint}
```

### 8.3 Prompt C：Profile 汇总

#### 系统提示词

```text
你是标准办事指南 profile 汇总专家。

你会收到：
1. 文档基础元数据
2. 已按 section 抽取出的结构结果

请将它们合并为统一的 StandardServiceGuideProfile JSON，要求：
1. 不新增未提供的事实
2. 优先保留 section 抽取结果
3. 生成根级镜像字段
4. 生成 guide_search_text
5. 生成 quality.missing_fields 和 warnings
6. `matter_identity.colloquial_names` 作为别名事实源
7. `process_info` 不展开为 step 级对象
```

#### 用户提示词模板

```text
文档元数据：
{metadata_json}

section 抽取结果：
{section_results_json}

请输出完整 StandardServiceGuideProfile JSON。
```

### 8.4 为什么不用单 Prompt

原因：

1. 标准指南内容长，单 prompt 容易遗漏栏目
2. 材料表、窗口表、收费表需要不同抽取粒度
3. 失败时分段重试成本更低
4. 更便于 trace 与人工复核

---

## 9. Section 输出 schema hint 建议

### 9.1 materials section hint

```json
{
  "materials": [
    {
      "material_name": "",
      "requirement_level": "required|conditional|optional",
      "original_count": null,
      "copy_count": null,
      "form_types": [],
      "paper_spec": "",
      "electronic_license_linked": null,
      "exempt_submission": null,
      "reusable_previous_submission": null,
      "material_type": "",
      "source_channel": "",
      "fill_instructions": "",
      "notes": "",
      "applicable_conditions": [],
      "blank_form_available": null,
      "sample_available": null,
      "download_hint": ""
    }
  ]
}
```

### 9.2 fees section hint

```json
{
  "fees": [
    {
      "fee_name": "",
      "amount_text": "",
      "amount_value": null,
      "currency": "CNY",
      "charging_body": "",
      "charging_method": "",
      "reducible": null,
      "notes": ""
    }
  ]
}
```

### 9.3 service_windows section hint

```json
{
  "service_windows": [
    {
      "window_name": "",
      "location": "",
      "office_phone": "",
      "office_hours": "",
      "navigation": "",
      "scope": ""
    }
  ]
}
```

### 9.4 cross_region_service section hint

```json
{
  "cross_region_service": [
    {
      "service_scope_type": "",
      "regions_summary": [],
      "regions_detail": [],
      "regions_truncated": false,
      "service_modes": [],
      "notes": "",
      "raw_text": ""
    }
  ]
}
```

### 9.5 process_info section hint

```json
{
  "process_info": {
    "summary": "",
    "step_titles": [],
    "raw_text": "",
    "notes": "",
    "needs_review": false
  }
}
```

---

## 10. 规则与归一化建议

### 10.1 布尔字段归一化

以下常见表达统一映射：

- 是 / 支持 / 可 / 允许 -> `true`
- 否 / 不支持 / 不可 / 不允许 -> `false`

### 10.2 时间归一化

将：

- `7个工作日`
- `30个工作日`

映射为：

```json
{
  "raw_text": "7个工作日",
  "duration": "7",
  "unit": "工作日",
  "is_working_day": true
}
```

### 10.3 材料必要性归一化

规则建议：

- `必要` -> `required`
- `非必要` -> `optional`
- `仅限...提交`、`加注需要`、`换发/补发需要` -> `conditional`

### 10.4 数量归一化

从以下格式提取：

- `原件：1 复印件：0`
- `原件:1 复印件:0`

提取为整数值。

### 10.5 破损表格保守降级

当出现表头断裂、单元格错位、多行备注粘连时：

1. 先保留行级 `raw_text` 或 section 原文
2. 只填可直接确认的字段
3. 对无法确认的数量、布尔、枚举字段返回 null
4. 在 `warnings` 中明确记录问题位置

---

## 11. 绑定策略草案

### 11.1 Matter 绑定

优先级建议：

1. 编码精确匹配
2. 事项名称精确匹配
3. 日常用语命中 Matter alias
4. name contains + 语义排序

### 11.2 Material 绑定

Guide 材料行不直接依赖图谱 Material 存活。

推荐规则：

1. 先保留 `material_name`
2. 再尝试匹配已有 Material
3. 匹配不上也不算抽取失败

### 11.3 Organization / Region 绑定

优先使用现有图谱搜索服务，但绑定失败不阻塞 Guide 写入。

---

## 12. API 草案

API 建议分为两组：

1. 对外查询 API
2. Admin / 运维 API

### 12.1 对外查询 API

#### `GET /api/v1/service-guides`

用途：标准办事指南搜索

查询参数建议：

- `query`
- `implementation_code`
- `basic_code`
- `matter_type`
- `handled_org`
- `region`
- `express_supported`
- `reservation_supported`
- `must_onsite`
- `page`
- `page_size`

响应示例：

```json
{
  "total": 12,
  "page": 1,
  "page_size": 10,
  "items": [
    {
      "profile_id": "guide_xxx",
      "doc_id": "doc_xxx",
      "matter_name": "办理及加注普通护照",
      "colloquial_names": ["办理护照及加注"],
      "implementation_code": "1144...",
      "matter_version": "75",
      "handled_org_names": ["广州市公安局"],
      "region_names": ["广州市"],
      "express_supported": true,
      "reservation_supported": true,
      "needs_review": false
    }
  ]
}
```

#### `GET /api/v1/service-guides/{profile_id}`

用途：获取完整结构化详情

#### `GET /api/v1/service-guides/by-doc/{doc_id}`

用途：通过文档定位 Guide 详情

#### `GET /api/v1/service-guides/by-matter/{matter_id}`

用途：返回与 Matter 绑定的 Guide 列表

#### 查询权限要求

所有对外查询接口必须：

1. 默认允许匿名访问公开 Guide
2. 无 JWT 时仅查询 `acl_ids` 为空的公开 Guide
3. 有 JWT 时，公开 Guide 与授权 Guide 一并返回
4. 对非公开 Guide，复用现有 PermissionService / `acl_ids` 过滤逻辑

说明：

- `POST /api/v1/service-guides/precheck` 暂不纳入首版，待搜索、详情和 ACL 链路稳定后再评估

### 12.2 Admin / 运维 API

#### `POST /api/v1/admin/service-guides/extract-preview`

用途：对单文档进行抽取预览，不正式写库。

请求：

```json
{
  "doc_id": "doc_xxx",
  "force": false,
  "include_raw_sections": true
}
```

响应：

```json
{
  "detected": true,
  "detection_confidence": 0.94,
  "profile": {"...": "..."},
  "warnings": [],
  "quality": {
    "completeness_score": 0.91,
    "confidence_score": 0.89,
    "needs_review": false
  }
}
```

#### `POST /api/v1/admin/service-guides/rebuild/{doc_id}`

用途：重抽取并覆盖写入指定文档的 Guide 结构。

#### `POST /api/v1/admin/service-guides/backfill`

用途：按查询条件批量回填 Guide 索引。

请求参数建议：

- `query`
- `knowledge_category`
- `document_scene_type`
- `limit`
- `dry_run`

#### `GET /api/v1/admin/service-guides/tasks/{task_id}`

用途：查看 Guide 回填任务状态。

### 12.3 API 错误语义

建议保持与现有接口风格一致：

- 查询不到：`404`
- 参数错误：`400`
- 公开 Guide 查询不要求 `401`
- 显式受限 Guide 在无权限时返回 `403`
- 后台任务已提交：返回 `202`
- 内部错误：`500`

---

## 13. API Schema 草案

### 13.1 查询结果模型

```python
class ServiceGuideListItem(BaseModel):
    profile_id: str
    doc_id: str
    matter_name: str
    colloquial_names: list[str] = Field(default_factory=list)
    implementation_code: str = ""
    matter_version: str = ""
    handled_org_names: list[str] = Field(default_factory=list)
    region_names: list[str] = Field(default_factory=list)
    express_supported: bool | None = None
    reservation_supported: bool | None = None
    needs_review: bool = False
```

### 13.2 详情模型

```python
class ServiceGuideDetailResponse(BaseModel):
    profile_id: str
    doc_id: str
    scene_type: str
    guide_version: str = ""
    profile: dict[str, Any]
```

### 13.3 预览模型

```python
class ServiceGuideExtractPreviewResponse(BaseModel):
    detected: bool
    detection_confidence: float = 0.0
    profile: dict[str, Any] | None = None
    quality: dict[str, Any] = Field(default_factory=dict)
    warnings: list[str] = Field(default_factory=list)
```

---

## 14. 入库管道接入草案

### 14.1 建议新增阶段名

为了与现有 trace 体系兼容，建议增加以下 stage：

- `service_guide_detect`
- `service_guide_section_split`
- `service_guide_extract`
- `service_guide_bind`
- `service_guide_index`

### 14.2 建议管道伪代码

```python
guide_result = await service_guide_extractor.extract(payload)

if not guide_result.detected:
    meta["document_scene_type"] = "other"
elif guide_result.profile is None:
    meta["document_scene_type"] = "standard_service_guide"
    meta["service_guide_status"] = "failed"
else:
  guide_result.profile["acl_ids"] = payload.acl_ids
    await es_client.raw.index(
        index=settings.es_service_guide_index,
        id=guide_result.profile["profile_id"],
        body=guide_result.profile,
    )
    meta["document_scene_type"] = "standard_service_guide"
    meta["service_guide_status"] = "completed"
    meta["guide_profile_id"] = guide_result.profile["profile_id"]
```

  补充要求：

  - 新内容路径与重复内容快路径都要接入 Guide 抽取/补建逻辑
  - 权限更新链路需要按 `doc_id` 覆盖更新已存在 Guide 文档的 `acl_ids`
  - 文档删除链路需要按 `doc_id` 清理对应 Guide 文档，避免脏数据残留

### 14.3 与 Matter 图的协同建议

可以先独立落 Guide 抽取，不把 Matter 同步作为首版硬依赖。

这样做的好处：

1. 降低首版复杂度
2. 有利于先验证 Guide schema 是否稳定
3. 避免 Matter 图改造阻塞 Guide 详情能力上线

---

## 15. 测试建议

### 15.1 单元测试

建议覆盖：

1. 场景识别命中/未命中
2. section 切分正确率
3. materials 行级抽取
4. fees 行级抽取
5. service_windows 行级抽取
6. 缺字段时空值语义
7. profile 汇总根字段生成
8. `acl_ids` 透传与覆盖写入

### 15.2 API 测试

建议覆盖：

1. `GET /service-guides` 查询参数组合
2. `GET /service-guides/{profile_id}` 404 语义
3. `GET /service-guides*` ACL 过滤
4. `POST /admin/service-guides/extract-preview` 鉴权
5. `POST /admin/service-guides/backfill` 任务创建

### 15.3 样例集

首版至少准备三类样例：

1. 标准事项网页导出文本
2. 带长材料表格的指南
3. 只有部分栏目、格式不规范的指南

---

## 16. 最终建议

`service_guide_extractor` 的关键不在于“模型一次抽得多全”，而在于：

1. 识别可靠
2. section 切分可控
3. 行级对象不丢
4. 失败可降级
5. 结果可回链

按这个思路实现，后续无论是事项预审、咨询答复，还是标准办事指南详情页，都会比现在仅靠 Matter 图或全文 QA 稳定得多。