98 lines
7.6 KiB
Markdown
98 lines
7.6 KiB
Markdown
# 检索与上下文历史导出
|
||
|
||
本接口为后续实验保留当时的检索证据与配置,不重新检索、不加载 Embedding 或调用生成模型,也不生成质量分数。四种模式仍保持各自证据粒度,不能把条目与 chunk 混为同一评测样本。
|
||
|
||
## 请求与权限
|
||
|
||
`POST /api/retrieval/exports`,管理员会话、允许的 Origin 和 `X-CSRF-Token` 均为必需。普通用户 403,匿名/失效会话 401;错误 Origin 或 CSRF 为 403。继承 [认证契约](AUTH_API.md),不会返回用户、会话或凭据字段。
|
||
|
||
```json
|
||
{
|
||
"run_ids": ["00000000-0000-4000-8000-000000000001"],
|
||
"include_contexts": false
|
||
}
|
||
```
|
||
|
||
编号仅为格式示例,实际应先从管理员 `GET /api/retrieval/runs` 选择已存在的记录。`run_ids` 必须为 1–100 个不重复 UUID,大小写规范化后仍重复也拒绝;`include_contexts` 为严格布尔值,默认 false。额外字段、空列表和非法编号为 422。
|
||
|
||
明确选择避免导出无边界的全库记录。任一检索编号不存在即 404,不生成部分文件。可选上下文是这些检索记录关联的**全部已保存准备记录**,不含其他检索的上下文。
|
||
|
||
## 下载与 JSONL 契约
|
||
|
||
- 成功为 200,`Content-Type: application/x-ndjson; charset=utf-8`,`Content-Disposition: attachment; filename="nalati-retrieval-<export UUID>.jsonl"`。
|
||
- 响应固定 `Cache-Control: no-store` 和 `X-Content-Type-Options: nosniff`;`X-Records-SHA256` 等于文件首行的 `records_sha256`。
|
||
- UTF-8 编码,每行一个 JSON 对象,每行(包括末行)以 LF 结束;内容中的换行由 JSON 转义。不是 CSV,也不是一个 JSON 数组。
|
||
- 首行为 `record_type=manifest`。其余先为检索记录,再为上下文记录,各组按数据库 `created_at,id` 升序。请求 UUID 列表的顺序不影响记录区顺序。
|
||
|
||
首行字段:
|
||
|
||
| 字段 | 含义 |
|
||
|---|---|
|
||
| `schema_version` | 固定 `nalati-retrieval-export-v1` |
|
||
| `export_id`、`exported_at` | 本次导出 UUID 与带时区 UTC 时间 |
|
||
| `selection` | 排序后的检索 UUID 及是否包含上下文 |
|
||
| `counts` | 实际 `retrieval_runs` 与 `rag_contexts` 条数,不含首行 |
|
||
| `records_sha256` | **首行之后全部原始 UTF-8 字节**的 SHA-256,含每条末尾 LF |
|
||
| `snapshot_semantics` | 固定 `stored_history_not_current_corpus` |
|
||
| `generation_measurements` | `status=not_called`,`usage/cost/currency=null`;没有生成调用或费用测量,不能解释为零成本 |
|
||
|
||
检索行有 `record_type=retrieval_run`、`id`、`ranking_unit` 和 `payload`。`payload` 保留原已保存响应,不重新计算或修改分数、耗时、文本、来源及版本:
|
||
|
||
| `payload.mode` | `ranking_unit` | 保留的配置与证据 |
|
||
|---|---|---|
|
||
| `keyword` | `knowledge_item` | 条目 BM25、中文分词/标题权重、片段与来源版本 |
|
||
| `keyword_chunks` | `chunk` | 文本发布/切分/语料版本、正文 BM25、chunk 原文出处 |
|
||
| `vector` | `chunk` | 文本/向量发布、模型完整 revision、空间/维度与精确余弦配置 |
|
||
| `hybrid` | `chunk` | 同一 chunk 粒度、两路完整候选/排名/原始分数/贡献、RRF 与模型空间配置 |
|
||
|
||
上下文行有 `record_type=rag_context`、`id` 和原保存的 `payload`;保留准备参数、证据、排除原因、提示词版本、请求体及请求摘要。`model_called=false`,即使 `status=prepared` 也不是模型回答。原字段详见 [RAG_CONTEXT_API.md](RAG_CONTEXT_API.md)。不导出数据库 `created_by` 或认证表数据。
|
||
|
||
## 快照、容量与失败
|
||
|
||
- 检索与可选上下文通过**一个 PostgreSQL SQL 语句快照**读取,没有跨页读取造成的漂移;执行后新增的上下文留到下一次导出。
|
||
- 已修改、删除或重新发布的知识不会改写历史。导出成功只说明当时记录可读取,不说明它仍是当前有效发布或来源仍受支持。
|
||
- 每次最多 100 条检索、100 条关联上下文,完整 UTF-8 文件(含 manifest)最多 8 MiB。超量为 413,不静默截断;某个检索自身关联超过 100 条准备记录时,本版可先不含上下文导出,不能借此声称已导出全部准备记录。
|
||
- 输出前完成结构、编号/关联、数据库元数据和准备请求摘要校验;未知/缺失字段、非有限数值或关联异常为 409。它是历史格式一致性检查,不是来源真实性或事实正确性证明。
|
||
- 服务端先完整验证并组装文件,再返回下载响应;校验、容量或数据库失败不会先发出部分 JSONL。此接口不写入新的检索/上下文记录,不重建索引。
|
||
- 同一历史选择的记录区字节和摘要可重复;manifest 的导出 UUID/时间每次变化,因此不要比较整个文件的摘要。若新增上下文后再次启用 `include_contexts`,记录区也会变化。SHA-256 仅供字节完整性核对,不是审阅签名或质量指标。
|
||
|
||
`elapsed_ms` 沿用各模式原始计时范围,不是生成模型或完整浏览器交互耗时。模型、语料与参数不一致的记录不能只凭分数直接做实验对比。
|
||
|
||
## 调用与核对
|
||
|
||
已有管理员登录并按认证契约准备 Cookie 文件和当前 CSRF 后,可在仓库根目录调用。Cookie 文件、真实请求、JSONL 和实际日志均留在 Git 元数据目录或仓库外;下面的编号仍需替换成真实已保存的检索编号。
|
||
|
||
```bash
|
||
mkdir -p .git/exports
|
||
curl --fail-with-body --request POST http://127.0.0.1:8000/api/retrieval/exports \
|
||
--cookie "$COOKIE_JAR" \
|
||
--header 'Origin: http://localhost:5173' \
|
||
--header "X-CSRF-Token: $CSRF_TOKEN" \
|
||
--header 'Content-Type: application/json' \
|
||
--data '{"run_ids":["00000000-0000-4000-8000-000000000001"],"include_contexts":true}' \
|
||
--output .git/exports/history.jsonl
|
||
```
|
||
|
||
仅在 HTTP 200 后按 JSONL 读取文件。核对摘要时不应重新序列化记录:
|
||
|
||
```python
|
||
import hashlib
|
||
import json
|
||
from pathlib import Path
|
||
|
||
header, records = Path(".git/exports/history.jsonl").read_bytes().split(b"\n", 1)
|
||
manifest = json.loads(header)
|
||
assert manifest["schema_version"] == "nalati-retrieval-export-v1"
|
||
assert hashlib.sha256(records).hexdigest() == manifest["records_sha256"]
|
||
rows = [json.loads(line) for line in records.rstrip(b"\n").split(b"\n")]
|
||
assert len(rows) == sum(manifest["counts"].values())
|
||
```
|
||
|
||
前端调用层 `services/exports.ts` 的 `exportRetrievalHistory` 返回 Blob、受限文件名和记录摘要,沿用同一 Cookie/CSRF、401 撤销及错误处理,不放入 localStorage。下载页面已通过 PR #36 合入,可从管理员工作台进入。真实浏览器跨页选择、可选上下文、落盘文件与摘要核对见 [导出浏览器验收](EXPORT_BROWSER_ACCEPTANCE.md);调用层模拟网络测试与真实浏览器路径分别记录。
|
||
|
||
## 验证与撤回
|
||
|
||
新增专项覆盖混合模式原始快照、可选准备记录、无命中、软删除后历史、摘要稳定性、权限/CSRF、未知字段/关联损坏拒绝、字节/条数上限、并发新增上下文以及真实 HTTP 后端重启后的同一记录区。坐标 fixture 仅用于导出协议,不构成真实领域模型实验。专项命令 `pytest -q backend/tests/test_exports.py`:**30 passed**;完整 `pytest -q backend/tests`:**254 passed, 1 skipped**,skip 为显式启用的真实 Embedding 用例,本阶段没有重跑模型。`ruff check backend`、`ruff format --check backend` 通过;前端在 `frontend/` 执行 `npm run test:session`:**3 passed**,`npm run build` 通过。
|
||
|
||
数据库不新增迁移,继续为 `0007_rag_contexts`。撤回导出路由、服务、调用层即可停止新下载;原检索/上下文历史与已保存文件不变,不为回滚删除数据库记录。
|