企业内网也能用的智能搜索:一套可落地的私有化部署方案与源码实践
AI搜索 私有化部署方案|附源码
在企业数字化转型过程中,知识检索一直是一个高频且关键的场景。过去,企业通常依赖传统全文检索系统,例如 Elasticsearch、Solr、OpenSearch 等,通过关键词匹配来查找文档、制度、合同、产品资料、客服知识库等内容。然而,随着大语言模型和向量数据库的发展,单纯依靠关键词匹配已经无法满足更复杂的语义理解需求。
例如,用户输入“员工离职后社保怎么处理”,传统搜索可能只能匹配包含“离职”“社保”等关键词的文档;而 AI 搜索则可以理解用户真正想问的是“离职手续中的社保转移或停缴流程”,从而返回更准确的答案,并附带相关资料来源。
本文将系统介绍一套可私有化部署的 AI 搜索方案,包括整体架构、核心模块、技术选型、部署流程、数据处理方式、问答流程以及示例源码。该方案适用于企业内部知识库、政企文档检索、法律条文查询、客服知识库、研发文档搜索、医疗资料检索等场景。
一、为什么需要 AI 搜索私有化部署?
目前市面上有很多在线 AI 搜索产品和大模型服务,但对于企业来说,直接使用公有云 API 往往会面临以下问题。
1. 数据安全要求高
企业知识库中可能包含大量敏感信息,例如:
- 内部制度文件
- 客户合同
- 财务报表
- 技术方案
- 源代码文档
- 用户隐私数据
- 项目管理资料
如果将这些数据上传到第三方平台,可能会带来数据泄露和合规风险。因此,对于金融、医疗、政务、制造、能源等行业来说,私有化部署是更加稳妥的选择。
2. 网络环境受限
不少企业内部系统运行在内网环境中,无法直接访问公网服务。AI 搜索系统如果依赖外部 API,会出现网络不可控、延迟高、服务不稳定等问题。私有化部署可以让整个检索和问答链路运行在企业内网中。
3. 成本可控
当搜索请求量较大时,调用外部大模型 API 的成本会持续增长。通过私有化部署开源模型和向量数据库,可以将成本转化为固定的服务器资源投入,长期来看更适合大规模使用。
4. 可定制能力强
不同企业的文档格式、权限模型、搜索规则、答案风格都不一样。私有化部署可以根据自身业务深度定制,例如:
- 接入企业统一登录系统
- 按部门和岗位控制文档权限
- 自定义召回策略
- 自定义提示词模板
- 集成 OA、CRM、ERP、知识库系统
- 接入内部大模型或国产模型
二、AI 搜索的基本原理
AI 搜索通常基于 RAG 架构,即 Retrieval-Augmented Generation,中文可译为“检索增强生成”。
它的核心思想是:先从知识库中检索相关内容,再让大语言模型基于检索结果生成答案。
一个完整的 AI 搜索流程大致如下:
- 用户输入问题;
- 系统将问题转换为向量;
- 在向量数据库中检索相似文档片段;
- 同时可以使用关键词检索进行补充召回;
- 对召回结果进行重排序;
- 将最相关的文档片段拼接到 Prompt 中;
- 调用大语言模型生成最终答案;
- 返回答案和引用来源。
相比传统搜索,AI 搜索不仅能找文档,还能直接生成结构化答案,并指出答案来源,用户体验更接近“问专家”。
三、整体架构设计
本方案采用前后端分离架构,后端负责文档处理、向量检索、问答生成,前端负责用户交互和结果展示。
整体架构如下:
用户浏览器
│
▼
前端 Web UI
│
▼
后端 API 服务 FastAPI
│
├── 文档上传模块
├── 文档解析模块
├── 文本切片模块
├── 向量生成模块
├── 向量检索模块
├── 关键词检索模块
├── 结果重排序模块
└── 大模型问答模块
│
├── 向量数据库 Milvus / Qdrant / Chroma
├── 关系数据库 PostgreSQL / MySQL
├── 对象存储 MinIO
└── 本地大模型服务 Ollama / vLLM / Xinference
四、技术选型
1. 后端框架:FastAPI
FastAPI 是 Python 生态中非常适合构建 AI 应用接口的框架,具有以下优点:
- 性能较好;
- 原生支持异步;
- 自动生成 Swagger 文档;
- 与 Python AI 生态兼容性强;
- 开发效率高。
2. 向量数据库:Qdrant
向量数据库用于存储文档片段对应的语义向量,并支持相似度检索。常见选择包括 Milvus、Qdrant、Chroma、Weaviate 等。
本文示例采用 Qdrant,原因是它部署简单、性能稳定、API 清晰,适合中小型企业快速落地。
3. 文本向量模型:bge-m3
Embedding 模型用于将文本转换成向量。推荐使用开源中文效果较好的模型,例如:
- BAAI/bge-small-zh
- BAAI/bge-base-zh
- BAAI/bge-large-zh
- BAAI/bge-m3
其中 bge-m3 支持多语言、长文本和多种检索模式,适合企业知识库场景。
4. 大语言模型:Qwen / DeepSeek / Llama
私有化部署时,可以根据硬件资源选择不同的大模型:
- 低资源环境:Qwen2.5-7B、DeepSeek-R1-Distill-Qwen-7B
- 中等资源环境:Qwen2.5-14B、GLM-4-9B
- 高资源环境:Qwen2.5-32B、DeepSeek 系列大模型
- 国产化环境:通义千问、智谱、百川、书生浦语等开源模型
如果只是构建内部知识搜索,7B 或 14B 模型通常已经可以满足基础需求。若对推理能力、总结能力和复杂问答要求较高,则可以使用更大的模型。
5. 模型服务:Ollama
Ollama 适合快速部署和管理本地大模型,命令简单,适合原型验证和中小规模场景。
例如拉取 Qwen 模型:
ollama pull qwen2.5:7b
启动服务后,默认会监听:
http://localhost:11434
后端可以通过 HTTP API 调用 Ollama 完成问答。
五、硬件配置建议
不同规模的 AI 搜索系统对硬件要求不同。以下是一个参考配置。
1. 测试环境
适合个人开发、POC 验证、小型知识库。
CPU:8 核
内存:32GB
GPU:可选,建议 12GB 显存以上
硬盘:500GB SSD
模型:Qwen2.5-7B
向量库:Qdrant
2. 中小企业生产环境
适合几十到几百人使用,文档量在数十万片段以内。
CPU:16 核以上
内存:64GB ~ 128GB
GPU:NVIDIA 4090 / L20 / A10 / A100
显存:24GB 以上
硬盘:1TB ~ 4TB SSD
模型:Qwen2.5-14B 或 32B
向量库:Qdrant / Milvus
3. 大规模生产环境
适合政企级知识平台、多业务线、多租户场景。
CPU:32 核以上
内存:256GB 以上
GPU:多卡 A100 / H800 / L40S
硬盘:分布式存储
模型服务:vLLM 多实例
向量库:Milvus 集群
数据库:PostgreSQL 高可用
对象存储:MinIO 集群
六、数据处理流程
AI 搜索效果好不好,很大程度取决于数据处理质量。很多项目失败并不是模型不够强,而是文档解析、文本切片、元数据管理做得不好。
1. 文档上传
系统需要支持常见格式:
- Word
- Excel
- PPT
- TXT
- Markdown
- HTML
- CSV
- 图片 OCR
上传后,文件应保存到对象存储或本地文件系统,同时在数据库中记录文件元信息,例如文件名、上传人、部门、权限、时间、文件类型等。
2. 文档解析
不同格式需要不同解析方式:
- PDF:使用 PyMuPDF、pdfplumber;
- Word:使用 python-docx;
- Excel:使用 pandas、openpyxl;
- Markdown:直接读取文本;
- 图片:使用 PaddleOCR 或 Tesseract OCR。
解析时要尽量保留标题、段落、页码、表格结构等信息。因为最终返回答案时,用户通常需要知道答案来自哪个文件、哪一页、哪个章节。
3. 文本切片
大语言模型不能直接处理无限长文档,因此需要将文档切成多个小片段。常见切片方式包括:
- 按固定字符数切片;
- 按段落切片;
- 按标题层级切片;
- 滑动窗口切片;
- 语义切片。
推荐策略是:按标题和段落优先,结合滑动窗口。
例如每个片段控制在 500 到 1000 个中文字符之间,相邻片段保留 100 到 200 字重叠。这样既能保证上下文完整,又能避免片段过长导致检索不精准。
4. 向量化入库
每个文档片段都需要生成向量,并存入向量数据库。存储内容通常包括:
{
"chunk_id": "唯一片段ID",
"doc_id": "文档ID",
"title": "文档标题",
"content": "片段正文",
"page": 12,
"department": "人力资源部",
"permission": ["hr", "manager"],
"created_at": "2025-01-01"
}
在检索时,不仅可以根据向量相似度召回,还可以根据部门、权限、文档类型等元数据进行过滤。
七、检索策略设计
高质量 AI 搜索不应只依赖单一向量检索,而应采用混合检索。
1. 向量检索
向量检索擅长理解语义。例如:
用户问题:
试用期员工能不能提前离职?
系统可以召回包含以下内容的文档:
劳动者在试用期内提前三日通知用人单位,可以解除劳动合同。
虽然文本中没有完全出现“能不能提前离职”,但语义相近,因此可以被召回。
2. 关键词检索
关键词检索擅长精确匹配,例如合同编号、产品型号、制度名称、人名、项目编号等。
例如:
查询 HT-2024-0918 合同付款条款
这类问题必须依赖关键词精确召回,否则向量检索可能不稳定。
3. 混合检索
推荐采用以下方式:
最终召回结果 = 向量检索 TopK + 关键词检索 TopK
然后对结果去重,再交给重排序模型进行排序。
4. 重排序
Embedding 检索召回速度快,但排序不一定最准确。可以使用 reranker 模型进一步判断“问题”和“文档片段”的相关性。
推荐模型:
- BAAI/bge-reranker-base
- BAAI/bge-reranker-large
- bge-reranker-v2-m3
重排序后,只选择最相关的 3 到 8 个片段交给大模型生成答案。
八、问答生成 Prompt 设计
Prompt 是 AI 搜索效果的关键之一。一个基础模板如下:
你是企业内部知识库问答助手。
请只根据给定的参考资料回答用户问题。
如果参考资料中没有答案,请明确说明“根据现有资料无法确定”,不要编造。
回答要结构清晰,必要时使用条目。
回答后请列出引用来源。
用户问题:
{question}
参考资料:
{context}
为了减少幻觉,必须要求模型“只根据参考资料回答”。同时要返回引用来源,让用户可以追溯原文。
九、项目目录结构
下面给出一个简化版 AI 搜索项目结构:
ai-search-private/
├── app/
│ ├── main.py
│ ├── config.py
│ ├── models/
│ │ └── schema.py
│ ├── services/
│ │ ├── document_parser.py
│ │ ├── text_splitter.py
│ │ ├── embedding_service.py
│ │ ├── vector_store.py
│ │ ├── llm_service.py
│ │ └── qa_service.py
│ └── utils/
│ └── file_utils.py
├── data/
│ └── uploads/
├── requirements.txt
├── docker-compose.yml
└── README.md
十、核心源码示例
以下源码为一个最小可运行版本,主要演示“上传文档、切片入库、检索问答”的核心流程。生产环境需要补充权限控制、任务队列、异常处理、日志监控、异步索引等能力。
1. requirements.txt
fastapi==0.115.0
uvicorn==0.30.6
qdrant-client==1.11.3
sentence-transformers==3.0.1
python-multipart==0.0.9
requests==2.32.3
pydantic==2.8.2
2. docker-compose.yml
version: "3.8"
services:
qdrant:
image: qdrant/qdrant:v1.11.0
container_name: qdrant
ports:
- "6333:6333"
- "6334:6334"
volumes:
- ./qdrant_storage:/qdrant/storage
启动向量数据库:
docker compose up -d
3. config.py
import os
class Settings:
QDRANT_HOST = os.getenv("QDRANT_HOST", "localhost")
QDRANT_PORT = int(os.getenv("QDRANT_PORT", "6333"))
COLLECTION_NAME = os.getenv("COLLECTION_NAME", "enterprise_knowledge")
EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "BAAI/bge-small-zh-v1.5")
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://localhost:11434/api/generate")
LLM_MODEL = os.getenv("LLM_MODEL", "qwen2.5:7b")
UPLOAD_DIR = os.getenv("UPLOAD_DIR", "./data/uploads")
settings = Settings()
4. text_splitter.py
def split_text(text: str, chunk_size: int = 800, overlap: int = 120):
"""
将长文本切分为多个带重叠的片段。
chunk_size:每个片段最大字符数
overlap:相邻片段重叠字符数
"""
text = text.replace("\r\n", "\n").strip()
chunks = []
start = 0
length = len(text)
while start < length:
end = min(start + chunk_size, length)
chunk = text[start:end].strip()
if chunk:
chunks.append(chunk)
if end == length:
break
start = end - overlap
return chunks
5. embedding_service.py
from sentence_transformers import SentenceTransformer
from app.config import settings
class EmbeddingService:
def __init__(self):
self.model = SentenceTransformer(settings.EMBEDDING_MODEL)
def encode(self, texts):
vectors = self.model.encode(
texts,
normalize_embeddings=True
)
return vectors.tolist()
embedding_service = EmbeddingService()
6. vector_store.py
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
from app.config import settings
from app.services.embedding_service import embedding_service
import uuid
class VectorStore:
def __init__(self):
self.client = QdrantClient(
host=settings.QDRANT_HOST,
port=settings.QDRANT_PORT
)
self.collection_name = settings.COLLECTION_NAME
self._init_collection()
def _init_collection(self):
collections = self.client.get_collections().collections
names = [c.name for c in collections]
if self.collection_name not in names:
self.client.create_collection(
collection_name=self.collection_name,
vectors_config=VectorParams(
size=512,
distance=Distance.COSINE
)
)
def add_texts(self, texts, metadatas):
vectors = embedding_service.encode(texts)
points = []
for text, vector, metadata in zip(texts, vectors, metadatas):
point_id = str(uuid.uuid4())
payload = {
"content": text,
**metadata
}
points.append(
PointStruct(
id=point_id,
vector=vector,
payload=payload
)
)
self.client.upsert(
collection_name=self.collection_name,
points=points
)
return len(points)
def search(self, query: str, top_k: int = 5):
query_vector = embedding_service.encode([query])[0]
results = self.client.search(
collection_name=self.collection_name,
query_vector=query_vector,
limit=top_k
)
return [
{
"score": item.score,
"content": item.payload.get("content"),
"source": item.payload.get("source"),
"doc_id": item.payload.get("doc_id")
}
for item in results
]
vector_store = VectorStore()
注意:上面
VectorParams(size=512)需要与实际 Embedding 模型输出维度一致。bge-small-zh-v1.5的向量维度为 512。如果更换模型,需要同步调整。
7. llm_service.py
import requests
from app.config import settings
class LLMService:
def generate(self, prompt: str):
response = requests.post(
settings.OLLAMA_URL,
json={
"model": settings.LLM_MODEL,
"prompt": prompt,
"stream": False
},
timeout=120
)
response.raise_for_status()
data = response.json()
return data.get("response", "")
llm_service = LLMService()
8. qa_service.py
from app.services.vector_store import vector_store
from app.services.llm_service import llm_service
class QAService:
def ask(self, question: str):
docs = vector_store.search(question, top_k=5)
context_text = ""
for index, doc in enumerate(docs, start=1):
context_text += f"""
资料{index}:
来源:{doc.get("source")}
内容:{doc.get("content")}
"""
prompt = f"""
你是企业内部知识库问答助手。
请严格根据参考资料回答用户问题。
如果参考资料中没有相关答案,请回答:根据现有资料无法确定。
不要编造事实。
回答要清晰、准确、简洁。
最后列出引用来源。
用户问题:
{question}
参考资料:
{context_text}
"""
answer = llm_service.generate(prompt)
return {
"question": question,
"answer": answer,
"references": docs
}
qa_service = QAService()
9. main.py
import os
import uuid
from fastapi import FastAPI, UploadFile, File
from pydantic import BaseModel
from app.config import settings
from app.services.text_splitter import split_text
from app.services.vector_store import vector_store
from app.services.qa_service import qa_service
app = FastAPI(title="AI搜索私有化部署示例")
class AskRequest(BaseModel):
question: str
@app.post("/upload")
async def upload_file(file: UploadFile = File(...)):
os.makedirs(settings.UPLOAD_DIR, exist_ok=True)
doc_id = str(uuid.uuid4())
file_path = os.path.join(settings.UPLOAD_DIR, file.filename)
with open(file_path, "wb") as f:
f.write(await file.read())
# 简化示例:仅支持 txt / md 文本文件
with open(file_path, "r", encoding="utf-8") as f:
text = f.read()
chunks = split_text(text)
metadatas = []
for i, chunk in enumerate(chunks):
metadatas.append({
"doc_id": doc_id,
"source": file.filename,
"chunk_index": i
})
count = vector_store.add_texts(chunks, metadatas)
return {
"message": "上传并索引成功",
"doc_id": doc_id,
"chunks": count
}
@app.post("/ask")
async def ask(req: AskRequest):
return qa_service.ask(req.question)
十一、运行步骤
1. 安装依赖
pip install -r requirements.txt
2. 启动 Qdrant
docker compose up -d
3. 启动 Ollama 并拉取模型
ollama pull qwen2.5:7b
ollama serve
如果 Ollama 已经作为服务运行,则无需重复执行 ollama serve。
4. 启动后端服务
uvicorn app.main:app --host 0.0.0.0 --port 8000
访问接口文档:
http://localhost:8000/docs
十二、接口使用示例
1. 上传文档
curl -X POST "http://localhost:8000/upload" \
-F "file=@员工手册.txt"
返回示例:
{
"message": "上传并索引成功",
"doc_id": "5c377e8f-8a11-45ff-9d70-a983f9c3db01",
"chunks": 16
}
2. 提问
curl -X POST "http://localhost:8000/ask" \
-H "Content-Type: application/json" \
-d '{"question":"试用期员工离职需要提前几天通知公司?"}'
返回示例:
{
"question": "试用期员工离职需要提前几天通知公司?",
"answer": "根据员工手册,试用期员工如需离职,应提前三日通知公司,并按照公司要求完成工作交接手续。\n\n引用来源:员工手册.txt",
"references": [
{
"score": 0.82,
"content": "员工在试用期内解除劳动合同,应提前三日通知公司,并完成相关交接流程。",
"source": "员工手册.txt",
"doc_id": "5c377e8f-8a11-45ff-9d70-a983f9c3db01"
}
]
}
十三、生产环境优化建议
上面的源码适合演示核心逻辑,但生产环境还需要进一步增强。
1. 增加权限控制
企业知识库往往存在严格的权限边界。例如普通员工不能查看财务合同,销售人员只能查看自己部门的客户资料。权限控制建议在文档元数据中加入:
{
"department": "finance",
"roles": ["finance_manager"],
"owner": "user_001"
}
检索时根据当前登录用户过滤可访问文档,避免越权访问。
2. 使用异步任务队列
大文件解析和向量化耗时较长,不应在上传接口中同步完成。推荐引入:
- Celery
- RQ
- Dramatiq
- Kafka 消费任务
上传接口只负责保存文件并创建任务,后台 Worker 负责解析、切片和入库。
3. 增加文档版本管理
企业文档经常更新,需要支持:
- 文档重新索引;
- 旧版本归档;
- 删除文档时同步删除向量;
- 根据版本号检索;
- 回滚历史版本。
4. 引入混合检索
纯向量检索在处理编号、金额、日期、专业术语时可能不稳定。因此建议同时引入 Elasticsearch 或 OpenSearch,构建关键词索引,再与向量检索结果融合。
5. 增加重排序模型
对于复杂知识库,召回结果可能包含噪声。引入 reranker 后,可以显著提升答案准确率。典型流程:
用户问题
→ 向量召回 20 条
→ 关键词召回 20 条
→ 去重合并
→ reranker 重排
→ 取前 5 条生成答案
6. 完善可观测性
AI 搜索系统应记录完整链路日志,包括:
- 用户问题;
- 召回文档;
- 召回分数;
- Prompt 内容;
- 模型输出;
- 响应耗时;
- 用户反馈。
这样可以持续优化检索策略和 Prompt。
十四、常见问题与解决方案
1. 为什么上传文档后搜不到?
可能原因包括:
- 文档解析失败;
- 文本切片过短或过长;
- Embedding 模型效果不佳;
- 向量维度配置错误;
- 检索 TopK 太小;
- 问题表述与文档语义差距过大。
建议先查看向量库中是否有数据,再打印召回结果进行分析。
2. 为什么答案会胡编?
这是大语言模型常见的幻觉问题。解决方法包括:
- Prompt 中明确要求只根据资料回答;
- 当召回结果分数低时直接提示无答案;
- 返回引用来源;
- 对答案做事实校验;
- 使用更强的模型;
- 优化文档切片和召回。
3. 文档切片多大合适?
一般中文知识库建议每个片段 500 到 1000 字,重叠 100 到 200 字。如果是法律、制度类文档,可以按条款切分;如果是技术文档,可以按标题层级切分。
4. 是否一定需要 GPU?
如果只做向量检索,CPU 也可以运行,但速度较慢。大模型推理强烈建议使用 GPU。对于小规模测试,可以使用量化模型在 CPU 上运行,但生产环境不推荐。
十五、总结
AI 搜索私有化部署的核心价值在于:在保障企业数据安全的前提下,让内部知识真正“可问、可查、可追溯”。相比传统搜索,它不只是返回一堆文档链接,而是能够理解用户问题,自动检索相关资料,并生成结构化答案。
本文给出的方案采用 FastAPI、Qdrant、Sentence Transformers 和 Ollama 构建了一套最小可运行的 AI 搜索系统。它具备以下能力:
- 文档上传;
- 文本切片;
- 向量生成;
- 向量入库;
- 语义检索;
- 本地大模型问答;
- 引用来源返回。
对于真实生产环境,还需要继续完善权限管理、混合检索、重排序、文档版本管理、异步任务、监控告警和多租户能力。
如果企业希望快速落地 AI 搜索,建议先从一个小范围知识库开始,例如员工手册、产品说明书、客服 FAQ 或研发文档。通过 POC 验证检索效果、响应速度和用户体验后,再逐步扩展到更多业务系统。最终,AI 搜索可以成为企业内部知识管理、智能客服、办公助手和决策支持的重要基础设施。