RAG 检索增强生成工程化完全指南:从一次"企业知识库助手幻觉编造内容客户当场炸毛"看懂为什么 LangChain demo 远远不够

2024 年我们做一个企业知识库问答给客户内部的 5000 份 PDF 政策文件做 RAG retrieval augmented generation 问答助手原型阶段 LangChain 加 OpenAI text embedding 3 small 加 GPT 4 一周做完 demo 老板看了说牛逼上线结果上线第一天我们陆续踩了一堆坑第一种最让我傻眼用户问公司年假怎么算模型一本正经地胡说八道引用了一段我根本没在知识库里的内容客户当场炸毛 RAG 居然能幻觉到这种地步第二种最难缠用户问 1 月份的销售目标是多少模型返回了 2023 年的目标我们没做时间过滤检索把所有年份的文档都召回了模型选了最相似的不管是不是当年的第三种最离谱我们用 chunk size 1000 一刀切一份合同里关键条款第 17 条的内容被切到两个 chunk 中间断在但乙方不得后面接着转让本协议项下权利模型只看到前一半输出的答案缺了核心限制第四种最致命用户问我们公司的脱敏规则答案里居然返回了用户的真实数据因为我们 embedding 索引时把示例数据也建进去了隐私泄露差点被告第五种最莫名其妙同一个问题早上问与晚上问答案不一样排查发现是检索 top k 5 取的是近似最邻近 HNSW 索引在并发更新时返回有抖动模型答案随之变化我盯着这一连串问题想了很久才彻底想明白第一版错在一个根本的认知上我以为 RAG 就是文档切片加 embedding 加检索加 LLM 一通跑就有答案可这个认知是错的真正能用的 RAG 是一个文档预处理与 chunk 策略加 embedding 模型选型加检索召回精排加 prompt 工程与引用加幻觉控制与拒答加评估与持续优化的整套工程方法论

2024 年我们做一个企业知识库问答 给客户内部的 5000 份 PDF 政策文件做 RAG retrieval-augmented generation 问答助手。原型阶段 LangChain 加 OpenAI text-embedding-3-small 加 GPT-4 一周做完 demo 老板看了说牛逼上线。结果上线第一天我们陆续踩了一堆坑。第一种最让我傻眼 用户问 公司年假怎么算 模型一本正经地胡说八道 引用了一段我根本没在知识库里的内容 客户当场炸毛 RAG 居然能幻觉到这种地步。第二种最难缠 用户问 1 月份的销售目标是多少 模型返回了 2023 年的目标 我们没做时间过滤 检索把所有年份的文档都召回了 模型选了最相似的不管是不是当年的。第三种最离谱 我们用 chunk_size=1000 一刀切 一份合同里 关键条款 第 17 条 的内容被切到两个 chunk 中间断在 但乙方不得 后面接着 转让本协议项下权利 模型只看到前一半 输出的答案缺了核心限制。第四种最致命 用户问 我们公司的脱敏规则 答案里居然返回了用户的真实数据 因为我们 embedding 索引时把示例数据也建进去了 隐私泄露差点被告。第五种最莫名其妙 同一个问题 早上问与晚上问答案不一样 排查发现是检索 top_k=5 取的是近似最邻近 HNSW 索引在并发更新时返回有抖动 模型答案随之变化。我盯着这一连串问题想了很久才彻底想明白第一版错在一个根本的认知上我以为 RAG 就是 文档切片 加 embedding 加 检索 加 LLM 一通跑就有答案 可这个认知是错的真正能用的 RAG 是一个 文档预处理与 chunk 策略 加 embedding 模型选型 加 检索召回精排 加 prompt 工程与引用 加 幻觉控制与拒答 加 评估与持续优化 的整套工程方法论 任何一环没做都可能让你的助手胡说八道或者泄露数据本文从头梳理 RAG 工程化的要点 chunk 怎么切 embedding 怎么选 hybrid search 怎么用 reranker 怎么加 prompt 怎么设计 幻觉怎么控制 评估怎么做 以及一些把 RAG 做扎实要避开的工程坑

问题背景:为什么 RAG 不是切 chunk 加 embed 就完事

很多人对 RAG 的认知是 切 chunk 加 embed 加 retrieve 加 LLM 一条龙 但生产里你会发现 chunk 切错关键内容断开 embedding 召回不到核心文档 LLM 幻觉编造答案 引用错文档 时间维度乱 隐私数据泄漏。问题的根源在于:

  • chunk 策略决定召回上限:固定切片粒度大了召不到 切了关键内容分散在多个 chunk 模型拼不起来。
  • 纯向量召回不够:embedding 对术语 缩写 专有名词不敏感 必须 BM25 加 dense vector 混合。
  • top_k 不是越多越好:k 大噪声多 LLM 容易被无关上下文干扰 反而幻觉更多。
  • prompt 必须强制引用:不强制引用文档 ID 模型就开始凭空想象 必须 cite source 加溯源。
  • 拒答机制必须有:检索没召回相关内容 模型必须拒答 不能强行回答 否则 100% 幻觉。
  • 评估集与回归测试:RAG 链路长 任何一环改动都可能让回答质量变差 必须自动化评估。

一 文档预处理与 chunk 策略

chunk 是 RAG 的基础 切的好 召回就好 切坏了一切白搭。常见错误是 chunk_size=1000 一刀切 不考虑文档结构 关键内容被断开。生产推荐 按结构切 然后按 token 限制保护 这两层结合既保留语义又控制大小。

from langchain.text_splitter import RecursiveCharacterTextSplitter
import tiktoken
import re

class StructuredChunker:
    """按文档结构 + token 限制双层切片"""

    def __init__(self, max_tokens: int = 500, overlap_tokens: int = 50):
        self.max_tokens = max_tokens
        self.overlap = overlap_tokens
        self.encoder = tiktoken.encoding_for_model('gpt-4')

    def chunk_markdown(self, text: str, doc_id: str) -> list[dict]:
        """按 Markdown 标题层级切 标题作为 metadata 保留"""
        chunks = []
        # 先按 H2 切大块
        sections = re.split(r'(?=^## )', text, flags=re.MULTILINE)
        for i, section in enumerate(sections):
            if not section.strip():
                continue
            # 提取标题与内容
            lines = section.split('\n', 1)
            heading = lines[0].strip('# ').strip()
            body = lines[1] if len(lines) > 1 else ''

            # 检查 token 长度
            tokens = self.encoder.encode(body)
            if len(tokens) <= self.max_tokens:
                chunks.append({
                    'doc_id': doc_id,
                    'chunk_id': f'{doc_id}-h2-{i}',
                    'heading': heading,
                    'text': f'## {heading}\n{body}',
                    'token_count': len(tokens),
                })
            else:
                # 超长 进一步切
                sub_chunks = self._split_by_tokens(body, heading, doc_id, i)
                chunks.extend(sub_chunks)
        return chunks

结构化切片只解决了大段切分 真正长段落超过 max_tokens 还要二次切 按 token 切的时候必须保留 overlap 让前一 chunk 的末尾出现在后一 chunk 开头 这样跨 chunk 的语义不会断 这是 RAG chunk 工程的关键 trick。

    def _split_by_tokens(self, text: str, heading: str,
                          doc_id: str, section_idx: int) -> list[dict]:
        """按 token 数切 保留 overlap 防语义断裂"""
        tokens = self.encoder.encode(text)
        chunks = []
        start = 0
        idx = 0
        while start < len(tokens):
            end = min(start + self.max_tokens, len(tokens))
            sub_tokens = tokens[start:end]
            sub_text = self.encoder.decode(sub_tokens)
            chunks.append({
                'doc_id': doc_id,
                'chunk_id': f'{doc_id}-h2-{section_idx}-{idx}',
                'heading': heading,
                'text': f'## {heading}\n{sub_text}',
                'token_count': len(sub_tokens),
            })
            start += self.max_tokens - self.overlap
            idx += 1
        return chunks

    def chunk_contract(self, text: str, doc_id: str) -> list[dict]:
        """合同条款类文档 按 第 N 条 切 不要让条款被断开"""
        pattern = re.compile(r'(?=第[一二三四五六七八九十百\d]+条)')
        clauses = pattern.split(text)
        chunks = []
        for i, clause in enumerate(clauses):
            if not clause.strip():
                continue
            chunks.append({
                'doc_id': doc_id,
                'chunk_id': f'{doc_id}-clause-{i}',
                'heading': clause.split('\n')[0][:50],
                'text': clause,
                'token_count': len(self.encoder.encode(clause)),
            })
        return chunks

chunk 策略的工程经验 法律合同按 第 N 条 切 技术文档按 H2 H3 标题切 公司政策按段落或列表项切 不要一刀切 chunk_size 配合 overlap 50 token 让语义跨 chunk 不断裂 这是被 80% 的 RAG 项目忽视的细节。我们公司知识库 chunk 改成按结构切之后 召回率从 65% 提到 88% 没动任何模型只换了切法。

二 Embedding 模型选型与向量索引

Embedding 模型决定召回质量。中文场景 OpenAI text-embedding-3-small 不够好 中文专用 bge-large-zh m3e-large 效果更好。向量索引 Milvus Qdrant Weaviate 各有优势 中小规模 100 万以下 pgvector 已经够用。

from sentence_transformers import SentenceTransformer
import numpy as np
from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance, PointStruct

class EmbeddingService:
    """统一管理 embedding 与向量索引"""

    def __init__(self):
        # 中文场景推荐 bge-large-zh-v1.5 维度 1024
        self.model = SentenceTransformer('BAAI/bge-large-zh-v1.5')
        self.model_version = 'bge-large-zh-v1.5'
        self.client = QdrantClient(host='localhost', port=6333)
        self.collection = 'knowledge_base'
        self._init_collection()

    def _init_collection(self):
        try:
            self.client.create_collection(
                collection_name=self.collection,
                vectors_config=VectorParams(size=1024, distance=Distance.COSINE),
            )
        except Exception:
            pass  # already exists

    def embed(self, texts: list[str]) -> np.ndarray:
        # bge 需要前缀 query 用 表示 检索查询
        # 文档 embedding 不加前缀 query 才加
        return self.model.encode(texts, normalize_embeddings=True)

    def embed_query(self, query: str) -> np.ndarray:
        # bge 查询前缀 必加 否则召回质量降 10%
        prefixed = f'为这个句子生成表示以用于检索相关文章 {query}'
        return self.model.encode([prefixed], normalize_embeddings=True)[0]

embedding 服务搭好后 真正写入与检索还要考虑 批量索引 metadata 过滤 时间过滤 这些是生产 RAG 必须的功能 不然召回会出现跨年份混淆 或者无法按部门按文档类型筛选 下面是 Qdrant 上的索引与检索实现 关键是 payload 里要保留 doc_id heading model_version indexed_at 这些 metadata 检索时可以用 filter。

    def index_chunks(self, chunks: list[dict]):
        """批量索引 chunk"""
        texts = [c['text'] for c in chunks]
        vectors = self.embed(texts)
        points = [
            PointStruct(
                id=hash(c['chunk_id']) & 0xFFFFFFFF,
                vector=vector.tolist(),
                payload={
                    'doc_id': c['doc_id'],
                    'chunk_id': c['chunk_id'],
                    'heading': c['heading'],
                    'text': c['text'],
                    'model_version': self.model_version,
                    'indexed_at': time.time(),
                },
            )
            for c, vector in zip(chunks, vectors)
        ]
        self.client.upsert(collection_name=self.collection, points=points)

    def search(self, query: str, top_k: int = 10,
               filter_dict: dict = None) -> list[dict]:
        query_vec = self.embed_query(query)
        from qdrant_client.models import Filter, FieldCondition, MatchValue
        qdrant_filter = None
        if filter_dict:
            qdrant_filter = Filter(must=[
                FieldCondition(key=k, match=MatchValue(value=v))
                for k, v in filter_dict.items()
            ])
        results = self.client.search(
            collection_name=self.collection,
            query_vector=query_vec.tolist(),
            limit=top_k,
            query_filter=qdrant_filter,
        )
        return [{'score': r.score, **r.payload} for r in results]

Embedding 选型的工程经验 中文场景 bge-large-zh-v1.5 是免费开源里最强 商业付费选 OpenAI text-embedding-3-large 维度 3072 但中文略弱于 bge bge 查询必须加前缀 为这个句子生成表示 否则召回降 10% 这是 bge 论文里的官方推荐。向量索引 100 万 chunk 以下 pgvector 够用 千万级用 Qdrant Milvus 亿级用 Vespa Elasticsearch dense_vector。

三 Hybrid Search:向量 + BM25 + Reranker

纯向量召回对术语 缩写 专有名词不敏感 比如用户搜 KPI 向量召回可能返回 绩效考核 但漏掉了真正含 KPI 字面的文档。Hybrid search 把 BM25 关键词召回与 向量召回 用 RRF reciprocal rank fusion 融合 召回率提升 15-20%。

from rank_bm25 import BM25Okapi
import jieba
from sentence_transformers import CrossEncoder

class HybridRetriever:
    """向量 + BM25 + Reranker 三层召回"""

    def __init__(self, embedding_service, chunks: list[dict]):
        self.embed = embedding_service
        self.chunks = chunks
        # BM25 中文要分词
        corpus = [list(jieba.cut(c['text'])) for c in chunks]
        self.bm25 = BM25Okapi(corpus)
        # Reranker 用 bge-reranker-large 精排
        self.reranker = CrossEncoder('BAAI/bge-reranker-large')

    def retrieve(self, query: str, top_k: int = 5,
                 candidates: int = 30) -> list[dict]:
        # 1 向量召回 候选 30
        vector_results = self.embed.search(query, top_k=candidates)

        # 2 BM25 召回 候选 30
        query_tokens = list(jieba.cut(query))
        bm25_scores = self.bm25.get_scores(query_tokens)
        bm25_top_idx = np.argsort(bm25_scores)[-candidates:][::-1]
        bm25_results = [{
            'chunk_id': self.chunks[i]['chunk_id'],
            'text': self.chunks[i]['text'],
            'doc_id': self.chunks[i]['doc_id'],
            'score': bm25_scores[i],
        } for i in bm25_top_idx]

        # 3 RRF 融合 两路召回的 rank 倒数相加
        merged = self._rrf_merge(vector_results, bm25_results, k=60)

        # 4 Reranker 精排 top 20 选 top_k
        reranked = self._rerank(query, merged[:20])
        return reranked[:top_k]

Hybrid 召回的核心是 RRF 融合 把两路召回的排名倒数相加 排名靠前的获得高分 这是论文里证明过比 score 直接加权更稳的方法 不需要校准两路分数尺度。Reranker 是 cross-encoder 对 query 与每个候选独立打分 比双塔模型精度高 10-15% 但只能用在 candidates 上不能全库扫。

    def _rrf_merge(self, list_a: list[dict], list_b: list[dict],
                    k: int = 60) -> list[dict]:
        """Reciprocal Rank Fusion 两路召回结果融合"""
        scores = {}
        for rank, item in enumerate(list_a):
            cid = item['chunk_id']
            scores[cid] = scores.get(cid, 0) + 1 / (k + rank + 1)
        for rank, item in enumerate(list_b):
            cid = item['chunk_id']
            scores[cid] = scores.get(cid, 0) + 1 / (k + rank + 1)

        # 按融合分数排序
        all_items = {item['chunk_id']: item for item in list_a + list_b}
        sorted_ids = sorted(scores.keys(), key=lambda x: scores[x], reverse=True)
        return [{**all_items[cid], 'rrf_score': scores[cid]}
                for cid in sorted_ids]

    def _rerank(self, query: str, candidates: list[dict]) -> list[dict]:
        """Cross-encoder 精排"""
        pairs = [(query, c['text']) for c in candidates]
        scores = self.reranker.predict(pairs)
        for c, s in zip(candidates, scores):
            c['rerank_score'] = float(s)
        return sorted(candidates, key=lambda x: x['rerank_score'], reverse=True)

Hybrid + Reranker 的工程经验 向量召回 30 + BM25 召回 30 + RRF 融合 + Reranker 精排 top 20 取 top 5 这套组合是 RAG 召回质量的黄金标准 召回率比纯向量提升 20%+ 精确率提升 30%+ 多出来的延迟用 Reranker 通常 50-100ms 完全可接受。我们公司知识库上 hybrid 后 用户满意度从 65% 提到 85% 投诉幻觉数下降 60%。

四 Prompt 工程与引用溯源

Prompt 决定 LLM 怎么用检索结果。错的 prompt 让模型胡说八道 对的 prompt 强制引用 没引用就拒答 这是控制幻觉的最关键一环。

RAG_PROMPT_TEMPLATE = '''你是一位专业的企业知识库问答助手 请严格按以下规则回答用户问题

规则
1 只能基于下面提供的参考文档回答 不能使用任何外部知识
2 每个关键陈述必须用 [文档 N] 标注来源 不允许无引用陈述
3 如果参考文档中没有相关信息 必须直接回答 抱歉 知识库中没有找到相关信息 不要编造
4 如果参考文档之间信息冲突 列出每个来源的说法并说明冲突
5 引用必须精确 不要泛泛地说 根据文档 必须标 [文档 1] [文档 2] 这样

参考文档
{context}

用户问题
{question}

回答 请严格遵循上述规则
'''

def build_rag_prompt(question: str, retrieved: list[dict]) -> str:
    """构造 RAG prompt 强制引用"""
    context_parts = []
    for i, r in enumerate(retrieved, 1):
        context_parts.append(
            f'[文档 {i}] 来源 {r["doc_id"]} 章节 {r["heading"]}\n{r["text"]}\n'
        )
    context = '\n---\n'.join(context_parts)
    return RAG_PROMPT_TEMPLATE.format(context=context, question=question)

def parse_citations(answer: str) -> list[int]:
    """解析答案中的 [文档 N] 引用"""
    pattern = re.compile(r'\[文档 (\d+)\]')
    return list(set(int(m) for m in pattern.findall(answer)))

def validate_answer(answer: str, retrieved: list[dict]) -> dict:
    """答案校验 检查是否有引用 是否在 retrieved 范围内"""
    citations = parse_citations(answer)
    no_info = '没有找到相关信息' in answer or '抱歉' in answer[:30]
    has_citation = len(citations) > 0

    if no_info:
        return {'valid': True, 'reason': 'rejected_no_info'}
    if not has_citation:
        return {'valid': False, 'reason': 'no_citation',
                'message': '答案缺少引用 可能是幻觉'}
    if max(citations) > len(retrieved) or min(citations) < 1:
        return {'valid': False, 'reason': 'invalid_citation',
                'message': f'引用了不存在的文档 {citations}'}
    return {'valid': True, 'citations': citations}

Prompt 工程的关键在于规则越严越好 强制引用 不允许无引用陈述 没相关信息必须拒答 三条加起来能把幻觉率降 70%+ 没引用就 reject 答案 让 LLM 重写或返回 cannot_answer 这是 RAG 上线的硬规范。我们公司 RAG 助手有自动校验 答案没引用直接返回 知识库未覆盖 客户反而觉得这种诚实回答比胡说八道好得多。

[mermaid]flowchart TD
A[用户问题] --> B[Query 改写 扩展]
B --> C[向量召回 30]
B --> D[BM25 召回 30]
C --> E[RRF 融合]
D --> E
E --> F[Reranker 精排]
F --> G[top_k 5 取出]
G --> H[构造 prompt 强制引用]
H --> I[LLM 生成]
I --> J[校验引用与拒答]
J -->|无引用| K[reject 重试或拒答]
J -->|有引用| L[返回答案+引用链]
K --> H

五 评估与回归测试

RAG 链路长 chunk 切法 embedding 模型 检索参数 prompt 模板 LLM 模型任何一项改动都可能让回答变差。必须建立自动化评估 每次改动跑全套 任何指标下降拒绝上线。

from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision, context_recall

class RAGEvaluator:
    """RAG 链路自动评估"""

    def __init__(self, retriever, llm_client):
        self.retriever = retriever
        self.llm = llm_client

    def evaluate_full_pipeline(self, eval_set: list[dict]) -> dict:
        """跑全 pipeline 评估"""
        results = {
            'faithfulness': [],
            'answer_relevancy': [],
            'context_precision': [],
            'context_recall': [],
            'citation_rate': [],
            'rejection_rate': [],
        }
        for ex in eval_set:
            question = ex['question']
            ground_truth = ex['ground_truth']
            expected_docs = ex['expected_doc_ids']

            # 1 检索
            retrieved = self.retriever.retrieve(question, top_k=5)
            retrieved_doc_ids = set(r['doc_id'] for r in retrieved)

            # 2 召回率 expected 在 retrieved 里的比例
            recall = len(set(expected_docs) & retrieved_doc_ids) / max(len(expected_docs), 1)
            results['context_recall'].append(recall)

            # 3 LLM 生成
            prompt = build_rag_prompt(question, retrieved)
            answer = self.llm.generate(prompt)

            # 4 引用率
            citations = parse_citations(answer)
            results['citation_rate'].append(1 if citations else 0)

            # 5 拒答率 期望拒答的问题是否拒答
            should_reject = ex.get('should_reject', False)
            actual_reject = '没有找到相关信息' in answer
            if should_reject == actual_reject:
                results['rejection_rate'].append(1)
            else:
                results['rejection_rate'].append(0)

        return {k: np.mean(v) for k, v in results.items()}

    def check_regression(self, current: dict, baseline: dict,
                          threshold: float = 0.03) -> list[str]:
        """对比基线 任何指标下降 threshold 报警"""
        regressions = []
        for metric, value in current.items():
            base = baseline.get(metric, 0)
            if value < base - threshold:
                regressions.append(
                    f'{metric} 从 {base:.3f} 降到 {value:.3f}'
                )
        return regressions

RAG 评估的工程经验 评估集必须包含 答得对的问题 答不对应该拒答的问题 时间敏感问题 多文档冲突问题 四种 不能只测好走的路 ragas 框架的 faithfulness answer_relevancy context_recall context_precision 四个核心指标足够 加上自定义的 citation_rate rejection_rate 完整覆盖。我们公司每次 RAG 改动跑 200 条 evaluation 集 任何指标下降 3% 拒绝上线 这是 RAG 持续优化的核心机制。

六 RAG 的工程坑:那些 demo 时学不到的

讲完原理来说几个真实生产里踩过的坑。第一个坑是 query 改写被严重低估 用户问 年假怎么算 模型可能在文档里找不到 但 query 改成 公司年休假规定 假期天数 召回立刻命中 用 GPT-3.5 在召回前先改写 query 召回率提升 15%。第二个坑是 时间过滤必须做 用户问 销售目标 必须知道是哪年哪月的 metadata 加 year month 维度 检索时按时间 filter 避免召回过期文档。第三个坑是 多文档冲突 用户问 出差报销额度 文档 A 说 500 元文档 B 说 800 元 模型必须列出两个来源说明冲突 不能擅自选一个 这就是 prompt 第 4 条规则的作用。第四个坑是 隐私数据必须脱敏 索引前过 PII 检测 银行卡号 身份证 手机号都要脱敏 否则示例数据会被检索出来泄漏隐私 这是合规硬要求。第五个坑是 LLM 的 context 长度有限 top_k 不是越大越好 GPT-4 8k context 实际可用 6k 5 个 chunk 已经 4k 加 prompt 模板就快满 top_k 超过 10 模型反而记不住前面的 chunk 召回率与精度都下降

关键概念速查

概念 含义 工程价值
结构化 chunk 按标题层级切 保留语义边界
overlap chunk 重叠 跨 chunk 语义不断
bge-large-zh 中文 embedding 开源最强
bge 查询前缀 encode 前加前缀 召回率提升 10%
Hybrid Search 向量 + BM25 召回率提 20%
RRF 排名倒数融合 无需校准分数
Cross-Encoder Reranker 精排 精确率提 30%
强制引用 prompt cite source 幻觉降 70%
拒答机制 检索空时拒答 诚实优于编造
ragas 评估 多维度指标 回归测试

避坑清单

  1. chunk 按文档结构切 法律按条款 技术文档按 H2 公司文件按段落 不要一刀切 chunk_size 配 overlap 50 token。
  2. 中文 embedding 用 bge-large-zh-v1.5 查询必须加前缀 为这个句子生成表示 不要图省事用 OpenAI 中文偏弱。
  3. 必做 hybrid 召回 向量 30 + BM25 30 + RRF 融合 + Reranker 精排 这套是 RAG 召回质量的黄金标准。
  4. prompt 必须强制引用 没引用拒答 检索空必须拒答 不允许编造 三条规则降幻觉 70%。
  5. 评估集必须包含拒答样本 时间敏感样本 多文档冲突样本 不能只测好走的路。
  6. query 改写不能省 用 GPT-3.5 在召回前改写扩展 query 召回率提升 15%。
  7. 时间维度做 metadata 检索时 filter 否则用户问当年的数据可能被旧文档答错。
  8. 隐私数据索引前必脱敏 PII 检测 银行卡 身份证 手机号 否则示例数据被检索泄漏。
  9. top_k 控制在 5-8 不要贪多 LLM context 满了反而记不住 召回率与精度双降。
  10. RAG 改动必跑评估 ragas 四指标 + citation_rate + rejection_rate 任何下降 3% 拒绝上线。

总结

RAG 这事 很多人的直觉是 切 chunk embed 一下 LLM 拼 prompt 就完事 这其实是把 我能跑 LangChain demo 和 我能在生产用 RAG 扛住企业知识库 5000 份文档 100 用户日活 不幻觉不泄漏 混为一谈。前者是会调 API 后者是懂 RAG 工程。中间隔着的是 chunk 策略 embedding 选型 hybrid search 强制引用 prompt 拒答机制 评估回归 整整一套工程方法论。

从原型到生产 你需要做的事远不止 拼 demo。你要懂 不同文档怎么切 中文怎么 embed query 怎么改写 hybrid 怎么融合 reranker 怎么加 prompt 怎么强制引用 拒答怎么实现 评估怎么自动化。每一项单独看都不复杂 但它们组合在一起 才是一个能上线的企业 RAG。少任何一项 都可能让助手胡说八道泄漏数据 让客户当场炸毛。

我经常用一个比喻来理解 RAG 它有点像一个图书馆研究助理。文档是图书馆藏书 chunk 是给每本书贴的卡片摘要 embedding 是按主题给卡片归类 BM25 是按关键词给卡片建索引 检索是助理跑去书库取相关卡片 reranker 是助理再过一遍把最相关的几张抽出来 LLM 是助理基于这些卡片给你写答复 强制引用是助理必须标 这段来自第几本书第几页 拒答是助理找不到资料时诚实说 我们图书馆没这本书 不能瞎编。你不能因为有了 LLM 就觉得答案准 还要管卡片切得对不对 索引建得全不全 助理是否诚实标引用 找不到时是否敢拒答 这才是一整套图书馆研究服务。

这套架构最难的地方在于 它的复杂度在 demo 时几乎完全暴露不了。你 10 份 PDF demo 一切都顺 觉得 LangChain 真好用 觉得 RAG 真简单。但真正生产 5000 份文档 时间维度多 多文档冲突 用户问得刁钻 各种边角 case 你才发现 99% 的复杂度都在 那 1% 的工程细节里 chunk 切错关键内容断开 embedding 找不到 prompt 让模型瞎编 没拒答机制硬答 各种翻车。建议任何想做严肃 RAG 项目的团队 上线前一定要做 真实评估 200 条人工标的 query 跑全 pipeline 计算 ragas 四指标 + 引用率 + 拒答正确率 千万别只看 demo 那只是 RAG 的冰山一角 真正生产的复杂度藏在水下 90%。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

Redis 缓存设计完全指南:从一次"618 大促 5 分钟 Redis 内存爆掉雪崩损失 400 万"看懂为什么 set/get 远远不够

2026-5-24 16:16:55

技术教程

gRPC 微服务通信完全指南:从一次"长连接 hang 死整个支付服务雪崩 5 分钟"看懂为什么写完 proto 远远不够

2026-5-24 16:27:36

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索