码事漫谈头像
关注
DB for AI 实战:用 KingbaseES 向量引擎搭建企业级 RAG 知识库封面图

DB for AI 实战:用 KingbaseES 向量引擎搭建企业级 RAG 知识库

上半年接了个内部知识库项目,需求听着很标准:把几十万份技术文档喂给大模型,让同事能直接问答。我照例画了张架构图——业务数据走关系库,Embedding 另存一套向量库,中间再加条同步链路。图画完越看越眼熟,这不就是前两年我们踩过的那套坑么?

那次项目上线后,隔三差五就出幻觉:业务表刚更新完,向量库还没同步完,模型已经拿着过期的 chunk 一本正经地乱答。更要命的是排查链路,得同时查两套库、两套监控,出了问题根本说不清是哪边没对齐。

所以这次我换了个思路:能不能不折腾两套存储,把向量和业务数据直接塞进一个数据库里?

试下来,KingbaseES V9 的原生向量引擎基本能把这个事办了。

先说说为啥我排斥"向量库 + 关系库"这种拼盘架构

不是说独立向量库不能用,我在一些小项目里用过 pgvector、Milvus,效果都不错。但到了企业级知识库这个规模,拼盘架构有几处让我很难受的地方:

数据一致性。业务库更新和向量库同步天然有时间差,哪怕是几十毫秒,只要模型恰好在窗口期检索,就会拿到脏数据。RAG 的幻觉问题,根子往往不在模型,而在数据没对齐。

延迟叠加。一次 RAG 请求要在关系库和向量库之间来回跳,量小的时候无所谓,文档上亿之后 P99 就是另一回事了。

运维翻倍。备份、监控、容灾、权限审计全都要做两套,人力和成本都上去。

这些问题的本质其实就一个:向量和业务数据被物理隔离了。如果它们能共享同一个事务边界,很多麻烦自然就没了。

KingbaseES V9 的做法是把向量引擎直接做进内核,而不是外挂一个插件。这带来的最大好处不是某个查询快了多少,而是 Embedding 和业务行数据可以在同一张表、同一个事务里一起提交、一起回滚。

架构设计:我最后只放了三个组件

image.png

我的目标是尽量少依赖。最终链路很干净:

  • 一个 KingbaseES V9 实例,同时管关系和向量;
  • 一个 Embedding 模型,我用的 BGE-M3;
  • 一个 LLM,对接的是 Qwen。

没有独立的向量数据库,没有 ETL 同步工具,也没有消息队列。示意图还是用了 KingbaseES 官方那张,逻辑就是:用户提问 → Embedding → KingbaseES 混合检索 → 拼 Prompt → LLM 生成。

数据模型:向量和元数据放一起

这是我的建表 SQL,核心思路是把 embedding 列和 department、security_level 这些业务字段放在同一张表里:

CREATE TABLE knowledge_base (
    id          BIGSERIAL PRIMARY KEY,
    doc_id      VARCHAR(64) NOT NULL,
    chunk_id    INTEGER NOT NULL,
    title       VARCHAR(512),
    content     TEXT NOT NULL,
    embedding   VECTOR(1024) NOT NULL,
    source_type VARCHAR(32),
    department  VARCHAR(64),
    security_level VARCHAR(16) DEFAULT 'internal',
    created_at  TIMESTAMPTZ DEFAULT NOW(),
    updated_at  TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_kb_embedding ON knowledge_base
    USING hnsw (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 64);

CREATE INDEX idx_kb_dept ON knowledge_base (department);
CREATE INDEX idx_kb_source ON knowledge_base (source_type);
CREATE INDEX idx_kb_security ON knowledge_base (security_level);

CREATE INDEX idx_kb_content_ft ON knowledge_base
    USING gin (to_tsvector('simple', content));

之所以这么设计,是因为实际检索时几乎都是混合条件:按部门过滤、按密级过滤,再算语义相似度。如果向量和元数据分表或者分库,光 Join 和权限校验就能把人写疯。

动手搭起来

开向量扩展

CREATE EXTENSION IF NOT EXISTS vector;
SELECT '[1,2,3]'::vector(3);

注意一下,KingbaseES 的 vector 扩展不是默认启用的,得先确认安装包带了。我当时装的 V9 版本里直接有,低版本可能需要单独装插件。

文档入库

下面是简化后的 Python 入库脚本。实际生产里我会用 execute_batch 批量写,这里为了看得清楚,把逻辑拆开了:

import psycopg2
from embedding_model import BGE_M3_Encoder
from text_chunker import SemanticChunker

conn = psycopg2.connect(
    host="localhost", port=54321,
    database="rag_db", user="system", password="******"
)

encoder = BGE_M3_Encoder()
chunker = SemanticChunker(max_length=512, overlap=64)

def ingest_document(doc_id, title, content, source_type, department):
    chunks = chunker.split(content)
    records = []
    for idx, chunk in enumerate(chunks):
        embedding = encoder.encode(chunk)
        records.append((
            doc_id, idx, title, chunk,
            str(embedding.tolist()),
            source_type, department
        ))

    with conn.cursor() as cur:
        # 生产环境这里用 execute_batch,别学我单条插入
        execute_batch(cur, """
            INSERT INTO knowledge_base
                (doc_id, chunk_id, title, content, embedding,
                 source_type, department)
            VALUES (%s, %s, %s, %s, %s, %s, %s)
        """, records)
    conn.commit()
    print(f"{doc_id} 入库完成,{len(chunks)} 个 chunk")

分块策略我踩过坑:最早图省事按固定 512 字符切,结果很多 chunk 把一句话拦腰截断,检索时上下文稀碎。后来换成按段落和标题边界切,overlap 给 64~128 字符,召回质量明显好了一截。

三路召回的混合检索

只跑向量检索不够。我现在的做法是同时走三条路:向量召回、全文召回、元数据过滤,最后合并重排。

def hybrid_search(query, top_k=10, department=None, security_level=None):
    query_vec = str(encoder.encode(query).tolist())

    sql = """
        WITH vec_results AS (
            SELECT id, content, title, source_type,
                   1 - (embedding <=> %s::vector) AS vec_score
            FROM knowledge_base
            WHERE (%s IS NULL OR department = %s)
              AND (%s IS NULL OR security_level <= %s)
            ORDER BY embedding <=> %s::vector
            LIMIT %s
        ),
        fts_results AS (
            SELECT id, content, title, source_type,
                   ts_rank(to_tsvector('simple', content),
                           plainto_tsquery('simple', %s)) AS fts_score
            FROM knowledge_base
            WHERE to_tsvector('simple', content)
                  @@ plainto_tsquery('simple', %s)
              AND (%s IS NULL OR department = %s)
              AND (%s IS NULL OR security_level <= %s)
            ORDER BY fts_score DESC
            LIMIT %s
        )
        SELECT COALESCE(v.id, f.id) AS id,
               COALESCE(v.content, f.content) AS content,
               COALESCE(v.title, f.title) AS title,
               COALESCE(v.source_type, f.source_type) AS source_type,
               COALESCE(v.vec_score, 0) * 0.7
               + COALESCE(f.fts_score, 0) * 0.3 AS final_score
        FROM vec_results v
        FULL OUTER JOIN fts_results f ON v.id = f.id
        ORDER BY final_score DESC
        LIMIT %s
    """

    params = (
        query_vec, department, department,
        security_level, security_level, query_vec, top_k,
        query, query, department, department,
        security_level, security_level, top_k,
        top_k
    )

    with conn.cursor() as cur:
        cur.execute(sql, params)
        return cur.fetchall()

这里我重点解释两个地方:

  1. embedding <=> %s::vector 算的是余弦距离,1 - 距离 就是相似度。
  2. 元数据过滤条件直接写在 CTE 的 WHERE 里,这样优化器能在向量扫描阶段就把不符合部门和密级要求的 chunk 先过滤掉。不要先跑 Top-K 再在应用层过滤,否则很可能返回空结果。

权重 0.7 / 0.3 是我这个项目里调出来的,不是通用最优值。不同数据集差异很大,建议自己拿一批问题做标注后调参。

接大模型生成答案

from llm_client import QwenClient

llm = QwenClient()

def rag_answer(question, department=None):
    results = hybrid_search(question, top_k=5, department=department)
    if not results:
        return "没找到相关资料,回答不了。"

    context = "\n\n".join(
        f"[{i}] 来源:{row[3]} | 标题:{row[2]}\n{row[1]}"
        for i, row in enumerate(results, 1)
    )

    prompt = f"""你是一个企业知识库助手。请根据参考资料回答。
要求:只基于资料内容回答,不要编造;如果资料里没有答案,直接说明。

参考资料:
{context}

用户问题:{question}

回答:"""

    return llm.chat(prompt)

Prompt 我写得比较保守,因为企业场景最怕模型胡说。你 Prompt 里不写明边界,它很容易把检索结果和训练记忆混着用。

实测效果:至少在我这个场景里差别很明显

我们压测用的库大概是 50 万份文档、200 万条 chunk。 KingbaseES 融合方案和之前独立向量库方案的对比大致如下:

指标独立向量库方案KingbaseES 融合方案
检索 P9952ms18ms
数据同步延迟200~500ms0ms(同库同事务)
组件数4 个1 个
回答准确率76%89%

准确率提升主要来自两块:一是没了同步延迟导致的脏读幻觉,二是混合召回比纯向量召回找得更全。

P99 从 52ms 降到 18ms 当然好看,但我更看重的是少维护一套集群。对小团队来说,运维成本有时候比毫秒级延迟更真实。

性能调优:我实际调过的几个参数

HNSW 索引

-- 高召回场景
SET hnsw.ef_search = 200;

-- 高吞吐场景
SET hnsw.ef_search = 40;

m 和 ef_construction 在建索引时定死,我一般用 m=16、ef_construction=64,算是召回和内存的平衡点。ef_search 可以按场景动态改,查询时 SET 一下就行。

增量更新

KingbaseES 的向量索引支持增量写入,新文档插入后索引实时更新,不需要全量重建。这点对我们很关键,因为知识库是持续在增长的。

ingest_document(
    doc_id="DOC-2026-0803",
    title="新版部署手册",
    content=document_text,
    source_type="manual",
    department="IT"
)
# 下一秒就能检索到
results = hybrid_search("新版部署手册说了什么?")

预过滤 vs 后过滤

后过滤的写法很多人一开始都会写错:

-- 不推荐:先向量 Top-K,再应用层过滤
SELECT * FROM knowledge_base
ORDER BY embedding <=> query_vec
LIMIT 10;

如果前 10 个最相似的 chunk 都不符合当前用户的部门或密级,应用层一过滤就什么都没了。正确做法是把过滤条件直接下推到 SQL:

SELECT * FROM knowledge_base
WHERE department = '研发部'
  AND security_level <= 'internal'
ORDER BY embedding <=> query_vec
LIMIT 10;

KingbaseES 的优化器会根据过滤条件的选择率决定执行路径,亿级数据下这个差异可能是数量级的。

顺便聊聊 KingbaseES 的 AI 方向

搭完 RAG 知识库之后,我对 KingbaseES 另一个功能也挺感兴趣:内置的"的卢智能运维体"。简单说就是数据库里自带了一个运维 Agent,能理解自然语言,转成 SQL 或运维操作。

比如你可以问它:"帮我查一下昨天下午三点开始的慢查询,建议加什么索引。"它会自己跑:

SELECT sql_text, exec_time, rows_examined
FROM sys_statements_log
WHERE exec_time > 1000
  AND log_time BETWEEN '2026-08-02 15:00:00' AND '2026-08-02 15:10:00'
ORDER BY exec_time DESC;

然后输出索引建议。这对 DBA 不干活,但对开发自己查问题挺省事的。

KingbaseES 现在的 AI 战略大致两条线:一条是 AI for DB,让数据库自己更智能;另一条是 DB for AI,让数据库更好地服务 RAG 这类 AI 应用。原生向量引擎属于后者,的卢属于前者。

另外我注意到 MCP 协议最近很热。如果 KingbaseES 能通过 MCP 暴露表结构、执行计划这些元信息,那任何 AI Agent 都可以把它当作一个"可对话的数据源"直接接入。这个想象空间比单个 NL2SQL 工具要大得多。

结语

我现在做 RAG 方案选型,第一个问题已经从"用什么向量库"变成"能不能把向量和业务数据放在同一个事务里"。

KingbaseES V9 的原生向量能力让我可以用一个实例跑完整条链路:存储、检索、权限、审计全在一起。这不是说独立向量库没价值,而是对于不想维护两套集群的团队来说,少一套系统就少一堆坑。

如果你也在做企业知识库,建议直接拿你的真实文档和查询测一测。纸面参数再漂亮,也不如你自己的数据跑一轮来得准。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/Z_oioihoii/article/details/163450645

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--