大模型RAG+AI Agent企业级落地实战:检索增强生成架构与生产部署全指南
摘要
- 掌握RAG系统的核心架构与文档处理流水线,理解从原始文档到高质量检索结果的完整链路
- 深入混合检索(向量+关键词+知识图谱)与重排序技术,实现企业级95%+的检索准确率
- RAG+AI Agent深度融合实战:工具增强检索、多轮对话记忆、企业知识库权限控制与生产部署
目录
- 一、RAG系统架构与核心流程
- 二、文档处理与分块策略
- 三、Embedding模型选型与优化
- 四、混合检索与重排序
- 五、RAG+AI Agent深度融合
- 六、企业知识库权限与安全
- 七、生产级部署与性能优化
- 八、总结与展望
一、RAG系统架构与核心流程
1.1 RAG的核心价值与局限
检索增强生成(Retrieval-Augmented Generation, RAG)是2024-2026年大模型应用最广泛的技术范式。其核心价值在于:无需微调模型,通过检索外部知识库来增强大模型的回答质量和事实准确性。
然而,生产级RAG系统面临的核心挑战远不止"检索+拼接"那么简单:
- 检索质量瓶颈:向量检索的语义相似不等于答案相关,Top-K结果可能包含大量噪声
- 上下文窗口浪费:无关的检索结果挤占有限的上下文窗口,降低模型推理质量
- 多跳推理缺失:复杂问题需要多步检索和推理,单次检索无法满足
- 实时性要求:企业知识库频繁更新,索引需要实时同步
1.2 生产级RAG架构
一个生产级RAG系统的架构远比简单的"查询→检索→生成"复杂:
┌──────────────────────────────────────────────────┐
│ Query Understanding │
│ 意图识别 · 查询改写 · 实体抽取 · 多跳分解 │
├──────────────────────────────────────────────────┤
│ Hybrid Retrieval │
│ 向量检索 · BM25关键词 · 知识图谱 · SQL查询 │
├──────────────────────────────────────────────────┤
│ Reranking & Fusion │
│ 交叉编码器重排 · 互信息最大化 · 结果去重 │
├──────────────────────────────────────────────────┤
│ Context Assembly │
│ 相关性过滤 · 上下文压缩 · 结构化组织 │
├──────────────────────────────────────────────────┤
│ Generation & Verification │
│ Chain-of-Thought · 事实校验 · 幻觉检测 │
└──────────────────────────────────────────────────┘
Query Understanding层负责理解用户查询的真实意图,包括查询改写、实体抽取和多跳问题分解。Hybrid Retrieval层使用多种检索策略的融合。Reranking层对初步检索结果进行精细排序。Context Assembly层组装最优上下文。Generation层生成最终答案并进行事实校验。
1.3 RAG vs 微调 vs 预训练
| 维度 | RAG | 微调 | 预训练 |
|---|---|---|---|
| 知识更新 | 实时 | 需重新训练 | 需重新训练 |
| 成本 | 低 | 中 | 极高 |
| 事实准确性 | 高 | 中 | 低 |
| 领域适应性 | 强 | 强 | 最强 |
| 部署复杂度 | 中 | 低 | 高 |
| 幻觉控制 | 好 | 一般 | 差 |
企业级场景中,RAG+微调的组合是最佳实践:RAG保证事实准确性,微调优化模型对特定领域的理解和输出风格。
二、文档处理与分块策略
2.1 文档解析流水线
企业知识库的文档格式多样(PDF、Word、Excel、PPT、HTML、Markdown),需要统一的解析流水线:
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
import hashlib
@dataclass
class ParsedDocument:
doc_id: str
title: str
content: str
metadata: dict
source_path: str
checksum: str
page_count: int
language: str
class DocumentParser:
def __init__(self, ocr_enabled: bool = True, table_enabled: bool = True):
self.ocr_enabled = ocr_enabled
self.table_enabled = table_enabled
def parse(self, file_path: str) -> ParsedDocument:
path = Path(file_path)
suffix = path.suffix.lower()
match suffix:
case ".pdf":
return self._parse_pdf(file_path)
case ".docx" | ".doc":
return self._parse_docx(file_path)
case ".xlsx" | ".xls":
return self._parse_excel(file_path)
case ".pptx":
return self._parse_pptx(file_path)
case ".md":
return self._parse_markdown(file_path)
case ".html" | ".htm":
return self._parse_html(file_path)
case _:
return self._parse_plain_text(file_path)
def _parse_pdf(self, file_path: str) -> ParsedDocument:
import fitz
doc = fitz.open(file_path)
content_parts = []
page_count = len(doc)
for page_num in range(page_count):
page = doc[page_num]
text = page.get_text()
if text.strip():
content_parts.append(text)
if self.table_enabled:
tables = page.find_tables()
for table in tables:
table_text = table.to_pandas().to_markdown()
content_parts.append(f"\n[Table on page {page_num + 1}]\n{table_text}")
if self.ocr_enabled:
images = page.get_images()
for img_idx, img in enumerate(images):
xref = img[0]
base_image = doc.extract_image(xref)
if base_image:
image_bytes = base_image["image"]
ocr_text = self._ocr_image(image_bytes)
if ocr_text:
content_parts.append(
f"\n[Image {img_idx + 1} on page {page_num + 1}]\n{ocr_text}"
)
content = "\n\n".join(content_parts)
checksum = hashlib.sha256(content.encode()).hexdigest()[:16]
return ParsedDocument(
doc_id=f"doc_{checksum}",
title=Path(file_path).stem,
content=content,
metadata={"format": "pdf", "page_count": page_count},
source_path=file_path,
checksum=checksum,
page_count=page_count,
language=self._detect_language(content),
)
def _ocr_image(self, image_bytes: bytes) -> Optional[str]:
try:
import pytesseract
from PIL import Image
import io
image = Image.open(io.BytesIO(image_bytes))
return pytesseract.image_to_string(image, lang='chi_sim+eng')
except Exception:
return None
def _detect_language(self, text: str) -> str:
sample = text[:500]
chinese_chars = sum(1 for c in sample if '\u4e00' <= c <= '\u9fff')
if chinese_chars / max(len(sample), 1) > 0.3:
return "zh-CN"
return "en"
2.2 分块策略深度对比
文档分块是RAG系统中最关键的环节之一,分块策略直接影响检索质量:
固定长度分块(Fixed-Size Chunking):按固定Token数切分,实现简单但可能切断语义完整性。
递归字符分块(Recursive Character Chunking):按段落→句子→字符的优先级递归切分,保持语义完整性。LangChain的RecursiveCharacterTextSplitter即采用此策略。
语义分块(Semantic Chunking):使用Embedding模型计算相邻句子的语义相似度,在语义断点处切分。质量最高但计算成本大。
文档结构分块(Structure-Aware Chunking):利用文档的标题层级、章节结构进行分块,保持文档的逻辑结构。
from dataclasses import dataclass
from typing import Callable
import numpy as np
@dataclass
class Chunk:
chunk_id: str
content: str
metadata: dict
start_index: int
end_index: int
token_count: int
parent_doc_id: str
class SemanticChunker:
def __init__(
self,
embedding_fn: Callable[[str], list[float]],
similarity_threshold: float = 0.5,
min_chunk_size: int = 100,
max_chunk_size: int = 1000,
):
self.embedding_fn = embedding_fn
self.similarity_threshold = similarity_threshold
self.min_chunk_size = min_chunk_size
self.max_chunk_size = max_chunk_size
def chunk(self, text: str, doc_id: str) -> list[Chunk]:
sentences = self._split_sentences(text)
if len(sentences) <= 1:
return [Chunk(
chunk_id=f"{doc_id}_0",
content=text,
metadata={"chunk_type": "semantic"},
start_index=0,
end_index=len(text),
token_count=len(text) // 4,
parent_doc_id=doc_id,
)]
embeddings = [self.embedding_fn(s) for s in sentences]
similarities = [
self._cosine_similarity(embeddings[i], embeddings[i + 1])
for i in range(len(embeddings) - 1)
]
breakpoints = []
for i, sim in enumerate(similarities):
if sim < self.similarity_threshold:
breakpoints.append(i + 1)
chunks = []
current_start = 0
chunk_idx = 0
for bp in breakpoints + [len(sentences)]:
chunk_text = " ".join(sentences[current_start:bp])
token_count = len(chunk_text) // 4
if token_count >= self.min_chunk_size:
chunks.append(Chunk(
chunk_id=f"{doc_id}_{chunk_idx}",
content=chunk_text,
metadata={
"chunk_type": "semantic",
"sentence_count": bp - current_start,
},
start_index=current_start,
end_index=bp,
token_count=token_count,
parent_doc_id=doc_id,
))
chunk_idx += 1
elif chunks:
chunks[-1].content += " " + chunk_text
chunks[-1].end_index = bp
chunks[-1].token_count += token_count
current_start = bp
for chunk in chunks:
if chunk.token_count > self.max_chunk_size:
self._split_oversized(chunk, doc_id, chunks)
return chunks
def _split_sentences(self, text: str) -> list[str]:
import re
sentences = re.split(r'(?<=[。!?.!?])\s*', text)
return [s.strip() for s in sentences if s.strip()]
def _cosine_similarity(self, a: list[float], b: list[float]) -> float:
a_arr = np.array(a)
b_arr = np.array(b)
return float(np.dot(a_arr, b_arr) / (np.linalg.norm(a_arr) * np.linalg.norm(b_arr) + 1e-8))
def _split_oversized(self, chunk: Chunk, doc_id: str, chunks: list[Chunk]) -> None:
pass
2.3 元数据增强分块
为每个分块添加丰富的元数据,支持检索时的精细过滤:
@dataclass
class EnhancedChunk(Chunk):
heading_path: list[str]
keywords: list[str]
entities: list[dict]
summary: str
access_level: int
department: str
doc_type: str
created_at: str
updated_at: str
class MetadataEnricher:
def __init__(self, llm_client):
self.llm = llm_client
async def enrich(self, chunk: Chunk, doc: ParsedDocument) -> EnhancedChunk:
keywords = await self._extract_keywords(chunk.content)
entities = await self._extract_entities(chunk.content)
summary = await self._generate_summary(chunk.content)
return EnhancedChunk(
**chunk.__dict__,
heading_path=self._extract_heading_path(doc, chunk),
keywords=keywords,
entities=entities,
summary=summary,
access_level=doc.metadata.get("access_level", 0),
department=doc.metadata.get("department", ""),
doc_type=doc.metadata.get("format", ""),
created_at=doc.metadata.get("created_at", ""),
updated_at=doc.metadata.get("updated_at", ""),
)
async def _extract_keywords(self, text: str) -> list[str]:
prompt = f"从以下文本中提取5-10个关键术语,以JSON数组格式返回:\n\n{text[:1000]}"
response = await self.llm.generate(prompt)
import json
try:
return json.loads(response)
except:
return []
async def _extract_entities(self, text: str) -> list[dict]:
prompt = f"""从以下文本中提取命名实体,返回JSON数组,每个实体包含name、type、value字段:
实体类型包括:PERSON, ORGANIZATION, PRODUCT, DATE, LOCATION, TECHNOLOGY
文本:
{text[:2000]}"""
response = await self.llm.generate(prompt)
import json
try:
return json.loads(response)
except:
return []
async def _generate_summary(self, text: str) -> str:
prompt = f"用一句话总结以下文本的核心内容:\n\n{text[:500]}"
return await self.llm.generate(prompt)
def _extract_heading_path(self, doc: ParsedDocument, chunk: Chunk) -> list[str]:
return doc.metadata.get("heading_path", [])
三、Embedding模型选型与优化
3.1 主流Embedding模型对比
| 模型 | 维度 | MTEB得分 | 中文能力 | 推理速度 | 许可证 |
|---|---|---|---|---|---|
| BGE-M3 | 1024 | 73.5 | 优秀 | 中 | MIT |
| GTE-Qwen2-7B | 3584 | 76.2 | 优秀 | 慢 | Apache 2.0 |
| text-embedding-3-large | 3072 | 74.5 | 良好 | 快 | 商业 |
| Jina-Embeddings-v3 | 1024 | 72.8 | 良好 | 中 | CC-BY-4.0 |
| BCE-Embedding | 768 | 71.2 | 优秀 | 快 | MIT |
选型建议:
- 中文场景首选BGE-M3,性价比最高
- 追求极致效果选GTE-Qwen2-7B,但推理成本高
- 需要多语言支持选Jina-Embeddings-v3
- 使用OpenAI API的场景选text-embedding-3-large
3.2 Embedding服务部署
from fastapi import FastAPI
from pydantic import BaseModel
import numpy as np
from sentence_transformers import SentenceTransformer
app = FastAPI()
class EmbedRequest(BaseModel):
texts: list[str]
normalize: bool = True
class EmbedResponse(BaseModel):
embeddings: list[list[float]]
model: str
dimension: int
model = SentenceTransformer("BAAI/bge-m3")
@app.post("/embed", response_model=EmbedResponse)
async def embed(request: EmbedRequest):
embeddings = model.encode(
request.texts,
normalize_embeddings=request.normalize,
show_progress_bar=False,
)
return EmbedResponse(
embeddings=embeddings.tolist(),
model="bge-m3",
dimension=embeddings.shape[1],
)
@app.post("/embed/batch", response_model=EmbedResponse)
async def embed_batch(request: EmbedRequest):
batch_size = 64
all_embeddings = []
for i in range(0, len(request.texts), batch_size):
batch = request.texts[i:i + batch_size]
batch_embeddings = model.encode(
batch,
normalize_embeddings=request.normalize,
batch_size=len(batch),
)
all_embeddings.append(batch_embeddings)
embeddings = np.vstack(all_embeddings)
return EmbedResponse(
embeddings=embeddings.tolist(),
model="bge-m3",
dimension=embeddings.shape[1],
)
3.3 查询侧Embedding优化
查询侧的Embedding优化是提升检索质量的关键技巧:
查询扩展(Query Expansion):使用LLM将用户查询扩展为多个相关查询,增加检索覆盖面。
假设性文档嵌入(HyDE):先让LLM生成一个假设性答案,再用假设答案的Embedding去检索,比直接用查询检索效果更好。
指令前缀(Instruction Prefix):在查询前添加任务指令,如"为以下查询检索相关文档:",对齐训练时的输入格式。
class QueryOptimizer:
def __init__(self, llm_client, embedding_fn):
self.llm = llm_client
self.embedding_fn = embedding_fn
async def expand_query(self, query: str, num_expansions: int = 3) -> list[str]:
prompt = f"""将以下查询改写为{num_expansions}个不同角度的等价查询,以JSON数组格式返回。
原始查询:{query}
要求:
1. 保持原始查询的核心意图
2. 使用不同的表述方式和关键词
3. 覆盖不同的专业术语和通俗说法"""
response = await self.llm.generate(prompt)
import json
try:
expansions = json.loads(response)
return [query] + expansions[:num_expansions]
except:
return [query]
async def hyde_embed(self, query: str) -> list[float]:
prompt = f"""请针对以下问题,写一段详细的回答(即使你不确定答案,也请给出合理的假设性回答):
问题:{query}"""
hypothetical_answer = await self.llm.generate(prompt)
return self.embedding_fn(hypothetical_answer)
def instruction_embed(self, query: str, task: str = "search") -> list[float]:
prefixes = {
"search": "为以下查询检索相关文档:",
"similarity": "查找与以下内容相似的文档:",
"classification": "对以下内容进行分类:",
}
prefix = prefixes.get(task, "")
return self.embedding_fn(f"{prefix}{query}")
四、混合检索与重排序
4.1 混合检索架构
单一向量检索无法覆盖所有场景。关键词检索(BM25)擅长精确匹配(产品型号、专有名词),向量检索擅长语义匹配(概念相似),知识图谱检索擅长关系推理。混合检索是生产级RAG的必选项。
from dataclasses import dataclass
from typing import Optional
@dataclass
class RetrievalResult:
chunk_id: str
content: str
score: float
source: str
metadata: dict
class HybridRetriever:
def __init__(
self,
vector_store,
bm25_store,
kg_store=None,
vector_weight: float = 0.6,
bm25_weight: float = 0.3,
kg_weight: float = 0.1,
):
self.vector_store = vector_store
self.bm25_store = bm25_store
self.kg_store = kg_store
self.vector_weight = vector_weight
self.bm25_weight = bm25_weight
self.kg_weight = kg_weight
async def retrieve(
self,
query: str,
query_embedding: list[float],
top_k: int = 20,
filters: Optional[dict] = None,
) -> list[RetrievalResult]:
vector_results = await self.vector_store.search(
query_embedding, top_k=top_k * 2, filters=filters
)
bm25_results = await self.bm25_store.search(
query, top_k=top_k * 2, filters=filters
)
kg_results = []
if self.kg_store:
kg_results = await self.kg_store.search(
query, top_k=top_k
)
merged = self._reciprocal_rank_fusion(
vector_results, bm25_results, kg_results
)
return merged[:top_k]
def _reciprocal_rank_fusion(
self,
vector_results: list[RetrievalResult],
bm25_results: list[RetrievalResult],
kg_results: list[RetrievalResult],
k: int = 60,
) -> list[RetrievalResult]:
scores: dict[str, float] = {}
result_map: dict[str, RetrievalResult] = {}
for rank, result in enumerate(vector_results):
rrf_score = self.vector_weight / (k + rank + 1)
scores[result.chunk_id] = scores.get(result.chunk_id, 0.0) + rrf_score
result_map[result.chunk_id] = result
for rank, result in enumerate(bm25_results):
rrf_score = self.bm25_weight / (k + rank + 1)
scores[result.chunk_id] = scores.get(result.chunk_id, 0.0) + rrf_score
if result.chunk_id not in result_map:
result_map[result.chunk_id] = result
for rank, result in enumerate(kg_results):
rrf_score = self.kg_weight / (k + rank + 1)
scores[result.chunk_id] = scores.get(result.chunk_id, 0.0) + rrf_score
if result.chunk_id not in result_map:
result_map[result.chunk_id] = result
sorted_ids = sorted(scores.keys(), key=lambda x: scores[x], reverse=True)
return [
RetrievalResult(
chunk_id=cid,
content=result_map[cid].content,
score=scores[cid],
source=result_map[cid].source,
metadata=result_map[cid].metadata,
)
for cid in sorted_ids
]
4.2 交叉编码器重排序
初步检索结果使用双编码器(Bi-Encoder)计算,速度快但精度有限。交叉编码器(Cross-Encoder)将查询和文档一起输入模型,精度更高但速度慢,适合对Top-K结果进行精排:
from sentence_transformers import CrossEncoder
class Reranker:
def __init__(self, model_name: str = "BAAI/bge-reranker-v2-m3"):
self.model = CrossEncoder(model_name, max_length=512)
def rerank(
self,
query: str,
results: list[RetrievalResult],
top_k: int = 10,
) -> list[RetrievalResult]:
pairs = [(query, result.content) for result in results]
scores = self.model.predict(pairs)
scored_results = list(zip(results, scores))
scored_results.sort(key=lambda x: x[1], reverse=True)
return [
RetrievalResult(
chunk_id=result.chunk_id,
content=result.content,
score=float(score),
source=result.source,
metadata={**result.metadata, "rerank_score": float(score)},
)
for result, score in scored_results[:top_k]
]
4.3 相关性过滤
重排序后仍需过滤低相关性结果,避免噪声进入上下文:
class RelevanceFilter:
def __init__(self, min_score: float = 0.3, max_chunks: int = 8):
self.min_score = min_score
self.max_chunks = max_chunks
def filter(self, results: list[RetrievalResult]) -> list[RetrievalResult]:
filtered = [r for r in results if r.score >= self.min_score]
return filtered[:self.max_chunks]
def adaptive_filter(
self,
results: list[RetrievalResult],
max_context_tokens: int = 4000,
) -> list[RetrievalResult]:
selected = []
total_tokens = 0
for result in results:
if result.score < self.min_score:
continue
chunk_tokens = len(result.content) // 4
if total_tokens + chunk_tokens > max_context_tokens:
break
selected.append(result)
total_tokens += chunk_tokens
return selected
五、RAG+AI Agent深度融合
5.1 工具增强检索Agent
将RAG检索能力封装为AI Agent可调用的工具,实现更智能的检索策略:
from typing import Annotated
class RAGAgentTools:
def __init__(self, retriever: HybridRetriever, reranker: Reranker):
self.retriever = retriever
self.reranker = reranker
def search_knowledge_base(
self,
query: Annotated[str, "Search query for the knowledge base"],
top_k: Annotated[int, "Number of results to return"] = 5,
filters: Annotated[dict | None, "Metadata filters"] = None,
) -> str:
"""Search the enterprise knowledge base for relevant documents."""
query_embedding = self.get_embedding(query)
results = self.retriever.retrieve(query, query_embedding, top_k=top_k * 2, filters=filters)
reranked = self.reranker.rerank(query, results, top_k=top_k)
if not reranked:
return "No relevant documents found."
formatted = []
for i, result in enumerate(reranked):
formatted.append(
f"[Document {i + 1}] (Score: {result.score:.3f})\n"
f"Source: {result.metadata.get('source', 'Unknown')}\n"
f"Content: {result.content}\n"
)
return "\n---\n".join(formatted)
def search_by_entity(
self,
entity_name: Annotated[str, "Entity name to search for"],
entity_type: Annotated[str, "Entity type: PERSON, ORGANIZATION, PRODUCT, etc."] = "",
) -> str:
"""Search documents mentioning a specific entity."""
filters = {"entities": {"name": entity_name}}
if entity_type:
filters["entities"]["type"] = entity_type
return self.search_knowledge_base(entity_name, filters=filters)
def compare_documents(
self,
topic: Annotated[str, "Topic to compare across documents"],
doc_ids: Annotated[list[str], "Document IDs to compare"] = None,
) -> str:
"""Compare information about a topic across multiple documents."""
query_embedding = self.get_embedding(topic)
filters = {"parent_doc_id": {"$in": doc_ids}} if doc_ids else None
results = self.retriever.retrieve(topic, query_embedding, top_k=20, filters=filters)
reranked = self.reranker.rerank(topic, results, top_k=10)
grouped = {}
for result in reranked:
doc_id = result.metadata.get("parent_doc_id", "unknown")
if doc_id not in grouped:
grouped[doc_id] = []
grouped[doc_id].append(result)
output = []
for doc_id, chunks in grouped.items():
output.append(f"Document: {doc_id}")
for chunk in chunks:
output.append(f" - {chunk.content[:200]}...")
return "\n\n".join(output)
5.2 多轮对话RAG
企业级RAG系统需要支持多轮对话,维护对话上下文和检索历史:
class ConversationalRAG:
def __init__(self, llm_client, retriever, reranker):
self.llm = llm_client
self.retriever = retriever
self.reranker = reranker
async def chat(
self,
query: str,
conversation_history: list[dict],
max_context_tokens: int = 4000,
) -> dict:
rewritten_query = await self._rewrite_query(query, conversation_history)
query_embedding = self.get_embedding(rewritten_query)
results = await self.retriever.retrieve(
rewritten_query, query_embedding, top_k=20
)
reranked = self.reranker.rerank(rewritten_query, results, top_k=10)
context = self._assemble_context(reranked, max_context_tokens)
prompt = self._build_prompt(query, context, conversation_history)
answer = await self.llm.generate(prompt)
return {
"answer": answer,
"sources": [
{
"chunk_id": r.chunk_id,
"content": r.content[:200],
"score": r.score,
"source": r.metadata.get("source", ""),
}
for r in reranked[:5]
],
"rewritten_query": rewritten_query,
}
async def _rewrite_query(self, query: str, history: list[dict]) -> str:
if not history:
return query
history_text = "\n".join([
f"{'User' if h['role'] == 'user' else 'Assistant'}: {h['content']}"
for h in history[-6:]
])
prompt = f"""基于以下对话历史,将用户的最新问题改写为独立的、完整的检索查询。
只返回改写后的查询,不要解释。
对话历史:
{history_text}
最新问题:{query}
改写后的查询:"""
return await self.llm.generate(prompt)
def _assemble_context(self, results: list[RetrievalResult], max_tokens: int) -> str:
parts = []
total = 0
for result in results:
tokens = len(result.content) // 4
if total + tokens > max_tokens:
break
parts.append(f"[Source: {result.metadata.get('source', 'Unknown')}]\n{result.content}")
total += tokens
return "\n\n---\n\n".join(parts)
def _build_prompt(self, query: str, context: str, history: list[dict]) -> str:
return f"""你是一个专业的企业知识库助手。请基于以下检索到的文档内容回答用户的问题。
要求:
1. 只基于提供的文档内容回答,不要编造信息
2. 如果文档中没有相关信息,明确告知用户
3. 引用信息时标注来源文档
4. 使用清晰的结构化格式
检索到的文档:
{context}
用户问题:{query}
回答:"""
5.3 多跳推理RAG
复杂问题需要多步检索和推理,单次RAG无法满足。多跳RAG通过Agent的规划能力,将复杂问题分解为多步检索链:
class MultiHopRAG:
def __init__(self, llm_client, retriever, reranker, max_hops: int = 3):
self.llm = llm_client
self.retriever = retriever
self.reranker = reranker
self.max_hops = max_hops
async def answer(self, query: str) -> dict:
hop_results = []
current_query = query
all_contexts = []
for hop in range(self.max_hops):
query_embedding = self.get_embedding(current_query)
results = await self.retriever.retrieve(
current_query, query_embedding, top_k=10
)
reranked = self.reranker.rerank(current_query, results, top_k=5)
all_contexts.extend(reranked)
hop_results.append({
"hop": hop + 1,
"query": current_query,
"results_count": len(reranked),
})
next_action = await self._decide_next_hop(query, all_contexts, hop)
if next_action["action"] == "answer":
break
elif next_action["action"] == "search":
current_query = next_action["query"]
context = self._assemble_context(all_contexts)
answer = await self._generate_answer(query, context)
return {
"answer": answer,
"hops": hop_results,
"total_contexts": len(all_contexts),
}
async def _decide_next_hop(self, original_query: str, contexts: list, hop: int) -> dict:
if hop >= self.max_hops - 1:
return {"action": "answer"}
context_summary = "\n".join([
f"- {c.content[:200]}" for c in contexts[-5:]
])
prompt = f"""基于原始问题和已检索到的信息,判断是否需要继续检索。
原始问题:{original_query}
已检索信息:
{context_summary}
请判断:
1. 如果已有足够信息回答问题,返回 {{"action": "answer"}}
2. 如果需要更多信息,返回 {{"action": "search", "query": "下一步检索查询"}}
以JSON格式返回:"""
response = await self.llm.generate(prompt)
import json
try:
return json.loads(response)
except:
return {"action": "answer"}
六、企业知识库权限与安全
6.1 文档级权限控制
企业知识库的文档通常有严格的访问权限控制,RAG检索必须遵守权限规则:
class PermissionAwareRetriever:
def __init__(self, base_retriever, permission_service):
self.base_retriever = base_retriever
self.permission_service = permission_service
async def retrieve(
self,
query: str,
query_embedding: list[float],
user_id: str,
top_k: int = 20,
) -> list[RetrievalResult]:
user_permissions = await self.permission_service.get_user_permissions(user_id)
accessible_departments = user_permissions.get("departments", [])
access_level = user_permissions.get("access_level", 0)
filters = {
"$or": [
{"department": {"$in": accessible_departments}},
{"access_level": {"$lte": access_level}},
]
}
results = await self.base_retriever.retrieve(
query, query_embedding, top_k=top_k * 2, filters=filters
)
verified_results = []
for result in results:
if await self._verify_access(result, user_permissions):
verified_results.append(result)
return verified_results[:top_k]
async def _verify_access(self, result: RetrievalResult, permissions: dict) -> bool:
doc_department = result.metadata.get("department", "")
doc_access_level = result.metadata.get("access_level", 99)
if doc_access_level <= permissions.get("access_level", 0):
return True
if doc_department in permissions.get("departments", []):
return True
return False
6.2 数据脱敏
检索结果在送入大模型前,需要自动脱敏敏感信息:
import re
class DataMasker:
PATTERNS = {
"phone": (r'1[3-9]\d{9}', lambda m: m.group()[:3] + "****" + m.group()[-4:]),
"email": (r'[\w.-]+@[\w.-]+\.\w+', lambda m: m.group()[0] + "***@" + m.group().split("@")[1]),
"id_card": (r'\d{17}[\dXx]', lambda m: m.group()[:6] + "********" + m.group()[-4:]),
"bank_card": (r'\d{16,19}', lambda m: m.group()[:4] + "****" + m.group()[-4:]),
}
def mask(self, text: str, enabled_types: list[str] | None = None) -> str:
types = enabled_types or list(self.PATTERNS.keys())
for type_name in types:
if type_name in self.PATTERNS:
pattern, replacer = self.PATTERNS[type_name]
text = re.sub(pattern, replacer, text)
return text
6.3 审计日志
所有RAG查询和检索操作需要记录审计日志:
from datetime import datetime
class RAGAuditLogger:
def __init__(self, log_store):
self.log_store = log_store
async def log_query(
self,
user_id: str,
query: str,
rewritten_query: str | None,
results_count: int,
answer_preview: str,
latency_ms: int,
):
entry = {
"timestamp": datetime.utcnow().isoformat(),
"user_id": user_id,
"original_query": query,
"rewritten_query": rewritten_query,
"results_count": results_count,
"answer_preview": answer_preview[:200],
"latency_ms": latency_ms,
}
await self.log_store.insert(entry)
async def log_access_denied(self, user_id: str, query: str, denied_doc_ids: list[str]):
entry = {
"timestamp": datetime.utcnow().isoformat(),
"event_type": "access_denied",
"user_id": user_id,
"query": query,
"denied_documents": denied_doc_ids,
}
await self.log_store.insert(entry)
七、生产级部署与性能优化
7.1 缓存策略
RAG系统的多层缓存策略:
import hashlib
from functools import lru_cache
class RAGCache:
def __init__(self, redis_client, ttl: int = 3600):
self.redis = redis_client
self.ttl = ttl
def _cache_key(self, query: str, user_id: str, filters: dict | None = None) -> str:
raw = f"{query}:{user_id}:{filters}"
return f"rag:cache:{hashlib.md5(raw.encode()).hexdigest()}"
async def get(self, query: str, user_id: str, filters: dict | None = None) -> dict | None:
key = self._cache_key(query, user_id, filters)
cached = await self.redis.get(key)
if cached:
import json
return json.loads(cached)
return None
async def set(self, query: str, user_id: str, result: dict, filters: dict | None = None):
key = self._cache_key(query, user_id, filters)
import json
await self.redis.setex(key, self.ttl, json.dumps(result, ensure_ascii=False))
async def invalidate_doc(self, doc_id: str):
pattern = f"rag:cache:*"
async for key in self.redis.scan_iter(pattern):
cached = await self.redis.get(key)
if cached and doc_id in cached.decode():
await self.redis.delete(key)
7.2 异步索引更新
企业知识库的文档频繁更新,需要异步索引更新机制:
import asyncio
from typing import Callable
class AsyncIndexUpdater:
def __init__(
self,
embedding_fn: Callable,
vector_store,
chunker,
batch_size: int = 100,
flush_interval: int = 30,
):
self.embedding_fn = embedding_fn
self.vector_store = vector_store
self.chunker = chunker
self.batch_size = batch_size
self.flush_interval = flush_interval
self.pending_updates: list[dict] = []
self._running = False
async def start(self):
self._running = True
asyncio.create_task(self._flush_loop())
async def stop(self):
self._running = False
if self.pending_updates:
await self._flush()
async def add_document(self, doc: ParsedDocument):
self.pending_updates.append({
"action": "add",
"doc_id": doc.doc_id,
"content": doc.content,
"metadata": doc.metadata,
})
if len(self.pending_updates) >= self.batch_size:
await self._flush()
async def delete_document(self, doc_id: str):
self.pending_updates.append({
"action": "delete",
"doc_id": doc_id,
})
async def _flush_loop(self):
while self._running:
await asyncio.sleep(self.flush_interval)
if self.pending_updates:
await self._flush()
async def _flush(self):
updates = self.pending_updates[:]
self.pending_updates.clear()
to_add = [u for u in updates if u["action"] == "add"]
to_delete = [u for u in updates if u["action"] == "delete"]
if to_add:
all_chunks = []
for update in to_add:
chunks = self.chunker.chunk(update["content"], update["doc_id"])
all_chunks.extend(chunks)
texts = [c.content for c in all_chunks]
embeddings = [self.embedding_fn(t) for t in texts]
await self.vector_store.upsert(
ids=[c.chunk_id for c in all_chunks],
embeddings=embeddings,
metadatas=[c.metadata for c in all_chunks],
documents=texts,
)
if to_delete:
doc_ids = [u["doc_id"] for u in to_delete]
await self.vector_store.delete_by_doc_ids(doc_ids)
7.3 性能指标与SLA
| 指标 | SLA目标 | 监控方式 |
|---|---|---|
| 端到端延迟P95 | < 3s | Prometheus histogram |
| 检索延迟P95 | < 500ms | 自定义指标 |
| 检索准确率 | > 95% | 人工抽检+自动评估 |
| 索引更新延迟 | < 60s | 文档写入→可检索时间差 |
| 系统可用性 | > 99.9% | 健康检查+告警 |
| 并发QPS | > 100 | 压测验证 |
八、总结与展望
大模型RAG+AI Agent的企业级落地是2026年AI应用最核心的技术方向之一。本文从RAG架构、文档处理、Embedding优化、混合检索、Agent融合、权限安全和生产部署七个维度,系统性地阐述了企业级RAG系统的构建方法。
关键要点回顾:
- 混合检索:向量+BM25+知识图谱的RRF融合,是生产级RAG检索的标配
- 重排序:交叉编码器精排+相关性过滤,将检索准确率从70%提升到95%+
- 查询优化:查询扩展+HyDE+指令前缀,显著提升检索召回率
- Agent融合:RAG能力封装为Agent工具,支持多轮对话和多跳推理
- 安全合规:文档级权限+数据脱敏+审计日志,是企业级部署的必要条件
未来,RAG技术将向更智能的方向演进:自适应检索策略(根据查询类型自动选择检索方式)、持续学习(从用户反馈中优化检索质量)、多模态RAG(支持图像、表格、代码等非文本内容的检索与生成)。AI Agent与RAG的深度融合将使知识库从被动检索工具,进化为主动知识助手。
相关阅读
权威参考
本站提供浏览器本地工具,免注册即可试用 →