96SEO 2026-08-09 23:21 0
很多团队接入了AI后发现一个尴尬的问题:AI回答得头头是道,但跟你的业务完全没关系。
”AI答这方面,“方式如下…,”这就是典型的知识缺失问题。AI预训练时根本不知道你公司的规章制度,需要通过RAG把知识注入进去。
RAG = Retrieval + Augmentation + Generation
今天这篇文章。我用一个完整的公司内部知识问答程序案例,带你从0到1掌握Spring AI RAG开发,让上述痛点迎刃而解。
某科技公司需要一个内部知识问答程序,让员工可以:
┌─────────────────────────────────────────────────────────────────┐
│ 使用者查询界面 │
│ │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Spring Boot 应用 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ REST API │ │ ChatClient│ │ Document │ │ Vector Search │ │
│ │ Controller│ │ │ │ Loader │ │ Service │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────────────┘ │
└────────────────────────────┬────────────────────────────────────┘
┌────────────────────╂─────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Milvus向量库 │ │ 文档存储 │ │ 通义Qwen API │
│ │ │ │ │ │
└───────────────┘ └───────────────┘ └───────────────┘
Sprint AI: 提供统一的Embedding/Chat抽象;Milvus v2.x: 高性能向量检索,引擎支持过滤和混合搜索;LLM - 通义千问 Qwen‑plus : 国内可商用的大模型,支持流式输出;说起来,Docker Compose + Spring Boot : 一键本地/私有云部署。
company-kb/
├── docker-compose.yml
├── spring-ai-kb-app/
│ ├── src/main/java/com/company/kb/
│ │ ├── CompanyKbApplication.java
│ │ ├── config/
│ │ │ ├── AiConfig.java
│ │ │ └── MilvusConfig.java
│ │ ├── controller/
│ │ │ ├── ChatController.java
│ │ │ ├── DocumentController.java
│ │ ├── service/
│ │ │ ├── RagService.java
│ │ │ ├── DocumentService.java
│ │ │ └── EmbeddingService.java
│ │ ├── model/
│ │ │ ├── DocumentChunk.java
│ | | ├── ChatRequest.java
│ | └─ loader/
│ ├── PdfLoader.java
│ ├── WordLoader.java …├── src/main/resources/
| └─ application.yml
├── documents/ # 原始文档目录
└── embeddings/ # 嵌入缓存
# docker-compose.yml
version: '3.8'
services:
# Milvus 向量数据库
milvus-etcd:
至于image,quay.io/coreos/etcd:v3.5.9
environment:
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_AUTO_COMPACTION_RETENTION=1000
- ETCD_QUOTA_BACKEND_BYTES=4294967296
volumes:
- ./volumes/etcd:/etcd
command: etcd --advertise-client-urls=http://0.0.0.0:2379 \
--listen-client-urls http://0.0.0.0:2379 \
--data-dir /etcd
milvus-minio:
image的观点是,minio/minio:RELEASE.2024-03-01T08-12-52Z
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
volumes:
- ./volumes/minio:/minio_data
command: minio server /minio_data
milvus-standalone:
再看image,milvusdb/milvus:v2.4.5
container_name: milvus-standalone
说到ports,- "19530:19530"
- "9091:9091"
volumes:
- ./volumes/milvus:/var/lib/milvus
environment:
ETCD_ENDPOINTS: milvus-etcd:2379
MINIO_ADDRESS: milvus-minio:9000
depends_on:
- milvus-etcd
- milvus-minio
restart: always
# Spring AI 应用
spring-ai-kb:
从build来看,./spring-ai-kb-app
container_name: spring-ai-kb
再看ports,- "8080:8080"
environment:
- SPRING_AI_ALIBABA_API_KEY=${ALI_API_KEY}
volumes:
- ./documents:/data/documents
depends_on:
- milvus-standalone
restart: always
networks:
default:
name这方面,milvus-network
*启动命令*
# 启动所有服务
docker-compose up -d
# 查看运行状态
docker-compose ps
# 看日志
docker-compose logs -f spring-ai-kb
说到spring。application:
name的观点是,company-kb-system
servlet:
multipart:
max-file-size: 100MB # 单文件最大100MB
max-request-size: 500MB # 单次请求最大500MB
从ai来看,alibaba:
api-key: ${ALI_API_KEY}
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
说到chat,options:
从model来看,qwen-plus # 大模型选择
temperature: 0.1 # RAG 场景低温度保证准确性
max-tokens: 2048
embedding:
options:
从model来看,text-embedding-v4 # 嵌入模型
spring.ai.vectorstore.milvus:
client这方面,host: localhost # Docker 内部网络默认 localhost
再看port,19530 # Milvus 默认端口
从app来看,storage:
documents: /data/documents # 挂载到容器内的文档目录
--- # 可选的嵌入缓存方法
server的观点是,port :8080
logging.level:
org.springframework.ai : DEBUG
com.company.kb : DEBUG
The quality of chunking decides ~80% of RAG performance.
| 策略 | 块大小 | 重叠 | 适用场景 | |||||||
|---|---|---|---|---|---|---|---|---|---|---|
| 固定字数 | 500 字 | 50 字 | 通用场景 | |||||||
| 段落分块 | 按段落边界 | 0 字 | 结构清晰的手册/规章制度 | 语义分块 | ≈100 字 | 20 字 高度语义化内容,如研发方案或代码注释 | ||||
package com.company.kb.loader;import org.springframework.stereotype.Component;import org.springframework.web.multipart.MultipartFile;import java.io.IOException;老实说,import java.nio.file.Path;import java.util.List;老实说,/** 文档加载器统一接口 */
public interface DocumentLoader {
boolean supports;List load throws IOException;说起来,}
@Component
public class PdfLoader implements DocumentLoader {
@Override public boolean supports {
return fileName.toLowerCase.endsWith;...
private List smartChunk {
// 清理 & 按段落切分 → 合并形成适当大小块 + 重叠保留上下文 ...
}
...
}
@Component public class WordLoader implements DocumentLoader {
...
private List smartChunk{
// 基于句子边界进行截断并保留 overlap ...
}
...
}
@Component public class MdLoader implements DocumentLoader {
...
private List chunkBySection{
// 按标题拆分 → 必要时再细粒度切块 …}
...
}
package com.company.kb.config;import io.milvus.client.MilvusClient;话说回来,import io.milvus.param.ConnectParam;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configuration public class MilvusConfig {
@Value private String host;@Value private int port;@Bean public MilusClient milusClient {
return new MilusClient
.withHost
.withPort
.build);}
}
...
@Service @RequiredArgsConstructor @Slf4j public class DocumentService {
private final List loaders;private final EmbeddingModel embeddingModel;@Value private String documentsPath;private VectorStore vectorStore;@PostConstruct public void init{
this.vectorStore = MilusVectorStore.builder)
.collectionName
.dimension // text‑embedding‑v4维度固定为1024
.build;}
public UploadResult uploadDocument throws Exception{
// 保存原始文件 → 找到对应 loader → 分块 → 构造 Document 带元数据 → 写入向量库 …}
...
}
...
mermaid
graph LR
A --> B
A --> C
A --> D
B & C & D --> E
E --> F
F --> G
\t\t\t\t\t\t\t\t
\r\r\r\r
java @Service @RequiredArgsConstructor@Slf4j public class MultiRetrieverService {
private final VectorStore vectorStore;private final EmbeddingModel embeddingModel;
/** 多路召回 */
public List
/** 向量语义检索 */
private List
/** 简易关键词匹配 */
private List
/** 同源元数据补齐 */
private List
/** 重排 – 用交叉编码得分 */
private List
private final ChatClient chatClient;private final MultiRetrieverService retriever;话说回来,
/** 主入口 – 问答 */
public RagAnswer answer{
long start = System.currentTimeMillis;List
if){
return new RagAnswer。null,System.currentTimeMillis-start);说起来,
}
// 构造上下文 —— 自动截断防止 token 爆炸 String context = buildContext;
// Prompt 工程—— 强制要求引用来源、不编造内容 String prompt = buildPrompt;怎么说呢,
String answer;按理说,try{ answer = chatClient.prompt.user.call.content;} catch{ log.error;说起来,answer = "服务暂不可用,请稍后再试";}
return new RagAnswer(answer。buildSources,chunks.size,System.currentTimeMillis-start);}
/* 辅助方法 …说起来,buildContext / buildPrompt / estimateTokens …*/ }
java
public Flux
return chatClient.prompt
.user
.stream
.content;怎么说呢,// SSE 输出每个 token 块
}
java @RestController @RequestMapping @RequiredArgsConstructor public class KnowledgeBaseController {
private final RagService ragService;private final DocumentService documentService;
/** 问答入口 */ @PostMapping public RagAnswer ask{ return ragService.answer);}
/** 流式返回 */
@PostMapping(value="/ask/stream",produces=MediaType.TEXTEVENTSTREAM_VALUE)
public Flux
/** 文档上传 */ @PostMapping public DocumentService.UploadResult upload MultipartFile f) throws Exception{ return documentService.uploadDocument;}
/* …其他查询·删除等,*/ }
${s.excerpt}javascript
// 简单 fetch 示例
async function ask{
const q=document.getElementById.value;const r=await fetch('/api/kb/ask',{
method的观点是,'POST'。headers:{'Content-Type':'application/json'},body:`{"question":"${q}"}`});const d=await r.json;其实,document.getElementById.innerText=d.answer;// 展示来源
document.getElementById.innerHTML =
d.sources.map(s=>`
java @Async // 异步上传。明显提高吞吐 public CompletableFutureUploadResult<\/span> uploadAsync{…说起来,}
public void batchEmbed{…} // 批处理一次性算多个向量
@Bean CacheManager cacheManager{return new ConcurrentMapCacheManager;} @Cacheable\") public float embedWithCache{return embeddingModel.embed;}
java @Configuration public class SecurityConfig{
@Bean SecurityFilterChain filterChainthrows Exception{ return http.csrf) .authorizeHttpRequests(auth->auth .requestMatchers.aunticated .requestMatchers.hasRole ) .httpBasic) .build;} }
java @Bean MeterRegistry meterRegistry{ return new PromeusMeterRegistry;}
private void recordMetrics{ meterRegistry.counter.increment;meterRegistry.timer .record,TimeUnit.MILLISECONDS);}
bash
curl -X POST -F "file=@./docs/年假制度.pdf" http://localhost/api/kb/upload
curl -X POST -H "Content-Type:" application/json \ -d '{"question":"我的年假有多少天?"}' \ http这方面,//localhost/api/kb/ask
curl -X POST -H "Content-Type:" application/json \ -d '{"question":"请详细说公司的请假流程"}' \ 从http来看。//localhost/api/kb/ask/stream | while read line;do echo "$line";done
| # 指标 # 含义 # 阈值目标 | ||
|---|---|---|
| 召回率 | 相关文档被成功检出的比例 | %95 |
| 准确率 | 返回结果确实匹配使用者意图的比例 | %92 |
| 响应时延 | 从提问到得到完整答案所耗时间<𢙑 s | <𢙑 s |
| Token 消耗&$ 成本 | 每千次对话平均 token 使用情况及对应费用 | <$$10/月 |
RAG 程序 = 文档处理 + 向量嵌入 + 检索 + LLM生成关键技术点:
├─ 智能分块
├─ 向量存储
├─ 多路召回
├─ 重排序
├─ Prompt工程
╰─ 流式输出
| #方向 | #实现思路 | #价值说明 |
|---|
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback