1. 先搞清楚这个方案到底解决了什么问题

如果你正在搭建一个基于大模型的问答系统,尤其是需要处理企业文档、技术手册这类内部知识,那你肯定遇到过两个核心痛点:一是传统的RAG(检索增强生成)方案,面对复杂的、涉及多个实体和关系的查询时,回答容易变得零散、不准确;二是技术栈太杂,向量库、关系型数据库、图数据库各管一摊,开发和运维成本高。

这篇文章要讲的,就是如何用 Apache Doris (或它的云托管版 SelectDB )作为统一的数据底座,从零搭建一个完整的RAG系统,并且更进一步,引入 知识图谱 来增强复杂问题的回答能力。它最核心的价值不是展示某个新潮的AI功能,而是提供了一个 工程上可落地、架构上更简洁 的实践路径:用一个数据库,同时搞定向量检索、结构化查询和图关系存储,把系统复杂度降下来,把回答的准确性和逻辑性提上去。

整个流程会覆盖从文档处理、向量化存储、基础检索,到实体关系抽取、知识图谱构建和基于图谱的增强检索。我会把环境准备、代码实现、参数调优和实际踩过的坑都拆开讲清楚。无论你是想快速验证一个原型,还是为生产环境做技术选型,这套方案都能给你一个清晰的参考。

2. 环境与工具准备:别急着写代码,先把路铺平

在动手之前,先把需要的“家伙事儿”备齐。这个方案的核心组件都是当前主流且易于获取的,但版本和配置对后续的顺利运行至关重要。

2.1 核心组件清单与选型理由

  1. 向量数据库/分析数据库:Apache Doris
    • 为什么是它? 传统方案里,你可能需要用一个向量数据库(如 Milvus)存向量,再用一个关系库存元数据,复杂查询还得靠应用层拼接。Doris 的 HSAP(混合服务与分析处理) 能力,让它能在一张表里同时做高效的向量近似检索(ANN)和灵活的结构化 SQL 过滤。这意味着你不需要维护两套系统,简化了架构。生产环境如果不想自己运维集群,可以直接用 SelectDB Cloud ,它是基于 Doris 的托管服务。
  2. 大语言模型 (LLM):DeepSeek API
    • 为什么选它? 我们需要两个 LLM 能力:一是用于最终答案生成,二是用于从文本中抽取实体和关系。DeepSeek API 性价比高,对中文支持好,且提供了稳定的 Chat 接口,非常适合这种实验和生产之间的场景。你也可以替换为 OpenAI GPT、通义千问等任何兼容 OpenAI 格式的 API。
  3. 嵌入模型 (Embedding Model):Ollama + bge-m3:latest
    • 为什么本地部署? 向量生成是高频操作,使用本地部署的嵌入模型可以避免网络延迟和 API 调用成本。 bge-m3 模型在中文文本表征上表现优异,且支持多语言。Ollama 则是一个极其方便的本地大模型运行和管理的工具。
  4. 开发框架:LangChain
    • 为什么用它? LangChain 提供了丰富的工具链来处理文档加载、文本分割(分块)、向量化等流程,能极大减少我们编写样板代码的工作量。虽然它有时显得“重”,但对于快速构建和验证 RAG 流水线非常合适。
  5. 数据处理与图谱构建:Pandas, NetworkX, Pyvis
    • Pandas 用于数据清洗和格式化,方便导入 Doris。
    • NetworkX 是 Python 经典的图计算库,用于在内存中构建和操作知识图谱。
    • Pyvis 用于将 NetworkX 图生成交互式的 HTML 可视化页面,方便调试和展示。

2.2 具体安装与配置步骤

第一步:部署 Apache Doris 这是整个系统的基石。对于本地测试,单机部署是最快的方式。

  1. 下载与解压: Apache Doris 官网 下载最新稳定版的二进制包。
  2. 启动 FE(前端): 进入 fe 目录,修改 conf/fe.conf ,确保元数据目录正确,然后运行 ./bin/start_fe.sh --daemon
  3. 启动 BE(后端): 进入 be 目录,修改 conf/be.conf ,配置存储路径,然后运行 ./bin/start_be.sh --daemon
  4. 连接与初始化: 使用 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 及嵌入模型

  1. 前往 Ollama 官网 下载并安装。
  2. 安装完成后,拉取 bge-m3 模型:
    ollama pull bge-m3:latest
    
  3. 启动 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 性能、扩展性与稳定性

  1. 向量索引调优
    • 问题 :数据量达到百万级后,检索速度变慢。
    • 排查 :监控 Doris BE 节点的 CPU、内存和 I/O。使用 EXPLAIN 分析向量检索 SQL。
    • 优化 :调整 HNSW 索引参数 ( ef_construction , max_degree )。对于超大规模向量,考虑使用 Doris 的 倒排索引 进行前置过滤,减少向量检索范围。定期对表进行 Compaction 以优化数据分布。
  2. 数据更新与一致性
    • 问题 :文档更新后,如何同步更新向量和知识图谱?
    • 方案 :为每个文档块和知识图谱条目增加版本号或更新时间戳。采用“标记删除+重新导入”的策略。对于图谱,实现增量抽取和更新算法,避免全量重建。
  3. 系统架构
    • 单体应用瓶颈 :所有逻辑写在一个脚本里,扩展性差。
    • 演进 :将系统拆分为微服务。例如: 文档处理服务 向量检索服务 图谱查询服务 LLM 网关服务 。Doris 作为统一的数据服务层,为这些微服务提供数据访问能力。
  4. 成本控制
    • LLM API 调用 :对检索结果进行重排序(Re-ranking),只将最相关的少量上下文发送给 LLM,减少 Token 消耗。对常见问题建立答案缓存。
    • 嵌入模型 :对于非关键路径或冷数据,可以考虑使用更小、更快的嵌入模型。

5.2 效果优化与高级技巧

  1. 查询意图分类(Routing)
    • 在检索前,先用一个轻量级模型或规则判断用户问题是“简单事实型”还是“复杂关系型”。
    • 简单问题走基础 RAG 路径(检索 doc_chunks ),复杂问题走知识图谱增强路径(检索 knowledge_graph )。两者可以并行执行,然后根据置信度融合结果。
  2. 检索增强(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;
    
  3. 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 作为底层存储,可以平滑地支撑你的架构演进。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐