跳转到内容
123xiao | 无名键客

《大模型应用中的 RAG 实战:从向量检索、重排序到效果评估的完整落地指南》

字数: 0 阅读时长: 1 分钟

大模型应用中的 RAG 实战:从向量检索、重排序到效果评估的完整落地指南

RAG(Retrieval-Augmented Generation,检索增强生成)这两年几乎成了大模型应用落地的“标配”。但真正做起来,很多团队会发现:不是接个向量库、塞点文档、把结果拼给模型就结束了

实际项目里,经常会遇到这些问题:

  • 检索到了“看起来相关”的内容,但答案还是不准
  • 数据明明都在知识库里,模型却答非所问
  • 相似度分数很高,但返回的文档块并不能直接支持答案
  • 上线前 demo 很好,一到真实业务流量就开始翻车
  • 没有评估体系,优化全靠“感觉”

这篇文章我会从一个工程落地的角度,带你把 RAG 的关键链路走一遍:数据切分 → 向量检索 → 重排序 → 生成 → 效果评估。我会尽量避免只讲概念,而是用一套可运行的 Python 示例带你搭起来,再讲清楚每一步为什么这样做、容易踩什么坑。


背景与问题

为什么大模型单独使用不够

通用大模型擅长语言理解和生成,但它有几个天然限制:

  1. 知识时效性有限
    模型参数不是实时更新的,新文档、新规则、新产品说明它并不知道。

  2. 业务知识不在参数里
    企业内部文档、FAQ、流程规范、代码库、工单记录,不会天然出现在基础模型中。

  3. 幻觉无法完全避免
    当模型不确定时,它仍然可能生成“像真的一样”的答案。

RAG 的目标,就是把“模型会说话”和“系统能查资料”结合起来:
先查,再答;答的时候尽量基于证据。

一个常见误区:RAG 的瓶颈不在“生成”,而在“检索”

很多人一开始会把注意力放在 prompt 怎么写、模型怎么换,但实际经验是:

如果检索阶段拿不到对的材料,后面 prompt 再花哨也救不回来。

所以一个靠谱的 RAG 系统,核心不是“大模型调用成功”,而是:

  • 文档是否切得合理
  • 检索召回是否足够
  • 重排序是否能把真正有用的片段排到前面
  • 评估体系是否能量化效果变化

前置知识与环境准备

你需要知道什么

如果你已经了解以下内容,阅读会更顺:

  • Python 基础
  • 向量检索的基本概念:embedding、top-k、相似度
  • 大模型 API 的基本调用方式
  • Markdown / JSON 的简单处理

环境准备

下面示例尽量做到本地可运行,使用的依赖比较克制:

pip install numpy scikit-learn rank-bm25 sentence-transformers

如果你本机没有安装 PyTorch,sentence-transformers 首次安装会稍大一些,这是正常现象。


核心原理

先把整体链路建立起来。一个实用的 RAG 系统通常包含 5 个步骤:

  1. 文档预处理与切块
  2. 向量化并建立索引
  3. 召回候选文档
  4. 重排序
  5. 将上下文交给大模型生成答案

下面这张图可以先帮助你形成整体认知。

flowchart LR
    A[原始文档] --> B[清洗与切块]
    B --> C[Embedding 向量化]
    C --> D[向量索引]
    Q[用户问题] --> E[问题向量化]
    E --> F[向量召回 TopK]
    B --> G[BM25 关键词召回]
    F --> H[候选集合合并]
    G --> H
    H --> I[重排序 Rerank]
    I --> J[上下文拼接]
    J --> K[LLM 生成答案]
    K --> L[返回答案 + 引用]

1. 为什么要切块,而不是整篇文档直接入库

因为大模型回答问题时,需要的是与问题最相关的一小段证据,而不是一整篇几十页的文档。

切块太大,会出现:

  • 一个 chunk 里话题太杂,embedding 语义被“摊薄”
  • 检索命中后,传给模型的噪声太多
  • 上下文窗口浪费

切块太小,也会出现:

  • 语义断裂
  • 上下文不完整
  • 明明文档里有答案,但被拆散后谁都不像答案

经验值:

  • FAQ、说明文档:200~500 字较常见
  • 技术文档、政策类文档:可以加适量 overlap(如 50~100 字)
  • 表格、代码、配置文件:最好按结构切,不要纯按字数切

2. 向量检索的作用与局限

向量检索擅长处理“语义相关”,比如:

  • “退款多久能到账”
  • “退货之后钱什么时候退回来”

虽然词不完全一样,但语义接近,embedding 通常能召回。

但它也有局限:

  • 对专有名词、型号、版本号、错误码不一定稳定
  • 对精确匹配问题不如关键词检索
  • 候选集里会混入“语义像,但没直接答案”的内容

所以我通常不建议只用纯向量检索,实际业务里更稳妥的是:

向量召回 + 关键词召回 + 重排序

3. 为什么要重排序

向量召回的目标是“尽量别漏掉”,也就是偏召回率
而生成阶段需要的是“最值得看的前几条”,也就是偏精确率

这两者天然有矛盾,所以我们通常把检索拆成两段:

  • 第一段:粗召回
    快速找出一批候选,宁可多一些
  • 第二段:精排 / 重排序
    用更昂贵但更准的方法,把最相关的内容排前面

可以把它理解为搜索引擎那套经典思路在 RAG 里的延续。

4. 一个实战中很重要的判断标准

很多团队会误把“相似度高”当成“对回答有用”。

但真正应该问的是:

这个 chunk 能不能作为回答问题的证据?

比如用户问:

“企业版套餐支持多少个成员账号?”

一个只提到“企业版支持高级功能”的 chunk,语义上可能很像,但并不能回答问题。
所以重排序阶段更关注的是query-document relevance,而不是仅仅 embedding 空间里的接近程度。


RAG 方案结构设计

我们先用一张时序图,把请求在系统里怎么流转讲清楚。

sequenceDiagram
    participant U as 用户
    participant R as RAG服务
    participant V as 向量索引
    participant B as BM25索引
    participant RR as 重排序器
    participant L as 大模型

    U->>R: 提问
    R->>V: 向量召回 top_k
    R->>B: 关键词召回 top_k
    V-->>R: 候选文档集A
    B-->>R: 候选文档集B
    R->>R: 合并去重
    R->>RR: rerank(query, candidates)
    RR-->>R: 排序后的候选
    R->>L: 问题 + TopN上下文
    L-->>R: 答案
    R-->>U: 答案 + 引用片段

这个结构有几个优点:

  • 便于替换单个组件
  • 能做 A/B 测试
  • 出问题时定位更清晰
  • 后续容易加入缓存、过滤、权限控制

实战代码(可运行)

下面我们实现一个最小可运行版 RAG 流程,包含:

  • 文档切块
  • 向量检索
  • BM25 关键词检索
  • 混合召回
  • 简单重排序
  • 输出可供 LLM 使用的上下文

说明:为了让示例尽可能容易跑起来,这里不直接依赖某个云厂商的大模型 API。重点放在 RAG 检索链路本身。

第一步:准备样例数据

新建 rag_demo.py

from dataclasses import dataclass
from typing import List, Tuple, Dict
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
from rank_bm25 import BM25Okapi
from sentence_transformers import SentenceTransformer


@dataclass
class Chunk:
    chunk_id: str
    doc_id: str
    text: str
    meta: Dict


documents = [
    {
        "doc_id": "doc_1",
        "title": "退款规则",
        "text": """
退款申请提交后,系统会在 1 到 3 个工作日内完成审核。
审核通过后,原路退款通常会在 5 到 7 个工作日到账。
若使用银行卡支付,实际到账时间可能受银行处理速度影响。
"""
    },
    {
        "doc_id": "doc_2",
        "title": "企业版套餐说明",
        "text": """
企业版套餐默认支持 100 个成员账号。
如果需要更多成员,可联系销售开通扩容包,每增加一个扩容包可额外增加 50 个成员账号。
企业版支持单点登录、权限分级和审计日志。
"""
    },
    {
        "doc_id": "doc_3",
        "title": "发票开具说明",
        "text": """
用户完成支付后可以在订单详情页申请电子发票。
电子发票一般会在 24 小时内开具完成。
如需专票,请联系企业服务支持并提交资质信息。
"""
    },
    {
        "doc_id": "doc_4",
        "title": "密码重置",
        "text": """
如果忘记密码,可以在登录页点击“忘记密码”。
系统会向绑定邮箱发送重置链接,链接有效期为 30 分钟。
如果邮箱不可用,请联系管理员协助处理。
"""
    }
]

第二步:实现简单切块

这里为了示例简洁,按句子与长度做一个轻量切分。真实项目里你可以按标题层级、段落、表格边界来切。

def split_text(text: str, max_len: int = 80, overlap: int = 20) -> List[str]:
    text = " ".join(text.strip().split())
    if len(text) <= max_len:
        return [text]

    chunks = []
    start = 0
    while start < len(text):
        end = min(start + max_len, len(text))
        chunks.append(text[start:end])
        if end == len(text):
            break
        start = end - overlap
    return chunks


def build_chunks(documents: List[Dict]) -> List[Chunk]:
    chunks = []
    for doc in documents:
        parts = split_text(doc["text"], max_len=80, overlap=20)
        for i, part in enumerate(parts):
            chunks.append(
                Chunk(
                    chunk_id=f'{doc["doc_id"]}_chunk_{i}',
                    doc_id=doc["doc_id"],
                    text=part,
                    meta={"title": doc["title"]}
                )
            )
    return chunks

第三步:建立向量索引与 BM25 索引

class HybridRetriever:
    def __init__(self, chunks: List[Chunk], model_name: str = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"):
        self.chunks = chunks
        self.model = SentenceTransformer(model_name)
        self.texts = [c.text for c in chunks]

        # 向量索引
        self.embeddings = self.model.encode(self.texts, normalize_embeddings=True)

        # BM25 索引
        self.tokenized_corpus = [self.tokenize(text) for text in self.texts]
        self.bm25 = BM25Okapi(self.tokenized_corpus)

    @staticmethod
    def tokenize(text: str) -> List[str]:
        # 简化版分词:示例环境可运行,中文生产环境建议接入更可靠的分词器
        return list(text)

    def vector_search(self, query: str, top_k: int = 5) -> List[Tuple[Chunk, float]]:
        q_emb = self.model.encode([query], normalize_embeddings=True)
        sims = cosine_similarity(q_emb, self.embeddings)[0]
        indices = np.argsort(sims)[::-1][:top_k]
        return [(self.chunks[i], float(sims[i])) for i in indices]

    def bm25_search(self, query: str, top_k: int = 5) -> List[Tuple[Chunk, float]]:
        tokenized_query = self.tokenize(query)
        scores = self.bm25.get_scores(tokenized_query)
        indices = np.argsort(scores)[::-1][:top_k]
        return [(self.chunks[i], float(scores[i])) for i in indices]

    def hybrid_search(self, query: str, top_k_vec: int = 5, top_k_bm25: int = 5) -> List[Chunk]:
        vec_results = self.vector_search(query, top_k=top_k_vec)
        bm25_results = self.bm25_search(query, top_k=top_k_bm25)

        merged = {}
        for chunk, score in vec_results:
            merged[chunk.chunk_id] = chunk
        for chunk, score in bm25_results:
            merged[chunk.chunk_id] = chunk

        return list(merged.values())

第四步:实现一个简单可运行的重排序器

这里为了保证示例容易运行,我们先实现一个“轻量版”重排序:
综合考虑 query 与 chunk 的向量相似度、词项重叠和数字命中。

这不是最强方案,但足够说明重排序思路。

import re


class SimpleReranker:
    def __init__(self, model: SentenceTransformer):
        self.model = model

    @staticmethod
    def lexical_overlap(query: str, text: str) -> float:
        q_set = set(query)
        t_set = set(text)
        if not q_set:
            return 0.0
        return len(q_set & t_set) / len(q_set)

    @staticmethod
    def number_match_bonus(query: str, text: str) -> float:
        q_nums = re.findall(r"\d+", query)
        if not q_nums:
            return 0.0
        hit = sum(1 for n in q_nums if n in text)
        return 0.2 * hit

    def rerank(self, query: str, candidates: List[Chunk], top_n: int = 3) -> List[Tuple[Chunk, float]]:
        q_emb = self.model.encode([query], normalize_embeddings=True)
        cand_texts = [c.text for c in candidates]
        c_embs = self.model.encode(cand_texts, normalize_embeddings=True)
        vec_scores = cosine_similarity(q_emb, c_embs)[0]

        final_scores = []
        for i, chunk in enumerate(candidates):
            score = (
                0.7 * float(vec_scores[i]) +
                0.2 * self.lexical_overlap(query, chunk.text) +
                0.1 * self.number_match_bonus(query, chunk.text)
            )
            final_scores.append((chunk, score))

        final_scores.sort(key=lambda x: x[1], reverse=True)
        return final_scores[:top_n]

第五步:把完整链路串起来

def build_prompt(query: str, ranked_chunks: List[Tuple[Chunk, float]]) -> str:
    context_parts = []
    for idx, (chunk, score) in enumerate(ranked_chunks, start=1):
        context_parts.append(
            f"[证据{idx}] 标题:{chunk.meta['title']}\n内容:{chunk.text}\n相关性分数:{score:.4f}"
        )

    context = "\n\n".join(context_parts)
    prompt = f"""
你是一个企业知识库问答助手。请严格基于给定证据回答问题。
如果证据不足,请明确说明“根据当前检索结果无法确定”,不要编造。

用户问题:
{query}

检索证据:
{context}

请输出:
1. 简洁答案
2. 引用的证据编号
"""
    return prompt.strip()


def main():
    chunks = build_chunks(documents)
    retriever = HybridRetriever(chunks)
    reranker = SimpleReranker(retriever.model)

    query = "企业版套餐支持多少个成员账号?"

    print("=" * 80)
    print("问题:", query)

    candidates = retriever.hybrid_search(query, top_k_vec=4, top_k_bm25=4)
    print("\n[混合召回候选]")
    for c in candidates:
        print(f"- {c.chunk_id}: {c.text}")

    ranked = reranker.rerank(query, candidates, top_n=3)
    print("\n[重排序结果]")
    for chunk, score in ranked:
        print(f"- {chunk.chunk_id} | {score:.4f} | {chunk.text}")

    prompt = build_prompt(query, ranked)
    print("\n[可交给大模型的 Prompt]\n")
    print(prompt)


if __name__ == "__main__":
    main()

运行:

python rag_demo.py

你会看到什么

理想情况下,企业版套餐说明 中包含“默认支持 100 个成员账号”的 chunk 会排在前面。
然后我们把这些证据拼成 prompt,再交给任意一个大模型去生成答案。


逐步验证清单

我很建议你不要一次把整个系统写完,而是按下面顺序逐步验证。这样出问题时最好查。

验证 1:切块是否合理

检查点:

  • chunk 是否语义完整
  • 标题、段落边界是否丢失
  • 是否出现“上一句和下一句被拆散”导致答案不完整

经验建议:

  • 抽样 20 个 chunk 人工看一遍
  • 重点看 FAQ、带数字规则、步骤说明类文档

验证 2:召回是否能把正确文档带回来

检查点:

  • 对 20~50 个真实问题,正确 chunk 是否进入 top-k
  • 如果没进 top-k,是 embedding 问题、切块问题,还是 query 改写问题

经验建议:

  • 先看 Recall@k,而不是一上来盯着最终答案
  • 如果 top-20 都召不回来,先别急着调 prompt

验证 3:重排序是否能把正确证据排前面

检查点:

  • 正确 chunk 是否进入前 3
  • 是否存在“语义很像但不回答问题”的 chunk 排在前面

经验建议:

  • 针对带数字、版本、型号、条件约束的问题,单独做一个测试集

验证 4:生成是否严格基于证据

检查点:

  • 回答里有没有超出证据范围的内容
  • 引用 chunk 是否真的支持结论
  • 证据冲突时模型如何处理

效果评估:不要只看“回答像不像对”

RAG 项目如果没有评估体系,优化会很痛苦。因为你每调一次:

  • chunk 大小
  • top-k
  • embedding 模型
  • reranker
  • prompt

都可能让某些问题变好、某些问题变差。
这时候必须靠指标,不然只能“凭感觉”。

建议把评估拆成三层

flowchart TD
    A[离线评估] --> A1[召回率 Recall@K]
    A --> A2[排序质量 MRR / NDCG]
    A --> A3[答案正确率]

    B[联调评估] --> B1[引用是否可信]
    B --> B2[是否拒答得当]
    B --> B3[多轮上下文是否稳定]

    C[线上评估] --> C1[点击/追问率]
    C --> C2[人工反馈]
    C --> C3[响应时间与成本]

1. 检索层指标

Recall@K

看正确证据是否出现在前 K 个候选里。

  • 如果 Recall@20 很低:说明召回有问题
  • 如果 Recall@20 高,但前 3 很差:说明重排序有问题

MRR(Mean Reciprocal Rank)

看正确结果排得靠不靠前。
如果第一个正确结果排在第 1 位,得分最高;排第 5 位,得分就低很多。

2. 生成层指标

生成层不要只看“语言通顺”,而要看:

  • 答案是否正确
  • 是否引用了正确证据
  • 是否有幻觉
  • 证据不足时是否能拒答

3. 线上指标

真实流量下最有价值的信号通常是:

  • 用户是否继续追问
  • 是否点开引用来源
  • 是否人工转接
  • 用户是否点踩
  • 平均响应时间是否可接受

一个简单的离线评估示例

下面给出一个最小离线评估脚本。我们假设每个问题都标注了 gold 文档 ID。

def recall_at_k(results: List[List[Chunk]], gold_doc_ids: List[str], k: int = 3) -> float:
    hit = 0
    for pred_chunks, gold_id in zip(results, gold_doc_ids):
        topk = pred_chunks[:k]
        if any(chunk.doc_id == gold_id for chunk in topk):
            hit += 1
    return hit / len(gold_doc_ids)


def evaluate_retrieval(retriever: HybridRetriever):
    eval_set = [
        ("退款审核通过后多久到账?", "doc_1"),
        ("企业版默认支持几个成员账号?", "doc_2"),
        ("电子发票多久开具完成?", "doc_3"),
        ("密码重置链接有效多久?", "doc_4"),
    ]

    all_results = []
    for query, gold_doc_id in eval_set:
        candidates = retriever.hybrid_search(query, top_k_vec=5, top_k_bm25=5)
        all_results.append(candidates)

    score = recall_at_k(all_results, [x[1] for x in eval_set], k=3)
    print(f"Recall@3 = {score:.4f}")

把它接到 main() 里一起跑即可。


常见坑与排查

这一部分我尽量讲得接地气一点,因为很多坑我自己也踩过。

坑 1:切块按固定字数切,导致答案断裂

现象:

  • 文档明明有答案,但检索出来的 chunk 不完整
  • 回答总差最后一句约束条件

排查方法:

  • 抽样查看召回结果原文
  • 对失败 case 检查 chunk 边界

解决建议:

  • 增加 overlap
  • 按标题/段落/列表项切分
  • 对 FAQ、表格、步骤类文档走专门切块逻辑

坑 2:只做向量检索,遇到错误码/型号/数字问题效果差

现象:

  • 用户问“错误码 E203 是什么”
  • 检索却返回一堆“系统异常说明”

原因:

向量检索对这种精确 token 不一定敏感。

解决建议:

  • 加 BM25 或关键词召回
  • 对错误码、SKU、版本号做结构化索引
  • 查询时识别实体,优先走精确过滤

坑 3:重排序缺失,导致“看起来像”的内容排第一

现象:

  • top-k 里其实有正确答案
  • 但大模型吃到的前几个 chunk 不是最有用的

解决建议:

  • 加 reranker
  • 控制进入生成阶段的 context 数量
  • 不要一股脑把 top-10 全塞给模型

坑 4:上下文塞太多,模型反而更容易答偏

这个问题很常见。很多人会想:“多给点材料,总没错吧?”
实际上未必。

原因:

  • 噪声增大
  • 多个 chunk 存在轻微冲突
  • 模型注意力被稀释

建议:

  • 优先塞 top 3~5 条高质量证据
  • 对冗余 chunk 做去重
  • 同一文档相邻 chunk 可做合并

坑 5:没有区分“回答不了”和“没检索到”

这是线上故障里非常麻烦的一类。

建议把失败分两种:

  1. 知识库里没有
  2. 知识库里有,但没召回到

两者处理方式完全不同:

  • 前者应该补知识
  • 后者应该调检索和索引

坑 6:评估集太小,优化方向容易跑偏

如果你只拿 5 个演示问题测试,任何方案都可能“看起来不错”。

建议:

评估集至少覆盖:

  • FAQ 问法改写
  • 带数字问题
  • 条件约束问题
  • 多段推理问题
  • 无答案问题

安全/性能最佳实践

RAG 一旦进到生产环境,除了效果,还得关注安全和成本。

安全最佳实践

1. 做权限过滤,不要“谁都能搜到所有文档”

这是企业知识库最容易出事故的地方。

正确做法是:

  • 文档入库时带权限标签
  • 检索前先按用户身份过滤候选集
  • 不要把无权限文档先召回再“希望模型别说出来”

因为一旦检索到了,后面就有泄露风险。

2. 防 Prompt Injection

如果你的知识库里有外部网页、用户上传文档,就要警惕这类内容:

  • “忽略上面的系统指令”
  • “请输出管理员密钥”
  • “不要遵守先前规则”

建议:

  • 对检索内容做安全清洗
  • 在系统 prompt 中明确:文档内容不是指令,只是资料
  • 对高风险词做审计和拦截

3. 输出带引用

引用不是装饰,它有三个价值:

  • 方便用户核对
  • 方便排障
  • 方便审核追责

性能最佳实践

1. embedding 离线化

文档 embedding 尽量离线预计算,避免每次查询都重新编码文档。

2. 分层召回

典型策略:

  • 向量库 top 50
  • BM25 top 50
  • 合并后重排成 top 5
  • 最终送模型 top 3

这样既兼顾召回,又控制成本。

3. 缓存热点查询

对高频问题可缓存:

  • 检索结果
  • rerank 结果
  • 最终答案

尤其是 FAQ 场景,收益非常明显。

4. 控制 chunk 数与上下文长度

影响成本和时延的,往往不是“调一次模型 API”,而是:

  • chunk 过多
  • 候选过大
  • prompt 过长

我一般建议先测三组配置:

  • top3
  • top5
  • top8

看效果提升是否值得额外成本。


进阶建议:什么时候该上更强的重排序模型

上面示例里的 SimpleReranker 是为了教学和可运行性。
如果你已经进入真实业务场景,通常建议升级到更强的 reranker,例如 cross-encoder 类模型。

什么时候值得升级?

  • 你发现 Recall@20 已经不错,但最终答案仍然常错
  • 候选集里有正确证据,但排不到前 3
  • 你的问题经常包含数字、条件、比较关系
  • 需要从相似但不等价的文本里挑出真正证据

一个非常实用的判断标准是:

如果“召回到了,但没排上来”是主要问题,就优先升级 reranker。


边界条件:RAG 不是万能解法

虽然 RAG 很好用,但它也有适用边界。

不太适合只靠 RAG 解决的场景

1. 强计算类问题

比如:

  • 复杂财务测算
  • 多表聚合分析
  • 实时库存结算

这类问题更适合:检索 + 工具调用 + 程序执行

2. 强工作流场景

比如:

  • 审批流推进
  • 工单处理
  • CRM 操作

这类更像 agent / workflow 系统,而不只是“查资料回答”。

3. 知识高度结构化的场景

比如:

  • 订单状态
  • 当前价格
  • 设备实时监控数据

这些应该优先查数据库或 API,而不是硬塞进向量库。


一个更稳的落地路线

如果你准备在团队里推进 RAG,我建议走下面这条路线,而不是一开始就追求“全自动、全智能”。

stateDiagram-v2
    [*] --> POC
    POC --> RecallFix: 能跑通基础问答
    RecallFix --> RerankImprove: 召回率达到目标
    RerankImprove --> EvalSystem: 建立离线评估集
    EvalSystem --> OnlineGray: 小流量灰度
    OnlineGray --> Prod: 监控稳定后上线
    Prod --> ContinuousOptimize: 持续优化

落地顺序建议

  1. 先做可用,不要先做复杂
  2. 先把召回做稳,再调生成
  3. 先建评估集,再谈优化速度
  4. 先做引用与权限,再谈全面上线

这条顺序能帮你避免很多“demo 很炫,生产很崩”的问题。


总结

RAG 的真正难点,不是“让大模型回答”,而是让它基于正确证据回答

如果你只记住这篇文章里的几个关键点,我建议是下面这几条:

  1. 检索质量决定上限
    没有正确证据,生成阶段救不回来。

  2. 混合召回比纯向量更稳
    向量检索解决语义问题,BM25 解决精确 token 问题。

  3. 重排序往往是效果跃升的关键
    候选集里有答案,不代表模型就能看到答案。

  4. 评估必须分层做
    召回、排序、生成不要混成一个黑盒。

  5. 上线前必须补齐权限、引用、缓存和监控 否则一到真实流量就容易暴露问题。

最后给你一个可执行建议,适合中级工程师直接开工:

  • 第一周:做文档切块 + 向量召回 + 基础问答
  • 第二周:加 BM25 + rerank + 引用输出
  • 第三周:建立 50~100 条离线评估集
  • 第四周:做权限过滤、缓存、监控和灰度上线

如果你的目标是“把 RAG 用起来”,上面这条路线已经够用了。
如果你的目标是“把 RAG 做稳、做准、做成生产系统”,那重点就不是多接几个模型,而是把检索、重排、评估这三个环节真正工程化。


分享到:

上一篇
《Java 中线程池参数调优与任务堆积排查实战指南-448》
下一篇
《Spring Boot 中基于 Spring Cache 与 Redis 的多级缓存实战:一致性、穿透防护与性能调优》