LLM RAG + AI Agent Enterprise Implementation: Retrieval-Augmented Generation Architecture and Production Deployment Guide
Summary
- Master the core architecture and document processing pipeline of RAG systems, understanding the complete chain from raw documents to high-quality retrieval results
- Deep dive into hybrid retrieval (vector + keyword + knowledge graph) and reranking techniques to achieve enterprise-grade 95%+ retrieval accuracy
- RAG + AI Agent deep integration in practice: tool-augmented retrieval, multi-turn conversation memory, enterprise knowledge base access control, and production deployment
Table of Contents
- 1. RAG System Architecture and Core Workflow
- 2. Document Processing and Chunking Strategies
- 3. Embedding Model Selection and Optimization
- 4. Hybrid Retrieval and Reranking
- 5. RAG + AI Agent Deep Integration
- 6. Enterprise Knowledge Base Permissions and Security
- 7. Production-Grade Deployment and Performance Optimization
- 8. Conclusion and Outlook
1. RAG System Architecture and Core Workflow
1.1 Core Value and Limitations of RAG
Retrieval-Augmented Generation (RAG) is the most widely adopted technical paradigm for LLM applications in 2024-2026. Its core value lies in enhancing the response quality and factual accuracy of large models by retrieving from external knowledge bases, without requiring model fine-tuning.
However, the core challenges of production-grade RAG systems go far beyond simple "retrieve + concatenate":
- Retrieval quality bottleneck: Semantic similarity in vector retrieval does not equal answer relevance; Top-K results may contain significant noise
- Context window waste: Irrelevant retrieval results consume the limited context window, degrading model reasoning quality
- Multi-hop reasoning gap: Complex questions require multi-step retrieval and reasoning that single-pass retrieval cannot satisfy
- Real-time requirements: Enterprise knowledge bases are frequently updated, and indexes need real-time synchronization
1.2 Production-Grade RAG Architecture
A production-grade RAG system architecture is far more complex than a simple "query → retrieve → generate" pipeline:
┌──────────────────────────────────────────────────┐
│ Query Understanding │
│ Intent Recognition · Query Rewriting · Entity │
│ Extraction · Multi-hop Decomposition │
├──────────────────────────────────────────────────┤
│ Hybrid Retrieval │
│ Vector Search · BM25 Keywords · Knowledge Graph│
│ · SQL Queries │
├──────────────────────────────────────────────────┤
│ Reranking & Fusion │
│ Cross-Encoder Reranking · Mutual Information │
│ Maximization · Result Deduplication │
├──────────────────────────────────────────────────┤
│ Context Assembly │
│ Relevance Filtering · Context Compression · │
│ Structured Organization │
├──────────────────────────────────────────────────┤
│ Generation & Verification │
│ Chain-of-Thought · Fact Verification · │
│ Hallucination Detection │
└──────────────────────────────────────────────────┘
The Query Understanding layer is responsible for understanding the user's true intent, including query rewriting, entity extraction, and multi-hop question decomposition. The Hybrid Retrieval layer uses a fusion of multiple retrieval strategies. The Reranking layer performs fine-grained sorting of initial retrieval results. The Context Assembly layer assembles the optimal context. The Generation layer produces the final answer and performs fact verification.
1.3 RAG vs Fine-Tuning vs Pre-Training
| Dimension | RAG | Fine-Tuning | Pre-Training |
|---|---|---|---|
| Knowledge Update | Real-time | Requires retraining | Requires retraining |
| Cost | Low | Medium | Very High |
| Factual Accuracy | High | Medium | Low |
| Domain Adaptability | Strong | Strong | Strongest |
| Deployment Complexity | Medium | Low | High |
| Hallucination Control | Good | Fair | Poor |
In enterprise scenarios, the RAG + Fine-Tuning combination is the best practice: RAG ensures factual accuracy, while fine-tuning optimizes the model's understanding of specific domains and output style.
2. Document Processing and Chunking Strategies
2.1 Document Parsing Pipeline
Enterprise knowledge bases contain diverse document formats (PDF, Word, Excel, PPT, HTML, Markdown), requiring a unified parsing pipeline:
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 Deep Comparison of Chunking Strategies
Document chunking is one of the most critical components of a RAG system, as chunking strategy directly impacts retrieval quality:
Fixed-Size Chunking: Splits by a fixed token count. Simple to implement but may break semantic coherence.
Recursive Character Chunking: Recursively splits by paragraph → sentence → character priority, preserving semantic integrity. LangChain's RecursiveCharacterTextSplitter uses this strategy.
Semantic Chunking: Uses an Embedding model to compute semantic similarity between adjacent sentences and splits at semantic breakpoints. Highest quality but computationally expensive.
Structure-Aware Chunking: Leverages document heading hierarchy and section structure for chunking, preserving the document's logical structure.
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 Metadata-Enriched Chunking
Adding rich metadata to each chunk enables fine-grained filtering during retrieval:
@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"Extract 5-10 key terms from the following text, returning as a JSON array:\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"""Extract named entities from the following text, returning a JSON array where each entity contains name, type, and value fields.
Entity types include: PERSON, ORGANIZATION, PRODUCT, DATE, LOCATION, TECHNOLOGY
Text:
{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"Summarize the core content of the following text in one sentence:\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", [])
3. Embedding Model Selection and Optimization
3.1 Mainstream Embedding Model Comparison
| Model | Dimensions | MTEB Score | Chinese Capability | Inference Speed | License |
|---|---|---|---|---|---|
| BGE-M3 | 1024 | 73.5 | Excellent | Medium | MIT |
| GTE-Qwen2-7B | 3584 | 76.2 | Excellent | Slow | Apache 2.0 |
| text-embedding-3-large | 3072 | 74.5 | Good | Fast | Commercial |
| Jina-Embeddings-v3 | 1024 | 72.8 | Good | Medium | CC-BY-4.0 |
| BCE-Embedding | 768 | 71.2 | Excellent | Fast | MIT |
Selection Recommendations:
- For Chinese scenarios, BGE-M3 is the top choice with the best value
- For maximum performance, choose GTE-Qwen2-7B, though inference cost is higher
- For multilingual support, choose Jina-Embeddings-v3
- For OpenAI API scenarios, choose text-embedding-3-large
3.2 Embedding Service Deployment
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 Query-Side Embedding Optimization
Query-side Embedding optimization is a key technique for improving retrieval quality:
Query Expansion: Use an LLM to expand the user query into multiple related queries, increasing retrieval coverage.
Hypothetical Document Embedding (HyDE): First have the LLM generate a hypothetical answer, then use the hypothetical answer's Embedding for retrieval, which works better than direct query retrieval.
Instruction Prefix: Add task instructions before the query, such as "Retrieve relevant documents for the following query:", to align with the input format used during training.
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"""Rewrite the following query into {num_expansions} equivalent queries from different angles, returning as a JSON array.
Original query: {query}
Requirements:
1. Preserve the core intent of the original query
2. Use different phrasing and keywords
3. Cover different technical terms and common expressions"""
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"""Please write a detailed answer to the following question (even if you are unsure of the answer, provide a reasonable hypothetical response):
Question: {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": "Retrieve relevant documents for the following query: ",
"similarity": "Find documents similar to the following content: ",
"classification": "Classify the following content: ",
}
prefix = prefixes.get(task, "")
return self.embedding_fn(f"{prefix}{query}")
4. Hybrid Retrieval and Reranking
4.1 Hybrid Retrieval Architecture
Single vector retrieval cannot cover all scenarios. Keyword retrieval (BM25) excels at exact matching (product models, proper nouns), vector retrieval excels at semantic matching (concept similarity), and knowledge graph retrieval excels at relational reasoning. Hybrid retrieval is a must-have for production-grade 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 Cross-Encoder Reranking
Initial retrieval results are computed using a Bi-Encoder, which is fast but has limited precision. A Cross-Encoder feeds both the query and document into the model together, achieving higher precision but slower speed, making it suitable for fine-ranking Top-K results:
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 Relevance Filtering
After reranking, low-relevance results still need to be filtered to prevent noise from entering the context:
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
5. RAG + AI Agent Deep Integration
5.1 Tool-Augmented Retrieval Agent
Encapsulating RAG retrieval capabilities as tools callable by AI Agents enables more intelligent retrieval strategies:
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 Multi-Turn Conversational RAG
Enterprise-grade RAG systems need to support multi-turn conversations, maintaining conversation context and retrieval history:
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"""Based on the following conversation history, rewrite the user's latest question as a standalone, complete retrieval query.
Return only the rewritten query without explanation.
Conversation history:
{history_text}
Latest question: {query}
Rewritten 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"""You are a professional enterprise knowledge base assistant. Please answer the user's question based on the retrieved document content below.
Requirements:
1. Answer only based on the provided document content; do not fabricate information
2. If the documents do not contain relevant information, clearly inform the user
3. Cite source documents when referencing information
4. Use clear, structured formatting
Retrieved documents:
{context}
User question: {query}
Answer:"""
5.3 Multi-Hop Reasoning RAG
Complex questions require multi-step retrieval and reasoning that single-pass RAG cannot satisfy. Multi-hop RAG leverages Agent planning capabilities to decompose complex questions into multi-step retrieval chains:
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"""Based on the original question and the retrieved information, determine whether further retrieval is needed.
Original question: {original_query}
Retrieved information:
{context_summary}
Please determine:
1. If there is enough information to answer the question, return {{"action": "answer"}}
2. If more information is needed, return {{"action": "search", "query": "next retrieval query"}}
Return in JSON format:"""
response = await self.llm.generate(prompt)
import json
try:
return json.loads(response)
except:
return {"action": "answer"}
6. Enterprise Knowledge Base Permissions and Security
6.1 Document-Level Access Control
Enterprise knowledge base documents typically have strict access control, and RAG retrieval must comply with permission rules:
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 Data Masking
Retrieval results need to be automatically masked for sensitive information before being sent to the LLM:
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 Audit Logging
All RAG queries and retrieval operations need to be recorded in audit logs:
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. Production-Grade Deployment and Performance Optimization
7.1 Caching Strategy
Multi-layer caching strategy for RAG systems:
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 Asynchronous Index Updates
Enterprise knowledge base documents are frequently updated, requiring an asynchronous index update mechanism:
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 Performance Metrics and SLA
| Metric | SLA Target | Monitoring Method |
|---|---|---|
| End-to-end latency P95 | < 3s | Prometheus histogram |
| Retrieval latency P95 | < 500ms | Custom metrics |
| Retrieval accuracy | > 95% | Manual sampling + automated evaluation |
| Index update latency | < 60s | Document write → searchable time delta |
| System availability | > 99.9% | Health checks + alerting |
| Concurrent QPS | > 100 | Load testing verification |
8. Conclusion and Outlook
The enterprise implementation of LLM RAG + AI Agent is one of the most core technical directions for AI applications in 2026. This article systematically covers the construction of enterprise-grade RAG systems across seven dimensions: RAG architecture, document processing, Embedding optimization, hybrid retrieval, Agent integration, permission security, and production deployment.
Key takeaways:
- Hybrid Retrieval: RRF fusion of vector + BM25 + knowledge graph is the standard for production-grade RAG retrieval
- Reranking: Cross-encoder fine-ranking + relevance filtering boosts retrieval accuracy from 70% to 95%+
- Query Optimization: Query expansion + HyDE + instruction prefixes significantly improve retrieval recall
- Agent Integration: Encapsulating RAG capabilities as Agent tools supports multi-turn conversations and multi-hop reasoning
- Security Compliance: Document-level permissions + data masking + audit logging are essential for enterprise deployment
Looking ahead, RAG technology will evolve toward greater intelligence: adaptive retrieval strategies (automatically selecting retrieval methods based on query type), continuous learning (optimizing retrieval quality from user feedback), and multimodal RAG (supporting retrieval and generation of non-text content such as images, tables, and code). The deep integration of AI Agents and RAG will transform knowledge bases from passive retrieval tools into proactive knowledge assistants.
Related Reading
- AI Agent Multimodal Collaboration Architecture in Practice
- K8s 1.30+ LLM Inference Platform Deployment Guide
- Rust Vector Database Engine Development in Practice
Authoritative References
Try these browser-local tools — no sign-up required →