note/app_prd/题目解析.md

226 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 产品需求文档:题目解析与结构化服务 (QPES)
**Question Parsing & Extraction Service - Backend**
| 文档版本 | V1.0 |
| :------- | :--------------------- |
| **依赖模块** | UDC-M (通用文档转换服务 V2.0) |
| **最后更新** | 2025-12-09 |
| **状态** | 待开发 |
| **涉及端** | 后端 API, LLM Agent, 数据库 |
---
## 1. 项目背景与目标
### 1.1 背景
模块一UDC-M已经将 PDF/Word/Excel 变成了带有 MinIO 图片链接的标准 Markdown 流。但此时的数据仍然是**非结构化**的“一坨文本”。
我们需要将这些文本中的每一道试题(题干、选项、答案、解析)精准切割出来,变成数据库中的一行行记录,以便后续进行知识点打标和组卷搜索。
### 1.2 目标
构建一个**智能分块与语义提取服务**。
* **语义切分**:利用 LLM 识别题目边界,解决“跨页中断”、“题目粘连”问题。
* **结构化输出**:将非结构化文本转化为标准 JSON 格式(`content`, `answer`, `analysis`)。
* **多模态保留**:在切分过程中,**绝对保留** Markdown 中的图片链接(公式截图、几何图)。
* **无缝衔接**:通过 `document_id` 与 UDC-M 的资产表深度关联。
---
## 2. 系统架构设计
### 2.1 核心流程
1. **Input**: 接收 `document_id` -> 从 UDC-M 获取 Markdown 文本。
2. **Chunking Strategy (切片引擎)**:
* 由于 LLM 上下文限制(即使 DeepSeek 支持 128k过长也会导致注意力分散需采用 **“滑动窗口 (Sliding Window)”** 策略。
* *策略*: 设定窗口大小(如 3000 Tokens 或 5 页),重叠大小(如 500 Tokens 或 1 页)。
3. **LLM Processor (提取引擎)**:
* 构造 Prompt要求 LLM 输出特定 Schema 的 JSON。
* 处理重叠区域的**题目去重 (Deduplication)**。
4. **Validation**: 使用 Pydantic 校验 LLM 返回的 JSON 格式,失败则重试。
5. **Storage**: 写入 `questions` 表。
---
## 3. 功能需求详细说明
### 3.1 提取任务管理
* **异步处理**: 提取过程耗时较长LLM 生成),必须异步。
* **状态机**: `queued` -> `processing` -> `deduplicating` (去重中) -> `success` / `failed`
### 3.2 智能分块策略 (Sliding Window)
* **场景**: 一道大题可能跨越第 5 页和第 6 页。如果硬切,题目会断裂。
* **逻辑**:
* **Chunk N**: 读取第 1-1000 行。Prompt: "提取所有完整题目,如果末尾题目被截断,请**不要**提取,并在 JSON 中标记 `truncated: true`"。
* **Chunk N+1**: 读取第 800-1800 行(重叠 200 行。Prompt: "提取所有完整题目。注意:如果是开头被截断的题目(即上一块的末尾),请尝试修复并提取"。
* **去重逻辑**:
* 对提取出的每一道题计算 `MD5(content_text)`
* 如果后一个 Chunk 提取出的题目 Hash 与前一个 Chunk 已存的题目 Hash 一致,则丢弃,防止重复。
### 3.3 Prompt Engineering (核心资产)
* **输入**: Markdown 片段。
* **要求**:
1. 识别选择题、填空题、解答题。
2. **关键**: 保持文本中的 `![](...)` 图片链接不被 LLM 吞掉或篡改。
3. **关键**: 数学公式保持 LaTeX 格式(`$` 包裹)。
4. 输出 list of objects。
### 3.4 结构化存储
* 将提取结果存入 `questions` 表。
* **预留字段**: `knowledges``methods` 字段初始化为 `NULL`,等待模块三(知识增强)处理。
---
## 4. 数据库设计 (PostgreSQL)
基于 SQLModel 设计,与 UDC-M 的 `documents` 表关联。
### 4.1 Table: `questions` (题库核心表)
| 字段名 | 类型 | 说明 |
| :--- | :--- | :--- |
| `id` | BIGINT (PK) | 题目唯一主键 |
| `document_id` | UUID (FK) | **关联 UDC-M 的 documents 表** |
| `batch_id` | UUID | 关联本次提取任务 ID |
| `question_seq` | INT | 在原文档中的顺序号 (1, 2, 3...) |
| `question_type` | VARCHAR | `choice` (选择), `fill` (填空), `essay` (解答) |
| `content_md` | TEXT | **题干内容** (含 Markdown 图片和 Latex) |
| `options_json` | JSONB | 选择题选项 `[{"label":"A","text":"..."}, ...]` |
| `answer_md` | TEXT | 参考答案 |
| `analysis_md` | TEXT | 解析 (如果有) |
| `image_urls` | JSONB | 提取出的图片链接列表 `["http...", "http..."]` (冗余字段,方便索引) |
| `content_hash` | VARCHAR(64) | 用于去重 |
| `enrich_status` | VARCHAR | `pending` (待打标), `done` (已打标) |
| `created_at` | TIMESTAMP | |
### 4.2 Table: `extraction_tasks` (提取任务记录)
用于记录每次 LLM 调用的消耗和状态。
| 字段名 | 类型 | 说明 |
| :--- | :--- | :--- |
| `id` | UUID (PK) | 任务 ID |
| `document_id` | UUID | 关联文档 |
| `model_name` | VARCHAR | 使用的模型 (e.g., `deepseek-v3`) |
| `token_usage` | INT | 本次任务消耗 Token 数 (用于成本核算) |
| `status` | VARCHAR | 状态 |
| `error_log` | TEXT | 错误日志 |
---
## 5. API 接口定义 (RESTful)
**Base URL**: `/api/v1/extractions`
### 5.1 发起提取任务
* **Endpoint**: `POST /`
* **Description**: 对指定文档进行题目提取。
* **Request Body**:
```json
{
"document_id": "uuid-of-document",
"chunk_size": 3000, // 可选Token数
"overlap": 500 // 可选重叠Token数
}
```
* **Logic**:
1. 检查 `documents` 表中该文档是否存在且 `parse_result_url` (Markdown) 是否就绪。
2. 创建 `extraction_task`
3. 推送到 Redis 队列 `llm-extraction-queue`
* **Response**: `{"task_id": "...", "status": "queued"}`
### 5.2 查询提取状态
* **Endpoint**: `GET /{task_id}`
* **Response**:
```json
{
"status": "processing",
"progress": "3/10 chunks",
"extracted_count": 45
}
```
### 5.3 获取提取结果 (核心)
* **Endpoint**: `GET /documents/{document_id}/questions`
* **Description**: 获取某文档下的所有题目(支持分页)。
* **Response**:
```json
{
"total": 12,
"items": [
{
"id": 1001,
"question_seq": 1,
"content_md": "已知函数 $f(x)=x^2$ ... ![](http://minio.../a.jpg)",
"enrich_status": "pending"
},
...
]
}
```
### 5.4 人工修正接口 (可选)
* **Endpoint**: `PUT /questions/{question_id}`
* **Description**: 人工校对 OCR 错误或切分错误。
---
## 6. 技术选型 (Vibe Coding Stack)
除了沿用模块一的 **FastAPI + Celery + PostgreSQL** 外,本模块的核心在于 **LLM 交互**
| 组件 | 选型 | 理由 |
| :--- | :--- | :--- |
| **LLM Model** | **DeepSeek-V3** | API 成本极低Context 窗口大,逻辑推理能力强,适合处理长文档。 |
| **LLM SDK** | **OpenAI SDK** | DeepSeek 完美兼容 OpenAI 接口协议。不要引入 LangChain (太重),直接用原生 SDK 配合 Pydantic 即可。 |
| **Schema Validation** | **Pydantic (Instructor)** | **强烈推荐**使用 `instructor` 库(基于 Pydantic它可以强制 LLM 输出完全符合 Pydantic 定义的 JSON 结构,极大降低解析错误率。 |
| **Markdown Splitter** | **LangChain TextSplitter** | 虽然不用 LangChain 的 Chain但它的 `MarkdownHeaderTextSplitter` 工具类非常好用,用于预处理 Chunk。 |
---
## 7. 开发难点与 Vibe Coding 提示
在开发此模块时,请注意以下 Prompt 技巧,通过 Cursor/Windsurf 编写代码时由于重要:
### 7.1 "Instructor" 模式示例
不要让 LLM 返回纯文本然后自己 `json.loads()`,这很容易崩。请使用 Schema 驱动模式:
```python
from pydantic import BaseModel, Field
from typing import List, Optional
# 1. 定义题目结构
class QuestionItem(BaseModel):
content: str = Field(..., description="The main text of the question, including LaTeX and image links.")
options: Optional[List[str]] = Field(None, description="List of options if it is a multiple choice question.")
answer: Optional[str] = Field(None, description="The answer key.")
type: str = Field(..., description="Type: choice, fill, or essay")
class ExtractionResult(BaseModel):
questions: List[QuestionItem]
# 2. Vibe Coding 调用 (伪代码)
# client = instructor.patch(OpenAI(...))
# resp = client.chat.completions.create(
# model="deepseek-chat",
# response_model=ExtractionResult, <-- 关键!强制返回对象
# messages=[...]
# )
```
### 7.2 图片链接保护 Prompt
在 System Prompt 中必须加入:
> "You are a strictly structural parser. You must NOT simplify, summarize, or remove any content from the original text.
> **CRITICAL RULE**: Do NOT remove or modify any image links formatted as `![](...)`. They must be preserved exactly as they appear in the text."
### 7.3 去重逻辑实现
建议在 Python 代码中实现去重,而不是让 LLM 去重。
* **方法**: 维护一个 `Set`,存储 `hash(content)`
* **流程**: Worker 处理完 Chunk 1将题目入库并存 Hash 到 Set。处理 Chunk 2 时,先计算 Hash如果在 Set 里,直接跳过。
---
## 8. 与后续模块的接口
该模块完成后,数据库中的 `questions` 表已经有了干净的题目数据。
* **字段状态**: `knowledges` 为 NULL, `enrich_status``pending`
* **下一步**: 模块三(知识增强服务)只需要 `SELECT * FROM questions WHERE enrich_status = 'pending'`,然后逐条调用 Summary API 进行填空即可。
这套设计保证了模块的**高内聚、低耦合**。即使模块三挂了,模块二依然可以照常解析入库,不会阻塞业务。