去年接手公司内部知识库优化项目时,我发现工程师们宁愿在聊天工具里反复询问同事,也不愿查阅已经整理好的技术文档。这种现象促使我开始思考:如何让静态文档"活"起来?经过三个月的迭代,我们基于Spring AI Alibaba ReactAgent和Qdrant构建的RAG问答系统,最终将文档利用率提升了60%。下面分享这套系统的完整实现思路和实战经验。
选择Spring AI Alibaba ReactAgent作为Agent框架,主要基于三个实际考量:
向量数据库选用Qdrant而非Milvus,则是出于以下对比:
| 对比维度 | Qdrant | Milvus |
|---|---|---|
| 部署复杂度 | 单二进制部署 | 需要多个组件 |
| 内存占用 | 支持内存映射 | 全内存加载 |
| 中文支持 | 自带中文分词 | 需额外配置 |
| 社区响应 | 24小时内 | 3-5天 |
提示:企业级选型要特别注意技术债务风险。我们曾测试过直接调用OpenAI Embedding+PGVector的方案,虽然开发快但存在数据出境风险,最终放弃。
整个系统采用分层设计,各模块职责明确:
code复制[文档接入层] --> [预处理流水线] --> [增强索引层]
--> [混合检索引擎] --> [问答生成层]
核心处理流程耗时分布(实测数据):
我们对接了三种文档来源:
语雀API调用示例:
java复制YuqueClient client = new YuqueClient(TOKEN);
List<YuqueDocument> docs = client.getRepoDocuments(NAMESPACE)
.stream()
.filter(doc -> "published".equals(doc.getStatus()))
.collect(Collectors.toList());
遇到的坑:Confluence的HTML内容包含大量样式标签,需要用Jsoup清洗:
java复制String cleanContent = Jsoup.parse(rawHtml)
.select("div.content")
.text()
.replaceAll("\\s+", " ");
经过20+次调整,我们总结出最佳分块策略:
结构感知分块:
动态分块算法:
java复制public List<Chunk> splitDocument(Document doc) {
if (doc.length() < 800) return List.of(fullDocChunk);
List<Section> sections = parseHeadings(doc);
if (!sections.isEmpty()) {
return splitBySections(sections);
}
return splitByParagraphs(doc);
}
code复制[产品手册] > [安装指南] > [Docker部署]
实测发现:带上下文的chunk在检索准确率上比普通分块高37%
除了原始文本,我们提取了三种增强特征:
python复制def extract_keywords(text):
tfidf = TfidfVectorizer().fit([text])
tr = TextRank().analyze(text)
return list(set(tfidf.top_terms(5) + tr.top_terms(3)))
java复制String summary = SummaryGenerator.generate(content)
.filter(sent -> sent.length() > 15)
.limit(3)
.collect(Collectors.joining("。"));
java复制List<String> entities = HanLP.segment(content)
.stream()
.filter(term -> "nt".equals(term.nature))
.map(term -> term.word)
.distinct()
.collect(Collectors.toList());
我们测试了三种embedding方式:
| 模型 | 维度 | 中文效果 | 速度(doc/s) |
|---|---|---|---|
| text2vec-large | 1024 | ★★★★☆ | 120 |
| m3e-base | 768 | ★★★★ | 250 |
| bge-small-zh | 512 | ★★★☆ | 400 |
最终选择text2vec-large,虽然速度慢但准确率高。存储时采用"特征拼接法":
java复制String vectorContent = String.join("\n",
chunk.getTitle(),
String.join(",", chunk.getKeywords()),
chunk.getSummary(),
chunk.getContent()
);
float[] vector = embeddingModel.embed(vectorContent);
mermaid复制graph TD
A[用户查询] --> B{Query Rewrite}
B --> C[向量检索]
B --> D[BM25检索]
C --> E[RRF融合]
D --> E
E --> F[结果去重]
F --> G[最终排序]
核心参数配置:
yaml复制retrieval:
vector:
top_k: 15
similarity_threshold: 0.78
keyword:
top_k: 10
fusion:
method: rrf
k: 60
实现对话上下文感知的改写:
java复制public String rewriteQuery(String currentQuery, List<ChatMessage> history) {
if (history.size() > 2) {
String lastAnswer = history.get(history.size()-1).getContent();
return currentQuery + "[上下文]" + lastAnswer;
}
return currentQuery;
}
典型改写案例:
code复制原始查询:"这个参数怎么设置?"
改写后:"[上下文]您说的是数据库连接池大小参数吗?这个参数怎么设置?"
java复制@Bean
public ReactAgent qaAgent(ChatModel chatModel, VectorSearchTool searchTool) {
return ReactAgent.builder()
.name("QA-Agent")
.model(chatModel)
.methodTools(searchTool)
.maxIterations(3)
.stopOnObservation(true)
.build();
}
工具注册关键点:
采用两阶段生成:
markdown复制检索到5个相关文档:
1. 《安装指南》第3章(匹配度87%)
2. 《常见问题》Q15(匹配度79%)
...
请确认是否需要更多信息?
markdown复制根据《安装指南》第3章建议:
1. 修改config.yaml中的pool_size
2. 建议值:生产环境50-100
3. 需要重启服务生效
[来源]:https://internal-docs/install#ch3
三级缓存架构:
使用Caffeine实现:
java复制LoadingCache<String, List<Document>> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(30, TimeUnit.MINUTES)
.build(query -> vectorSearch(query));
耗时操作异步化设计:
java复制CompletableFuture<List<Chunk>> chunkFuture = CompletableFuture
.supplyAsync(() -> chunker.chunk(doc), ioExecutor);
CompletableFuture<List<Vector>> vectorFuture = chunkFuture
.thenComposeAsync(chunks ->
CompletableFuture.supplyAsync(() ->
chunks.stream().map(embedder::embed).toList(),
computeExecutor
)
);
线程池配置建议:
问题现象:
"Java线程池"被错误分词为["Java", "线", "程池"]
解决方案:
自定义Qdrant分词器配置:
json复制{
"tokenizers": {
"chinese": {
"type": "jieba",
"dict_path": "/path/to/user_dict.txt"
}
}
}
用户词典示例:
code复制线程池 n
连接池 n
SpringBoot n
问题现象:
相似文档的余弦相似度波动达±0.15
解决方案:
java复制float[] normalized = normalize(vector);
我们建立了三维评估体系:
检索层面:
生成层面:
业务层面:
持续优化方法:
这套系统已在内部运行8个月,处理了超过15万次查询。最大的体会是:RAG系统就像园丁,需要持续修剪(优化检索)和施肥(更新数据),才能保持旺盛的生命力。最近我们正在试验将用户反馈自动转化为训练数据的方法,或许下次可以分享这个方向的实践。