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

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:

  1. Hybrid Retrieval: RRF fusion of vector + BM25 + knowledge graph is the standard for production-grade RAG retrieval
  2. Reranking: Cross-encoder fine-ranking + relevance filtering boosts retrieval accuracy from 70% to 95%+
  3. Query Optimization: Query expansion + HyDE + instruction prefixes significantly improve retrieval recall
  4. Agent Integration: Encapsulating RAG capabilities as Agent tools supports multi-turn conversations and multi-hop reasoning
  5. 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.

Authoritative References

Try these browser-local tools — no sign-up required →

#RAG#AI Agent#大模型#向量检索#企业级落地#2026