
# 文件入库全链路追踪与时间线

### 1.1 需求合理性判断

该需求整体合理，而且是当前系统进入“可运营、可排障、可审计”阶段后的必然增强项。

原因如下：

- 当前入库链路已经是异步、多阶段、跨组件流程：上传 / Celery / 转换服务 / Docling / Embedding / ES / Neo4j / LLM。只看最终 `completed` 或 `failed` 不足以定位问题。
- 图谱提取、LLM 提取、文档转换都属于黑盒感较强的步骤，如果没有过程可见性，用户和管理员都无法判断“失败在哪一步”“结果为什么不对”。
- 后续如果要提升知识图谱质量、Prompt 质量、分块质量、转换质量，这类过程级日志是最直接的优化依据。
- 该能力不仅服务研发排障，也服务业务验收、运维监控、数据治理与审计追踪。

但该需求不能简单理解成“把 logger 打得更细”。

更合理的产品定义应为：

- 为每一次文档入库建立一条可查询的 `ingest trace`
- 将每个阶段记录为结构化事件，并按时间线展示
- 将大体积细节作为可展开的 `artifact` 存储，而不是全部塞进普通日志行

结论：

- 这是合理需求。
- 优先级建议为 `P0.5 / P1`，优先于继续堆叠普通日志文本。
- 应按“结构化阶段事件 + 细节工件 + 管理端时间线 UI”来设计，而不是按“文件日志增强”来设计。

### 1.2 当前系统的主要差距

基于当前实现，系统已有零散的结构化日志和文档级状态字段，但还不具备真正的“全流程追踪”：

- `gov_doc_meta` 目前只保存文档级 `status / error / chunk_count / task_id` 等摘要信息，不保存阶段事件时间线。
- 入库 pipeline 内部虽然有 `ingest_start`、`ingest_embedded`、`graph_build_complete` 等日志，但这些日志没有统一 `trace_id`，也没有按文档实例聚合查询的能力。
- 转换服务已有独立的“转换记录”页面，但它与入库主链路是割裂的，无法和上传、分块、图谱提取、入库结果串成同一条时间线。
- LLM 调用当前只输出成功/失败摘要，不保留 prompt 模板版本、渲染后的 prompt、返回原文、解析结果、耗时、模型版本等调试关键信息。
- 图谱构建当前只返回 `entity_count / relation_count / referenced_doc_count`，缺少“具体抽取了哪些实体、哪些关系、哪些被过滤”的结构化可视结果。

因此，这个需求不是“已有功能的小修补”，而是需要补一层新的可观测性模型。

### 1.3 建议新增的核心目标

建议把该需求定义为“文件入库追踪中心”，目标包含四层：

1. 任务层：一份文档的一次入库，对应一个唯一逻辑 trace；`Phase A` 建议直接复用首次入队返回的 `task_id` 作为 `trace_id`，`Phase B` 如需跨任务聚合或并行对比，再引入独立物理 `trace_id`
2. 事件层：每个步骤都产生结构化事件，能够按时间顺序查询
3. 工件层：对大体积结果单独存储，例如 chunk 样本、实体关系列表、prompt 与返回结果；`Phase A` 可先不落独立工件索引，仅在事件 `details` 中保留轻量摘要
4. 展示层：后台提供时间线视图、阶段详情视图、失败定位视图

### 1.4 事件模型建议

每条阶段事件建议至少包含以下字段：

| 字段 | 说明 |
|------|------|
| `trace_id` | 一次完整入库流程的唯一标识；`Phase A` 可直接复用 `task_id` |
| `doc_id` | 文档 ID |
| `task_id` | Celery 任务 ID |
| `content_hash` | 内容哈希，用于识别去重链路 |
| `stage` | 阶段名，例如 `upload` / `convert` / `chunk` / `graph_extract` |
| `event_type` | `started` / `completed` / `failed` / `warning` / `artifact_created` |
| `status` | 当前状态 |
| `seq` | 同一 trace 内递增序号，保证时间线稳定排序 |
| `timestamp` | 事件时间 |
| `duration_ms` | 阶段耗时，开始事件可为空 |
| `operator` | 触发来源，例如 webhook / manual / system |
| `service` | backend / converter / worker / graph |
| `retry_count` | 当前重试次数 |
| `summary` | 一行摘要，供时间线直接展示 |
| `details` | 结构化明细 JSON，供详情面板展示 |
| `artifact_refs` | 关联工件 ID 列表 |
| `error_code` | 结构化错误码 |
| `error_message` | 错误摘要 |

注意：

- 时间线排序不能只依赖 `timestamp`，还应有 `seq`，否则跨进程日志合并时顺序可能不稳定。
- `details` 应为结构化 JSON，不要只存拼接字符串，否则后续无法筛选统计。
- `Phase A` 推荐将 `trace_id` 作为逻辑字段保留，但其取值直接等于 Celery `task_id`，避免额外引入一条新的 ID 透传链路。

### 1.5 建议覆盖的阶段

建议最少覆盖以下阶段，而不是只覆盖用户提到的 6 个口径：

| 阶段 | 是否必须 | 触发条件 / 说明 |
|------|----------|-------------------|
| `upload_received` | 必须 | 收到上传 / webhook，请求已入系统 |
| `task_queued` | 必须 | Celery 已入队 |
| `task_started` | 必须 | Worker 开始执行 |
| `file_type_detected` | 必须 | 检测文件类型、是否支持 |
| `content_hash_computed` | 必须 | 哈希完成，准备 dedup |
| `file_stored` | 必须 | 按 `content_hash` 将原文件写入 content-addressed storage |
| `dedup_checked` | 必须 | 是否命中重复内容 |
| `document_converted` | 条件 | 仅需格式转换时触发 |
| `document_parsed` | 条件 | 新内容路径触发；纯文本直读或 Docling 解析 |
| `document_chunked` | 条件 | 新内容路径触发 |
| `metadata_extracted` | 条件 | 仅 `auto_extract=true` 时触发 |
| `summary_generated` | 条件 | 当前 pipeline 已接入，但仅在 `summary_enabled=true` 且启用 summary generator 时触发；`Phase A` 也建议至少采集轻量 `started/completed` 事件 |
| `embedding_generated` | 条件 | 新内容路径触发 |
| `chunks_indexed` | 条件 | 新内容路径触发 |
| `doc_meta_written` | 必须 | 新内容和 dedup 路径都会触发 |
| `graph_extract_llm` | 条件 | 新内容路径且启用图谱构建时触发 |
| `graph_normalized` | 条件 | 新内容路径且启用图谱构建时触发 |
| `graph_written` | 条件 | 新内容路径且启用图谱构建时触发 |
| `chunk_acl_recomputed` | 条件 | 仅 dedup 命中已有内容时触发，当前实现对应 `recompute_chunk_acl(...)` |
| `ingest_completed` | 必须 | 总流程完成 |
| `ingest_failed` | 必须 | 总流程失败 |

这样设计的原因是：

- 真正排障时，问题经常不是“图谱提取失败”，而是“前面已经 dedup 走了快路径”“转换失败”“分块为空”“embedding 超时”“Neo4j 写入失败”。
- 如果只记录用户提到的 6 个阶段，会丢失很多关键诊断点。

需要额外强调的是，`content_hash_computed` 之后会先执行 `file_stored`，然后再进入 dedup 判断；`dedup_checked` 之后实际存在两条执行路径：

1. 新内容路径：`file_stored -> dedup_checked -> document_converted -> document_parsed -> document_chunked -> metadata_extracted -> summary_generated -> embedding_generated -> chunks_indexed -> doc_meta_written -> graph_* -> ingest_completed`
2. 重复内容路径：`file_stored -> dedup_checked -> doc_meta_written -> chunk_acl_recomputed -> ingest_completed`

前端时间线必须按“条件阶段”来渲染，而不是假设所有文档都走完整新内容路径。否则对 dedup 短路完成的文档，会错误地表现成“缺了一串阶段”。

### 1.6 各阶段建议记录的细化内容

#### 1.6.1 上传 / 入队

建议记录：

- 上传来源：webhook / 管理端手动上传 / 脚本批量入库
- 原始文件名、文件大小、扩展名、检测出的 MIME / file_type
- 上传用户 / 系统来源
- 初始元数据摘要：`title / doc_number / issuing_org / acl_ids`
- 任务入队时间、队列名、Celery `task_id`

#### 1.6.2 转换

建议记录：

- 原始格式、目标格式
- 是否需要转换，命中的转换规则
- 转换服务地址、服务版本
- 转换耗时、输出文件路径、输出大小
- 转换警告与失败原因
- 转换后的页数或结构摘要

#### 1.6.3 文件存储

建议记录：

- 存储路径是否命中 content-addressed 规则
- 目标文件名，例如 `${content_hash}.${ext}`
- 是否已存在同 content hash 文件
- 是否执行了实际复制，以及复制耗时
- 存储后文件大小与扩展名

#### 1.6.4 文档解析

建议记录：

- 使用的解析路径：纯文本直读 / Docling
- OCR 是否开启
- 解析出的总字符数、页数、结构块数
- 是否发生正文截断、空文本、乱码回退
- 文档结构摘要，例如标题层级数量、表格块数量、图片块数量

#### 1.6.5 文档分块

建议记录：

- 分块器名称与版本
- 分块策略参数：目标 token、最小 token、overlap 大小
- 产生的 chunk 总数、平均 token、最大/最小 token
- 被过滤的过小 chunk 数量
- 建议额外存一个 chunk 工件，至少包含：
    - 前 5 个 chunk 样本
    - 每个 chunk 的 `chunk_index / page_numbers / heading_hierarchy / token_estimate`
    - 是否跨页、是否来自表格、是否来自标题段

不建议把所有 chunk 正文直接放进时间线列表，可通过详情抽屉按需查看。

#### 1.6.6 LLM 智能提取

这一部分必须细化，否则后续基本无法调 Prompt。

建议记录：

- `prompt_template_id`
- `prompt_template_version`
- 使用模型、温度、max_tokens、是否开启 thinking
- 输入字符数、估算 token 数
- 渲染后的 prompt 摘要预览
- 完整 prompt 是否留存，以及留存级别
- 返回结果原文摘要
- JSON 解析是否成功
- 结构化输出结果
- 调用耗时、失败原因、重试次数

但这里要加一条重要约束：

- 默认不建议对所有文档永久保存完整 prompt 和完整返回文本。
- 更合理的是默认保存“模板 ID + 模板版本 + 输入摘要 + 输出摘要 + 可选脱敏预览”，完整原文仅在管理员开启调试、白名单文档、或者短期留存策略下保存。

原因：

- 可能包含敏感信息、内部公文原文、个人信息
- 存储量会明显膨胀
- 容易形成新的权限与合规风险

#### 1.6.7 图谱提取

图谱提取建议拆成至少三类可见结果，而不是只给一个数量：

1. LLM 原始抽取结果
2. 归一化 / 去重 / 过滤后的结果
3. Neo4j 最终写入结果

建议记录：

- 抽取到的实体列表
- 抽取到的关系列表
- 引用文号列表
- 被过滤掉的非法实体 / 非法关系及原因
- 归一化前后差异
- 最终写入的节点数、关系数、占位文档节点数

展示上建议：

- 时间线中显示摘要，例如“抽取实体 18 个、关系 26 个、过滤 4 个”
- 详情面板中显示实体表和关系表
- 对超长结果仅展示前 N 条，并支持下载 JSON 工件

#### 1.6.8 向量化与入库

建议记录：

- embedding 模型、批次大小、总 chunk 数、总向量数
- 向量生成耗时、失败批次数
- ES bulk 写入成功数、失败数、失败样本
- 元数据写入结果
- 是否走了 dedup 快路径
- ACL 回写影响的 chunk 数、文档数

### 1.7 时间线展示建议

前端不建议直接渲染“原始日志文本流”，建议使用两层视图：

1. 时间线摘要层
2. 阶段详情层

建议形态：

- 左侧或中部为按时间排序的阶段时间线
- 每个节点展示：阶段名、状态、摘要、时间、耗时
- 点击节点后右侧展开详情 JSON / 表格 / prompt 预览 / chunk 样本 / 实体关系列表
- 对运行中的任务支持增量刷新或 SSE 推送
- 对失败节点突出显示，并直接展示 `error_code / error_message / retry_count`

筛选维度建议支持：

- 按 `doc_id`
- 按 `trace_id`
- 按状态
- 按阶段
- 按日期范围
- 按文件类型
- 按是否包含 LLM 调用失败 / 图谱失败 / 转换失败

### 1.8 存储设计建议

不建议把全量时间线和大对象细节继续塞进现有 `gov_doc_meta`。

最终形态建议新增三个存储对象：

1. `gov_doc_ingest_traces`
2. `gov_doc_ingest_events`
3. `gov_doc_ingest_artifacts`

其中：

- `Phase A` 先落 `traces + events`
- `Phase B` 再引入 `artifacts`

建议职责如下：

- `gov_doc_meta`：文档当前摘要状态，用于主业务检索
- `gov_doc_ingest_traces`：任务摘要状态，用于列表和总览
- `gov_doc_ingest_events`：阶段事件时间线，轻量、可筛选、可分页
- `gov_doc_ingest_artifacts`：大体积结果，如 chunk 样本、实体关系列表、prompt/response、错误堆栈

如果 artifact 体积过大，还可以进一步下沉到对象存储，仅在事件中保存引用地址。

### 1.9 还应额外补充的细化约束

除了用户直接提出的内容，建议同步补充以下细化约束，否则后续容易返工：

#### 1.9.1 权限控制

- 谁能看完整时间线
- 谁能看完整 prompt / response
- 谁只能看摘要，不可看正文

建议默认仅管理员可见完整调试细节。

#### 1.9.2 留存策略

- 普通阶段事件保留多久
- 完整 prompt / response 保留多久
- 大体积 artifact 是否自动清理

建议从一开始定义 TTL，例如事件保留 30 至 90 天，完整 LLM 原文保留 7 至 30 天。

#### 1.9.3 脱敏策略

- 公文正文是否需要截断
- prompt 中敏感字段是否脱敏
- error stack 是否对外隐藏内部路径

#### 1.9.4 重试与幂等

- 同一个 trace 内多次重试如何展示
- 是生成新 trace，还是沿用原 trace + 新 attempt
- dedup 快路径是否要单独标注为“短路完成”

#### 1.9.5 失败分类

建议建立统一错误码，而不是只存自然语言报错，例如：

- `INGEST_UNSUPPORTED_FILE`
- `INGEST_CONVERT_FAILED`
- `INGEST_EMPTY_TEXT`
- `INGEST_CHUNK_EMPTY`
- `INGEST_EMBED_TIMEOUT`
- `INGEST_GRAPH_LLM_FAILED`
- `INGEST_GRAPH_WRITE_FAILED`
- `INGEST_ES_BULK_PARTIAL_FAILED`

#### 1.9.6 版本可追溯

建议记录以下版本信息：

- prompt 模板版本
- 图谱 schema 版本
- chunker 参数版本
- embedding 模型版本
- converter 服务版本
- Docling 配置版本

否则后面即使看到了日志，也无法解释“为什么同类文档这周和上周的效果不一样”。

### 1.10 推荐的分期方案

#### Phase A：最小可用版

目标：先把“按文档查看完整时间线”做出来。

范围：

- 直接复用 `task_id` 作为 `trace_id`
- 为关键阶段写结构化事件
- 仅落 `traces + events` 两个索引
- 新增管理端时间线查询接口
- 后台提供时间线列表和失败详情

这一阶段先不默认保存完整 prompt / response 原文，也不引入独立 artifact 索引。

#### Phase B：深度诊断版

目标：让研发和算法能真正调优分块、图谱和 LLM。

范围：

- chunk 样本 artifact
- 实体 / 关系 artifact
- prompt 模板版本与渲染摘要
- 可选完整 response 留存
- 阶段参数与模型版本追踪

#### Phase C：在线运维版

目标：支持运行中观察和长期运营。

范围：

- 运行中任务实时刷新 / SSE
- 失败告警
- trace 导出
- 统计报表，例如失败率、平均耗时、各阶段瓶颈分布

### 1.11 验收标准建议

该需求完成后，至少应满足以下标准：

- 管理员能够根据 `doc_id` 或 `trace_id` 查看一次入库的完整时间线。
- 能明确判断失败发生在哪个阶段，而不是只看到最终 `failed`。
- 能查看 chunk 数量、chunk 样本、图谱实体关系摘要。
- 能查看 LLM 调用使用了哪个 prompt 模板版本、模型版本，以及返回结果摘要。
- 对于图谱提取，能看见“原始抽取结果”“过滤后结果”“最终写入结果”的差异。
- 对于大对象细节，支持按需展开，不影响时间线主视图性能。
- 对敏感 prompt / response 具备权限控制、脱敏和留存策略。

### 1.12 最终建议

如果只把这个需求落成“每一步打印更多日志”，很快会遇到三个问题：

- 日志太散，无法按文档聚合
- 日志太大，难以查询和展示
- LLM / 图谱细节太敏感，无法直接长期暴露

因此更合理的实现定义是：

- 一条入库主线
- 一组结构化阶段事件
- 一批按需展开的调试工件
- 一个管理端时间线界面

这会比“简单加日志”多一层设计，但能一次性把排障、验收、调优、审计四类问题都解决掉。

---

## 2. 后端索引设计

### 2.1 设计原则

为避免把检索主索引和调试追踪混在一起，落地时建议采用“三层索引”而不是继续复用现有 `gov_doc_meta`。

推荐新增以下索引：

1. `gov_doc_ingest_traces`：一条入库任务一条摘要记录，用于列表页和总览页
2. `gov_doc_ingest_events`：阶段事件明细，用于时间线和阶段筛选
3. `gov_doc_ingest_artifacts`：大对象工件，用于 chunk 样本、实体关系列表、prompt/response 等详情

分期约束：

- `Phase A` 仅建设 `gov_doc_ingest_traces` 和 `gov_doc_ingest_events`
- `gov_doc_ingest_artifacts` 作为 `Phase B` 能力引入

现有索引职责保持不变：

- `gov_doc_meta`：文档业务主数据与当前状态
- `gov_doc_chunks`：分块和检索内容

### 2.2 Trace 摘要索引：`gov_doc_ingest_traces`

用途：

- 支撑后台列表页
- 快速按 `doc_id / trace_id / task_id / status / stage / time range` 查询
- 避免列表页去聚合全量事件索引

建议字段：

| 字段 | 类型 | 说明 |
|------|------|------|
| `trace_id` | keyword | 主键 |
| `doc_id` | keyword | 文档 ID |
| `task_id` | keyword | Celery 任务 ID |
| `content_hash` | keyword | 内容哈希 |
| `source_type` | keyword | `webhook` / `manual` / `script` |
| `status` | keyword | `pending` / `running` / `completed` / `failed` / `partial_failed` |
| `current_stage` | keyword | 当前所处阶段 |
| `file_type` | keyword | 文件类型 |
| `original_filename` | keyword | 原始文件名 |
| `title` | text + keyword | 文档标题或回退文件名 |
| `operator` | keyword | 操作者或系统来源 |
| `attempt_count` | integer | 尝试次数 |
| `latest_seq` | integer | 当前已写入的最大事件序号 |
| `started_at` | date | 任务开始时间 |
| `finished_at` | date | 任务结束时间 |
| `duration_ms` | long | 总耗时 |
| `error_code` | keyword | 失败错误码 |
| `error_message` | text, index=false | 失败摘要 |
| `artifact_count` | integer | 工件数量 |
| `created_at` | date | 记录创建时间 |
| `updated_at` | date | 记录更新时间 |

补充说明：

- `status` 是任务摘要状态，不等同于文档主状态字段。
- `current_stage` 用于列表页直接展示“卡在哪一步”。
- `attempt_count` 用于展示重试，不建议为每次重试创建新 trace。
- `Phase A` 中 `trace_id` 字段建议直接等于 Celery `task_id`，`doc_id` 继续作为业务筛选键保留。

### 2.3 事件索引：`gov_doc_ingest_events`

用途：

- 支撑时间线详情
- 支撑按阶段、状态、错误码、服务来源过滤
- 保留结构化阶段事件，不直接依赖原始日志文件

建议字段：

| 字段 | 类型 | 说明 |
|------|------|------|
| `event_id` | keyword | 事件唯一 ID |
| `trace_id` | keyword | 所属 trace |
| `doc_id` | keyword | 文档 ID |
| `task_id` | keyword | Celery 任务 ID |
| `content_hash` | keyword | 内容哈希 |
| `stage` | keyword | 阶段名 |
| `event_type` | keyword | `started` / `completed` / `failed` / `warning` / `artifact_created` |
| `status` | keyword | 当前事件状态 |
| `seq` | integer | trace 内顺序号 |
| `attempt` | integer | 第几次尝试 |
| `service` | keyword | `api` / `worker` / `converter` / `docling` / `llm` / `neo4j` / `opensearch` |
| `operator` | keyword | 触发来源 |
| `file_type` | keyword | 文件类型 |
| `summary` | text + keyword | 时间线摘要 |
| `duration_ms` | long | 阶段耗时 |
| `severity` | keyword | `info` / `warning` / `error` |
| `error_code` | keyword | 错误码 |
| `error_message` | text, index=false | 错误摘要 |
| `artifact_refs` | keyword | 工件 ID 列表 |
| `details` | object, enabled=false | 结构化明细 JSON |
| `timestamp` | date | 事件发生时间 |
| `created_at` | date | 写入时间 |

设计约束：

- `details` 仅用于详情展示，不参与全文检索。
- 需要检索的字段必须显式提升为顶层字段，例如 `stage`、`error_code`、`service`。
- `summary` 仅保存一句可读摘要，不存大段正文。
- `Phase A` 推荐先不在事件索引中引入 `artifact_refs` 依赖，轻量摘要直接放入 `details` 即可；字段可以预留，但不要求首版填充。

### 2.4 工件索引：`gov_doc_ingest_artifacts`

用途：

- 存放大对象细节
- 支撑详情面板展开、下载和 JSON 预览
- 和事件记录解耦，避免时间线列表过重

分期说明：

- 本索引不属于 `Phase A` 必做项
- 仅当需要落 chunk preview、图谱实体关系明细、完整 prompt/response 留存时，在 `Phase B` 引入

建议字段：

| 字段 | 类型 | 说明 |
|------|------|------|
| `artifact_id` | keyword | 工件唯一 ID |
| `trace_id` | keyword | 所属 trace |
| `event_id` | keyword | 来源事件 |
| `doc_id` | keyword | 文档 ID |
| `stage` | keyword | 所属阶段 |
| `artifact_type` | keyword | `chunk_preview` / `graph_entities` / `graph_relations` / `llm_prompt_preview` / `llm_response_preview` / `error_stack` |
| `retention_level` | keyword | `short` / `standard` / `debug` |
| `is_redacted` | boolean | 是否已脱敏 |
| `content_encoding` | keyword | `json_inline` / `text_inline` / `object_storage_ref` |
| `preview_text` | text, index=false | 预览摘要 |
| `payload_json` | object, enabled=false | 内联 JSON 工件 |
| `payload_text` | text, index=false | 内联文本工件 |
| `storage_backend` | keyword | `opensearch` / `s3` / `minio` / `file` |
| `storage_path` | keyword, index=false | 外部存储引用 |
| `content_bytes` | long | 工件体积 |
| `expires_at` | date | 到期时间 |
| `created_at` | date | 创建时间 |

推荐策略：

- 小于 32 KB 的 JSON 工件允许内联存储。
- 大于 32 KB 的 prompt / response / chunk 全量正文不内联，落对象存储，仅保留预览和引用。
- `error_stack` 默认短期保留，不长期保留。

映射约束：

- `payload_json` 实现时必须使用 `{"type": "object", "enabled": false}`，避免被错误建成全文可检索字段。
- `payload_text`、`preview_text` 等大字段必须关闭全文索引；只用于展示，不参与检索。
- 所有仅作为存储引用的大字段，均不应开启不必要的索引和列式存储，避免占用 OpenSearch 内存与磁盘。

### 2.5 索引示例

Trace 示例（`Phase A` 中 `trace_id` 与 `task_id` 取值相同，均为 Celery UUID）：

```json
{
    "trace_id": "a6c3d5b2-4b8f-4f0a-91d3-3d8e9f2a7c11",
    "doc_id": "OA-2026-00123",
    "task_id": "a6c3d5b2-4b8f-4f0a-91d3-3d8e9f2a7c11",
    "status": "running",
    "current_stage": "graph_extract_llm",
    "file_type": "docx",
    "original_filename": "关于产业扶持政策的通知.docx",
    "attempt_count": 1,
    "latest_seq": 12,
    "started_at": "2026-03-13T10:21:33Z",
    "updated_at": "2026-03-13T10:21:52Z"
}
```

Event 示例：

```json
{
    "event_id": "evt_000012",
    "trace_id": "a6c3d5b2-4b8f-4f0a-91d3-3d8e9f2a7c11",
    "doc_id": "OA-2026-00123",
    "stage": "document_chunked",
    "event_type": "completed",
    "status": "completed",
    "seq": 12,
    "summary": "完成分块，共 37 个 chunk，平均 284 tokens",
    "duration_ms": 186,
    "artifact_refs": ["art_chunk_preview_01"],
    "details": {
        "chunk_count": 37,
        "avg_tokens": 284,
        "max_tokens": 496,
        "min_tokens": 62
    },
    "timestamp": "2026-03-13T10:21:46Z"
}
```

Artifact 示例：

```json
{
    "artifact_id": "art_chunk_preview_01",
    "trace_id": "a6c3d5b2-4b8f-4f0a-91d3-3d8e9f2a7c11",
    "stage": "document_chunked",
    "artifact_type": "chunk_preview",
    "retention_level": "standard",
    "is_redacted": true,
    "content_encoding": "json_inline",
    "payload_json": {
        "items": [
            {"chunk_index": 0, "page_numbers": [1], "token_estimate": 233}
        ]
    }
}
```

### 2.6 留存与清理策略

建议默认策略：

- `gov_doc_ingest_traces`：保留 180 天
- `gov_doc_ingest_events`：保留 90 天
- `gov_doc_ingest_artifacts`：
    - `standard` 保留 30 天
    - `debug` 保留 7 天
    - `short` 保留 3 天

实现上建议采用定时清理任务，不要求第一阶段引入复杂 ILM 机制。

---

## 3. API 设计

### 3.1 设计目标

API 设计应兼容当前管理端风格，挂在现有 `/api/v1/admin` 下，默认仅管理员可访问。

建议保留现有接口：

- `/admin/ingest-logs` 继续保留，作为兼容列表接口
- 新增 `/admin/ingest-traces*` 系列接口，逐步替代旧接口

补充说明：

- `Phase A` 只需要 trace list / detail / events 三类核心接口
- artifact 相关接口放在 `Phase B` 实现

### 3.2 接口清单

#### 3.2.1 查询 trace 列表

`GET /api/v1/admin/ingest-traces`

用途：

- 列表页展示
- 支持条件筛选和分页

Query 参数建议：

| 参数 | 类型 | 说明 |
|------|------|------|
| `page` | int | 页码 |
| `page_size` | int | 每页数量 |
| `doc_id` | string | 按文档 ID 筛选 |
| `trace_id` | string | 按 trace ID 精确筛选 |
| `task_id` | string | 按任务 ID 筛选 |
| `status` | string | 任务状态 |
| `current_stage` | string | 当前阶段 |
| `file_type` | string | 文件类型 |
| `source_type` | string | 来源类型 |
| `has_error` | bool | 是否仅看失败/异常 |
| `start_time` | datetime | 开始时间 |
| `end_time` | datetime | 结束时间 |
| `keyword` | string | 标题/文件名模糊搜索 |

响应示例：

```json
{
    "total": 128,
    "page": 1,
    "page_size": 20,
    "records": [
        {
            "trace_id": "a6c3d5b2-4b8f-4f0a-91d3-3d8e9f2a7c11",
            "doc_id": "OA-2026-00123",
            "task_id": "a6c3d5b2-4b8f-4f0a-91d3-3d8e9f2a7c11",
            "title": "关于产业扶持政策的通知",
            "original_filename": "关于产业扶持政策的通知.docx",
            "status": "failed",
            "current_stage": "graph_extract_llm",
            "file_type": "docx",
            "attempt_count": 2,
            "started_at": "2026-03-13T10:21:33Z",
            "duration_ms": 18742,
            "error_code": "INGEST_GRAPH_LLM_FAILED"
        }
    ]
}
```

#### 3.2.2 查询 trace 详情

`GET /api/v1/admin/ingest-traces/{trace_id}`

用途：

- 详情页头部摘要
- 返回当前 trace 的概览信息和阶段统计

建议返回：

- trace 基本信息
- 阶段数量汇总
- 最新错误
- artifact 数量
- 是否正在运行

#### 3.2.3 查询事件时间线

`GET /api/v1/admin/ingest-traces/{trace_id}/events`

用途：

- 时间线展示
- 阶段过滤

Query 参数建议：

- `stage`
- `status`
- `event_type`
- `page`
- `page_size`

响应建议：

```json
{
    "trace_id": "a6c3d5b2-4b8f-4f0a-91d3-3d8e9f2a7c11",
    "records": [
        {
            "event_id": "evt_000001",
            "seq": 1,
            "stage": "upload_received",
            "status": "completed",
            "summary": "收到 webhook 上传，文件大小 2.3 MB",
            "timestamp": "2026-03-13T10:21:33Z",
            "duration_ms": 0,
            "artifact_refs": []
        }
    ]
}
```

#### 3.2.4 查询单个事件详情

`GET /api/v1/admin/ingest-events/{event_id}`

用途：

- 点击时间线节点后，拉取完整 `details`
- 避免列表接口一次返回过大的 `details`

建议返回：

- 事件基础字段
- `details`
- `artifact_refs`
- 错误信息

#### 3.2.5 查询工件列表

`GET /api/v1/admin/ingest-traces/{trace_id}/artifacts`

阶段：`Phase B`

用途：

- 展示 trace 关联的工件
- 按类型过滤

Query 参数建议：

- `artifact_type`
- `stage`
- `page`
- `page_size`

#### 3.2.6 查询工件详情

`GET /api/v1/admin/ingest-artifacts/{artifact_id}`

阶段：`Phase B`

用途：

- 详情面板查看 JSON / 文本预览
- 支持大对象按需获取

Query 参数建议：

- `mode=preview|full`

约束：

- `full` 模式需要更高权限
- 对 `debug` 等级工件返回前先做脱敏校验

#### 3.2.7 实时流接口

`GET /api/v1/admin/ingest-traces/{trace_id}/stream`

阶段：`Phase C`

用途：

- 对运行中任务做 SSE 增量刷新
- 让时间线不必轮询全量详情

#### 3.2.8 统计接口

`GET /api/v1/admin/ingest-traces/stats`

阶段：`Phase C`

用途：

- 展示失败率、平均耗时、阶段瓶颈、文件类型分布

### 3.3 返回模型建议

建议新增以下 schema：

- `IngestTraceSummary`
- `IngestTraceDetail`
- `IngestEventSummary`
- `IngestEventDetail`
- `IngestArtifactSummary`
- `IngestArtifactDetail`

分期说明：

- `IngestArtifactSummary` 和 `IngestArtifactDetail` 属于 `Phase B` 模型

字段命名建议与现有 `IngestStatus` 保持一致风格，统一使用 snake_case。

### 3.4 兼容与迁移策略

迁移建议：

1. 保留现有 `/admin/ingest-logs`
2. 新增 `/admin/ingest-traces`
3. 前端新页面优先接入新接口
4. 待新页面稳定后，再决定是否让旧接口内部复用 trace summary 索引

---

## 4. 前端时间线页面设计

### 4.1 页面定位

建议新增一个管理页，而不是复用现有“转换记录”页面。

建议路由：

- `/admin/ingest-traces`

页面名称建议：

- “入库追踪”
- 或 “文档入库追踪中心”

### 4.2 页面结构

推荐采用“两层主视图 + 一个详情抽屉”的结构。

主页面结构：

1. 顶部筛选区
2. 概览统计卡片
3. Trace 列表表格
4. 点击行后打开右侧详情抽屉

详情抽屉结构：

1. 头部摘要
2. 时间线 Tab
3. 原始 JSON Tab

`Phase B` 再补：

4. 工件 Tab

### 4.3 列表页设计

筛选区建议包含：

- 关键词搜索：标题 / 文件名 / doc_id / trace_id
- 状态筛选
- 当前阶段筛选
- 文件类型筛选
- 来源类型筛选
- 时间范围筛选
- 仅失败开关

统计卡片建议包含：

- 今日入库总数
- 运行中数量
- 失败数量
- 平均总耗时

列表表格建议列：

| 列名 | 说明 |
|------|------|
| 文档标题/文件名 | 优先显示标题，回退显示原始文件名 |
| `doc_id` | 文档 ID |
| `trace_id` | Trace ID，可复制 |
| 文件类型 | file_type |
| 当前阶段 | current_stage |
| 状态 | status |
| 耗时 | duration_ms |
| 开始时间 | started_at |
| 最新错误 | error_code / error_message 摘要 |
| 操作 | 查看详情 |

交互建议：

- 点击整行或“查看详情”按钮打开抽屉
- 对失败状态使用红色高亮
- 对运行中状态使用自动刷新提示

### 4.4 详情抽屉设计

头部摘要建议展示：

- 标题 / 文件名
- `doc_id`
- `trace_id`
- `task_id`
- 当前状态
- 当前阶段
- 开始时间 / 结束时间 / 总耗时
- 重试次数

时间线 Tab：

- 使用 `a-timeline`
- 每个节点展示阶段名、状态、摘要、时间、耗时
- 节点颜色规则：
    - 成功：绿色
    - 运行中：蓝色
    - 警告：橙色
    - 失败：红色
- 点击节点后在右侧详情区域显示该事件的 `details`

工件 Tab：

`Phase B` 能力，`Phase A` 默认不展示该 Tab。

- 按工件类型分组展示
- 支持 JSON 预览、文本预览、下载
- 对受权限控制的完整 prompt / response 显示“需更高权限”

原始 JSON Tab：

- 便于研发排障
- 支持复制 trace summary / event detail JSON

### 4.5 运行中任务的交互

`Phase A`：

- 抽屉打开后每 5 秒轮询一次详情和时间线
- 任务结束后自动停止轮询

`Phase C`：

- 切换为 SSE 增量更新
- 仅追加新事件，不重拉全量数据

### 4.6 前端代码落点建议

建议新增或修改以下前端位置：

- 新增页面：`frontend/src/views/admin/IngestTraces.vue`
- 新增类型：`frontend/src/types/ingest-trace.ts`
- 路由注册：`frontend/src/router/index.ts`
- 菜单注册：`frontend/src/App.vue`
- Dashboard 跳转入口：`frontend/src/views/admin/Dashboard.vue`
- 如需抽组件：
    - `IngestTraceFilterBar.vue`
    - `IngestTraceDrawer.vue`
    - `IngestEventDetailPanel.vue`

`Phase B` 如需工件展示，再新增：

- `IngestArtifactPreview.vue`

### 4.7 与现有页面关系

建议关系如下：

- “转换记录”页面继续保留，定位为 converter 服务视角
- 新的“入库追踪”页面定位为整条链路视角
- 在“入库追踪”详情里，如果某个阶段是转换阶段，可直接跳转到转换记录或展示关联转换摘要
- Dashboard 中现有“近期入库日志”表格继续保留，定位为文档状态摘要视图
- Dashboard 建议为每行补一个“追踪”入口，跳转到 `/admin/ingest-traces?doc_id=xxx`

---

## 5. 埋点改造清单

### 5.1 核心改造原则

本次改造不建议在业务代码中零散插入大量 `logger.info(...)`。

推荐做法是新增一个统一的 `IngestTraceRecorder` 或类似服务，负责：

- 维护逻辑 trace 标识；`Phase A` 中直接复用 `task_id` 作为 `trace_id`
- 维护 `seq`
- 写 trace summary
- 写 event
- `Phase B` 再写 artifact
- 绑定 `trace_id / doc_id / task_id` 到日志上下文

实现约束：

- 当前入库在单个 Celery task 内、单事件循环执行，`seq` 由 recorder 实例内自增即可，无需额外锁。
- recorder 写 OpenSearch 失败时必须降级为普通运行日志，不得导致主入库流程失败。
- 不建议让主链路在每个事件点都阻塞等待 OpenSearch 完成写入；可采用轻量缓冲或尽量短路径写入策略。

### 5.2 后端改造清单

#### A. API 层

文件：`backend/app/api/v1/ingest.py`

改造项：

- 在拿到 Celery `task_id` 后，直接将其作为 `Phase A` 的 `trace_id`
- 将该逻辑 trace 标识传给后续 recorder / pipeline 上下文
- 写入 `upload_received`、`task_queued` 两个初始事件
- `_write_pending_meta` 保持现有能力，但补充 trace 关联字段

#### B. Celery 任务层

文件：`backend/app/tasks/ingest_task.py`

改造项：

- 任务参数增加 `trace_id`
- 任务启动时写 `task_started`
- 重试前写 `warning` 或 `retry_scheduled`
- 最终异常时写 `ingest_failed`
- 将 `self.request.retries` 写入 attempt 信息；逻辑上仍归并到同一 trace

#### C. Pipeline 编排层

文件：`backend/app/core/ingest_pipeline.py`

改造项：

- `ingest_document` 增加 `trace_id` 和 recorder 依赖
- 对每个阶段改成 `started -> completed/failed` 成对写事件
- 覆盖以下阶段：
    - `file_type_detected`
    - `content_hash_computed`
    - `file_stored`
    - `dedup_checked`
    - `document_converted`
    - `document_parsed`
    - `document_chunked`
    - `metadata_extracted`
    - `summary_generated`
    - `embedding_generated`
    - `chunks_indexed`
    - `doc_meta_written`
    - `chunk_acl_recomputed`
    - `ingest_completed`
- `_update_meta_status` 继续写文档业务状态，但不承担时间线职责

补充约束：

- `file_stored` 位于 `content_hash_computed` 和 `dedup_checked` 之间，必须作为显式阶段记录，否则 content-addressed storage 的耗时无从解释。
- `summary_generated` 当前业务流程已接入，`Phase A` 也建议至少采集轻量 `started/completed` 事件；`Phase B` 再补更细的摘要内容或工件。
- `chunk_acl_recomputed` 必须标注为 dedup 条件阶段，避免前端误判阶段缺失。
- 对 dedup 路径和新内容路径要分别产出摘要，不能假设阶段固定完整出现。

#### D. 解析与分块层

文件：`backend/app/core/docling_processor.py`

改造项：

- 返回更多结构统计，例如标题数、表格块数、图片块数
- 输出 OCR 是否启用、解析路径、总字符数

文件：`backend/app/core/chunker.py`

改造项：

- 抽一个分块统计 helper
- `Phase A` 先将 chunk 数量、token 分布、过滤数量写入事件 `details`
- `Phase B` 再产出 chunk preview artifact

#### E. LLM 调用层

文件：`backend/app/infrastructure/llm_client.py`

改造项：

- 增加可选 trace hook
- 统一记录模型名、温度、max_tokens、thinking 开关、耗时
- 支持 `preview_only` 与 `debug_full` 两种留存模式

文件：

- `backend/app/core/metadata_extractor.py`
- `backend/app/core/summary_generator.py`
- `backend/app/core/graph_builder.py`

改造项：

- 标记 `prompt_template_id`
- 标记 `prompt_template_version`
- 记录输入长度、输出长度、JSON 解析结果
- `Phase A` 先写图谱提取数量摘要和过滤统计
- `Phase B` 再补：
    - 原始抽取 artifact
    - 归一化结果 artifact
    - 最终写入摘要事件

#### F. 存储层

文件：`backend/app/infrastructure/es_client.py`

改造项：

- 创建新索引 mapping
- 封装以下方法：
    - `create_ingest_trace_indices()`
    - `upsert_ingest_trace_summary()`
    - `append_ingest_event()`
    - `query_ingest_traces()`
    - `query_ingest_events()`

`Phase B` 再补：

- `save_ingest_artifact()`
- `query_ingest_artifacts()`

#### G. 管理 API 层

文件：`backend/app/api/v1/admin.py`

改造项：

- 新增 trace 列表、详情、事件、工件接口
- 保留 `/ingest-logs` 兼容接口
- 后续可让 `/ingest-logs` 内部复用 trace summary 查询逻辑

#### H. 日志上下文层

文件：`backend/app/utils/logger.py`

改造项：

- 通过 structlog contextvars 统一绑定 `trace_id / doc_id / task_id / stage`
- 保证普通运行日志也能按 trace 聚合

### 5.3 前端改造清单

文件：`frontend/src/router/index.ts`

改造项：

- 新增 `/admin/ingest-traces` 路由

文件：`frontend/src/App.vue`

改造项：

- 在“系统管理”菜单中新增“入库追踪”菜单项
- 菜单顺序建议放在“转换记录”之前，便于按链路视角浏览管理功能
- 在 `menuTitleMap` 和 `menuParentMap` 中注册 `/admin/ingest-traces`
- 在 `menuTitleMap` 和 `menuParentMap` 中注册 `/admin/ingest-traces`

文件：`frontend/src/views/admin/IngestTraces.vue`

改造项：

- 新建列表 + 详情抽屉页面
- 接入 trace list / detail / events / artifacts 接口
- 对运行中任务做轮询刷新

文件：`frontend/src/views/admin/ConvertLogs.vue`

改造项：

- 可选新增“查看关联入库追踪”跳转入口

文件：`frontend/src/views/admin/Dashboard.vue`

改造项：

- 在“近期入库日志”表格中为每行新增“追踪”按钮
- 点击跳转到 `/admin/ingest-traces?doc_id=xxx`

文件：`frontend/src/types/ingest-trace.ts`

改造项：

- 定义前端类型：trace、event、artifact、filters

### 5.4 测试改造清单

建议补齐以下测试：

- `backend/tests/test_api_admin.py`
    - `Phase A`：新增 trace list / detail / events API 测试
    - `Phase B`：再补 artifacts API 测试
- `backend/tests/test_api_ingest.py`
    - 验证触发入库后是否生成 trace 和初始事件
- `backend/tests/test_flow_ingest_search.py`
    - 验证完整链路事件是否按顺序写入
- `backend/tests/test_graph_builder_unit.py`
    - `Phase A`：验证图谱数量摘要和过滤统计
    - `Phase B`：验证 graph artifact
- 前端暂以页面编译和手工 smoke 为主

### 5.5 推荐实施顺序

建议按以下顺序推进：

1. 先完成索引和 recorder 基础设施
2. 接着改造 ingest API、Celery task、pipeline 主链路
3. 再补 graph / metadata / summary 的事件摘要
4. 然后新增 admin API
5. 最后接前端页面和轮询
6. `Phase B` 再补 artifact 索引、工件详情和 SSE

### 5.6 MVP 范围收敛

如果要先做第一版，建议只做以下范围：

- 复用 `task_id` 作为 `trace_id`
- trace summary 索引
- 关键阶段事件索引
- 管理端列表页 + 时间线抽屉
- chunk 数量、实体关系数量、错误摘要

先不做：

- 完整 prompt 原文长期留存
- 全量 chunk 正文下载
- artifact 独立索引
- 工件详情接口与工件 Tab
- SSE 实时流
- 复杂统计报表

这样能先把“看见每一步”和“快速定位失败阶段”两件最关键的事做出来。