⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议。
Skip to content

D2:AI 应用的数据层

Easy Data x AI 课程 · 术篇 · 第二期

上一期你学会了 Tool Use——大模型和外部数据之间的桥梁。这一期,我们来回答桥对面的问题:那个“外部数据”,到底应该用什么样的数据库来承载?

本期课程涉及的代码,均在 https://github.com/datawhalechina/easy-data-x-ai 项目的 code 目录中(欢迎大家 star 和参与课程共建)。

# ============================================================
# 示例代码简介:从 d2_1 到 d2_4 的演进
#
# d2_1  数据写入      → 文档切分、向量化并写入 seekdb
# d2_2  向量搜索      → 纯向量搜索示例
# d2_3  混合搜索      → 向量 + 全文 + 结构化过滤
# d2_4  对比实验      → 纯向量 vs 混合搜索结果对比
# ============================================================

复制 .env.example.env,并在 .env 文件中填写你的真实 API Key,即可直接执行对应章节的示例代码。

推荐大家可以通过在硅基流动上注册账号,获取 API 密钥,使用免费 Embedding 模型 BAAI/bge-m3 来体验向量化和检索。

cd easy-data-x-ai/code

# 安装依赖
pip install --upgrade -r requirements.txt

# 复制 .env.example 为 .env
cp .env.example .env

# 在 .env 文件中填写你的真实 API Key
vim .env

# 运行示例代码
python D2/d2_1_ingest.py

开场:桥的另一端是什么?

D1 讲了一件关键的事:Tool Use 让大模型从“只能说”变成了“能做事”。通过 Tool Use,Agent 可以调用外部工具、查询数据库、获取实时信息。你已经跑通了一个最小示例——定义一个查询知识库的 tool,让模型调用它,处理返回结果。

很好。但这里有一个被很多开发者忽略的问题:

桥搭好了,桥对面应该放什么?

你的 Agent 要查知识库——用什么数据库存?用什么方式查?你可能第一反应是“用个向量数据库不就行了”。毕竟现在满世界都在讲 Embedding、向量搜索、语义检索。

但如果你真的上手做过一个 AI 应用,你大概率会撞上一个让你困惑的问题:向量搜索好像很聪明,但有些时候又蠢得离谱。

今天这节课,我们来搞清楚这个问题。

第零部分:数据进库之前——切分与向量化

在讨论"用什么数据库"之前,有一个更基础的问题:原始文档怎么变成可以被检索的数据?

这个过程分两步:切分(Chunking)向量化(Embedding)

切分:把长文档变成可检索的片段

一篇技术文档可能有几千字。如果把整篇文档作为一个检索单元,有两个问题:

  • 向量化一整篇文档,语义会被"稀释"——一个向量很难同时代表文档里所有话题
  • 检索命中后,把几千字全塞给模型,浪费 Token,也干扰模型的注意力

正确的做法是把文档切成若干语义完整的小片段,每个片段单独向量化、单独存储。

python
# 简单的按字符数切分示例(带重叠)
def chunk_document(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]:
    """
    将长文档切分为带重叠的小片段
    overlap(重叠)确保片段边界处的语义不丢失
    """
    chunks = []
    start = 0
    while start < len(text):
        end = start + chunk_size
        chunks.append(text[start:end])
        start += chunk_size - overlap  # 每次前进 chunk_size - overlap
    return chunks

切分策略没有银弹,但有几条实用原则:

原则说明
按语义边界切优先在段落、标题处切,不要在句子中间断开
片段不要太长500~800 字是常见范围,太长语义稀释,太短上下文不足
保留重叠相邻片段保留 50~100 字的重叠,避免关键信息落在边界
保留元数据每个片段记录来源文档、章节、更新时间等,用于后续结构化过滤

向量化:把文字变成数字

切分完成后,每个片段需要通过 Embedding 模型转换为向量——一串浮点数,代表这段文字的语义位置。

python
import os
from openai import OpenAI

# 使用硅基流动的 Embedding API(兼容 OpenAI 格式)
client = OpenAI(
    api_key=os.environ["SILICONFLOW_API_KEY"],
    base_url="https://api.siliconflow.cn/v1"
)

def embed(text: str) -> list[float]:
    """将一段文字转换为向量"""
    response = client.embeddings.create(
        model="BAAI/bge-m3",  # 硅基流动提供的免费 Embedding 模型
        input=text
    )
    return response.data[0].embedding

# 示例
vector = embed("错误码 E-4012 表示数据库连接超时")
print(f"向量维度:{len(vector)}")  # 输出:向量维度:1024

Embedding 模型不需要你自己训练——直接调用 API 即可。BAAI/bge-m3 是目前中英文效果都不错的开源模型,在硅基流动上免费可用。

切分 + 向量化完成后,数据才真正准备好进入数据库。接下来的问题才是:用什么数据库存,用什么方式查?

第一部分:传统数据库为什么不够用

先说一件你可能已经直觉感受到但没有系统想过的事。

传统数据库——无论是 MySQL、PostgreSQL 还是 MongoDB——它们的设计哲学是精确匹配。你告诉数据库“找 id=123 的记录”,它帮你找到;你告诉它“找 name='张三' 的用户”,它帮你找到。整个查询逻辑建立在一个前提上:你知道你要找什么,而且你能精确描述它

python
# 传统数据库的典型查询
cursor.execute("SELECT * FROM products WHERE id = 123")
cursor.execute("SELECT * FROM users WHERE name = '张三'")

这在传统应用里没问题。用户搜商品,用商品 ID;查订单,用订单号;找客户,用手机号。一切都是精确的。

但 AI 应用面对的是一个完全不同的场景。用户不会说“找 id=123 的文档”,用户会说:

  • “我们上个月讨论过的那个性能优化方案是什么?”
  • “有没有关于用户权限设计的最佳实践?”
  • “帮我找一下和数据库迁移相关的文档”

这些查询的共同特点是:用户用自然语言描述他想要的东西,而不是给出精确的匹配条件。他不知道文档的 ID,不知道标题的确切措辞,甚至不知道那份文档到底叫什么——他只知道“大概是关于什么的”。

传统数据库对这种查询无能为力。你没法写一条 SQL 来“找意思最接近的段落”。

这就是 AI 应用和传统应用在数据层面上的根本错位:传统数据库做的是精确匹配,AI 应用需要的是语义匹配

第二部分:向量搜索——聪明,但没有你以为的那么聪明

为了解决语义匹配的问题,行业引入了向量搜索

原理不复杂。大模型(或者专门的 Embedding 模型)可以把一段文字变成一个高维向量——一串数字。意思相近的文字,向量也相近。所以“查找和这个问题最相关的段落”就变成了“找到向量空间中距离最近的几个点”。

python
# 向量搜索的基本逻辑
query_embedding = embed("用户权限设计的最佳实践")
results = vector_db.search(query_embedding, top_k=5)
# 返回语义上最相近的 5 个文档段落

这确实很厉害。用户问“权限设计”,向量搜索能找到标题叫“访问控制架构”的文档——因为它们语义相近。传统关键词搜索就做不到这一点,因为“权限设计”和“访问控制架构”之间没有一个共同的关键词。

但问题来了。

向量搜索的软肋

试想这个场景:你的知识库里有大量的错误码文档。用户遇到了一个问题,搜索:

“错误码 E-4012 的解决方案”

向量搜索会怎么做?它会把“错误码 E-4012 的解决方案”这句话变成一个向量,然后在向量空间中找最近的几个点。

结果它返回了什么?

  • E-4011 的解决方案
  • E-4013 的解决方案
  • E-4010 的解决方案

因为在向量空间中,“E-4011”“E-4012”“E-4013”这几个字符串的语义表示非常接近——它们都是“错误码”,都是“E-40xx 系列”,在语义上几乎没有区别。但用户需要的是精确匹配 E-4012,差一位数字就是完全不同的错误。

这不是个例。向量搜索在以下场景中都容易“犯蠢”:

查询内容向量搜索的问题
产品型号 “OB-4.2.1”可能返回 OB-3.x 或 OB-4.1.0 的文档
API 参数名 “max_retries”可能返回关于 “retry_count” 的文档
版本号 “v2.3.1”可能返回 v2.3.0 或 v2.4.0 的内容
配置项 “enable_ssl_verify”可能返回关于 SSL 证书的通用讨论
精确数字“超时时间 30 秒”可能返回“超时时间 60 秒”的文档

你注意到规律了吗?凡是需要精确匹配的内容——专有名词、型号编号、版本号、配置项名称、精确数值——向量搜索都不可靠。

为什么?因为 Embedding 模型在训练时学到的是“语义”,不是“字面”。“E-4012”和“E-4013”在语义空间中就是“差不多的东西”,模型无法理解“差一位数字意味着完全不同的错误”。这不是模型不够好的问题——这是向量搜索这个方法论的固有边界。

这不是一个模型问题,是一个搜索策略问题。

用个类比来说:向量搜索就像一个理解力很强但不太较真的同事。你问他“那个 E-4012 的文档在哪”,他会说“E-40 什么什么的嘛,我记得大概在这一带”——他抓住了大意,但搞混了细节。而你需要的是一个既懂大意又能精确定位的人。

第三部分:混合搜索——你需要的完整方案

既然向量搜索擅长语义理解但搞不定精确匹配,而传统的全文搜索(关键词匹配)擅长精确匹配但不懂语义——答案就呼之欲出了:

两个都要。

这就是混合搜索(Hybrid Search)的核心逻辑:

  • 向量搜索负责语义理解:“找意思最接近的内容”
  • 全文搜索负责关键词匹配:“找包含这个确切词的内容”
  • 关系过滤负责结构化条件:“只在最近更新的文档中找”“只在产品 A 的文档中找”

三种能力组合起来,才是一个 AI 应用数据层需要的完整检索覆盖。

回到那个错误码的例子:

搜索策略搜索“错误码 E-4012 的解决方案”的结果
纯向量搜索返回 E-4011、E-4013 等“语义相近”的文档
纯全文搜索精确匹配到 E-4012,但如果用户措辞不精确就找不到
混合搜索精确匹配 E-4012(全文搜索命中)+ 相关错误处理指南(向量搜索补充)

再看另一个场景。用户搜索“怎么优化数据库查询性能”:

搜索策略结果
纯向量搜索找到关于“查询优化”“索引设计”“执行计划分析”的文档——语义命中 ✓
纯全文搜索只找到标题或内容中包含“数据库查询性能”这几个字的文档——遗漏大量相关内容
混合搜索两者的结果融合——既有精确命中,也有语义扩展

这不是一个“哪个更好”的选择题。这是一个“两种能力分别覆盖不同场景”的工程现实。 任何只用其中一种的方案,都有明显的盲区。

而关系过滤则解决的是另一类需求:你不只想搜“性能优化”,你还想限定“只搜 OB-4.x 版本的文档”或者“只搜最近三个月更新的内容”。这是一个结构化条件,需要传统的关系查询能力。

三种能力,缺一不可。

第四部分:三个厨房的问题

理解了混合搜索的必要性,接下来你面对一个工程选择:怎么实现它?

目前行业中最常见的做法是这样的:

  • 用一个向量数据库(比如 Pinecone、Milvus)做语义检索
  • 用一个搜索引擎(比如 Elasticsearch)做全文检索
  • 用一个关系数据库(比如 PostgreSQL)做结构化查询和数据管理

然后,在应用层写胶水代码,把三个系统的查询结果合并起来。

python
# "三个系统拼凑"的典型代码
vector_results = pinecone.query(embedding, top_k=10)
fulltext_results = elasticsearch.search({"query": {"match": {"content": query}}})
metadata = postgres.execute("SELECT * FROM docs WHERE updated_at > '2025-01-01'")

# 然后手动合并三个系统的结果……
final_results = merge_results(vector_results, fulltext_results, metadata)

这段代码能跑通吗?能。但让我用一个类比来说明为什么这不是个好方案。

三个厨房

想象你开了一家餐厅。出于某种历史原因,你有三个独立的厨房:

  • 冷菜厨房在一楼,有自己的食材仓库和库存系统
  • 热菜厨房在二楼,也有自己的一套食材管理
  • 甜点厨房在三楼,同样独立运作

每来一位客人点餐,你需要:

  1. 把订单拆成三份,分别送到三个厨房
  2. 三个厨房各自准备各自的部分
  3. 有人负责跑上跑下,确认三个厨房的进度
  4. 最后把三个厨房的出品凑成一桌菜端给客人
  5. 如果客人说“冷菜不要香菜”——你得确保这个信息传达到了冷菜厨房,而不是被热菜厨房拦截了

三个厨房都能正常做菜,但协调成本是灾难性的。食材在三个仓库之间重复存放,有的食材一楼用完了二楼还有但没人知道。菜品之间的搭配靠前台经理人工协调,忙的时候难免出错。一楼厨房升级了设备,二楼三楼还在用旧的——三个系统的版本管理各自独立。

这就是同时维护三套数据系统的真实体验。

向量数据库、搜索引擎、关系数据库——三个独立系统,三套部署、三套运维、三套 API、三个数据同步流程。你得保证同一份文档在三个系统中的数据是一致的;你得在应用层写大量代码来合并三个系统的查询结果;任何一个系统出了问题,你的检索就是残缺的。

这不是正常状态。这是行业从“传统应用”过渡到“AI 应用”过程中的临时方案。就像那三个厨房——它不是你设计的,是历史遗留的,你一直在凑合着用。

正确的做法是什么?一个厨房,所有菜都能做。

我们的思考

我们做 seekdb 的出发点,就是上面这个判断。

AI 应用的数据层不应该是三个系统拼在一起。向量库管语义,搜索引擎管关键词,关系库管结构化数据,然后在应用层写胶水代码协调三个系统的结果——这不仅增加运维复杂度,还导致检索结果难以有机整合。一个系统用相关性分数排序,另一个系统用 BM25 分数排序,第三个系统用时间排序——你怎么把三套排序逻辑合成一个统一的结果排名?光这一件事就够你头疼一阵了。

正确的做法是在一个系统里原生支持三种检索能力。不是三个引擎拼起来假装一个,是一个引擎天生就能同时处理向量数据、全文数据和关系数据

seekdb 就是基于这个判断做的。当你创建一个数据集合时,你可以同时声明向量索引和全文索引。查询的时候,一条语句同时执行语义搜索、关键词搜索和结构化过滤——结果在引擎内部有机融合,而不是在应用层手动拼接。

用我们内部常说的一句话:

你做得对的方式也恰好是最简单的方式。

一个系统搞定,不仅架构更简洁,效果也更好。因为引擎内部可以让向量分数和全文分数做联合排序,而不是在外面做事后合并——后者永远丢信息。

光说不练假把式。我们来看看实际跑起来到底有多快。

五分钟跑通:你的第一个混合搜索

下面这段代码可以在你的笔记本电脑上直接运行。五分钟内,你就能体验一个同时支持向量搜索和全文搜索的 AI 数据层。

第一步:安装

python
# 终端运行
# pip install pyseekdb

第二步:创建集合并写入数据

python
from pyseekdb import SeekDB

db = SeekDB()

# 创建一个支持向量和全文搜索的集合
db.create_collection(
    name="knowledge_base",
    vector_column="content",     # 对 content 列建向量索引
    fulltext_columns=["content"] # 对 content 列建全文索引
)

# 准备示例文档
docs = [
    {
        "content": "错误码 E-4012 表示数据库连接超时。解决方案:检查网络配置,确认数据库服务端口是否开放,建议超时时间设置为 30 秒。",
        "category": "error_codes",
        "version": "4.2"
    },
    {
        "content": "错误码 E-4013 表示认证失败。解决方案:检查用户名和密码是否正确,确认账户是否被锁定。",
        "category": "error_codes",
        "version": "4.2"
    },
    {
        "content": "数据库查询性能优化指南:合理使用索引可以将查询速度提升 10 倍以上。建议对高频查询的 WHERE 条件列建立索引。",
        "category": "best_practices",
        "version": "4.2"
    },
    {
        "content": "访问控制架构设计:基于 RBAC 模型实现用户权限管理,支持角色继承和细粒度的资源级权限控制。",
        "category": "architecture",
        "version": "4.1"
    },
    {
        "content": "OB-4.2.1 版本新特性:支持在线 DDL 操作、改进了并行查询引擎、修复了分区表在特定条件下的数据倾斜问题。",
        "category": "release_notes",
        "version": "4.2.1"
    },
]

# 写入数据
db.insert(collection_name="knowledge_base", documents=docs)

第三步:执行混合查询

python
# 混合查询:同时使用向量搜索 + 全文搜索
results = db.hybrid_search(
    collection_name="knowledge_base",
    query_text="错误码 E-4012 的解决方案",
    top_k=3
)

for r in results:
    print(f"[相关度: {r['score']:.3f}]")
    print(r["content"])
    print("---")

运行这段代码,你会看到 E-4012 的文档被精确排在第一位——不是因为它“语义最相近”(E-4013 语义上也很近),而是因为全文搜索精确匹配了“E-4012”这个关键词,和向量搜索的语义分数一起参与了排序。

现在试试另一个查询:

python
# 语义查询:用户的措辞和文档标题完全不同
results = db.hybrid_search(
    collection_name="knowledge_base",
    query_text="怎么设计用户权限",
    top_k=3
)

for r in results:
    print(f"[相关度: {r['score']:.3f}]")
    print(r["content"])
    print("---")

这次你会看到“访问控制架构设计”的文档被找到了——尽管用户说的是“用户权限”,文档写的是“访问控制架构”,两者之间没有共同的关键词。这是向量搜索的功劳,它理解了语义上的等价性。

一个查询,两种搜索能力同时工作,结果在引擎内部融合。 你不需要写任何合并逻辑,不需要维护多个系统,不需要操心数据同步。

加上结构化过滤

python
# 混合查询 + 结构化过滤:只在 4.2 版本的文档中搜索
results = db.hybrid_search(
    collection_name="knowledge_base",
    query_text="性能优化",
    top_k=3,
    filters={"version": "4.2"}
)

向量搜索 + 全文搜索 + 关系过滤,三种能力在一条查询中完成。这就是“一个厨房,所有菜都能做”的实际体验。

对比一下:纯向量搜索 vs 混合搜索

为了让你直观感受差距,我们跑一个简单的对比实验。同样的数据,同样的查询,分别用纯向量搜索和混合搜索,看看结果有什么不同。

python
# 纯向量搜索
vector_only = db.vector_search(
    collection_name="knowledge_base",
    query_text="错误码 E-4012",
    top_k=3
)

# 混合搜索
hybrid = db.hybrid_search(
    collection_name="knowledge_base",
    query_text="错误码 E-4012",
    top_k=3
)

print("=== 纯向量搜索结果 ===")
for i, r in enumerate(vector_only):
    print(f"{i+1}. {r['content'][:50]}...")

print("\n=== 混合搜索结果 ===")
for i, r in enumerate(hybrid):
    print(f"{i+1}. {r['content'][:50]}...")

你大概率会看到类似这样的对比:

排名纯向量搜索混合搜索
1E-4013 认证失败(语义最近)E-4012 连接超时(精确命中)
2E-4012 连接超时E-4013 认证失败(语义补充)
3查询性能优化(也沾点边)OB-4.2.1 版本新特性

纯向量搜索把 E-4013 排在了 E-4012 前面——因为在向量空间中它们几乎一样近,排序带有随机性。但混合搜索因为有全文搜索的精确匹配信号,把 E-4012 精准地推到了第一位。

一个错误码搞错,如果这个结果直接被 Agent 用来回答用户,用户得到的就是错误的解决方案。这不是“效果差一点”的问题,是“对和错”的问题。

这就是 D3 会更深入展开的对比实验。在 D3 中,你会用更大规模的真实数据、更多样的查询类型来做系统性对比。到时候你会发现,在包含专有名词、版本号、技术术语的查询中,混合搜索和纯向量搜索的差距是肉眼可见的

回到全局:这件事在整个课程中的位置

让我们把视野拉回来,看看今天讲的内容在整个 Dev 路径中处于什么位置。

D1 你学会了 Tool Use——Agent 和外部世界交互的桥梁。今天你搭建了桥对面的数据层——一个能同时做语义检索、关键词匹配和结构化过滤的系统。

D3 你会把这两者连起来:用 Tool Use 让 Agent 调用 seekdb 的混合检索,构建一个完整的 Agentic RAG 系统,并通过对比实验亲眼看到数据层选型对最终效果的影响。

D4 你会在同一个数据层上构建记忆系统——记忆的存储、检索、降权,底层用的也是今天讲的这套混合检索能力。

一条主线贯穿始终:Agent 的每一项能力,拆到底都是数据的存储与检索。

这节课要留下的印象

如果这节课的所有内容你只记住一句话,记住这句:

AI 应用的数据层需要同时处理语义检索和精确检索——这是工程现实。一个系统搞定,比三个系统拼凑不仅更简单,效果也更好。

课后行动

  1. 跑通 Notebook:运行本模块的代码,五分钟内完成你的第一个混合查询。确认向量搜索和全文搜索各自的命中情况。

  2. 换成你自己的数据:这才是关键一步。找几份你实际项目中的文档——产品文档、API 文档、内部 Wiki,什么都行。把它们存入 seekdb,然后用你日常工作中会问的问题去查询。感受一下:

    • 哪些查询是向量搜索命中、全文搜索没命中的?(语义理解的价值)
    • 哪些查询是全文搜索命中、向量搜索漏掉或排错的?(精确匹配的价值)
    • 有没有查询是两者结合才给出最佳结果的?

带着你的观察进入下一期。D3 会在更大的数据规模上做系统性的对比实验——你会用数据证明“混合检索不是可选优化,是基本要求”这个判断。

延伸阅读

如果你对本期提到的概念想做进一步了解,以下是一些推荐资源:

  • 向量搜索的原理What are Vector Embeddings?,Pinecone 的入门教程,直观解释了 Embedding 和向量搜索的工作方式
  • BM25 与全文检索Understanding BM25,Elastic 的技术博客,解释了全文搜索背后的经典算法
  • RRF 融合算法:混合搜索中如何将向量分数和全文分数合并为统一排名,Reciprocal Rank Fusion 是业界常用的方案——D3 的延伸阅读会展开这个话题
  • seekdb 官方文档:完整的 API 参考、数据类型支持和混合查询的工程细节

下一期预告:D3 · Agentic RAG 实战——把 D1 的 Tool Use 和 D2 的数据层连起来,构建一个知识库问答系统。重头戏是一组对比实验:同样的查询,纯向量检索和混合检索的结果差距有多大?不需要看论文——跑一次实验就明白了。


欢迎各位老师在 https://github.com/datawhalechina/easy-data-x-ai 参与课程共建。

也欢迎各位老师加入 Data x AI 交流群~

Built with VitePress | GitHub 仓库