模块一:数据解析与分块(Data Ingestion & Chunking)

在企业级 RAG 的建设中,“垃圾进,垃圾出(Garbage In, Garbage Out)”是核心共识。如果数据解析不干净,后续算法优化均难以起效。

1. 痛点:传统 PDF 解析方案的局限性

传统解析工具(如 PyPDF, PDFminer)基于字符坐标抓取物理文本,面临三大致命痛点:

  • 多栏排版混淆:跨栏阅读时,左右两栏的文字横向拼凑在一起,导致语义完全断裂。

  • 表格结构退化:直接退化为无序的、失去物理对应关系的无格式文本空格,导致大模型无法还原数据行列关系。

  • 非文本元素丢失:直接忽略图表、图片等富媒体数据。

示例对比:表格解析的差异

  • 传统提取方案( scrambled text):

    季度 销售额 净利润 Q1 120万 30万 Q2 150万 45万
  • Layout-Aware 提取方案(结构化 Markdown):

    | 季度 (Quarter) | 销售额 (Revenue) | 净利润 (Net Profit) |
    | :--- | :--- | :--- |
    | Q1 | 120万 | 30万 |
    | Q2 | 150万 | 45万 |

2. 企业级解析与分块策略

  • 结构化解析方案 (如 Docling):目前企业多采用版面分析(Layout Analysis)加表格检测模型,将文档统一转换为保留标题层级(#, ##)和表格(|)的 Markdown。

  • 父子分块(Parent-Child / Hierarchical Chunking):

    • 痛点:小 Chunk(如 200 tokens)检索精准但缺乏上下文;大 Chunk(如 1000 tokens)上下文全但向量相似度易被稀释。

    • 解决:在向量库中仅存储并检索“子 Chunk”向量,检索命中后,通过元数据绑定的 parent_id 到 KV 数据库中提取出完整的“父 Chunk”送入大模型。

3. 父子分块核心 Python 代码实现

import uuid
import numpy as np
​
# 存储完整父 Chunk 的 NoSQL/KV 库
parent_document_store = {}
# 存储子 Chunk 向量的向量数据库
vector_database = []
​
def mock_get_embedding(text: str) -> list:
    np.random.seed(abs(hash(text)) % (10 ** 8))
    return np.random.rand(128).tolist()  # 模拟128维向量
​
def ingest_document(raw_text: str):
    # 1. 存入完整的父 Chunk
    parent_id = str(uuid.uuid4())
    parent_document_store[parent_id] = raw_text
​
    # 2. 切分成高内聚的子 Chunk 并做向量化
    child_chunks = [
        "大模型在2026年迎来了爆发式增长。",
        "企业级RAG系统成为了各家公司落地AI的核心技术方案。",
        "通过多模态解析和父子分块,可以大幅度降低RAG的幻觉率。"
    ]
​
    for child_text in child_chunks:
        child_id = str(uuid.uuid4())
        child_vector = mock_get_embedding(child_text)
        
        vector_record = {
            "id": child_id,
            "vector": child_vector,
            "text": child_text,
            "parent_id": parent_id  # 关联父 ID
        }
        vector_database.append(vector_record)
​
def retrieve_and_generate(user_query: str):
    query_vector = np.array(mock_get_embedding(user_query))
    
    # 计算最匹配的子 Chunk
    results = []
    for record in vector_database:
        record_vec = np.array(record["vector"])
        sim = np.dot(query_vector, record_vec) / (np.linalg.norm(query_vector) * np.linalg.norm(record_vec))
        results.append((sim, record))
    results.sort(key=lambda x: x[0], reverse=True)
    
    best_match_record = results[0][1]
    
    # 通过 parent_id 取出完整的父 Chunk
    target_parent_id = best_match_record["parent_id"]
    full_context = parent_document_store[target_parent_id]
    
    return full_context

4. 面试表达要点

“在处理企业级 RAG 数据源时,我们规避了简单的物理字符分块,转而使用 Docling 将复杂 PDF 统一转换为结构化 Markdown,以保留标题树结构与表格语义。同时,为了解决‘检索精度’与‘生成上下文完整度’的矛盾,我们实现了父子分块(Parent-Child Chunking),通过向量召回 256 tokens 的子片段,最终基于 parent_id 还原 1000 tokens 的完整父分块送给大模型,这使得业务生成的语义完整度得到了显著提升。”


模块二:混合检索与重排序(Hybrid Search & Rerank)

纯向量检索在处理精准词(如 SKU、代码、工号、缩写词)时往往表现欠佳,企业生产必须引入混合检索架构。

1. 技术路线:双路召回漏斗模型

  • BM25(稀疏检索):执行严格的词频、关键词精确匹配。

  • Vector Search(密集检索):捕捉概念层面的语义相似度。

  • 分数对齐难点:BM25 得分无界,向量相似度在 0 到 1 之间。必须通过 RRF(Reciprocal Rank Fusion,倒数排名融合) 算法,通过排序位置计算无界分数进行融合。

  • Rerank(精排):将融合后的 Top-30 结果送入 Reranker 模型(Cross-Encoder 架构)。计算 Query 和 Document 的交叉注意力机制。此过程计算成本高,不适合第一阶段的大规模筛选。

[ 100万个原始文本块 ]
        │
        ├── (BM25 检索)     ──── 召回 Top-100
        ├── (向量检索)       ──── 召回 Top-100
        │
        ▼
   [ RRF 融合排序 ]   ──── 合并并筛选 Top-30
        │
        ▼
 [ Rerank 模型精排 ]  ──── 输出高精度 Top-5 给 LLM

2. 混合检索与 RRF 算法核心实现

from collections import defaultdict
​
# 模拟两路召回的文档 ID 排名(从高到低)
bm25_ranked_list = ["doc1", "doc2", "doc3"]
vector_ranked_list = ["doc2", "doc3", "doc1"]
​
def reciprocal_rank_fusion(ranked_lists: list, k: int = 60):
    """
    RRF 算法:
    Score(d) = sum( 1 / (k + rank_d) )
    """
    rrf_scores = defaultdict(float)
    for r_list in ranked_lists:
        for rank, doc_id in enumerate(r_list, start=1):
            rrf_scores[doc_id] += 1.0 / (k + rank)
    
    # 按照分数由高到低重新排序
    sorted_docs = sorted(rrf_scores.items(), key=lambda x: x[1], reverse=True)
    return sorted_docs
​
# 执行融合
merged_results = reciprocal_rank_fusion([bm25_ranked_list, vector_ranked_list], k=60)
print("RRF 融合结果:", merged_results)

3. 面试表达要点

“在生产环境,只靠语义空间距离的向量检索会丢失对 SKU 或特定工号精确匹配的能力。我们的方案采用 BM25 与向量双路召回 的漏斗架构。通过 RRF (倒数排名融合) 算法规避了两路检索分值无法对齐的难题,合并出 Top-30。最后引入 BGE-Reranker(交叉编码器)进行精排打分,只留下最相关的 Top-5,这在检索耗时、硬件成本与最终准确率之间实现了最佳平衡。”


模块三:生成与大模型适配(LLM & Context Optimization)

如何科学地在 Prompt 中排布背景知识,对控制大模型幻觉和降低 Token 成本至关重要。

1. 优化策略

  • U型重新排序(U-Shape Re-ordering):克服“Lost in the Middle(迷失在中间)”现象。将相似度得分最高的 Chunk 分布在上下文的头部和尾部,最不相关的堆叠在中间。

  • XML 结构化包裹:用 XML 标签(如 <doc id="xxx">)包围 Chunk。这种物理隔离可以帮助大模型精准锚定文档边界。

  • 强引用规则约束:通过系统提示词(System Prompt)强制大模型在引用结论时在句尾附带 [doc_id] 标识,解决难以溯源的问题。

2. U型重排与 Prompt 构建实现

def ushape_reorder(chunks: list) -> list:
    """
    输入降序排列的 Chunks,重排为 U 型(两头得分最高,中间得分最低)
    """
    sorted_chunks = sorted(chunks, key=lambda x: x["score"], reverse=True)
    reordered = [None] * len(sorted_chunks)
    left, right = 0, len(sorted_chunks) - 1
    
    for i, chunk in enumerate(sorted_chunks):
        if i % 2 == 0:
            reordered[left] = chunk
            left += 1
        else:
            reordered[right] = chunk
            right -= 1
    return reordered
​
def build_enterprise_prompt(query: str, ordered_chunks: list) -> str:
    context_str = "<search_results>\n"
    for chunk in ordered_chunks:
        context_str += f'  <doc id="{chunk["doc_id"]}">\n'
        context_str += f'    <content>{chunk["text"]}</content>\n'
        context_str += "  </doc>\n"
    context_str += "</search_results>"
    
    prompt = f"""你是一个严谨的企业知识库助手。请根据下面给出的 <search_results> 中的内容回答用户的问题。
规则:
1. 只能根据 <search_results> 中提及的事实回答,若背景知识不足则直接说“抱歉,根据公司现有文档无法回答”,切勿编造。
2. 句末必须强制标注参考的文档ID(例如:“李雷在研发部[doc_1]。”)。
​
以下是背景知识:
{context_str}
​
当前用户提问:{query}
回答:"""
    return prompt

3. 面试表达要点

“大模型在处理超长上下文时存在‘Lost in the Middle’的缺陷。我们在生成端引入了 U-Shape 重排算法,将得分最高的 Chunks 分布于提示词的两端。同时利用 XML 标签 来规范召回文本的结构,并加入强引用规则指令。这不仅能让模型定位精确,还将系统的回答幻觉率控制在业务可容忍范围内。”


模块四:企业级工程、安全与性能(Security & Performance)

企业生产级应用对安全隔离有红线要求,对并发下的延迟和成本同样有极高要求。

1. 架构组件设计

  • 安全元数据前置过滤(Pre-filtering ACL):拒绝在检索完成后通过内存代码来剔除涉密数据(后置过滤导致召回率断崖下跌)。在向量检索发生的同时,通过数据库原生表达式(如 allowed_departments)在底层排除无权限数据。

  • 语义缓存(Semantic Cache):将用户的新 Query 向量化后,先在缓存向量库(如 Redis)中做余弦匹配。当相似度极高(如 $\ge 0.92$)时,直接秒级返回缓存的历史大模型回答,完全绕过后续的检索与 LLM 生成。

2. 前置过滤与语义缓存核心实现

import numpy as np
​
# 模拟向量缓存库
semantic_cache_store = []
SIMILARITY_THRESHOLD = 0.92
​
def get_embedding(text: str) -> list:
    np.random.seed(abs(hash(text)) % (10 ** 8))
    return np.random.rand(64).tolist()
​
# ----------------- 语义缓存判定 -----------------
def check_and_get_cache(new_query: str):
    new_vector = np.array(get_embedding(new_query))
    
    for cache_entry in semantic_cache_store:
        cached_vector = np.array(cache_entry["query_vector"])
        sim = np.dot(new_vector, cached_vector) / (np.linalg.norm(new_vector) * np.linalg.norm(cached_vector))
        
        if sim >= SIMILARITY_THRESHOLD:
            return cache_entry["answer"]
    return None
​
# ----------------- 前置权限过滤检索 -----------------
mock_vector_db = [
    {"id": "doc_hr_01", "text": "高层薪资标准...", "vector": get_embedding("薪资标准"), "allowed_departments": ["HR"]},
    {"id": "doc_tech_01", "text": "Git分支规范...", "vector": get_embedding("Git规范"), "allowed_departments": ["HR", "R&D"]}
]
​
def search_with_acl(user_query: str, user_dept: str):
    query_vector = np.array(get_embedding(user_query))
    valid_results = []
    
    for doc in mock_vector_db:
        # 权限判定 (Pre-filtering)
        if user_dept in doc["allowed_departments"]:
            doc_vec = np.array(doc["vector"])
            sim = np.dot(query_vector, doc_vec) / (np.linalg.norm(query_vector) * np.linalg.norm(doc_vec))
            valid_results.append((sim, doc))
            
    valid_results.sort(key=lambda x: x[0], reverse=True)
    return valid_results

3. 面试表达要点

“在处理多租户与权限控制时,我们没有采用不安全的后置过滤,而是通过向量数据库(如 Milvus)支持的原生表达式进行前置过滤(Pre-filtering),确保检索树中只检索当前 JWT 中携带部门权限的文档。为了对抗高并发、降低 Token 成本,我们在架构最前方引入了基于 Redis-Vector 的语义缓存(Semantic Cache),对于历史高度近似(相似度 $\ge 0.92$)的请求直接秒级返回缓存答案,成功将系统 TTFT 压到 20 毫秒以内,并为企业节省了可观的 API 调用费用。”


模块五:评估与观测(Evaluation & Observability)

如何客观、自动化地评估系统质量并追踪线上坏例(Bad Case)是 RAG 系统能够持续迭代的保证。

1. 评估体系:RAG 三元组指标

  • Context Relevance(上下文相关性):度量检索精度的指标。

  • Faithfulness(忠实度/无幻觉):度量大模型是否完全基于检索文本进行生成的指标。

  • Answer Relevance(答案相关性):度量模型是否正面作答的指标。

                    [ 用户的提问 ]
                     /          \
      答案相关性(QA)                上下文相关性(QC)
                   /              \
        [ 生成的答案 ]  ─────── [ 检索出的背景 ]
                        忠实度(AC)

2. 线上可观测(Observability)

通过链路追踪工具(如 Langfuse),为每个端到端请求生成 trace_id。 完整抓取“用户提问 -> 检索原始文档 -> Rerank 后文档 -> 发给大模型的完整 Prompt -> 大模型首字响应延迟(TTFT)与最终生成”,从而可以实现一站式的诊断。

3. 自动化评估 Faithfulness 核心逻辑实现(LLM-as-a-Judge)

import json
​
def evaluate_faithfulness(retrieved_context: str, generated_answer: str) -> float:
    # 模拟发送给裁判大模型(如 GPT-4)的 Prompt 与输出解析
    # 裁判模型将生成的回答拆解为陈述,判断是否能从上下文推导
    judge_output_mock = {
        "statements_and_verdicts": [
            {"statement": "李雷在研发部上班。", "verdict": "Yes"},
            {"statement": "他需要在周五主持技术评审会。", "verdict": "No"}  # 属于上下文未提及的幻觉
        ]
    }
    
    verdicts = judge_output_mock["statements_and_verdicts"]
    total = len(verdicts)
    yes_count = sum(1 for v in verdicts if v["verdict"] == "Yes")
    
    return yes_count / total if total > 0 else 0.0
​
# 自动化评估输出得分 0.50
print("Faithfulness 得分:", evaluate_faithfulness("...", "..."))

4. 面试表达要点

“我们放弃了低效的人工主观评测,转而构建了基于 Ragas 评估框架和 LLM-as-a-Judge 机制的自动化离线评估流水线。我们在每次迭代系统配置时,使用 200 个黄金测试样本重点跑 Faithfulness(忠实度)等三元组指标。在线上,我们集成 Langfuse 链路追踪。一旦有业务反馈 bad case,我们可以通过 trace_id 像调试微服务一样完整回放检索、重排和生成每个节点的耗时和数据输入,从而做到精准调优。”