企业级AI知识库(上):从0到1完整项目实战·架构设计篇
企业级AI知识库(上):从0到1完整项目实战·架构设计篇
适读人群:想做企业知识库项目的Java工程师,技术负责人,架构师 阅读时长:约20分钟
项目从一个内网搜索框开始
公司内网有个搜索系统,是2017年做的老项目,用Elasticsearch做全文搜索。我们内部叫它"知识库",但说实话,它就是个搜索引擎:你得知道搜什么关键词,才能找到你想要的东西。
有次一个新来的同事问我:"公司对接银行系统的接口文档在哪?"
我说你去知识库搜"银行接口"。
他搜了,没找到。因为文档里写的是"金融机构对接规范",关键词没匹配上。
他来问我,我帮他找到了,花了五分钟翻文件夹。
就这么一件小事,让我觉得:这个搜索系统不够智能,是整个团队的效率黑洞。
于是我提了一个方案:用AI重做这个知识库。方案通过了,给了我三个月时间、两个开发资源。这篇文章就是这个项目从0到1的完整记录,上篇讲架构设计,下篇讲核心实现。
需求分析:真正的痛点在哪里
在设计之前,我做了一件很重要的事:访谈了15个不同岗位的同事,问他们用知识库时遇到的最大问题。
收集到的痛点排名:
| 痛点 | 频次 | 影响程度 |
|---|---|---|
| 搜索找不到,要靠人问 | 13/15 | 高 |
| 文档太多不知道看哪个版本 | 11/15 | 高 |
| 跨部门文档格式不一致,难阅读 | 9/15 | 中 |
| 找到文档了,但不知道哪段是答案 | 8/15 | 高 |
| 文档更新不及时,有错误内容 | 7/15 | 高 |
| 移动端体验差 | 5/15 | 低 |
把这些痛点翻译成系统需求:
- 语义搜索:不靠关键词,靠语义理解——这对应RAG
- 版本管理:文档有版本,知道哪个是最新的
- 精准定位:不只返回文档,要返回文档中的具体段落
- 内容时效性:检测文档是否过期,及时提示
- 问答模式:直接回答问题,不只是搜索文档
整体架构设计
这个系统我把它分成四层:
这个架构有几个关键决策,我来逐一解释:
决策1:为什么用PgVector而不是专用向量数据库
团队里没有专门的DBA,运维能力有限。PgVector是PostgreSQL的扩展,我们本来就用PG,加个扩展就有向量能力,运维复杂度为0。
Qdrant或Milvus性能更好,但要维护额外的集群,对我们这个规模(10万文档)不值得。
决策2:为什么保留Elasticsearch
混合检索。向量检索对语义理解好,但对精确匹配(比如产品型号、合同编号)不如关键词搜索。两路并行,用RRF融合,效果比单一检索好20%+。
决策3:为什么有独立的质量评估服务
之前其他项目踩过坑:上线后不知道AI回答质量怎样。这次专门做了评估服务,记录每次检索的召回率、答案的用户反馈,让系统质量可量化、可改进。
核心模块详细设计
模块一:文档处理引擎
文档处理是整个系统的"消化系统",设计的好坏直接影响检索质量:
分块策略的选择逻辑:
- 有清晰标题结构的文档(如设计文档):按标题分块
- 散文类文档(如政策文件):按段落分块,512字符上限
- 代码文档、表格:特殊处理,不做细碎分块
模块二:检索引擎
采用混合检索架构:
用户查询
├── 向量检索(语义匹配) → Top-10
└── 关键词检索(BM25) → Top-10
↓
RRF融合排序
↓
权限过滤(重要!)
↓
重排序(可选,用于提升精度)
↓
返回 Top-5权限过滤是企业级系统的关键,必须在检索结果层面做,不能只做UI层:
@Service
public class SecureRetrievalService {
private final VectorStore vectorStore;
private final ElasticsearchClient esClient;
public List<Document> search(String query, UserContext userContext) {
// 构建权限过滤表达式
// 用户只能看到:公开文档 OR 自己部门的文档 OR 被授权的文档
String filterExpr = buildPermissionFilter(userContext);
// 向量检索(含权限过滤)
SearchRequest vectorRequest = SearchRequest.query(query)
.withTopK(10)
.withSimilarityThreshold(0.5)
.withFilterExpression(filterExpr);
List<Document> vectorResults = vectorStore.similaritySearch(vectorRequest);
// ES关键词检索(含权限过滤)
List<Document> keywordResults = esKeywordSearch(query, userContext);
// RRF融合
return rrfMerge(vectorResults, keywordResults, 5);
}
private String buildPermissionFilter(UserContext ctx) {
// PgVector的metadata filter语法
return String.format(
"visibility == 'PUBLIC' OR department == '%s' OR authorized_users like '%%%s%%'",
ctx.getDepartment(),
ctx.getUserId()
);
}
}模块三:版本管理设计
文档版本是企业知识库经常被忽略但很重要的功能:
状态机实现用Spring StateMachine:
@Configuration
@EnableStateMachineFactory
public class DocumentStateMachineConfig extends StateMachineConfigurerAdapter<DocState, DocEvent> {
@Override
public void configure(StateMachineStateConfigurer<DocState, DocEvent> states) throws Exception {
states.withStates()
.initial(DocState.DRAFT)
.states(EnumSet.allOf(DocState.class));
}
@Override
public void configure(StateMachineTransitionConfigurer<DocState, DocEvent> transitions) throws Exception {
transitions
.withExternal()
.source(DocState.DRAFT).target(DocState.REVIEW)
.event(DocEvent.SUBMIT).and()
.withExternal()
.source(DocState.REVIEW).target(DocState.PUBLISHED)
.event(DocEvent.APPROVE)
.action(publishAction()).and() // 发布时触发向量入库
.withExternal()
.source(DocState.REVIEW).target(DocState.DRAFT)
.event(DocEvent.REJECT).and()
.withExternal()
.source(DocState.PUBLISHED).target(DocState.DEPRECATED)
.event(DocEvent.SUPERSEDE)
.action(deprecateAction()); // 废弃时更新向量库标记
}
@Bean
public Action<DocState, DocEvent> publishAction() {
return context -> {
String docId = (String) context.getExtendedState()
.getVariables().get("docId");
documentIndexService.indexDocument(docId);
};
}
}数据库设计
核心表结构设计:
-- 文档主表
CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(500) NOT NULL,
file_path VARCHAR(1000),
file_type VARCHAR(20),
status VARCHAR(20) DEFAULT 'DRAFT', -- DRAFT/REVIEW/PUBLISHED/DEPRECATED/ARCHIVED
department VARCHAR(100),
visibility VARCHAR(20) DEFAULT 'DEPARTMENT', -- PUBLIC/DEPARTMENT/PRIVATE
version INTEGER DEFAULT 1,
parent_id UUID REFERENCES documents(id), -- 版本链
created_by VARCHAR(100),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP -- 文档过期时间,用于时效性提醒
);
-- 文档分块表(向量存储)
CREATE TABLE document_chunks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
doc_id UUID REFERENCES documents(id),
chunk_index INTEGER,
content TEXT NOT NULL,
embedding VECTOR(1536), -- OpenAI embedding维度
chunk_type VARCHAR(20), -- TEXT/TABLE/CODE/TITLE
page_num INTEGER,
metadata JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
-- 创建向量索引
CREATE INDEX ON document_chunks USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- 查询历史表(用于质量评估)
CREATE TABLE query_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id VARCHAR(100),
user_id VARCHAR(100),
query TEXT,
answer TEXT,
source_doc_ids JSONB,
latency_ms INTEGER,
user_rating INTEGER, -- 1-5分,用户评分
created_at TIMESTAMP DEFAULT NOW()
);非功能性设计
企业级系统,非功能性要求同样重要:
| 指标 | 目标 | 实现方案 |
|---|---|---|
| 检索延迟 | P99 < 2秒 | 向量索引 + Redis缓存热点问题 |
| 文档入库延迟 | < 30分钟 | 异步处理队列 + 优先级调度 |
| 系统可用性 | 99.5% | 多实例部署 + 健康检查 |
| 数据安全 | 部门级隔离 | 检索层权限过滤 + 审计日志 |
| 并发能力 | 100 QPS | 连接池 + 异步处理 + 水平扩展 |
缓存策略:
热点问题缓存(Redis TTL 1小时):
- Key: MD5(normalized_query)
- Value: {answer, source_docs, timestamp}
- 适用:高频重复问题,如"怎么报销"、"年假几天"
文档向量缓存(应用内存 LRU):
- 缓存最近1000个文档的向量,减少DB查询
检索结果缓存(Redis TTL 10分钟):
- 同一用户最近的检索结果
- 分页场景下避免重复检索第三方服务依赖规划
把第三方依赖分为必选和可替换,从一开始就为降本做好准备。LLM API用接口抽象,不耦合具体厂商,方便以后切到国产模型。
项目排期
| 阶段 | 时间 | 主要产出 |
|---|---|---|
| 架构设计 | Week 1 | 架构文档、技术选型、数据库设计 |
| 基础建设 | Week 2-3 | 文档解析、向量入库、基础检索 |
| 核心功能 | Week 4-6 | 问答生成、权限管理、版本管理 |
| 质量优化 | Week 7-8 | 混合检索、缓存、性能调优 |
| 集成测试 | Week 9-10 | 联调、压测、安全测试 |
| 上线灰度 | Week 11-12 | 10%流量、收集反馈、修复问题 |
小结
这篇讲的是架构设计,核心决策有三个:
- PgVector替代专用向量库,降低运维复杂度
- 混合检索(向量+关键词),覆盖不同查询模式
- 检索层权限过滤,保证数据安全
下篇会进入核心实现:文档处理引擎的完整代码、混合检索的具体实现、生成引擎的Prompt工程,以及上线后的效果数据。
