基于Apache Doris构建知识图谱增强RAG系统:从文档处理到智能问答
1. 先搞清楚这个方案到底解决了什么问题
如果你正在搭建一个基于大模型的问答系统,尤其是需要处理企业文档、技术手册这类内部知识,那你肯定遇到过两个核心痛点:一是传统的RAG(检索增强生成)方案,面对复杂的、涉及多个实体和关系的查询时,回答容易变得零散、不准确;二是技术栈太杂,向量库、关系型数据库、图数据库各管一摊,开发和运维成本高。
这篇文章要讲的,就是如何用 Apache Doris (或它的云托管版 SelectDB )作为统一的数据底座,从零搭建一个完整的RAG系统,并且更进一步,引入 知识图谱 来增强复杂问题的回答能力。它最核心的价值不是展示某个新潮的AI功能,而是提供了一个 工程上可落地、架构上更简洁 的实践路径:用一个数据库,同时搞定向量检索、结构化查询和图关系存储,把系统复杂度降下来,把回答的准确性和逻辑性提上去。
整个流程会覆盖从文档处理、向量化存储、基础检索,到实体关系抽取、知识图谱构建和基于图谱的增强检索。我会把环境准备、代码实现、参数调优和实际踩过的坑都拆开讲清楚。无论你是想快速验证一个原型,还是为生产环境做技术选型,这套方案都能给你一个清晰的参考。
2. 环境与工具准备:别急着写代码,先把路铺平
在动手之前,先把需要的“家伙事儿”备齐。这个方案的核心组件都是当前主流且易于获取的,但版本和配置对后续的顺利运行至关重要。
2.1 核心组件清单与选型理由
- 向量数据库/分析数据库:Apache Doris
- 为什么是它? 传统方案里,你可能需要用一个向量数据库(如 Milvus)存向量,再用一个关系库存元数据,复杂查询还得靠应用层拼接。Doris 的 HSAP(混合服务与分析处理) 能力,让它能在一张表里同时做高效的向量近似检索(ANN)和灵活的结构化 SQL 过滤。这意味着你不需要维护两套系统,简化了架构。生产环境如果不想自己运维集群,可以直接用 SelectDB Cloud ,它是基于 Doris 的托管服务。
- 大语言模型 (LLM):DeepSeek API
- 为什么选它? 我们需要两个 LLM 能力:一是用于最终答案生成,二是用于从文本中抽取实体和关系。DeepSeek API 性价比高,对中文支持好,且提供了稳定的 Chat 接口,非常适合这种实验和生产之间的场景。你也可以替换为 OpenAI GPT、通义千问等任何兼容 OpenAI 格式的 API。
- 嵌入模型 (Embedding Model):Ollama +
bge-m3:latest- 为什么本地部署? 向量生成是高频操作,使用本地部署的嵌入模型可以避免网络延迟和 API 调用成本。
bge-m3模型在中文文本表征上表现优异,且支持多语言。Ollama 则是一个极其方便的本地大模型运行和管理的工具。
- 为什么本地部署? 向量生成是高频操作,使用本地部署的嵌入模型可以避免网络延迟和 API 调用成本。
- 开发框架:LangChain
- 为什么用它? LangChain 提供了丰富的工具链来处理文档加载、文本分割(分块)、向量化等流程,能极大减少我们编写样板代码的工作量。虽然它有时显得“重”,但对于快速构建和验证 RAG 流水线非常合适。
- 数据处理与图谱构建:Pandas, NetworkX, Pyvis
- Pandas 用于数据清洗和格式化,方便导入 Doris。
- NetworkX 是 Python 经典的图计算库,用于在内存中构建和操作知识图谱。
- Pyvis 用于将 NetworkX 图生成交互式的 HTML 可视化页面,方便调试和展示。
2.2 具体安装与配置步骤
第一步:部署 Apache Doris 这是整个系统的基石。对于本地测试,单机部署是最快的方式。
- 下载与解压: 从 Apache Doris 官网 下载最新稳定版的二进制包。
- 启动 FE(前端): 进入
fe目录,修改conf/fe.conf,确保元数据目录正确,然后运行./bin/start_fe.sh --daemon。 - 启动 BE(后端): 进入
be目录,修改conf/be.conf,配置存储路径,然后运行./bin/start_be.sh --daemon。 - 连接与初始化: 使用 MySQL 客户端(如
mysql命令)连接 Doris FE(默认端口 9030,用户root,密码为空)。执行SHOW FRONTENDS;和SHOW BACKENDS;确认节点健康。
注意: 生产环境请务必参考官方文档进行集群化部署和参数调优。如果选择 SelectDB Cloud,这一步可以跳过,直接在控制台创建集群即可。
第二步:安装 Python 环境及依赖 建议使用 Python 3.8+ 版本,并创建独立的虚拟环境。
# 创建虚拟环境
python -m venv doris-rag-env
source doris-rag-env/bin/activate # Linux/macOS
# doris-rag-env\Scripts\activate # Windows
# 安装核心依赖
pip install langchain langchain-community langchain-openai pandas
pip install networkx pyvis
pip install pymysql # Doris Python 客户端依赖
第三步:部署 Ollama 及嵌入模型
- 前往 Ollama 官网 下载并安装。
- 安装完成后,拉取
bge-m3模型:ollama pull bge-m3:latest - 启动 Ollama 服务(通常安装后会自动启动)。检查服务是否运行在
http://localhost:11434。
第四步:准备 DeepSeek API 去 DeepSeek 官网注册账号并获取 API Key。记下你的 api_key 和 API 的 base_url (例如 https://ark.cn-beijing.volces.com/api/v3 )。
环境准备好后,我们才真正进入核心环节。很多人在这一步就卡住,问题往往出在 Doris 集群没启动成功、Ollama 服务没跑起来,或者 Python 环境冲突。我建议按顺序检查:Doris 进程 -> Ollama 服务 -> Python 包导入。
3. 基础 RAG 实现:从文档到智能问答
基础 RAG 是入门必备,它的流程清晰:文档处理 -> 向量化存储 -> 检索 -> 生成答案。我们用 Doris 来承载最关键的向量存储和检索环节。
3.1 在 Doris 中创建向量表
连接上 Doris 后,第一件事是建库建表。这张表要同时存储文本内容 ( content ) 和它的向量表示 ( embedding ),并且要对 embedding 列建立高效的 ANN 索引。
-- 创建专属数据库
CREATE DATABASE IF NOT EXISTS doris_rag_demo;
USE doris_rag_demo;
-- 创建核心向量表
CREATE TABLE IF NOT EXISTS `doc_chunks` (
`id` BIGINT NULL COMMENT '片段唯一ID',
`source` VARCHAR(255) NULL COMMENT '文档来源',
`content` TEXT NULL COMMENT '文本内容',
`embedding` ARRAY<FLOAT> NOT NULL COMMENT '1024维向量',
-- 关键:在embedding列上创建HNSW索引,用于近似最近邻搜索
INDEX idx_embedding (`embedding`) USING ANN PROPERTIES(
"dim" = "1024", -- 向量维度,必须与模型输出维度一致
"ef_construction" = "40", -- 索引构建参数,影响构建速度和精度
"index_type" = "hnsw", -- 索引类型,HNSW是当前主流
"max_degree" = "32", -- HNSW图中每个节点的最大连接数
"metric_type" = "inner_product" -- 距离度量方式,内积(对应余弦相似度)
)
) ENGINE=OLAP
DUPLICATE KEY(`id`) -- 明细模型,允许重复Key
DISTRIBUTED BY HASH(`id`) BUCKETS 4 -- 根据数据量调整分桶数
PROPERTIES (
"replication_allocation" = "tag.location.default: 1" -- 副本数
);
参数解释与调优建议:
dim=1024: 必须与你使用的嵌入模型(如bge-m3)的输出维度完全匹配,否则数据无法插入。metric_type="inner_product": 对于bge-m3这类经过归一化的向量,使用内积等价于计算余弦相似度。如果你的模型输出未归一化,可能需要使用L2(欧氏距离)。ef_construction和max_degree: 这是 HNSW 索引的核心参数。ef_construction越大,索引构建越慢,但精度越高。对于千万级以下数据,40-100 是常见范围。max_degree影响索引的复杂度和检索速度。通常 16-64 之间。- 新手建议 :首次测试直接用上述默认值。待数据量上来后,再根据 Doris 官方文档进行索引性能调优。
3.2 文档处理与向量化入库
现在,我们有一份关于 Apache Doris 的技术文档(假设是 doris_doc.txt )。直接把它扔给模型效果很差,必须进行分块(Chunking)。
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.embeddings import OllamaEmbeddings
import pandas as pd
# 1. 读取文档
with open('doris_doc.txt', 'r', encoding='utf-8') as f:
full_text = f.read()
# 2. 智能文本分块
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=400, # 每个块的最大字符数
chunk_overlap=50, # 块之间的重叠字符数,保持上下文连贯
length_function=len,
separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""] # 按语义分割
)
chunks = text_splitter.split_text(full_text)
print(f"文档被分割成 {len(chunks)} 个文本块。")
# 3. 初始化嵌入模型
embeddings = OllamaEmbeddings(
model='bge-m3:latest',
base_url='http://localhost:11434' # Ollama 服务地址
)
# 4. 批量生成向量(效率远高于单条生成)
chunk_texts = [chunk for chunk in chunks]
print("正在生成文本向量,这可能需要一些时间...")
vectors = embeddings.embed_documents(chunk_texts) # 返回 List[List[float]]
print("向量生成完毕。")
# 5. 组装数据,准备导入Doris
data = []
for idx, (chunk, vec) in enumerate(zip(chunks, vectors)):
data.append({
'id': idx + 1000, # 生成一个起始ID
'source': 'doris_doc.txt',
'content': chunk,
'embedding': vec
})
df = pd.DataFrame(data)
print(df[['id', 'content']].head(2)) # 查看前两条数据
关键点与避坑:
- 分块策略是 RAG 效果的基石 。
chunk_size太小,信息碎片化;太大,向量表征可能模糊,且检索时携带的无关噪声多。对于技术文档,400-800 字符是常见的起始点。chunk_overlap必不可少,它能防止关键信息被割裂在块边界。 - 批量嵌入 :务必使用
embed_documents而不是循环调用embed_query,前者效率高一个数量级。 - 内存与性能 :如果文档极大(如数万块),一次性生成所有向量可能内存不足。需要实现分批处理,并可能要将向量先暂存到本地文件,再导入 Doris。
3.3 将向量数据导入 Doris
我们需要使用 Doris 的 Python 客户端来连接并插入数据。这里演示使用 pymysql 客户端(因为 Doris 兼容 MySQL 协议)。
import pymysql
import json
# 配置 Doris 连接信息
connection = pymysql.connect(
host='localhost', # Doris FE 地址
port=9030, # MySQL 协议端口
user='root',
password='', # 你的密码
database='doris_rag_demo',
charset='utf8mb4'
)
cursor = connection.cursor()
# 准备插入SQL
insert_sql = """
INSERT INTO doc_chunks (id, source, content, embedding)
VALUES (%s, %s, %s, %s)
"""
# 逐条插入数据
for _, row in df.iterrows():
# 将Python list转换为Doris可识别的数组字符串格式,如 `[0.1, 0.2, ...]`
embedding_str = json.dumps(row['embedding'])
cursor.execute(insert_sql, (int(row['id']), row['source'], row['content'], embedding_str))
connection.commit()
cursor.close()
connection.close()
print(f"成功导入 {len(df)} 条向量数据到 Doris。")
插入优化提示:
- 对于海量数据,单条插入效率极低。应使用 Doris 的
Stream Load或Broker Load功能进行批量导入,速度可提升百倍以上。这里为了演示清晰,使用了单条插入。 - 插入后,可以执行
SELECT COUNT(*) FROM doc_chunks;和SELECT * FROM doc_chunks LIMIT 1;验证数据是否完整。
3.4 实现检索与问答闭环
数据就绪后,就可以实现问答了。流程是:用户提问 -> 将问题向量化 -> 在 Doris 中检索相似文本块 -> 将文本块作为上下文交给 LLM 生成答案。
from langchain_openai import ChatOpenAI
# 1. 用户查询
query = "Apache Doris 的存储模型有哪些?各自适用什么场景?"
# 2. 将查询转换为向量
query_vector = embeddings.embed_query(query) # 注意这里用 embed_query
# 3. 在 Doris 中执行向量检索 (使用 SQL)
search_sql = """
SELECT id, content,
cosine_similarity(embedding, %s) as similarity
FROM doc_chunks
ORDER BY similarity DESC
LIMIT 5
"""
# 注意:cosine_similarity 是 Doris 2.1+ 版本提供的函数。早期版本需用内积计算。
cursor = connection.cursor(pymysql.cursors.DictCursor)
cursor.execute(search_sql, (json.dumps(query_vector),))
top_chunks = cursor.fetchall()
cursor.close()
print("检索到的相关文本块:")
for i, chunk in enumerate(top_chunks):
print(f"\n--- 块 {i+1} (相似度: {chunk['similarity']:.4f}) ---")
print(chunk['content'][:200] + "...") # 打印前200字符
# 4. 构建提示词,调用 LLM 生成答案
context = "\n\n".join([chunk['content'] for chunk in top_chunks])
prompt_template = f"""你是一个专业的数据库技术助手。请严格根据以下提供的上下文信息来回答问题。如果上下文信息不足以回答问题,请直接说“根据已知信息无法回答该问题”。
上下文信息:
{context}
问题:{query}
请给出准确、清晰的回答:"""
llm = ChatOpenAI(
model='deepseek-chat', # 或你使用的其他模型名称
api_key='your_deepseek_api_key_here',
base_url='https://ark.cn-beijing.volces.com/api/v3',
temperature=0.1, # 降低随机性,让答案更确定
)
response = llm.invoke(prompt_template)
print("\n" + "="*50)
print("最终回答:")
print(response.content)
至此,一个可运行的基础 RAG 系统就完成了。它能回答文档中明确提及的事实性问题。但如果你问“ Doris 被哪些互联网公司使用,它们分别用它解决了什么问题? ”,基础 RAG 可能只会返回几个包含公司名字的片段,无法系统性地梳理出“公司-使用场景”的关系网络。这就需要知识图谱登场了。
4. 知识图谱增强 RAG:让回答更有逻辑
基础 RAG 的短板在于“知识是扁平的”,它检索到的是一堆相关但不连贯的文本片段。知识图谱则把知识变成“结构化的网络”,能明确表达“实体-关系-实体”这样的三元组。增强的思路是:先从文档中抽取出图谱,存入 Doris;当用户进行复杂查询时,先在图谱中检索相关实体和关系,形成一个逻辑子图,再将这个结构化的子图作为上下文给 LLM,从而生成逻辑性更强的答案。
4.1 从文档中抽取实体与关系
这是知识图谱构建最关键也最具挑战的一步。我们利用 LLM 强大的理解能力,通过设计精妙的提示词(Prompt)来实现信息抽取。
def extract_entities_relations(text_chunk, llm_client):
"""从单个文本块中抽取实体和关系"""
prompt = f"""
你是一个信息抽取专家。请从以下技术文档文本中,识别出实体以及实体之间的关系。
**实体类型限定为**:
- Organization(组织,如公司、基金会、社区)
- Person(人物,如开发者、创始人)
- Concept(概念,如技术术语、模型、特性)
- Product(产品,如软件、工具)
- Event(事件,如发布、捐赠、会议)
**输出格式必须严格遵守**:
1. 每个实体一行:`ENTITY|||实体名称|||实体类型|||实体描述`
2. 每个关系一行:`RELATION|||实体A名称|||实体B名称|||关系描述|||置信度(0-1)`
**要求**:
- 只输出识别出的实体和关系,不要有任何额外的解释、总结或标记。
- 关系描述应简洁、客观,如“开发了”、“使用了”、“捐赠给”、“基于...构建”。
- 置信度是你对这条关系是否真实存在于文本中的判断。
文本内容:
{text_chunk}
"""
try:
response = llm_client.invoke(prompt)
lines = response.content.strip().split('\n')
entities = []
relations = []
for line in lines:
if line.startswith('ENTITY|||'):
parts = line.split('|||')
if len(parts) >= 4:
entities.append({
'name': parts[1],
'type': parts[2],
'description': parts[3]
})
elif line.startswith('RELATION|||'):
parts = line.split('|||')
if len(parts) >= 6:
relations.append({
'source': parts[1],
'target': parts[2],
'relation': parts[3],
'strength': float(parts[4]) if parts[4].replace('.','',1).isdigit() else 0.5
})
return entities, relations
except Exception as e:
print(f"抽取过程出错: {e}")
return [], []
# 对之前分好的 chunks 进行抽取(可以选择实体密集的片段)
sample_chunks = chunks[:5] # 假设用前5个块演示
all_entities = []
all_relations = []
for chunk in sample_chunks:
ents, rels = extract_entities_relations(chunk, llm)
all_entities.extend(ents)
all_relations.extend(rels)
print(f"从块中抽取到 {len(ents)} 个实体,{len(rels)} 条关系。")
经验之谈:
- 提示词工程是关键 :定义清晰的实体类型和严格的输出格式,能极大提高 LLM 输出的结构化程度,方便后续解析。这里的
|||分隔符比 JSON 更稳定,不易被 LLM 意外格式化。 - 后处理不可少 :LLM 的输出可能有格式错误或冗余。需要健壮的解析代码,并考虑对抽取出的实体进行 归一化 (例如,“Apache Doris”和“Doris”应视为同一实体)。
- 性能考虑 :对大量文档进行全量抽取非常耗时耗钱。生产环境需要考虑增量抽取、缓存、以及使用更小的专用信息抽取模型。
4.2 构建图谱并存入 Doris
抽取出的实体和关系是离散的,我们用 NetworkX 在内存中构建图结构,并可视化检查,然后将图谱数据向量化后存入另一张 Doris 表。
import networkx as nx
from pyvis.network import Network
import uuid
# 1. 构建 NetworkX 图
G = nx.Graph()
# 添加实体节点
for ent in all_entities:
G.add_node(ent['name'], type=ent['type'], desc=ent['description'])
# 添加关系边
for rel in all_relations:
if rel['source'] in G and rel['target'] in G: # 确保节点已存在
G.add_edge(rel['source'], rel['target'], label=rel['relation'], strength=rel['strength'])
print(f"知识图谱构建完成。包含 {G.number_of_nodes()} 个实体,{G.number_of_edges()} 条关系。")
# 2. 可视化(可选,用于调试)
net = Network(notebook=True, height="750px", width="100%", bgcolor="#222222", font_color="white")
net.from_nx(G)
net.show("knowledge_graph.html") # 生成一个可交互的HTML文件
# 3. 准备存入 Doris 的数据
kg_records = []
# 处理实体节点
for node_name, node_data in G.nodes(data=True):
# 为实体生成向量:用“实体名: 描述”作为文本
text_for_embedding = f"{node_name}: {node_data.get('desc', '')}"
kg_records.append({
'id': str(uuid.uuid4()),
'type': 'entity',
'name': node_name,
'entity_type': node_data.get('type', ''),
'description': node_data.get('desc', ''),
'text_for_embedding': text_for_embedding,
'embedding': None # 稍后填充
})
# 处理关系边
for src, tgt, edge_data in G.edges(data=True):
text_for_embedding = f"{src} {edge_data.get('label', 'related_to')} {tgt}"
kg_records.append({
'id': str(uuid.uuid4()),
'type': 'relation',
'name': f"{src}_{tgt}",
'entity_type': '',
'description': edge_data.get('label', ''),
'from_entity': src,
'to_entity': tgt,
'text_for_embedding': text_for_embedding,
'embedding': None
})
# 4. 为图谱数据生成向量
texts_to_embed = [r['text_for_embedding'] for r in kg_records]
kg_vectors = embeddings.embed_documents(texts_to_embed) # 批量生成
for record, vec in zip(kg_records, kg_vectors):
record['embedding'] = vec
# 5. 在Doris中创建图谱存储表
create_kg_table_sql = """
CREATE TABLE IF NOT EXISTS `knowledge_graph` (
`id` VARCHAR(36) NOT NULL COMMENT 'UUID',
`type` VARCHAR(20) NOT NULL COMMENT 'entity or relation',
`name` VARCHAR(255) NULL COMMENT '实体或关系名称',
`entity_type` VARCHAR(50) NULL COMMENT '实体类型',
`description` TEXT NULL COMMENT '描述',
`from_entity` VARCHAR(255) NULL COMMENT '关系起始实体',
`to_entity` VARCHAR(255) NULL COMMENT '关系目标实体',
`text_for_embedding` TEXT NULL COMMENT '用于生成向量的文本',
`embedding` ARRAY<FLOAT> NOT NULL COMMENT '向量',
INDEX idx_kg_embedding (`embedding`) USING ANN PROPERTIES(
"dim" = "1024",
"index_type" = "hnsw",
"metric_type" = "inner_product"
)
) ENGINE=OLAP
DUPLICATE KEY(`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 4
PROPERTIES ("replication_allocation" = "tag.location.default: 1");
"""
# ... (执行建表SQL)
# 6. 将图谱数据批量导入Doris (这里简化,实际应用建议用Stream Load)
# ... (插入数据逻辑,类似基础RAG部分)
现在,我们有两张表: doc_chunks 存储原始文档片段, knowledge_graph 存储从文档中提炼出的结构化知识网络。它们都在同一个 Doris 集群中,可以通过 SQL 进行联合查询。
4.3 基于知识图谱的增强检索与问答
当用户提出一个复杂问题时,流程变为:1. 向量检索图谱中的相关实体;2. 根据实体查找它们之间的所有关系;3. 构建一个知识子图;4. 将子图信息作为上下文提问。
def search_in_kg(query_text, top_k_entities=10):
"""在知识图谱中检索相关实体和关系"""
# 1. 将问题向量化,检索相关实体
query_vec = embeddings.embed_query(query_text)
search_entity_sql = """
SELECT name, description, entity_type
FROM knowledge_graph
WHERE type = 'entity'
ORDER BY cosine_similarity(embedding, %s) DESC
LIMIT %s
"""
cursor.execute(search_entity_sql, (json.dumps(query_vec), top_k_entities))
relevant_entities = cursor.fetchall()
entity_names = [e['name'] for e in relevant_entities]
if not entity_names:
return "未在知识图谱中找到相关实体。"
# 2. 查找这些实体之间的所有关系
placeholders = ','.join(['%s'] * len(entity_names))
search_relation_sql = f"""
SELECT from_entity, to_entity, description
FROM knowledge_graph
WHERE type = 'relation'
AND (from_entity IN ({placeholders}) OR to_entity IN ({placeholders}))
"""
params = entity_names + entity_names
cursor.execute(search_relation_sql, params)
relevant_relations = cursor.fetchall()
# 3. 构建知识子图描述
knowledge_context = "【知识图谱检索结果】\n"
knowledge_context += "相关实体:\n" + "\n".join([f"- {e['name']} ({e['entity_type']}): {e['description']}" for e in relevant_entities])
knowledge_context += "\n\n实体间关系:\n" + "\n".join([f"- {r['from_entity']} --[{r['description']}]--> {r['to_entity']}" for r in relevant_relations])
return knowledge_context
# 示例:复杂查询
complex_query = "请介绍百度与Apache Doris项目之间的关系,以及Doris在互联网公司中的应用情况。"
kg_context = search_in_kg(complex_query)
# 结合图谱上下文和原始文档片段(可选)进行回答
combined_prompt = f"""请你作为一名技术分析师,综合以下两部分信息来回答问题。
第一部分是来自知识图谱的结构化信息:
{kg_context}
第二部分是来自原始文档的相关文本片段(作为补充):
{context} # 这里可以复用基础RAG检索到的top_chunks
问题:{complex_query}
请基于上述信息,组织一个条理清晰、信息准确的回答。如果信息不足,请明确指出。"""
final_answer = llm.invoke(combined_prompt)
print(final_answer.content)
通过这种方式,对于“关系型”问题,LLM 得到的上下文不再是几段可能提及“百度”、“Doris”、“公司”的孤立文本,而是一个明确的结构:“百度 --[捐赠给]--> Apache基金会”、“Apache Doris --[被使用于]--> 字节跳动”、“Apache Doris --[被使用于]--> 腾讯”。答案的逻辑性和准确性自然会大幅提升。
5. 生产级考量与常见问题排查
把 Demo 跑通只是第一步,要真正用到生产环境,还有一堆细节需要处理。下面是我从实际项目中总结的几个关键点和排查清单。
5.1 性能、扩展性与稳定性
- 向量索引调优 :
- 问题 :数据量达到百万级后,检索速度变慢。
- 排查 :监控 Doris BE 节点的 CPU、内存和 I/O。使用
EXPLAIN分析向量检索 SQL。 - 优化 :调整 HNSW 索引参数 (
ef_construction,max_degree)。对于超大规模向量,考虑使用 Doris 的 倒排索引 进行前置过滤,减少向量检索范围。定期对表进行 Compaction 以优化数据分布。
- 数据更新与一致性 :
- 问题 :文档更新后,如何同步更新向量和知识图谱?
- 方案 :为每个文档块和知识图谱条目增加版本号或更新时间戳。采用“标记删除+重新导入”的策略。对于图谱,实现增量抽取和更新算法,避免全量重建。
- 系统架构 :
- 单体应用瓶颈 :所有逻辑写在一个脚本里,扩展性差。
- 演进 :将系统拆分为微服务。例如:
文档处理服务、向量检索服务、图谱查询服务、LLM 网关服务。Doris 作为统一的数据服务层,为这些微服务提供数据访问能力。
- 成本控制 :
- LLM API 调用 :对检索结果进行重排序(Re-ranking),只将最相关的少量上下文发送给 LLM,减少 Token 消耗。对常见问题建立答案缓存。
- 嵌入模型 :对于非关键路径或冷数据,可以考虑使用更小、更快的嵌入模型。
5.2 效果优化与高级技巧
- 查询意图分类(Routing) :
- 在检索前,先用一个轻量级模型或规则判断用户问题是“简单事实型”还是“复杂关系型”。
- 简单问题走基础 RAG 路径(检索
doc_chunks),复杂问题走知识图谱增强路径(检索knowledge_graph)。两者可以并行执行,然后根据置信度融合结果。
- 检索增强(Hybrid Search) :
- 不要只依赖向量检索。Doris 支持 全文检索 。可以将向量相似度得分和全文检索的关键词匹配得分进行加权融合,提高召回率。
SELECT id, content, (0.7 * cosine_similarity(embedding, %s) + 0.3 * MATCH(content) AGAINST(%s)) AS combined_score FROM doc_chunks WHERE MATCH(content) AGAINST(%s) -- 全文检索条件 ORDER BY combined_score DESC LIMIT 10; - Agentic RAG :
- 对于极其复杂、需要多步推理的问题(例如“对比 Doris 和 ClickHouse 在实时场景下的优劣,并给出选型建议”),可以引入智能体(Agent)框架。Agent 可以规划步骤:先检索各自特点,再检索对比文章,最后检索应用案例,然后综合所有信息生成答案。Doris 可以作为 Agent 背后统一的知识存储和查询引擎。
5.3 典型问题排查清单
当你的 RAG 系统效果不佳时,可以按以下顺序排查:
| 问题现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 检索结果完全不相关 | 1. 嵌入模型不匹配(如用英文模型处理中文)。 2. 文本分块过大或过小。 3. 向量索引未正确建立或参数不合理。 |
1. 检查嵌入模型名称和维度。 2. 打印几个文本块和其向量,看是否合理。 3. 用简单查询测试向量检索 SQL 是否返回结果。 |
| LLM 回答胡言乱语或拒答 | 1. 检索到的上下文质量太差。 2. 提示词(Prompt)设计不佳。 3. 上下文过长,超出模型 Token 限制。 |
1. 先检查 top_chunks 的内容是否真的与问题相关。 2. 简化 Prompt,加入“根据上下文回答”的强指令。 3. 减少 LIMIT 数量,或对检索结果进行摘要。 |
| 知识图谱抽取结果混乱 | 1. 提示词定义模糊。 2. LLM 未严格遵循输出格式。 3. 实体归一化没做好。 |
1. 优化抽取 Prompt,提供更明确的例子。 2. 加强输出解析代码的容错性。 3. 实现实体链接,将“Doris”、“Apache Doris”合并。 |
| 系统响应缓慢 | 1. 向量检索慢。 2. LLM API 调用慢。 3. 网络或 Doris 集群负载高。 |
1. 检查 Doris 向量检索的 EXPLAIN 计划,优化索引。 2. 为 LLM 调用设置超时和重试,考虑异步化。 3. 监控集群资源,检查是否有慢查询。 |
| 数据无法导入 Doris | 1. 向量维度不匹配。 2. 数组格式错误。 3. 单次插入数据量太大。 |
1. 确认建表时的 dim 与向量实际维度一致。 2. 确保插入的向量字符串是合法的 JSON 数组格式。 3. 改用 Stream Load 进行批量导入。 |
这套基于 Apache Doris/SelectDB 的 RAG 方案,最大的优势在于 统一和简化 。你不需要在多个数据库之间同步数据、处理一致性问题。无论是简单的文档片段、它们的向量,还是复杂的知识图谱,都可以用 SQL 在一个地方管理起来。对于从零开始构建 AI 应用的数据团队来说,这能省去大量的架构设计和运维成本。
从基础 RAG 到知识图谱增强,是一个效果和复杂度逐步提升的过程。我建议的落地路线是: 先用基础 RAG 解决 80% 的简单问答,跑通全流程并稳定运行;再针对那 20% 的复杂关系型问题,引入知识图谱模块 。在这个过程中,Doris 作为底层存储,可以平滑地支撑你的架构演进。
更多推荐

所有评论(0)