# 检索与上下文历史导出 本接口为后续实验保留当时的检索证据与配置,不重新检索、不加载 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-.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。此阶段没有新增下载页面,页面入口由后续前端集成;调用层测试是模拟网络的契约测试,不冒充浏览器下载验收。 ## 验证与撤回 新增专项覆盖混合模式原始快照、可选准备记录、无命中、软删除后历史、摘要稳定性、权限/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`。撤回导出路由、服务、调用层即可停止新下载;原检索/上下文历史与已保存文件不变,不为回滚删除数据库记录。