RAG 检索增强生成 完整技术指南
RAG 检索增强生成 完整技术指南
一、什么是 RAG?
RAG = Retrieval Augmented Generation
检索 + 增强 + 生成
核心思想:
不把知识"硬编码"进模型参数
而是在回答时实时"查阅外部资料"再作答
类比:
❌ 纯 LLM = 闭卷考试(只凭记忆)
✅ RAG = 开卷考试(可以翻资料)
1.1 为什么需要 RAG?
1.2 RAG vs 微调 vs 纯提示
三种方案对比
知识时效性 私有知识 成本 幻觉控制 溯源性
纯 Prompt 工程 ❌ ❌ 低 ❌ ❌
RAG ✅ ✅ 中 ✅ ✅
微调 Fine-tuning ❌ ✅ 高 ❌ ❌
RAG + 微调(组合) ✅ ✅ 高 ✅✅ ✅
结论:
知识频繁更新、需要溯源 → RAG
固定领域风格/格式定制 → 微调
最优效果 → RAG + 微调组合
二、RAG 核心原理
2.1 基础 RAG 三步流程
┌─────────────────────────────────────────────────────────────┐
│ RAG 基础流程 │
│ │
│ 用户提问 │
│ │ │
│ ▼ │
│ ┌───────────┐ 相似度搜索 ┌─────────────┐ │
│ │ 问题向量化 │ ────────────▶ │ 向量数据库 │ │
│ │ Embedding │ │ 知识库文档 │ │
│ └───────────┘ └──────┬──────┘ │
│ │ 召回相关文档 │
│ ▼ │
│ ┌─────────────┐ │
│ │ 上下文组装 │ │
│ │ 问题+文档片段│ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ LLM │ │
│ │ 基于文档生成 │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ 最终答案(含来源) │
└─────────────────────────────────────────────────────────────┘
2.2 离线索引 + 在线检索 双阶段
━━━━━━━━━━━━━━ 离线阶段(构建索引) ━━━━━━━━━━━━━━
原始文档(PDF / Word / 网页 / 数据库)
│
▼
① 文档加载 解析各类格式
│
▼
② 文本切块 按语义/固定长度切分
│
▼
③ 向量化 Embedding 模型编码
│
▼
④ 存储索引 写入向量数据库
(Milvus / Chroma / Pinecone)
━━━━━━━━━━━━━━ 在线阶段(实时检索) ━━━━━━━━━━━━━━
用户查询
│
▼
① 查询向量化 与文档使用同一 Embedding 模型
│
▼
② 相似度检索 ANN 近似最近邻搜索
│
▼
③ 结果重排序 精排召回内容
│
▼
④ Prompt 组装 将文档片段注入 Prompt
│
▼
⑤ LLM 生成 基于上下文生成答案
2.3 相似度计算原理
import numpy as np
# 向量化示例
text_a = "信号机故障处置流程"
text_b = "信号设备出现问题如何应对"
text_c = "今天天气怎么样"
# Embedding 后变成高维向量(示例用低维演示)
vec_a = embed(text_a) # [0.8, 0.2, 0.9, ...]
vec_b = embed(text_b) # [0.7, 0.3, 0.8, ...] ← 语义相近
vec_c = embed(text_c) # [0.1, 0.9, 0.2, ...] ← 语义不相关
# 余弦相似度:向量夹角越小,语义越相近
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
print(cosine_similarity(vec_a, vec_b)) # 0.95 ← 高度相似
print(cosine_similarity(vec_a, vec_c)) # 0.12 ← 不相关
三、RAG 技术演进路线
RAG 发展三代
第一代:Naive RAG(朴素 RAG)
2020~2022 固定切块 → 单路检索 → 直接生成
问题:检索不准、切块破坏语义、无法处理复杂问题
第二代:Advanced RAG(高级 RAG)
2022~2023 优化切块 + 查询改写 + 重排序
改进:显著提升召回质量,但流程仍是线性
第三代:Modular RAG(模块化 RAG)
2023~今 多路检索 + 动态路由 + Agent 驱动
特点:灵活组合,解决复杂多步推理问题
四、Naive RAG(朴素 RAG)
4.1 实现代码
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
# ── Step 1:文档切块 ───────────────────────────────────────────────
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 每块 500 字符
chunk_overlap=50, # 相邻块重叠 50 字符(防信息割裂)
separators=["\n\n", "\n", "。", "!", "?", " "]
)
chunks = splitter.split_documents(docs)
# ── Step 2:向量化 + 存储 ──────────────────────────────────────────
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=OpenAIEmbeddings()
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
# ── Step 3:构建 RAG Chain ─────────────────────────────────────────
prompt = ChatPromptTemplate.from_template("""
基于以下上下文回答问题,若上下文中没有答案请如实说明:
上下文:
{context}
问题:{question}
""")
rag_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| ChatOpenAI(model="gpt-4o")
)
answer = rag_chain.invoke("信号机故障如何处置?")
4.2 朴素 RAG 的典型问题
❌ 问题一:切块破坏语义
"第三条规定员工必须..." 被切成 "第三条规定员" + "工必须..."
解决:语义感知切块
❌ 问题二:查询与文档表述不一致
用户问:"信号坏了怎么办"
文档写:"信号设备故障处置规程"
→ 向量相似度低,召回失败
解决:查询改写 / HyDE
❌ 问题三:Top-K 召回质量不稳定
相关文档在第 6 位,但只取 Top-5
解决:增大 K + 重排序
❌ 问题四:无法处理多跳问题
"A 站故障会影响哪些下游设备的运行?"
需要多步推理,单次检索不够
解决:Multi-hop / 迭代检索
五、Advanced RAG(高级 RAG)
5.1 高级 RAG 全链路
查询输入
│
▼
┌──────────────────────────────────────────────────────┐
│ Pre-Retrieval(检索前优化) │
│ ① 查询分类 判断问题类型,选择检索策略 │
│ ② 查询改写 同义扩展 / 纠错 / 拆解子问题 │
│ ③ HyDE 生成假设文档,增强语义匹配 │
└──────────────────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ Retrieval(混合检索) │
│ ④ 稠密检索 向量 ANN 语义搜索 │
│ ⑤ 稀疏检索 BM25 关键词搜索 │
│ ⑥ RRF 融合 倒排排名融合,综合两路结果 │
└──────────────────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ Post-Retrieval(检索后优化) │
│ ⑦ 重排序 Cross-Encoder 精排 │
│ ⑧ 上下文压缩 去除冗余,保留核心信息 │
│ ⑨ 来源标注 每段内容附带文档来源 │
└──────────────────────┬───────────────────────────────┘
│
▼
LLM 生成答案
5.2 查询改写(Query Rewriting)
from langchain_core.output_parsers import StrOutputParser
# ── 方法一:同义改写 ────────────────────────────────────────────────
rewrite_prompt = ChatPromptTemplate.from_template("""
将以下问题改写为更适合文档检索的形式:
- 展开缩写和口语化表达
- 补充领域专业术语
- 保持原意不变
原始问题:{question}
改写后:
""")
rewrite_chain = rewrite_prompt | llm | StrOutputParser()
# ── 方法二:Multi-Query(生成多路查询)──────────────────────────────
multi_query_prompt = ChatPromptTemplate.from_template("""
为以下问题生成 3 个不同角度的检索查询,每行一个:
问题:{question}
3个查询:
""")
def multi_query_retrieve(question: str) -> list:
"""多路查询 → 并行检索 → 结果去重合并"""
queries_text = (multi_query_prompt | llm | StrOutputParser()).invoke(
{"question": question}
)
queries = [q.strip() for q in queries_text.split("\n") if q.strip()]
# 并行检索所有子查询
all_docs = []
for query in queries:
docs = retriever.invoke(query)
all_docs.extend(docs)
# 按 doc_id 去重
seen = set()
unique_docs = []
for doc in all_docs:
doc_id = doc.metadata.get("id", doc.page_content[:50])
if doc_id not in seen:
seen.add(doc_id)
unique_docs.append(doc)
return unique_docs
5.3 HyDE(假设文档嵌入)
def hyde_retrieve(question: str) -> list:
"""
HyDE 核心思想:
直接检索:question_vec → 匹配 doc_vec (query 和 doc 在不同分布)
HyDE 检索:生成假设文档 → hypo_doc_vec → 匹配 doc_vec(同分布,效果更好)
"""
# Step1:让 LLM 生成一段假设性的答案文档
hypo_prompt = ChatPromptTemplate.from_template("""
请针对以下问题,生成一段可能包含答案的文档片段(约200字):
问题:{question}
文档片段:
""")
hypothesis_doc = (hypo_prompt | llm | StrOutputParser()).invoke(
{"question": question}
)
# Step2:用假设文档的向量去检索,而不是用问题向量
docs = vectorstore.similarity_search(
hypothesis_doc, # ← 关键:用假设文档而非原始问题
k=5
)
return docs
# 效果对比:
# 原始问题: "信号坏了咋整" → 口语化,向量偏移
# 假设文档: "信号设备故障处置规程第X条规定,当信号机出现..." → 专业化,向量对齐
5.4 混合检索 + RRF 融合
from pymilvus import AnnSearchRequest, WeightedRanker
class HybridRetriever:
"""稠密 + 稀疏混合检索,RRF 融合"""
def retrieve(self, query: str, top_k: int = 10) -> list:
# ① 稠密检索(语义理解强)
dense_results = self._dense_search(
query_vec=self.embedder.embed(query),
top_k=top_k * 2
)
# ② 稀疏检索(关键词精确匹配强)
sparse_results = self._sparse_search(
query=query,
top_k=top_k * 2
)
# ③ RRF 倒排融合
return self._rrf_fusion(
[dense_results, sparse_results],
k=60, # RRF 平滑系数
top_k=top_k
)
def _rrf_fusion(self, result_lists: list,
k: int = 60, top_k: int = 10) -> list:
"""
RRF 公式:score(d) = Σ 1/(k + rank(d))
某文档在多个列表中排名越靠前,综合分越高
"""
scores: dict[str, float] = {}
doc_map: dict[str, object] = {}
for result_list in result_lists:
for rank, doc in enumerate(result_list):
doc_id = doc.metadata["id"]
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
doc_map[doc_id] = doc
# 按融合分排序
sorted_ids = sorted(scores, key=lambda x: scores[x], reverse=True)
return [doc_map[doc_id] for doc_id in sorted_ids[:top_k]]
5.5 重排序(Reranking)
from sentence_transformers import CrossEncoder
class BGEReranker:
"""
两阶段检索:
粗排(向量 ANN):速度快,召回 Top-20~50
精排(CrossEncoder):精度高,从中选 Top-5
CrossEncoder 与 Bi-Encoder 区别:
Bi-Encoder:query 和 doc 分别编码,计算向量相似度(快,但精度略低)
CrossEncoder:query + doc 拼接输入,直接输出相关性分数(慢,但精度高)
"""
def __init__(self):
self.model = CrossEncoder("BAAI/bge-reranker-v2-m3")
def rerank(self, query: str,
candidates: list, top_k: int = 5) -> list:
# 构造 (query, doc) 配对
pairs = [(query, doc.page_content) for doc in candidates]
# CrossEncoder 计算每对的相关性分数
scores = self.model.predict(pairs)
# 按分数降序排列
scored_docs = sorted(
zip(scores, candidates),
key=lambda x: x[0],
reverse=True
)
return [doc for _, doc in scored_docs[:top_k]]
5.6 上下文压缩
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
# 上下文压缩:从召回文档中只提取与问题相关的部分
# 避免无关内容干扰 LLM,节省 Token
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=retriever
)
# 原始召回:500字完整文档片段
# 压缩后: 只保留与问题直接相关的 2~3 句话
compressed_docs = compression_retriever.invoke("信号故障处置流程")
六、Advanced RAG 切块策略
切块策略选型
┌─────────────────────────────────────────────────────┐
│ 策略一:固定大小切块(最简单) │
│ chunk_size=500, overlap=50 │
│ 优点:实现简单 缺点:破坏语义边界 │
├─────────────────────────────────────────────────────┤
│ 策略二:递归字符切块(推荐默认) │
│ 按段落→句子→词语优先级递归切分 │
│ 优点:尽量保持语义完整性 │
├─────────────────────────────────────────────────────┤
│ 策略三:语义切块(效果最好) │
│ 相邻句子向量相似度低于阈值时切割 │
│ 优点:切块边界完全语义对齐 │
├─────────────────────────────────────────────────────┤
│ 策略四:父文档检索(Parent Document) │
│ 小块检索 + 大块生成 │
│ 索引用小块(精准召回) │
│ 送给LLM用大块(完整上下文) │
└─────────────────────────────────────────────────────┘
6.1 语义切块实现
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
class SemanticChunker:
"""语义感知切块:相邻句子语义差异大时切割"""
def __init__(self, embedder, threshold: float = 0.3):
self.embedder = embedder
self.threshold = threshold # 相似度差异阈值
def chunk(self, text: str) -> list[str]:
# ① 按句子切分
sentences = self._split_sentences(text)
if len(sentences) <= 1:
return sentences
# ② 计算相邻句子的向量距离
embeddings = self.embedder.embed_documents(sentences)
breakpoints = []
for i in range(len(embeddings) - 1):
sim = cosine_similarity(
[embeddings[i]], [embeddings[i + 1]]
)[0][0]
distance = 1 - sim # 距离 = 1 - 相似度
# 距离超过阈值 → 语义发生跳变 → 切割点
if distance > self.threshold:
breakpoints.append(i)
# ③ 按切割点合并句子
return self._merge_by_breakpoints(sentences, breakpoints)
6.2 父文档检索(Parent Document Retriever)
from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore
# 小块:用于索引检索(chunk_size=200,精准匹配)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=200)
# 大块:检索命中后返回给 LLM(chunk_size=1000,完整上下文)
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=1000)
# 构建父文档检索器
parent_retriever = ParentDocumentRetriever(
vectorstore=vectorstore, # 存储小块向量
docstore=InMemoryStore(), # 存储大块原文
child_splitter=child_splitter,
parent_splitter=parent_splitter,
)
parent_retriever.add_documents(docs)
# 检索流程:
# 1. 用小块 (200字) 向量检索 → 精准定位相关位置
# 2. 找到对应父块 (1000字) → 返回完整上下文给 LLM
result = parent_retriever.invoke("信号故障处置")
七、Modular RAG(模块化 RAG)
7.1 整体架构
Modular RAG:将 RAG 各环节解耦为独立模块,按需组合
┌──────────────────────────────────────────────────────┐
│ 路由模块(Router) │
│ 根据问题类型动态选择检索策略 │
│ 简单事实查询 → 向量检索 │
│ 复杂推理问题 → Multi-hop 检索 │
│ 实时信息需求 → 网络搜索 Tool │
└────────────────────────┬─────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
向量检索 网络搜索 数据库查询
(知识库) (实时信息) (结构化数据)
│ │ │
└───────────────┴───────────────┘
│ 多路结果融合
▼
重排序 + 压缩
│
▼
┌─────────────────────┐
│ 生成模块(Generator)│
│ 引用验证 + 答案生成 │
└─────────────────────┘
7.2 Self-RAG(自适应检索)
"""
Self-RAG 核心思想:
让 LLM 自己决定"是否需要检索"、"检索结果是否有用"
四个特殊 Token:
[Retrieve] → 模型判断需要检索
[Relevant] → 检索结果与问题相关
[Irrelevant] → 检索结果不相关
[Supported] → 答案有文档支撑
[Unsupported] → 答案无文档支撑(幻觉)
"""
from langgraph.graph import StateGraph, END
from typing import TypedDict, List
class SelfRAGState(TypedDict):
question: str
need_retrieve: bool # 是否需要检索
documents: List[str] # 检索到的文档
is_relevant: bool # 文档是否相关
answer: str # 生成的答案
is_supported: bool # 答案是否有依据
iteration: int # 迭代次数
def check_retrieve_node(state: SelfRAGState) -> dict:
"""判断是否需要检索(LLM 自主决策)"""
prompt = f"""
问题:{state['question']}
判断是否需要检索外部文档来回答这个问题?
输出 JSON:{{"need_retrieve": true/false, "reason": "..."}}
"""
result = json.loads(llm.invoke(prompt).content)
return {"need_retrieve": result["need_retrieve"]}
def relevance_check_node(state: SelfRAGState) -> dict:
"""检索后判断文档是否相关"""
prompt = f"""
问题:{state['question']}
检索到的文档:{state['documents'][0]}
这些文档是否与问题相关?
输出 JSON:{{"is_relevant": true/false}}
"""
result = json.loads(llm.invoke(prompt).content)
return {"is_relevant": result["is_relevant"]}
def support_check_node(state: SelfRAGState) -> dict:
"""验证答案是否有文档支撑(防幻觉)"""
prompt = f"""
文档:{state['documents']}
生成的答案:{state['answer']}
答案中的每个关键信息是否都能在文档中找到依据?
输出 JSON:{{"is_supported": true/false, "unsupported_claims": [...]}}
"""
result = json.loads(llm.invoke(prompt).content)
return {"is_supported": result["is_supported"]}
# 构建 Self-RAG 图
graph = StateGraph(SelfRAGState)
graph.add_node("check_retrieve", check_retrieve_node)
graph.add_node("retrieve", retrieve_node)
graph.add_node("relevance_check", relevance_check_node)
graph.add_node("generate", generate_node)
graph.add_node("support_check", support_check_node)
graph.set_entry_point("check_retrieve")
# 条件路由
graph.add_conditional_edges("check_retrieve",
lambda s: "retrieve" if s["need_retrieve"] else "generate")
graph.add_conditional_edges("relevance_check",
lambda s: "generate" if s["is_relevant"] else "retrieve") # 不相关重新检索
graph.add_conditional_edges("support_check",
lambda s: END if s["is_supported"] else "retrieve") # 无依据重新检索
7.3 Multi-hop RAG(多跳推理)
"""
多跳问题示例:
"A 站信号故障会影响哪条线路的换乘,该换乘线路的备用方案是什么?"
需要:
Hop1: 查 A 站 → 找换乘线路 B
Hop2: 查线路 B → 找备用方案
单次检索无法完成,需要迭代推理
"""
class MultiHopRetriever:
def retrieve(self, question: str,
max_hops: int = 3) -> list:
all_docs = []
current_query = question
for hop in range(max_hops):
# 检索当前查询
docs = retriever.invoke(current_query)
all_docs.extend(docs)
# LLM 判断是否需要继续检索
context = "\n".join([d.page_content for d in docs])
check_prompt = f"""
原始问题:{question}
已检索内容:{context}
当前信息是否足够回答原始问题?
若不够,生成下一跳检索查询。
输出 JSON:
{{"sufficient": true/false, "next_query": "..."}}
"""
result = json.loads(llm.invoke(check_prompt).content)
if result["sufficient"] or hop == max_hops - 1:
break
# 更新查询,进行下一跳
current_query = result["next_query"]
print(f"Hop {hop+1} → 新查询: {current_query}")
return all_docs
八、RAG 评估体系(RAGAS)
8.1 核心评估指标
RAGAS 四大核心指标
┌─────────────────────────────────────────────────────┐
│ Faithfulness(忠实度) │
│ 答案中的每个陈述是否都能在检索文档中找到依据 │
│ 目标:> 0.90 幻觉检测核心指标 │
├─────────────────────────────────────────────────────┤
│ Answer Relevancy(答案相关性) │
│ 生成的答案是否真正回答了用户的问题 │
│ 目标:> 0.85 │
├─────────────────────────────────────────────────────┤
│ Context Precision(上下文精确率) │
│ 检索到的文档中有多少比例是真正有用的 │
│ 目标:> 0.80 衡量检索精准度 │
├─────────────────────────────────────────────────────┤
│ Context Recall(上下文召回率) │
│ 回答问题所需的信息有多少被成功检索到 │
│ 目标:> 0.85 衡量检索完整度 │
└─────────────────────────────────────────────────────┘
8.2 RAGAS 评测代码
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_precision,
context_recall,
)
from datasets import Dataset
# 构建评测数据集
eval_samples = [
{
"question": "信号机故障应急处置流程是什么?",
"answer": rag_chain.invoke("信号机故障应急处置流程是什么?"),
"contexts": [doc.page_content for doc in retriever.invoke("信号机故障")],
"ground_truth": "根据规程第X条,应立即..." # 专家标注的标准答案
},
# ... 更多测试样本
]
dataset = Dataset.from_list(eval_samples)
results = evaluate(
dataset=dataset,
metrics=[faithfulness, answer_relevancy,
context_precision, context_recall]
)
print(results)
# Output:
# faithfulness: 0.92
# answer_relevancy: 0.88
# context_precision: 0.85
# context_recall: 0.87
九、RAG 常见问题与优化方案
┌──────────────────────────────────────────────────────────────┐
│ 问题 │ 症状 │ 优化方案 │
├──────────────────────────────────────────────────────────────┤
│ 召回率低 │ 答非所问,信息缺失 │ HyDE + 多路查询 │
│ 检索精度低 │ 召回大量无关文档 │ 重排序 + 元数据过滤 │
│ 切块破坏语义 │ 答案不完整/断章取义 │ 语义切块 + 父文档 │
│ LLM 产生幻觉 │ 答案无文档支撑 │ Self-RAG 验证机制 │
│ 多跳问题处理不了 │ 复杂推理失败 │ Multi-hop 迭代检索 │
│ 响应速度慢 │ 用户等待时间长 │ 异步检索 + 流式输出 │
│ 知识更新不及时 │ 答案基于旧版本 │ 增量索引 + 版本管理 │
└──────────────────────────────────────────────────────────────┘
十、RAG 技术栈总览
┌─────────────────────────────────────────────────────────┐
│ RAG 完整技术栈 │
│ │
│ 文档解析 MinerU / PyPDF2 / Unstructured │
│ 文本切块 LangChain Splitter / 语义切块 │
│ Embedding BGE-M3(中英双语,稠密+稀疏) │
│ 向量数据库 Milvus(生产)/ Chroma(原型) │
│ 稀疏检索 BM25 / Elasticsearch │
│ 重排序 BGE-Reranker-v2 / Cohere Rerank │
│ RAG 框架 LangChain / LlamaIndex │
│ Agent 编排 LangGraph(复杂多跳 RAG) │
│ 评估框架 RAGAS(全自动评测) │
│ 链路追踪 LangSmith(可观测性) │
└─────────────────────────────────────────────────────────┘
十一、三代 RAG 核心差异总结
Naive RAG 固定切块 → 单路向量检索 → 直接生成
简单,但召回质量有限
Advanced RAG 智能切块 + 查询改写 + 混合检索 + 重排序
工程优化,显著提升召回质量
Modular RAG 路由 + 多路检索 + 迭代推理 + 自我验证
灵活组合,解决复杂业务场景
选型建议:
快速验证原型 → Naive RAG
生产环境标准场景 → Advanced RAG(首选)
复杂多步推理场景 → Modular RAG + LangGraph
核心结论:RAG 本质是"让 LLM 有据可查",提升效果的关键在于 切块质量 > 检索召回 > 重排精度 > Prompt 设计,四个环节环环相扣,任何一环薄弱都会影响最终效果。