从零搭一个企业知识库搜索:Qdrant + FastAPI 完整实战命令 用本地文档做语义搜索:一个可跑通的 AI 搜索案例 客服知识库怎么做语义检索?这套命令直接跑起来 从 Markdown 到搜索接口:企业 AI 搜索实战全流程 不用复杂平台,也能搭出一个可用的智能搜索系统 Qdrant 实战:把企业文档变成可语义检索的知识库 一下午跑通 AI 搜索:文档入库、向量检索到 API 封装 企业知识库搜索改造实录:从关键词匹配到语义检索 手把手搭建本地 AI 搜索服务,完整
AI搜索 实战案例分享|附完整命令
在过去一年里,“AI搜索”几乎成了知识库、客服系统、企业内网、内容平台的标配能力。相比传统关键词搜索,AI搜索不再只是匹配“有没有这个词”,而是能够理解用户问题背后的语义意图,例如用户输入“怎么退钱”,系统可以匹配到“退款流程”“取消订单后的资金返还规则”“售后费用退回说明”等内容。
本文将通过一个完整实战案例,演示如何从零搭建一个可运行的 AI 搜索系统。案例目标是:基于本地文档构建一个企业知识库搜索服务,支持语义检索,并为后续接入大模型问答打好基础。
文章会覆盖:
- AI搜索的基本架构;
- 实战案例背景;
- 环境准备;
- 向量数据库部署;
- 文档切分与向量化;
- 数据写入;
- 查询检索;
- 简单接口封装;
- 优化方向;
- 完整命令汇总。
一、什么是AI搜索?
传统搜索通常基于关键词倒排索引,例如用户搜索“退款”,系统会优先查找包含“退款”这个词的文档。如果文档中写的是“资金返还”,而没有出现“退款”,传统搜索可能就无法准确命中。
AI搜索的核心能力在于“语义理解”。它会把用户的问题和文档内容转换成向量,也就是一组高维数字。语义相近的文本,在向量空间中的距离也会更近。
例如:
用户问题:订单取消后钱怎么退?
文档内容:用户取消订单后,支付金额将在3个工作日内原路返还。
虽然两句话没有完全相同的关键词,但语义非常接近。AI搜索可以通过向量相似度找到这类内容。
一个典型的AI搜索系统通常包括以下几个模块:
- 数据源:PDF、Markdown、Word、网页、数据库等;
- 文档解析:提取文本内容;
- 文本切分:将长文档拆成适合检索的小片段;
- Embedding模型:将文本转成向量;
- 向量数据库:存储向量并进行相似度检索;
- 检索接口:接收用户问题并返回相关文档;
- 大模型生成:基于检索结果生成自然语言答案。
本文主要聚焦在“搜索”部分,即从文档入库到语义检索。
二、实战案例背景
假设我们要为一家电商公司的客服团队搭建一个内部知识库搜索系统。客服人员每天需要查询大量规则,例如:
- 退款多久能到账?
- 发票怎么开?
- 会员积分如何使用?
- 订单取消后优惠券是否退回?
- 物流异常怎么处理?
- 售后申请被拒绝怎么办?
这些规则原本分散在多个 Markdown 文档中,客服查询效率较低。我们希望搭建一个简单的 AI 搜索服务,让客服可以用自然语言提问,系统返回最相关的知识片段。
本案例采用如下技术栈:
| 模块 | 技术选择 |
|---|---|
| 开发语言 | Python |
| 向量数据库 | Qdrant |
| Embedding模型 | sentence-transformers |
| Web服务 | FastAPI |
| 数据格式 | Markdown文本 |
| 部署方式 | Docker + 本地Python |
为什么选择 Qdrant?
- 部署简单;
- 查询速度快;
- 支持向量检索和Payload过滤;
- 适合中小型AI搜索原型;
- 本地即可运行,无需复杂集群。
三、项目目录结构
我们先规划一个清晰的目录结构:
ai-search-demo/
├── data/
│ ├── refund.md
│ ├── invoice.md
│ ├── membership.md
│ └── logistics.md
├── scripts/
│ ├── ingest.py
│ └── search.py
├── app.py
├── requirements.txt
└── docker-compose.yml
其中:
data/:存放知识库文档;scripts/ingest.py:负责文档切分、向量化、写入Qdrant;scripts/search.py:命令行搜索测试;app.py:FastAPI接口服务;requirements.txt:Python依赖;docker-compose.yml:启动Qdrant。
四、创建项目
首先创建项目目录:
mkdir ai-search-demo
cd ai-search-demo
mkdir data scripts
touch requirements.txt docker-compose.yml app.py
touch scripts/ingest.py scripts/search.py
五、准备示例知识库文档
创建退款规则文档:
cat > data/refund.md <<'EOF'
# 退款规则
用户取消订单后,如果订单尚未发货,系统会自动发起退款。
退款金额将按照用户实际支付金额原路返回。使用银行卡支付的订单,通常会在1到3个工作日内到账;使用第三方支付平台的订单,通常会在24小时内到账。
如果订单已发货,用户需要先申请售后退货。商家收到退回商品并确认无误后,会在2个工作日内处理退款。
如果用户使用了优惠券,订单取消后,符合退回条件的优惠券会自动返还到用户账户。已经过期的优惠券通常不会延长有效期。
如果退款超过预计时间仍未到账,用户可以联系在线客服,并提供订单编号和支付流水号。
EOF
创建发票规则文档:
cat > data/invoice.md <<'EOF'
# 发票规则
用户可以在订单完成后申请开具发票。发票类型包括个人普通发票、企业普通发票和增值税专用发票。
申请企业发票时,需要填写企业名称、纳税人识别号、注册地址、联系电话、开户银行和银行账号。
电子发票通常会在申请后24小时内发送到用户填写的邮箱。纸质发票会通过快递寄出,寄送时间通常为3到7个工作日。
如果发票信息填写错误,用户可以在发票开具后30天内申请重开。已经超过期限的发票,通常不支持修改。
EOF
创建会员规则文档:
cat > data/membership.md <<'EOF'
# 会员积分规则
用户在平台购物后可以获得会员积分。积分数量通常按照订单实付金额计算,每消费1元获得1积分。
积分可以用于兑换优惠券、参与会员活动或抵扣部分订单金额。不同等级会员可享受不同积分兑换权益。
如果用户发生退款,对应订单产生的积分会被扣回。如果积分已经被使用,系统可能会从用户现有积分中抵扣。
会员等级根据用户过去12个月的累计消费金额计算。等级越高,可享受的专属权益越多。
EOF
创建物流规则文档:
cat > data/logistics.md <<'EOF'
# 物流异常处理
订单发货后,用户可以在订单详情页查看物流轨迹。
如果物流信息超过48小时没有更新,可能是快递揽收延迟、中转延迟或物流系统同步异常。用户可以联系在线客服协助查询。
如果包裹显示已签收但用户未收到,需要先确认是否由家人、门卫、前台或快递柜代收。
如果确认未收到包裹,客服需要联系物流公司核实签收情况。必要时,可以为用户发起补发或退款流程。
生鲜、易碎品等特殊商品发生物流破损时,用户需要提供外包装照片、商品照片和订单编号。
EOF
六、启动Qdrant向量数据库
编辑 docker-compose.yml:
cat > docker-compose.yml <<'EOF'
version: "3.8"
services:
qdrant:
image: qdrant/qdrant:latest
container_name: ai-search-qdrant
ports:
- "6333:6333"
- "6334:6334"
volumes:
- ./qdrant_storage:/qdrant/storage
EOF
启动服务:
docker compose up -d
检查容器状态:
docker ps
测试 Qdrant 是否启动成功:
curl http://localhost:6333
如果返回类似如下内容,说明启动正常:
{"title":"qdrant - vector search engine","version":"..."}
七、安装Python依赖
编辑 requirements.txt:
cat > requirements.txt <<'EOF'
qdrant-client==1.9.1
sentence-transformers==2.7.0
fastapi==0.111.0
uvicorn==0.30.1
python-dotenv==1.0.1
EOF
创建虚拟环境并安装依赖:
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
如果你使用的是 Windows,可以使用:
python -m venv .venv
.venv\Scripts\activate
pip install --upgrade pip
pip install -r requirements.txt
八、编写文档入库脚本
现在编写 scripts/ingest.py。这个脚本负责完成四件事:
- 读取
data/目录下的 Markdown 文档; - 按固定长度切分文本;
- 使用 Embedding 模型生成向量;
- 写入 Qdrant 向量数据库。
写入完整代码:
cat > scripts/ingest.py <<'EOF'
import os
import uuid
from pathlib import Path
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
from sentence_transformers import SentenceTransformer
COLLECTION_NAME = "customer_service_knowledge"
DATA_DIR = Path("data")
MODEL_NAME = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
def read_markdown_files(data_dir: Path):
documents = []
for file_path in data_dir.glob("*.md"):
text = file_path.read_text(encoding="utf-8")
documents.append({
"file_name": file_path.name,
"text": text
})
return documents
def chunk_text(text: str, chunk_size: int = 300, overlap: int = 50):
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunk = text[start:end].strip()
if chunk:
chunks.append(chunk)
start += chunk_size - overlap
return chunks
def main():
print("加载Embedding模型中...")
model = SentenceTransformer(MODEL_NAME)
vector_size = model.get_sentence_embedding_dimension()
print(f"模型加载完成,向量维度:{vector_size}")
client = QdrantClient(host="localhost", port=6333)
existing_collections = client.get_collections().collections
existing_names = [collection.name for collection in existing_collections]
if COLLECTION_NAME in existing_names:
print(f"集合 {COLLECTION_NAME} 已存在,先删除旧集合...")
client.delete_collection(collection_name=COLLECTION_NAME)
print(f"创建集合:{COLLECTION_NAME}")
client.create_collection(
collection_name=COLLECTION_NAME,
vectors_config=VectorParams(
size=vector_size,
distance=Distance.COSINE
)
)
documents = read_markdown_files(DATA_DIR)
points = []
total_chunks = 0
for doc in documents:
file_name = doc["file_name"]
text = doc["text"]
chunks = chunk_text(text)
for index, chunk in enumerate(chunks):
vector = model.encode(chunk).tolist()
point = PointStruct(
id=str(uuid.uuid4()),
vector=vector,
payload={
"file_name": file_name,
"chunk_index": index,
"content": chunk
}
)
points.append(point)
total_chunks += 1
print(f"共生成文本片段:{total_chunks}")
client.upsert(
collection_name=COLLECTION_NAME,
points=points
)
print("文档入库完成!")
if __name__ == "__main__":
main()
EOF
执行入库脚本:
python scripts/ingest.py
第一次执行时,模型会自动下载,可能需要一些时间。执行成功后,你会看到类似输出:
加载Embedding模型中...
模型加载完成,向量维度:384
创建集合:customer_service_knowledge
共生成文本片段:8
文档入库完成!
九、编写命令行搜索脚本
接下来编写搜索脚本 scripts/search.py:
cat > scripts/search.py <<'EOF'
import sys
from qdrant_client import QdrantClient
from sentence_transformers import SentenceTransformer
COLLECTION_NAME = "customer_service_knowledge"
MODEL_NAME = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
def search(query: str, limit: int = 3):
model = SentenceTransformer(MODEL_NAME)
client = QdrantClient(host="localhost", port=6333)
query_vector = model.encode(query).tolist()
results = client.search(
collection_name=COLLECTION_NAME,
query_vector=query_vector,
limit=limit
)
return results
def main():
if len(sys.argv) < 2:
print("用法:python scripts/search.py '你的问题'")
sys.exit(1)
query = sys.argv[1]
results = search(query)
print(f"用户问题:{query}")
print("=" * 60)
for i, result in enumerate(results, start=1):
payload = result.payload
print(f"结果 {i}")
print(f"相似度分数:{result.score}")
print(f"来源文件:{payload.get('file_name')}")
print(f"片段序号:{payload.get('chunk_index')}")
print("内容:")
print(payload.get("content"))
print("-" * 60)
if __name__ == "__main__":
main()
EOF
测试搜索:
python scripts/search.py "订单取消以后钱多久到账?"
可能返回:
用户问题:订单取消以后钱多久到账?
============================================================
结果 1
相似度分数:0.78
来源文件:refund.md
片段序号:0
内容:
# 退款规则
用户取消订单后,如果订单尚未发货,系统会自动发起退款。
退款金额将按照用户实际支付金额原路返回。使用银行卡支付的订单,通常会在1到3个工作日内到账;使用第三方支付平台的订单,通常会在24小时内到账。
------------------------------------------------------------
再测试几个问题:
python scripts/search.py "发票信息写错了还能改吗?"
python scripts/search.py "快递显示签收但是我没收到怎么办?"
python scripts/search.py "退款以后积分会不会扣掉?"
python scripts/search.py "优惠券取消订单后会退回来吗?"
可以看到,即使用户没有完全使用文档中的原词,AI搜索仍然能够命中语义相关内容。
十、封装成HTTP搜索接口
命令行测试适合开发阶段,但实际业务通常需要通过 HTTP API 提供服务。下面使用 FastAPI 封装一个简单接口。
编辑 app.py:
cat > app.py <<'EOF'
from typing import List
from fastapi import FastAPI, Query
from pydantic import BaseModel
from qdrant_client import QdrantClient
from sentence_transformers import SentenceTransformer
COLLECTION_NAME = "customer_service_knowledge"
MODEL_NAME = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
app = FastAPI(title="AI Search Demo")
model = SentenceTransformer(MODEL_NAME)
client = QdrantClient(host="localhost", port=6333)
class SearchResult(BaseModel):
score: float
file_name: str
chunk_index: int
content: str
class SearchResponse(BaseModel):
query: str
results: List[SearchResult]
@app.get("/health")
def health():
return {"status": "ok"}
@app.get("/search", response_model=SearchResponse)
def search(
q: str = Query(..., description="用户搜索问题"),
limit: int = Query(3, ge=1, le=10, description="返回结果数量")
):
query_vector = model.encode(q).tolist()
results = client.search(
collection_name=COLLECTION_NAME,
query_vector=query_vector,
limit=limit
)
items = []
for item in results:
payload = item.payload
items.append(SearchResult(
score=item.score,
file_name=payload.get("file_name", ""),
chunk_index=payload.get("chunk_index", 0),
content=payload.get("content", "")
))
return SearchResponse(
query=q,
results=items
)
EOF
启动接口服务:
uvicorn app:app --host 0.0.0.0 --port 8000 --reload
测试健康检查:
curl http://localhost:8000/health
测试搜索接口:
curl "http://localhost:8000/search?q=订单取消后退款多久能到账&limit=3"
为了让输出更易读,可以配合 jq:
curl -s "http://localhost:8000/search?q=快递显示签收但是没收到怎么办&limit=3" | jq
十一、这个AI搜索系统的工作流程
到这里,一个最小可用的 AI 搜索系统已经完成。它的工作流程如下:
Markdown文档
↓
读取文本
↓
文本切分
↓
Embedding模型生成向量
↓
写入Qdrant
↓
用户输入问题
↓
问题向量化
↓
Qdrant相似度检索
↓
返回最相关文本片段
它和传统搜索的最大区别在于:搜索时不是直接比较关键词,而是比较“问题向量”和“文档片段向量”的语义相似度。
例如用户问:
订单取消以后钱什么时候回来?
系统可以找到:
退款金额将按照用户实际支付金额原路返回。使用银行卡支付的订单,通常会在1到3个工作日内到账。
这就是语义检索的价值。
十二、实战中的关键经验
1. 文档切分不要太粗,也不要太细
如果切分太粗,一个片段里包含太多主题,检索结果会不够精准。
如果切分太细,一个片段可能缺少上下文,返回结果又不完整。
在本案例中,我们使用:
chunk_size = 300
overlap = 50
这表示每个片段大约300个字符,相邻片段重叠50个字符。
在真实项目中,可以根据文档类型调整:
| 文档类型 | 推荐切分方式 |
|---|---|
| FAQ | 按问答对切分 |
| Markdown | 按标题层级切分 |
| PDF说明书 | 按段落或页切分 |
| 法律合同 | 按条款切分 |
| 产品文档 | 按小节切分 |
对于知识库搜索,推荐优先保留语义完整的段落,而不是机械地按长度切分。
2. Embedding模型会显著影响搜索质量
本文使用的是:
sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
这个模型支持多语言,体积小,适合本地快速实验。但在生产环境中,如果中文搜索质量要求较高,可以考虑更强的中文或多语言Embedding模型。
常见选择包括:
- BGE系列;
- E5系列;
- text2vec系列;
- OpenAI Embedding;
- 通义、智谱、火山等云厂商Embedding API。
如果你的数据主要是中文客服知识库,建议优先测试中文优化过的Embedding模型。
3. 向量检索不等于最终答案
AI搜索返回的是“相关文本片段”,不是直接生成最终答案。如果你要做智能问答,通常还需要把检索结果交给大模型,让大模型根据上下文生成答案。
典型流程是:
用户问题
↓
语义检索
↓
获取Top-K相关文档
↓
构造Prompt
↓
调用大模型
↓
生成最终答案
这个流程通常被称为 RAG,也就是 Retrieval-Augmented Generation,检索增强生成。
4. 需要处理“找不到答案”的情况
AI搜索系统很容易出现一个问题:即使知识库里没有答案,它也会返回最相似的结果。因为向量数据库的检索逻辑是“找最近的”,而不是判断“有没有”。
因此,生产系统一定要设置相似度阈值。例如:
if result.score < 0.5:
return "知识库中没有找到足够相关的内容"
不过阈值不是固定的,需要根据模型、数据和业务测试确定。
5. 可以引入混合搜索
纯向量搜索擅长语义匹配,但有时对精确词、编号、型号、订单状态码不够敏感。
例如:
搜索:错误码 E1024
这种场景下,关键词搜索往往更稳定。
实际项目中常用“混合搜索”:
向量搜索 + 关键词搜索 + 重排序
例如:
- 使用向量搜索召回语义相关内容;
- 使用 BM25 召回关键词匹配内容;
- 合并候选结果;
- 使用 rerank 模型重新排序;
- 返回最终结果。
这会比单纯向量搜索更稳。
十三、生产环境优化建议
如果要把本文示例扩展到真实业务系统,建议重点关注以下方面。
1. 文档增量更新
示例脚本每次都会删除旧集合并重建:
client.delete_collection(collection_name=COLLECTION_NAME)
生产环境不能这么做,否则会影响线上服务。更合理的方案是:
- 给每个文档生成唯一ID;
- 记录文档更新时间;
- 只更新变更过的文档;
- 删除已下线文档对应的向量片段。
2. 元数据过滤
Qdrant支持Payload过滤。例如可以按业务线、文档类型、权限范围过滤。
比如不同客服只能搜索自己业务线下的知识:
业务线:生鲜、数码、服饰、家居
入库时可以加上:
payload={
"file_name": file_name,
"category": "refund",
"department": "customer_service",
"content": chunk
}
查询时再根据用户权限过滤。
3. 权限控制
企业知识库通常不是所有人都能看所有内容。例如:
- 普通客服只能看基础规则;
- 主管可以看赔付策略;
- 财务可以看发票和结算规则;
- 法务可以看合同和争议处理规则。
所以在AI搜索系统里,权限过滤非常重要。不能只在前端控制,而应该在检索阶段就限制可搜索范围。
4. 日志与评估
搜索系统上线后,需要持续记录:
- 用户搜索词;
- 返回结果;
- 点击结果;
- 用户是否追问;
- 人工反馈;
- 无结果问题;
- 低相似度问题。
这些数据可以帮助你持续优化文档质量、切分策略、Embedding模型和排序逻辑。
十四、完整命令汇总
下面是从零到运行的完整命令汇总。
创建项目:
mkdir ai-search-demo
cd ai-search-demo
mkdir data scripts
touch requirements.txt docker-compose.yml app.py
touch scripts/ingest.py scripts/search.py
创建 Qdrant 配置:
cat > docker-compose.yml <<'EOF'
version: "3.8"
services:
qdrant:
image: qdrant/qdrant:latest
container_name: ai-search-qdrant
ports:
- "6333:6333"
- "6334:6334"
volumes:
- ./qdrant_storage:/qdrant/storage
EOF
启动 Qdrant:
docker compose up -d
curl http://localhost:6333
创建依赖文件:
cat > requirements.txt <<'EOF'
qdrant-client==1.9.1
sentence-transformers==2.7.0
fastapi==0.111.0
uvicorn==0.30.1
python-dotenv==1.0.1
EOF
安装依赖:
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
执行文档入库:
python scripts/ingest.py
命令行测试搜索:
python scripts/search.py "订单取消以后钱多久到账?"
python scripts/search.py "发票信息写错了还能改吗?"
python scripts/search.py "快递显示签收但是我没收到怎么办?"
python scripts/search.py "退款以后积分会不会扣掉?"
启动接口服务:
uvicorn app:app --host 0.0.0.0 --port 8000 --reload
接口测试:
curl http://localhost:8000/health
curl "http://localhost:8000/search?q=订单取消后退款多久能到账&limit=3"
curl -s "http://localhost:8000/search?q=快递显示签收但是没收到怎么办&limit=3" | jq
停止服务:
docker compose down
如果需要清理本地向量数据:
rm -rf qdrant_storage
十五、总结
本文通过一个客服知识库案例,完整演示了如何搭建一个基础版 AI 搜索系统。我们使用 Markdown 文档作为数据源,使用 sentence-transformers 生成文本向量,使用 Qdrant 存储和检索向量,并通过 FastAPI 提供搜索接口。
这个案例虽然简单,但已经包含了 AI 搜索的核心链路:
文档采集 → 文本切分 → 向量化 → 向量入库 → 语义检索 → 接口服务
在真实业务中,AI搜索的价值不只是“搜得更准”,更重要的是让用户能够用自然语言表达需求,不必知道系统里具体使用了什么关键词。对于客服、企业知识库、产品文档、法律条款、技术支持、内部制度查询等场景,AI搜索都能显著提升信息获取效率。
如果继续扩展,可以在本文基础上加入:
- 大模型问答;
- 混合搜索;
- rerank重排序;
- 文档权限控制;
- 多租户隔离;
- 搜索日志分析;
- 用户反馈闭环;
- 自动文档更新。
最终,一个成熟的AI搜索系统,不只是一个向量数据库加一个Embedding模型,而是一套围绕数据质量、检索质量、权限安全和用户体验持续优化的工程体系。