最近在尝试把 RAG 系统从“玩具”搬到“产线”时我遇到了一个很典型的问题当用户问“Doris 是哪家公司捐赠给 Apache 基金会的又被哪些公司使用”时基础 RAG 返回的答案往往是割裂的。它可能从某个文档片段里找到“百度捐赠”从另一个片段里找到“字节跳动使用”但无法将这些信息组织成一个连贯、有逻辑的叙述。这背后暴露的是传统 RAG 在处理多实体、多关系复杂查询时的天然短板——知识碎片化。为了解决这个问题我开始探索将知识图谱Knowledge Graph与 RAG 结合。但随之而来的新问题是技术栈变得异常复杂。我需要一个向量数据库存文本片段一个图数据库存实体关系可能还需要一个传统数据库做元数据管理。系统复杂度陡增维护成本和查询延迟也随之而来。直到我深入研究了 Apache Doris一个想法逐渐清晰为什么不能用一个系统来统一承载这些需求Doris 作为一款高性能的实时分析型数据库其 HSAPHybrid Serving/Analytical Processing能力意味着它既能做低延迟的向量近似检索ANN也能高效执行复杂的结构化关系查询。这为构建一个“知识图谱增强的 RAG”系统提供了理想的数据基座。本文将分享我基于 Doris 和 SelectDB其商业发行版从零搭建这套系统的完整实战经验重点不是代码的堆砌而是如何设计一个兼顾性能、简洁性和扩展性的架构。1. 为什么是 Doris重新思考 RAG 系统的数据基座在构建 RAG 系统时我们通常首先想到的是专门的向量数据库。这没错但它们往往只解决了“检索”这一环。一个完整的、面向生产的 RAG 系统数据流要复杂得多原始文档管理文档的元数据来源、更新时间、权限需要管理。文本处理流水线分片、清洗、嵌入生成这个过程的中间状态和日志需要追踪。向量与元数据联合检索用户的问题往往需要结合语义相似度向量和属性过滤如“仅搜索某部门文档”、“仅搜索本周更新内容”来得到最相关片段。知识图谱的存储与查询实体、关系这类结构化知识需要支持高效的图遍历查询。系统监控与优化检索耗时、命中率、用户反馈等指标需要实时分析。如果用多个异构数据库来承载这些需求你会面临数据同步、一致性问题以及跨系统查询带来的复杂度和延迟。Doris 的吸引力在于它试图用一个系统来覆盖这些场景。其核心能力可以概括为三点统一的向量检索原生支持 HNSW 等 ANN 索引可以直接在表中定义ARRAYFLOAT类型的向量字段并创建索引实现毫秒级的近似最近邻搜索。强大的结构化分析作为 MPP 分析型数据库它对多表 JOIN、复杂聚合、窗口函数等 SQL 操作有极强的性能。这意味着你可以在一次查询中轻松地结合向量相似度得分和结构化过滤条件。高效的数据处理支持流式、批量数据导入并且能够高效处理半结构化数据如 JSON。这对于处理从 LLM 抽取出来的、格式可能不统一的实体关系数据非常友好。因此选择 Doris 或 SelectDB 作为数据基座不是一个简单的“用数据库存向量”的替代方案而是一种架构上的简化将 RAG 系统中分散的数据存储、计算和检索需求收敛到一个高性能、统一的数据平台中。这能显著降低运维复杂度并为后续的查询优化、系统扩展打下坚实基础。2. 搭建基础 RAG从文档到智能问答的完整链路在引入知识图谱之前我们先搭建一个稳固的基础 RAG 系统。这个系统虽然“基础”但包含了所有核心环节是后续增强的基石。整个过程可以概括为“离线处理在线服务”两个阶段。2.1 环境准备与数据建模首先你需要一个运行中的 Doris 实例。可以选择自行部署 Apache Doris或者使用 SelectDB Cloud 等托管服务后者能省去大量集群运维工作。这里假设你已经有一个可连接的 Doris 服务。接下来是设计表结构。在基础 RAG 中我们主要存储文本片段chunk及其向量。在 Doris 中创建表时关键是为向量字段创建 HNSW 索引。-- 创建专属数据库 CREATE DATABASE IF NOT EXISTS doris_rag_demo; USE doris_rag_demo; -- 创建核心的文本片段表 CREATE TABLE document_chunks ( id BIGINT NOT NULL, -- 片段唯一ID doc_id VARCHAR(255) NULL, -- 所属文档ID chunk_index INT NULL, -- 在文档中的顺序 content TEXT NULL, -- 文本内容 embedding ARRAYFLOAT NOT NULL, -- 1024维向量 metadata JSON NULL, -- 额外元数据如来源、作者、更新时间 INDEX idx_embedding (embedding) USING ANN PROPERTIES( -- 定义HNSW向量索引 dim 1024, ef_construction 40, index_type hnsw, max_degree 32, metric_type inner_product -- 内积相似度对应cosine相似度需归一化 ) ) ENGINEOLAP DUPLICATE KEY(id, doc_id) DISTRIBUTED BY HASH(id) BUCKETS 4 -- 根据数据量调整分桶数 PROPERTIES ( replication_allocation tag.location.default: 1 );关键点解析ARRAYFLOAT是 Doris 用于存储向量的数据类型。INDEX ... USING ANN定义了向量索引。dim必须与你的嵌入模型输出维度一致这里是1024。metric_type常用inner_product内积或l2欧氏距离。如果使用余弦相似度需要在生成向量时先做归一化然后使用内积来计算。ef_construction和max_degree是 HNSW 索引的参数影响构建速度和检索精度/速度通常默认值即可大数据量时可微调。2.2 离线处理从原始文档到向量入库这个阶段的目标是将非结构化的文档如 PDF、Word、Markdown转化为结构化的、带向量的数据块并存入 Doris。流程如下文档加载与解析使用 LangChain 的DocumentLoader如PyPDFLoader,UnstructuredMarkdownLoader加载文档。文本分片Chunking这是影响 RAG 效果的关键步骤。分片过大向量表征可能模糊分片过小上下文信息可能丢失。我通常使用RecursiveCharacterTextSplitter并设置重叠overlap来保持片段间的连贯性。from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个片段约500字符 chunk_overlap50, # 重叠50字符避免在句子中间切断重要信息 length_functionlen, separators[\n\n, \n, 。, , , , , , ] # 中文优先分隔符 ) chunks text_splitter.split_text(your_document_text)向量化Embedding为每个文本片段生成向量。可以使用本地部署的模型如通过 Ollama 运行bge-m3也可以调用云 API如 OpenAI, DashScope。务必确保此处使用的模型与建表时定义的向量维度dim匹配。from langchain_community.embeddings import OllamaEmbeddings embeddings OllamaEmbeddings(modelbge-m3:latest, base_urlhttp://localhost:11434) # 批量生成向量效率更高 vectors embeddings.embed_documents([chunk.content for chunk in chunks])数据入库将片段文本、向量、元数据组装成 Pandas DataFrame然后使用 Doris 的 Python 客户端或doris-vector-search库批量导入。import pandas as pd from doris_vector_search import DorisVectorClient, AuthOptions # 准备数据 data [] for i, (chunk, vec) in enumerate(zip(chunks, vectors)): data.append({ id: i, doc_id: doc_001, chunk_index: i, content: chunk, embedding: vec, metadata: {source: doris_manual.pdf, page: i//10} }) df pd.DataFrame(data) # 连接并导入Doris auth AuthOptions(hostyour_host, query_port9030, userroot, password) client DorisVectorClient(doris_rag_demo, auth_optionsauth) table client.create_table(document_chunks, df, if_existsappend) # 追加模式2.3 在线服务检索与生成当用户提问时系统需要实时工作查询向量化使用与离线阶段相同的嵌入模型将用户问题转换为向量。混合检索在 Doris 中执行 ANN 检索并可以轻松结合 SQL 进行过滤。这是 Doris 的优势所在。# 生成查询向量 query_vec embeddings.embed_query(Doris支持哪些存储模型) # 使用向量客户端进行检索并可能结合元数据过滤 search_result ( table.search(query_vec) .limit(5) # 返回最相似的5个片段 .where(metadata[source] doris_manual.pdf) # 可进行结构化过滤 .select([id, content, doc_id]) .to_pandas() )上下文构建与答案生成将检索到的文本片段拼接成上下文Prompt发送给 LLM如 DeepSeek、GPT-4生成最终答案。from langchain_openai import ChatOpenAI context \n\n.join([f[片段 {i1}]: {row[content]} for i, row in search_result.iterrows()]) prompt f基于以下关于Apache Doris的文档片段请回答问题。如果文档中没有相关信息请直接说“根据现有资料无法回答”。 文档片段 {context} 问题Doris支持哪些存储模型 答案 llm ChatOpenAI(modeldeepseek-chat, api_keyyour_key, base_url...) response llm.invoke(prompt) print(response.content)至此一个可运行的基础 RAG 系统就完成了。它能回答基于单一文档片段的简单事实性问题。但当问题涉及多个实体的复杂关系时它的局限性就暴露了。3. 从基础 RAG 到知识图谱增强解决复杂关系查询基础 RAG 的答案来源于检索到的几个独立文本片段。对于“Doris 的捐赠方和使用方”这类问题答案信息散落在不同片段LLM 需要自己“脑补”拼凑容易导致信息不全或逻辑混乱。知识图谱的引入就是为了将非结构文本中的结构化知识实体、关系显式地提取并组织起来。3.1 设计知识图谱增强 RAG 的架构我们的目标不是取代基础 RAG而是增强它。整体架构如下图所示此处为文字描述离线图谱构建流水线原始文档经过基础 RAG 的分片后额外的“信息抽取”模块会利用 LLM 从这些片段中抽取出实体和关系构建成知识图谱并同样存入 Doris。在线混合查询路由当用户查询到来时系统首先判断查询意图。如果是简单的事实问答走基础 RAG 路径如果是涉及多实体关系的复杂查询则走知识图谱路径。知识图谱路径先将查询在知识图谱中进行实体链接和子图检索获取结构化的关系网络再将这个网络作为上下文提供给 LLM 生成答案。这个架构的核心在于知识图谱和文本片段共享同一个数据存储Doris避免了跨系统数据同步的麻烦。3.2 实现知识图谱的构建与存储首先我们在 Doris 中创建存储知识图谱数据的表。这里设计了两类数据实体Entity和关系Relation。CREATE TABLE knowledge_graph ( id VARCHAR(36) NOT NULL, -- UUID type VARCHAR(20) NOT NULL COMMENT entity 或 relation, entity_name VARCHAR(255) NULL COMMENT 实体名称当typeentity时有效, from_entity VARCHAR(255) NULL COMMENT 关系起始实体当typerelation时有效, to_entity VARCHAR(255) NULL COMMENT 关系目标实体, relation_desc TEXT NULL COMMENT 关系描述, properties JSON NULL COMMENT 实体或关系的其他属性如类型、描述、置信度, source_chunk_id BIGINT NULL COMMENT 来源文本片段ID用于追溯, embedding ARRAYFLOAT NOT NULL, -- 实体/关系的向量化表示 INDEX idx_embedding (embedding) USING ANN PROPERTIES( dim 1024, index_type hnsw, metric_type inner_product ), INDEX idx_entity_name (entity_name) USING INVERTED, -- 为实体名创建倒排索引加速精确匹配 INDEX idx_relation_entities (from_entity, to_entity) USING INVERTED -- 加速关系查询 ) ENGINEOLAP DUPLICATE KEY(id) DISTRIBUTED BY HASH(id) BUCKETS 4 PROPERTIES ( replication_allocation tag.location.default: 1 );接下来使用 LLM 从文档片段中抽取知识。这里的关键是设计一个结构化的提示词Prompt让 LLM 以固定格式输出。def extract_kg_from_chunk(chunk_text, chunk_id): 从单个文本片段中抽取实体和关系 prompt f 请从以下文本中提取实体和关系。 实体类型包括[Organization, Person, Location, Event, Concept, Product]。 关系格式为(主语, 谓语, 宾语)。 请以JSON格式输出包含两个字段“entities”和“relations”。 示例输出 {{ entities: [ {{name: Apache Doris, type: Product, description: 一款实时分析型数据库}} ], relations: [ {{from: 百度, to: Apache Doris, relation: 捐赠给, description: 百度将Doris项目捐赠给Apache基金会}} ] }} 文本 {chunk_text} # 调用LLM response llm.invoke(prompt) # 解析JSON响应 try: result json.loads(response.content) # 为每个实体和关系生成唯一ID和向量 kg_records [] for entity in result.get(entities, []): record { id: str(uuid.uuid4()), type: entity, entity_name: entity[name], properties: json.dumps({type: entity[type], desc: entity.get(description, )}), source_chunk_id: chunk_id, embedding: embeddings.embed_query(f{entity[name]}: {entity.get(description, )}) } kg_records.append(record) for rel in result.get(relations, []): record { id: str(uuid.uuid4()), type: relation, from_entity: rel[from], to_entity: rel[to], relation_desc: rel[description], properties: json.dumps({relation: rel[relation]}), source_chunk_id: chunk_id, embedding: embeddings.embed_query(f{rel[from]} {rel[relation]} {rel[to]}: {rel[description]}) } kg_records.append(record) return kg_records except json.JSONDecodeError: print(fChunk {chunk_id} 解析失败) return []最后将抽取出的知识图谱数据批量导入knowledge_graph表。这样我们就拥有了一个可向量检索、也可通过实体名精确查询的知识库。3.3 实现基于知识图谱的检索与问答当处理复杂查询时流程如下实体链接先用查询语句在知识图谱中进行向量检索找出最相关的实体。这步利用了 Doris 的 ANN 索引。def search_related_entities(query, top_k5): query_vec embeddings.embed_query(query) # 使用向量客户端检索实体类型的记录 client DorisVectorClient(doris_rag_demo, auth_optionsauth) table client.open_table(knowledge_graph) results ( table.search(query_vec) .limit(top_k * 2) # 多取一些后续过滤 .where(type entity) # 只检索实体 .select([entity_name, properties]) .to_pandas() ) # 去重并返回实体名列表 entity_names results[entity_name].unique().tolist()[:top_k] return entity_names关系展开与子图构建根据找到的实体在 Doris 中执行精确的 SQL 查询找出所有与这些实体相关的关系构建一个局部知识子图。def get_related_relations(entity_names): if not entity_names: return [] # 构建SQL查询查找任意一端是这些实体的关系 placeholders , .join([%s] * len(entity_names)) sql f SELECT from_entity, to_entity, relation_desc, properties FROM knowledge_graph WHERE type relation AND (from_entity IN ({placeholders}) OR to_entity IN ({placeholders})) ORDER BY from_entity, to_entity # 执行查询获取关系列表 # ... (使用Doris Python客户端执行SQL) return relations结构化上下文生成与答案合成将得到的实体和关系以清晰的结构如列表或简短的陈述句组织成上下文再交给 LLM 生成最终答案。def answer_with_knowledge_graph(query): # 1. 实体链接 entities search_related_entities(query) # 2. 关系展开 relations get_related_relations(entities) # 3. 构建结构化上下文 context 知识图谱信息\n context 相关实体 , .join(entities) \n context 相关关系\n for rel in relations: context f- {rel[from_entity]} --[{rel[relation_desc]}]-- {rel[to_entity]}\n # 4. 调用LLM生成答案 prompt f你是一个知识图谱助手。请根据以下结构化的知识信息准确、简洁地回答用户问题。如果信息不足请说明。 {context} 用户问题{query} 答案 final_answer llm.invoke(prompt) return final_answer.content对于“Doris 是哪家公司捐赠给 Apache 基金会的又被哪些公司或组织所使用”这个问题这个流程会先找到“Apache Doris”、“百度”、“Apache基金会”、“字节跳动”、“腾讯”等实体然后检索出“百度捐赠给Apache基金会”、“字节跳动使用Doris”、“腾讯使用Doris”等关系最后将这些结构化的关系网络交给 LLM。LLM 基于这个清晰的“图”来组织答案其准确性和连贯性会远高于从几个独立文本片段中拼凑信息。4. 工程化思考从原型到生产的关键步骤把上述代码跑通只是一个开始。要让这个系统真正具备“工业级落地能力”还需要在以下几个层面进行深化。4.1 查询路由与混合检索策略一个成熟的系统不会武断地为所有查询选择知识图谱路径。我们需要一个查询路由Query Router机制。一个简单的实现是基于 LLM 对查询意图进行分类def route_query(query): 判断查询类型返回basic或graph classify_prompt f 请判断以下用户问题更适合用哪种方式回答 A. 基础检索basic问题明确答案可能存在于一段连续的文本中。例如“Doris的存储模型有哪些”“如何安装Doris” B. 知识图谱检索graph问题涉及多个实体及其之间的关系、比较、列举等。例如“Doris和ClickHouse有什么区别”“哪些公司使用了Doris它们分别用在哪里”“讲述一下Doris从诞生到开源的历史。” 问题{query} 只返回A或B。 response llm.invoke(classify_prompt).content.strip() return basic if A in response else graph更高级的策略可以是混合检索Hybrid Retrieval同时执行向量检索和知识图谱检索然后对两者的结果进行重排序Re-ranking选择最相关的信息组合成最终上下文。Doris 可以同时支持这两种检索模式为混合策略提供了便利。4.2 数据质量、更新与增量处理信息抽取的准确性LLM 抽取实体和关系可能存在错误或冗余。需要设计校验机制例如通过多轮抽取投票、与已有知识库对齐、或引入人工审核环节来提升质量。知识图谱的更新文档更新后如何增量更新知识图谱一种方案是记录每个文本片段的哈希值当文档变更时重新处理发生变化的片段并增量更新对应的图谱节点和边。Doris 支持高效的UPDATE和DELETE操作可以配合版本号字段来实现增量更新。向量索引的优化随着数据量增长需要关注 HNSW 索引的参数调优如ef_search以及 Doris 集群的扩容以维持毫秒级的检索延迟。4.3 系统的可观测性与评估在生产环境中必须监控系统的关键指标检索指标向量检索的召回率RecallK、响应时间。生成指标LLM 调用的耗时、Token 消耗。业务指标答案的准确率可通过人工或模型评估、用户满意度反馈。可以在 Doris 中创建专门的日志表记录每一次问答的请求、检索结果、最终答案和用户反馈便于后续分析和模型优化。4.4 走向 Agentic RAG知识图谱增强 RAG 是迈向更智能的 Agentic RAG 的重要一步。Agentic RAG 的核心是让系统具备“思考”和“规划”能力。例如面对一个非常复杂的问题Agent 可以将其分解为多个子问题。对于每个子问题动态决定使用基础 RAG、知识图谱查询甚至是调用外部工具如计算器、API。综合各子问题的结果合成最终答案。在这个过程中Doris 作为统一的数据层可以高效地支持 Agent 所需的各种数据查询需求无论是快速的向量相似度匹配还是复杂的多跳关系查询。从基础 RAG 到知识图谱增强再到未来的 Agentic RAG技术路径在演进但对底层数据平台的要求始终如一高性能、易扩展、统一接口。Apache Doris 或 SelectDB 的价值正是在于用一个系统满足了 RAG 架构中多样化的数据存储与查询需求让开发者能更专注于上层应用逻辑的创新而非底层数据基础设施的拼凑与维护。当你开始为 RAG 系统设计数据层时不妨将“统一”作为核心考量点这可能会为你省去未来许多不必要的麻烦。