Skip to content

核心领域模型

这一页介绍 domain/models.py 里定义的核心领域对象。这些 dataclass 是整个系统传递业务语义的「货币」,它们在应用层、适配器、API 之间流动,且不绑定任何框架。

代码位置:src/ultimate_rag/domain/models.py

1. 领域模型全家福

text
KnowledgeBase(知识库)
    └── Document(文档)
         ├── 状态:DocumentStatus
         ├── 原文件:MinIO ObjectKey
         ├── DocumentAsset[](图片资源元数据;二进制在 MinIO)
         └── 后台任务:IngestionJob
              ├── Chunk[](文本片段,存 PostgreSQL)
              │     └── 每个 Chunk 带 SourceLocator(来源定位)
              └── Milvus 向量(派生)

2. 状态枚举

DocumentStatus(文档状态,面向用户)

text
PENDING → PARSING → CHUNKING → EMBEDDING → INDEXING → READY
                                    任一环节失败 → FAILED

README 之前所有状态都代表「尚未完成处理」,绝不能被检索。

IngestionJobStatus(任务状态,面向调度)

text
PENDING → RUNNING → SUCCEEDED / FAILED

为什么有两种状态

文档状态表达「处理到哪一步」,面向用户展示; 任务状态表达「Worker 的领取/重试/完成语义」,面向调度。 二者刻意分开,避免把队列实现细节暴露到 API。

BlockType(语义块类型)

text
HEADING(标题) / TEXT(正文) / CODE(代码)
LIST(列表)   / QUOTE(引用) / TABLE(表格) / IMAGE(图片)

3. 核心对象逐个说明

KnowledgeBase —— 知识库

文档和检索范围的顶层容器。字段:idnamedescriptioncreated_atupdated_at

Document —— 文档元数据

描述一份上传的原始文档及其处理状态:

字段含义
id系统生成的 UUID
knowledge_base_id所属知识库
filename展示用文件名(已净化 basename)
mime_type / extension客户端声明,用于 Parser 路由
object_keyMinIO 对象键(系统生成)
sha256内容指纹
status当前处理状态
parser_name / parser_version实际使用的解析器
error_message失败原因(可操作)

SourceLocator —— 来源定位器(★ 关键设计)

记录「一段内容来自原始文件的哪里」。字段全部可选,因为不同格式定位精度不同:

字段使用场景
heading_pathMarkdown / Word / PPT / HTML / PDF:章节路径
pagePDF:页码
bboxPDF:坐标框(左上角原点)
sheet / cell_rangeExcel:工作表 + 单元格区域
slidePPT:幻灯片序号

它贯穿整个链路,最终形成 Citation:

text
Block → Chunk → PostgreSQL/Milvus → RetrievalResult → Citation → 前端展示

Block —— 解析后的最小语义块

Parser 的输出单元:id(稳定 UUID5)、type(BlockType)、contentlocatormetadata

ParsedDocument / ParsedAsset —— 统一解析结果

ParsedDocument 包含 document_id + blocks[] + assets[] + metadata这是所有 Parser 的统一 出口,下游不再感知原始格式。普通格式的 assets 为空;PDF 图片同时产生:

  • IMAGE Block:asset:// Markdown + 题注 + Vision 描述,供 Chunk/Embedding/Retrieval;
  • ParsedAsset:JPEG 字节、稳定 ID、Block ID、题名、摘要和 Locator,供应用层持久化。

DocumentAsset —— 持久化资源事实

记录 Asset 与 document_id / block_id / object_key / media_type / sha256 / locator 的关联。API 只公开 content_url,不会把 MinIO Object Key 交给浏览器或 LLM。

Chunk —— 可检索文本单元

真正被向量化、检索、引用的单元:

字段含义
id稳定 ID(由文档ID+序号+内容生成,幂等)
knowledge_base_id / document_id归属
index在文档中的顺序
content文本内容(含标题前缀)
heading_path章节路径(用于检索展示与 Citation)
token_countToken 数(与实际切分同一 Tokenizer)
locator来源定位
metadata切块策略、来源标签、文件名、V3 parent_id/parent_child_*

EmbeddedChunk —— Chunk + 向量

chunk + embedding。写入 Milvus 前的组合。

RetrievalResult —— 检索命中

除基础来源字段外,V3 还保留 dense_score / sparse_score / fusion_score / rerank_scoreretrieval_sourcesmatched_contentcontext_chunk_idscontent_typesassetsscore 表示当前最终排序实际使用的分数,不能跨模式或跨请求直接比较。

保留完整来源信息,检索结果可以直接构造 Citation,无需再查库(避免 N+1)。

Citation —— 面向 API 的引用

document_id / filename / chunk_id / heading_path / locator / context_chunk_ids。不暴露向量库内部字段, 并同时保留精确命中锚点与 Small2Big 实际上下文范围。

RetrievalOptions / RetrievalTrace / RetrievalRun

  • RetrievalOptions:一次请求的模式、候选宽度、阶段开关和文档白名单
  • RetrievalTrace:查询变体、候选/结果数、实际执行阶段和降级原因
  • RetrievalRun:不可变的 results + trace,供 Explain API 与 RAG 共用

IngestionJob —— 后台任务快照

Worker 处理的任务:id / document_id / status / attempts / max_attempts / available_at / locked_at / worker_id / error_message

ChatEvidence —— 历史回答的检索快照

助手消息除正文外还保存 Citation[] + RetrievalResult[] + RetrievalTrace。它不是新的知识事实, 而是“这次回答当时使用了什么证据”的审计快照,使刷新或恢复历史会话后,[来源 N] 侧栏与 asset:// 图片仍然可用。

4. 数据在哪些层如何变化

text
原始文件字节
   ↓ Parser
ParsedDocument + Block[] + ParsedAsset[]
   ↓ Chunker
Chunk[]                         (Domain 模型)
   ├─ Asset → MinIO + PostgreSQL document_assets
   ↓ Embedder
EmbeddedChunk[]                 (Domain 模型)
   ↓ Milvus Dense + BM25 → RRF → Rerank → Small2Big
检索 → RetrievalRun             (RetrievalResult[] + RetrievalTrace)
   ↓ ContextBuilder
上下文文本                       (普通字符串)
   ↓ LLM
答案 + Citation[] + ChatEvidence(Domain 模型)

注意到没有?从 ParsedDocument 到 RetrievalResult,全程都是不可变 dataclassfrozen=True)。这保证了数据在跨层传递时的安全性,也让测试非常方便。

5. 端口(Protocol)—— 可替换能力的契约

domain/ports.py 只为明确存在多实现或需要隔离外部依赖的能力定义小端口。下面是核心片段:

python
class DocumentParser(Protocol):
    name: str
    version: str
    def supports(self, source: DocumentSource) -> bool: ...
    async def parse(self, source: DocumentSource) -> ParsedDocument: ...

class Embedder(Protocol):
    async def embed_documents(self, texts: Sequence[str]) -> list[list[float]]: ...
    async def embed_query(self, query: str) -> list[float]: ...

class VectorStore(Protocol):
    async def ensure_collection(self) -> None: ...
    async def upsert(self, chunks: Sequence[EmbeddedChunk]) -> None: ...
    async def upsert_sparse(self, chunks: Sequence[Chunk]) -> None: ...
    async def search(self, query_vector, knowledge_base_id, top_k) -> list[RetrievalResult]: ...
    async def search_sparse(self, query, knowledge_base_id, top_k) -> list[RetrievalResult]: ...
    async def delete_by_document(self, document_id: str) -> None: ...
    async def delete_by_knowledge_base(self, knowledge_base_id: str) -> None: ...

class QueryRewriter(Protocol):
    async def rewrite(self, query: str) -> str | None: ...

class Reranker(Protocol):
    async def rerank(self, query, candidates, top_n) -> list[RerankResult]: ...

class LLMClient(Protocol):
    async def generate(self, system_prompt: str, user_prompt: str) -> str: ...
    def stream(self, system_prompt: str, user_prompt: str) -> AsyncIterator[str]: ...

DocumentSource —— Parser 的输入

交给 Parser 的「原始文档快照」:document_id / filename / mime_type / content。不包含任何基础设施句柄。

6. 业务异常

domain/exceptions.py 定义了可预期的业务异常,接口层依据它们生成稳定的 HTTP 状态码:

异常HTTP场景
ResourceNotFoundError404知识库/文档不存在
InvalidDocumentError400上传文件不合法
UnsupportedDocumentTypeError400没有可用的 Parser
DocumentBusyError409文档正在后台处理,暂不能删除
DocumentProcessingError502解析/切块/向量化/索引失败
ExternalServiceError502外部服务故障

下一步

UltimateRAG · 从最小可用 RAG 演进为企业级知识平台