在当今AI应用开发领域,大型语言模型(LLM)虽然展现出惊人的文本理解和生成能力,却始终面临两个致命缺陷:知识更新滞后和私有数据缺失。想象一下,当你向ChatGPT询问2023年之后发布的新技术规范时,它很可能会给出过时甚至错误的答案;同样,当你希望它基于公司内部技术文档回答问题时,它也会显得无能为力。这正是检索增强生成(Retrieval-Augmented Generation,简称RAG)技术诞生的背景。
RAG技术的核心思想可以用一个简单的类比来理解:就像一位经验丰富的律师在准备案件时,不仅依靠自己的法律知识储备,还会查阅最新的判例法和相关法律条文。RAG让大模型在生成回答前,先从一个可定制的外部知识库中检索相关信息,然后将这些信息作为上下文提供给模型,最终生成既准确又符合特定领域知识的回答。
在RAG出现之前,开发者主要采用两种方案来解决大模型的知识局限问题:
相比之下,RAG方案具有显著优势:
| 方案类型 | 成本 | 知识更新 | 私有数据支持 | 实施难度 |
|---|---|---|---|---|
| 全量微调 | 极高 | 困难 | 需要专业数据集 | 非常高 |
| 适配器微调 | 中高 | 较困难 | 需要标注数据 | 高 |
| RAG | 低 | 即时 | 直接支持 | 中等 |
一个完整的RAG系统通常包含三个关键阶段:
这种架构的最大优势在于,知识库可以独立于大模型进行更新和维护。当企业产品规格变更时,只需更新向量数据库中的对应文档,无需重新训练或调整大模型。
Embedding技术是RAG能够实现精准检索的核心支撑。简单来说,Embedding模型就像一个"语义翻译器",将人类可读的文本转换为机器可理解的向量表示。这种转换的关键在于保持语义相似性——意思相近的文本在向量空间中的距离会更近。
以OpenAI的text-embedding-ada-002模型为例,它会将每个文本转换为一个1536维的向量。计算两个向量之间的余弦相似度,就能量化它们的语义相关程度:
python复制from openai import OpenAI
import numpy as np
client = OpenAI()
def get_embedding(text):
return client.embeddings.create(input=[text], model="text-embedding-ada-002").data[0].embedding
# 计算余弦相似度
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
embedding1 = get_embedding("户外露营安全注意事项")
embedding2 = get_embedding("野外帐篷搭建的防护措施")
embedding3 = get_embedding("股票投资入门指南")
print(f"相似查询相似度: {cosine_similarity(embedding1, embedding2):.4f}")
print(f"不相关查询相似度: {cosine_similarity(embedding1, embedding3):.4f}")
输出结果可能类似于:
code复制相似查询相似度: 0.8723
不相关查询相似度: 0.1234
将长文档分割成适当的文本块(chunking)是RAG系统中的一个关键但常被忽视的环节。不合理的分块会导致两种问题:
实践中常用的分块策略包括:
以下是一个基于Python的智能分块示例:
python复制from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
length_function=len,
separators=["\n\n", "\n", "。", "!", "?", ";"]
)
long_text = "户外露营安全指南...(长文本内容)..."
chunks = text_splitter.split_text(long_text)
在pom.xml中添加必要的依赖:
xml复制<dependencies>
<!-- Spring Boot基础依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 智普AI集成 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-zhipuai</artifactId>
</dependency>
<!-- 文本处理工具 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-client-chat</artifactId>
<version>${spring-ai.version}</version>
</dependency>
</dependencies>
在application.properties中配置智普AI的访问参数:
properties复制# 智普AI基础配置
spring.ai.zhipuai.api-key=your-api-key
spring.ai.zhipuai.base-url=https://open.bigmodel.cn/api/paas
# Embedding模型配置
spring.ai.zhipuai.embedding.options.model=embedding-2
# Chat模型配置
spring.ai.zhipuai.chat.options.model=GLM-4-Flash
java复制@Service
public class RagService {
private final EmbeddingModel embeddingModel;
private final ChatClient chatClient;
private final List<String> docChunks = new ArrayList<>();
private final List<float[]> docVectors = new ArrayList<>();
public RagService(EmbeddingModel embeddingModel, ChatClient.Builder chatClientBuilder) throws IOException {
this.embeddingModel = embeddingModel;
this.chatClient = chatClientBuilder.build();
loadAndSplitDocument();
}
private void loadAndSplitDocument() throws IOException {
Resource resource = new ClassPathResource("户外旅行安全指南.txt");
String content = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
// 基于文档结构的分块策略
String[] chunks = content.split("----");
for (String chunk : chunks) {
String cleanChunk = chunk.strip();
if (!cleanChunk.isBlank()) {
docChunks.add(cleanChunk);
docVectors.add(embeddingModel.embed(cleanChunk));
}
}
}
}
java复制public String answer(String question) {
// 1. 问题向量化
float[] questionVector = embeddingModel.embed(question);
// 2. 检索最相关文本块
List<String> topChunks = retrieveTopChunks(questionVector, 2);
// 3. 构建提示词
String context = String.join("\n---\n", topChunks);
String prompt = String.format(
"基于以下旅行安全知识:\n%s\n请专业地回答:%s",
context, question
);
// 4. 调用大模型生成回答
return chatClient.prompt()
.system("你是一位专业的户外旅行安全顾问,回答要准确简洁。")
.user(prompt)
.call()
.content();
}
private List<String> retrieveTopChunks(float[] questionVector, int topK) {
// 使用优先队列维护TopK结果
PriorityQueue<ChunkSimilarity> heap = new PriorityQueue<>(
Comparator.comparingDouble(ChunkSimilarity::getSimilarity)
);
for (int i = 0; i < docVectors.size(); i++) {
double similarity = cosineSimilarity(questionVector, docVectors.get(i));
if (heap.size() < topK || similarity > heap.peek().similarity) {
heap.offer(new ChunkSimilarity(i, similarity));
if (heap.size() > topK) heap.poll();
}
}
return heap.stream()
.sorted(Comparator.reverseOrder())
.map(item -> docChunks.get(item.index))
.toList();
}
java复制@RestController
@RequestMapping("/api/rag")
public class RagController {
@Autowired
private RagService ragService;
@GetMapping("/query")
public ResponseEntity<Map<String, Object>> query(
@RequestParam String question,
@RequestParam(defaultValue = "2") int topK) {
long start = System.currentTimeMillis();
String answer = ragService.answer(question);
long latency = System.currentTimeMillis() - start;
return ResponseEntity.ok(Map.of(
"question", question,
"answer", answer,
"latency_ms", latency
));
}
}
单纯的向量检索有时会忽略关键词匹配的重要性。结合两种方法的混合检索(Hybrid Search)可以显著提升召回率:
java复制public List<String> hybridSearch(String query, int topK) {
// 向量检索
float[] queryVector = embeddingModel.embed(query);
List<ChunkSimilarity> vectorResults = vectorSearch(queryVector, topK*2);
// 关键词检索(使用BM25等算法)
List<ChunkSimilarity> keywordResults = bm25Search(query, topK*2);
// 结果融合与重排序
return fuseResults(vectorResults, keywordResults, topK);
}
通过同义词扩展、关联词挖掘等技术丰富原始查询:
java复制public String expandQuery(String originalQuery) {
// 获取同义词
Set<String> synonyms = getSynonyms(originalQuery);
// 获取关联实体
Set<String> relatedEntities = getRelatedEntities(originalQuery);
// 组合成新查询
return Stream.concat(
Stream.concat(Stream.of(originalQuery), synonyms.stream()),
relatedEntities.stream()
)
.distinct()
.collect(Collectors.joining(" "));
}
检索到的文档可能包含冗余信息,通过摘要或提取技术压缩上下文:
java复制public String compressContext(List<String> chunks, String question) {
String combined = String.join("\n", chunks);
// 使用大模型提取关键信息
String prompt = String.format(
"请从以下文本中提取与问题'%s'直接相关的信息,删除无关内容:\n%s",
question, combined
);
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
引导模型分步思考,提高回答的逻辑性:
java复制public String stepByStepReasoning(String question, String context) {
String prompt = String.format("""
请按照以下步骤回答问题:
1. 分析问题:%s
2. 从上下文中识别相关事实:%s
3. 综合这些事实,给出专业回答
""", question, context);
return chatClient.prompt()
.system("你是一位严谨的专家,回答问题要分步推理")
.user(prompt)
.call()
.content();
}
建立完善的评估体系对RAG系统至关重要:
| 指标类别 | 具体指标 | 说明 |
|---|---|---|
| 检索质量 | 召回率@K | 前K个结果中包含正确答案的比例 |
| 精确率@K | 前K个结果中相关结果的比例 | |
| 生成质量 | 事实一致性 | 回答与检索内容的一致性程度 |
| 流畅度 | 回答的语言流畅性 | |
| 系统性能 | 响应延迟 | 端到端处理时间 |
| 吞吐量 | 每秒处理的查询数 |
java复制@Aspect
@Component
@Slf4j
public class RagMonitorAspect {
@Autowired
private MetricsService metricsService;
@Around("execution(* com..RagService.answer(..))")
public Object monitorPerformance(ProceedingJoinPoint pjp) throws Throwable {
String question = (String) pjp.getArgs()[0];
long start = System.currentTimeMillis();
try {
Object result = pjp.proceed();
long latency = System.currentTimeMillis() - start;
metricsService.recordLatency(latency);
metricsService.recordQuery(question);
return result;
} catch (Exception e) {
metricsService.recordError();
throw e;
}
}
}
问题1:检索结果不相关
可能原因:
解决方案:
问题2:重要文档未被检索
可能原因:
解决方案:
问题1:回答与检索内容不符
可能原因:
解决方案:
问题2:回答包含幻觉信息
可能原因:
解决方案:
将传统文本RAG扩展为支持图像、音频等多模态数据的系统架构:
结合知识图谱增强RAG的推理能力:
mermaid复制graph LR
A[用户问题] --> B(实体识别)
B --> C{知识图谱查询}
C --> D[相关子图]
D --> E[向量检索]
E --> F[增强上下文]
F --> G[生成回答]
根据查询类型自动选择最优策略的智能系统:
在实际项目中,我发现RAG系统的性能很大程度上取决于检索阶段的质量。一个实用的技巧是为不同的知识领域建立专门的检索器,比如将产品文档、技术规范和常见问题解答分开处理,这样可以显著提高检索精度。另外,定期用真实用户问题测试系统的召回率也非常重要,这能帮助我们发现Embedding模型在特定领域的盲点。