RAG 完全指南:从一次"AI 答得头头是道却全是错的"看懂检索增强生成

2024 年我做一个公司内部知识库问答 AI,把几百篇文档喂进去。Demo 时答得头头是道,推广后却频繁出问题:答去年的旧标准、把文档里明明有的内容答成"未找到"、把两份产品参数掺一起编出根本不存在的配置。我以为是模型不够强,换强模型问题照旧。盯日志才反应过来:RAG 的回答质量根本不取决于生成那一步模型多强,而取决于检索那一步有没有捞对文档——模型只能基于你塞给它的上下文作答,捞来错的旧的不相关的资料,再强的模型也只会基于错误材料一本正经地编错误答案。本文把 RAG 从头梳理:它解决 LLM 知识截止和不知道私有数据这两个先天缺陷、本质是给模型一场开卷考试;完整流程分索引阶段(切块、算 embedding、入库)和查询阶段(检索、拼 prompt、生成);最被低估的切块——切大塞噪声切小丢上下文、固定长度要带重叠窗口、优先按结构切;检索的真相——向量相似不等于内容相关、top-k 不是越大越好、必须设相似度阈值、要叠加关键词做混合检索;两阶段检索——embedding 快速召回再用 cross-encoder 重排精选;最后四个工程坑——检索为空必须拒答别让模型用旧知识硬编、答案要可溯源、索引和查询必须同一个 embedding 模型、RAG 要分开评估检索命中率和生成质量。核心一句:Generation 是模型的事,Retrieval 从头到尾是你的事。

2024 年我做一个公司内部知识库问答 AI:把几百篇产品文档、规章制度喂进去,想让员工随便问、AI 照着文档答。Demo 那天我请同事问了几个我提前准备好的问题,答得头头是道,领导很满意,当场拍板推广。推广开之后,反馈陆续来了:有人问"差旅住宿一晚最多报多少",AI 信誓旦旦给了个数,结果是去年的旧标准;有人问一个文档里明明白白写着的流程,AI 却说"未找到相关信息";最离谱的一次,AI 把两份不同产品的参数掺在一起,编出一个根本不存在的配置,还答得有鼻子有眼。我一开始以为是模型不够强,换了个更强的模型,问题照旧。盯着请求日志看了很久才反应过来:RAG 的回答质量,根本不取决于"生成"那一步的模型有多强,而取决于"检索"那一步到底有没有捞对文档。模型只能基于你塞给它的上下文作答——你检索时捞来的是错的、旧的、不相关的资料,再强的模型也只会"基于错误的材料,一本正经地编出一个错误的答案"。我原来的认知是"RAG 就是把文档丢给 AI,它自己会查、会答",而真相恰恰相反:RAG 系统里真正难、真正决定成败的,是检索这一环;生成,不过是把检索结果复述出来。那次之后我才认真把 RAG 从头搞明白。这篇文章就把它梳理一遍:RAG 到底解决什么问题、完整流程是怎样的、文档怎么切块、检索为什么会捞错、重排在干什么,以及把知识库问答真正做准要避开的那些坑。

问题背景

先把那次事故的现象和我的误判讲清楚,后面所有的设计都是冲着纠正这个误判去的。

现象:用 RAG 做内部知识库问答,Demo 一切正常;推广后频繁出现答旧数据、把文档里有的内容答成"未找到"、混淆不同文档、甚至编造出文档里根本不存在的内容。换更强的模型也不见好。

我当时的错误认知:"RAG 就是把文档喂给 AI,模型自己会从里面查、会答;答不好,就是模型不够强,换个强的就行。"

真相:RAG = 检索(Retrieval)+ 生成(Generation),模型只能基于检索到的上下文作答。一次回答的质量上限,是由检索质量决定的——生成这一步无法"修复"一次糟糕的检索。检索捞错了,模型要么基于错误材料编答案,要么(更糟)绕开材料、用自己过时的训练知识硬答。换更强的模型,完全不解决检索这一环的问题。

要把 RAG 做对,需要几块认知:

  • RAG 到底解决 LLM 的什么先天缺陷,它的完整流程分哪两个阶段;
  • 文档怎么切块,切大、切小各自会出什么问题;
  • 检索为什么"向量相似"不等于"内容相关";
  • 重排(rerank)在干什么,为什么 top-k 召回之后还要再排一次;
  • 检索不到答案时,怎么让模型"老实说不知道",而不是编。

一、RAG 是什么:给大模型一场"开卷考试"

要理解为什么需要 RAG,先得看清大模型有两个绕不开的先天缺陷。

第一,它的知识有截止日期。模型是在某个时间点之前的数据上训练出来的,这个时间点之后发生的事、更新的标准,它一概不知道。第二,它不知道你的私有数据。你公司的内部文档、规章制度、产品手册,从来没进过它的训练集——你直接问,它要么说不知道,要么就凭一个"大概像那么回事"的印象编一个,这就是幻觉。

要让模型能回答"训练集之外"的问题,有两条路。一条是微调:把新知识重新训练进模型的权重里——成本高,而且知识一更新就得重训,不现实。另一条就是 RAG(Retrieval-Augmented Generation,检索增强生成):不动模型,而是把知识放在一个外部知识库里;每次用户提问时,先从知识库里检索出相关的资料片段,连同问题一起拼成 prompt 交给模型,让它照着这份资料来回答。

这本质上是把模型的角色从"闭卷考试"变成了"开卷考试"——它不再靠记忆答题,而是做阅读理解,基于你递给它的材料作答。这正是 RAG 的强项:知识存在外部库里,随时能更新,答案还能溯源到具体文档。但这也是它的命门——你递错了材料,它就照着错材料答错。所以 RAG 做得好不好,关键全在"递什么材料"这件事上。

二、完整流程:索引阶段与查询阶段

一个 RAG 系统,干活分两个阶段,想清楚这两个阶段,后面所有的优化才有地方落。

索引阶段(离线,事先做一次):把知识库里的文档切块(切成一小段一小段),给每一块算出一个 embedding 向量,然后把"文本块 + 它的向量"一起存进向量库。embedding 是关键——它把一段文本映射成一个向量,语义相近的文本,算出来的向量在空间里也相近。

查询阶段(在线,每次提问都走一遍):把用户的问题也算成一个 embedding 向量,拿它去向量库里找向量最接近的若干个文本块(这就是"检索"),再把这些块拼进 prompt,交给大模型生成答案。

下面先把索引和检索这两个最核心的能力用代码实现出来。这里不依赖任何现成的向量数据库,用 numpy 手写一个最小可用的版本,把原理看清楚:

from sentence_transformers import SentenceTransformer
import numpy as np

# 加载 embedding 模型:把一段文本映射成一个向量,语义相近的文本向量也相近
embedder = SentenceTransformer("BAAI/bge-small-zh-v1.5")


class VectorStore:
    """最小可用的向量库:保存"文本块 + 它的向量",支持按相似度检索。"""

    def __init__(self):
        self.chunks: list[str] = []          # 原始文本块
        self.vectors = None                  # 每块对应的归一化向量

    def add(self, chunks: list[str]) -> None:
        # 入库:把每个文本块算成向量,和原文一起存下来
        vecs = embedder.encode(chunks, normalize_embeddings=True)
        self.chunks.extend(chunks)
        self.vectors = (vecs if self.vectors is None
                        else np.vstack([self.vectors, vecs]))

    def search(self, query: str, top_k: int = 4) -> list[tuple[str, float]]:
        # 检索:把问题也算成向量,找库里和它最接近的 top_k 个块
        q = embedder.encode(query, normalize_embeddings=True)
        scores = self.vectors @ q            # 向量已归一化,点积即余弦相似度
        idx = np.argsort(scores)[::-1][:top_k]
        return [(self.chunks[i], float(scores[i])) for i in idx]

有了这个向量库,一个最朴素的 RAG 流程就能跑起来了——索引阶段把文档入库,查询阶段检索出相关片段:

# 索引阶段(离线做一次):把文档入库
docs = [
    "报销额度:2024 年起,差旅住宿每晚上限 500 元。",
    "请假流程:事假需提前一天在 OA 提交,由直属主管审批。",
    "报销流程:发票须在消费后 30 天内提交至财务部。",
]
store = VectorStore()
store.add(docs)

# 查询阶段(在线,每次提问都走一遍):检索出相关片段
hits = store.search("住宿一晚最多能报多少钱", top_k=2)
for text, score in hits:
    print(f"[相似度 {score:.3f}] {text}")

# 检索到的片段,接下来会和用户问题一起拼成 prompt 交给大模型作答。
# 注意:用户问的是"住宿报多少",文档里写的是"差旅住宿每晚上限" ——
# 用词并不完全一样,但 embedding 能匹配上,这正是向量检索的价值。

三、切块:RAG 里最被低估的一步

很多人做 RAG,把精力都花在选模型、调 prompt 上,却草草地把文档"每 1000 字切一刀"了事。其实切块(chunking)是直接决定检索质量的一步,它被严重低估了。

为什么切块这么关键?因为检索的最小单位就是"块"——检索捞回来的、最终塞进 prompt 的,是一个个块。块切得好不好,直接决定了"捞回来的东西到底相不相关、完不完整"。

切太大有问题:一个大块里往往混着好几个主题,其中只有一句和问题相关,其余全是噪声。这个块的向量是整段文本的"平均语义",会被无关内容稀释,检索精度下降;就算检索到了,塞进 prompt 的也是一大坨,真正有用的那句被埋在里面。切太小也有问题:一个句子被单独切成一块,它脱离了上下文——"上限是 500 元"这句话,脱离了"差旅住宿"这个上文,就成了一句没头没脑、检索也对不上、模型也用不了的话。

所以切块要在"块够小、噪声少"和"块够完整、有上下文"之间找平衡。最基础的做法是固定长度切块,但一定要带上重叠窗口(overlap)——让相邻两块有一小段重叠,避免一句完整的话正好被切点拦腰斩断:

def chunk_text(text: str, size: int = 300, overlap: int = 50) -> list[str]:
    """按固定长度切块,相邻块之间保留 overlap 个字的重叠。
    重叠是为了防止一句话被切点拦腰斩断,导致两边都丢失语义。"""
    chunks = []
    start = 0
    while start < len(text):
        end = start + size
        chunks.append(text[start:end])
        if end >= len(text):
            break
        start = end - overlap          # 回退 overlap 个字,和上一块重叠
    return chunks

固定长度切块简单,但它有个硬伤:它完全不看内容,可能正好在一个段落的中间、一句话的中间下刀。更好的做法是按文档结构切块——优先在段落、标题这些自然的语义边界处断开,让每一块自身的语义尽量完整:

import re

def chunk_by_structure(text: str, max_size: int = 500) -> list[str]:
    """按文档结构切块:优先在段落等自然边界处断开,
    而不是机械地数字符 —— 这样每一块的语义更完整。"""
    # 先按空行把文本切成段落
    paragraphs = [p.strip() for p in re.split(r"\n\s*\n", text) if p.strip()]

    chunks, buf = [], ""
    for para in paragraphs:
        # 当前缓冲再加这一段会超长,就先把缓冲落成一块
        if buf and len(buf) + len(para) > max_size:
            chunks.append(buf)
            buf = para
        else:
            buf = f"{buf}\n{para}" if buf else para
    if buf:
        chunks.append(buf)
    return chunks

实践中,按结构切块是首选;固定长度 + 重叠则适合那些没有清晰结构的纯文本。无论用哪种,切完都建议抽查几块看看——一块里是不是一个完整的意思,是判断切块好坏最直接的标准。

四、检索:向量"相似"不等于内容"相关"

块切好、入库了,接下来是查询阶段的核心:检索。开头那场事故里"答旧数据""未找到""混淆文档",根子大多都在这一步。这里要破除一个想当然的认知:向量检索返回的"相似",不等于你要的"相关"。

第一个要调的是 top-k——检索返回几个候选块。它是个两难:k 太小,真正相关的块可能没被召回(漏召回);k 太大,会把一堆只是"沾点边"的不相关块也塞进上下文,变成干扰模型的噪声。k 不是越大越好。

第二个、也是更要命的一点:库里压根没有答案的问题,检索依然会返回 k 个结果。向量检索的逻辑是"找最接近的",哪怕全库都和问题无关,它也会把"无关里相对没那么无关"的那几个返回给你——而且分数很低。如果你不管分数照单全收,这些低分噪声进了 prompt,模型就被带着基于无关材料硬答。所以必须设一个相似度阈值:低于阈值的结果直接丢弃,丢完要是一个不剩,就说明"这个问题库里没有答案"——这正是后面"拒答"的依据。

def search_with_threshold(store: VectorStore, query: str,
                          top_k: int = 8, min_score: float = 0.35
                          ) -> list[tuple[str, float]]:
    """带阈值的检索:top_k 只是"取几个候选",
    真正决定能不能用的是相似度阈值 —— 分数太低的宁可不要。"""
    hits = store.search(query, top_k=top_k)
    # 过滤掉相似度低于阈值的:它们大概率和问题无关
    good = [(text, score) for text, score in hits if score >= min_score]
    return good            # 若返回空列表,说明库里没有能回答这个问题的资料

第三个坑是向量检索的盲区:它擅长"语义相近",但对专有名词、型号、编号这种"必须精确匹配"的东西不敏感——问"X100 的功率",它可能把"X200 的功率"也算得很相似。对策是再加一路关键词检索,和向量检索做混合:向量负责"意思相近",关键词负责"专名精确命中",两路结果取并集,互补:

def hybrid_search(store: VectorStore, query: str, top_k: int = 6
                  ) -> list[str]:
    """混合检索:向量语义检索 + 关键词命中,两路结果取并集。
    向量擅长"语义相近",关键词擅长"专有名词精确命中",互补。"""
    # 第一路:向量语义检索
    vec_hits = [text for text, _ in store.search(query, top_k=top_k)]

    # 第二路:关键词检索 —— 块里包含问题中的词就算命中
    keywords = [w for w in query.split() if len(w) >= 2]
    kw_hits = [c for c in store.chunks
               if any(kw in c for kw in keywords)]

    # 合并去重,保持先后顺序
    seen, merged = set(), []
    for text in vec_hits + kw_hits:
        if text not in seen:
            seen.add(text)
            merged.append(text)
    return merged[:top_k]

五、重排与上下文构建:先粗筛,再精排

检索召回了一批候选,但它们够准吗?这里要理解 embedding 检索的一个固有局限:它是双塔模型——问题和文档是各自独立编码成向量的,模型在编码文档时根本不知道用户会问什么。这种方式速度快(文档向量能预先算好),但精度有限,它衡量的是"泛泛的语义接近",未必是"精确地回答了这个问题"。

所以业界的标准做法是两阶段检索:第一阶段用 embedding 检索,快,负责从海量文档里召回一批候选(比如 top 8 到 20);第二阶段用 cross-encoder 重排模型(reranker),把"问题 + 某个候选文档"拼在一起送进模型打一个相关性分数——它能看到问题和文档的交互,判断精准得多,但慢,所以只用它给少量候选做精排。一句话:embedding 负责"粗筛召回",reranker 负责"精排选优"。

from sentence_transformers import CrossEncoder

# 重排模型:输入(问题, 文档)一对,直接输出一个相关性分数。
# 它把问题和文档拼一起编码,比双塔 embedding 准,但更慢 ——
# 所以只拿它给"少量候选"做精排,不拿它做全库检索。
reranker = CrossEncoder("BAAI/bge-reranker-base")


def rerank(query: str, candidates: list[str], top_n: int = 3
           ) -> list[str]:
    """重排:对召回的候选逐一精确打分,取最相关的 top_n。"""
    if not candidates:
        return []
    pairs = [(query, doc) for doc in candidates]
    scores = reranker.predict(pairs)
    # 按精排分数从高到低排序
    ranked = sorted(zip(candidates, scores),
                    key=lambda x: x[1], reverse=True)
    return [doc for doc, _ in ranked[:top_n]]

精排出最相关的几段后,最后一步是把它们拼成上下文。这里有两个细节:其一,给每段编号,这样可以要求模型在回答里引用"来源[1]",答案就变得可溯源、可核对;其二,呼应"lost in the middle"——模型对 prompt 首尾的信息最敏感,所以把"问题本身"放在末尾再强调一次,别让它淹没在中间:

def build_context(query: str, docs: list[str]) -> str:
    """把检索到的片段拼成上下文。给每段编号,是为了让模型
    在回答里能引用"来源[1]",从而让答案可溯源、可核对。"""
    blocks = []
    for i, doc in enumerate(docs, 1):
        blocks.append(f"[{i}] {doc}")
    context = "\n\n".join(blocks)

    # 把问题放在末尾再说一次:模型对 prompt 首尾最敏感,别让它埋在中间
    return (f"已知资料:\n{context}\n\n"
            f"请仅依据上面的资料回答。问题:{query}")

六、工程坑:拒答、溯源、模型一致、评估

把 RAG 真正放进生产,还有四个绕不开的工程坑。每一个都直接影响答案能不能信。

坑 1:检索不到时,必须让模型拒答。这是 RAG 最危险的地方。最坏的情况不是"答错",而是"检索为空时,模型绕开资料、用自己过时的训练知识一本正经地编"——开头那个"答了去年旧标准"就是这么来的。对策是双保险:一是在 system prompt 里硬性约束"只能依据资料、资料里没有就明确说不知道";二是在代码里,检索结果为空时直接返回拒答,根本不进生成环节。

坑 2:答案要可溯源。给检索片段编号、要求模型在回答里标注引用了哪条,用户就能点回原文核对。一个不能溯源的 RAG 答案,用户没法判断它是真有依据还是编的,可信度大打折扣。

坑 3:embedding 模型,索引和查询必须用同一个。检索的本质是比较"文档向量"和"问题向量",这两个向量必须出自同一个模型、同一套向量空间,比较才有意义。换了 embedding 模型,整个向量库必须全量重建。还有个细节:有些模型(如 bge 系列)要求查询文本加一个特定前缀指令,文档则不加——这个不一致会悄悄拉低检索效果。

坑 4:RAG 必须评估,而且要分开评估检索和生成。回答不好时,要先定位是"检索没捞到"还是"捞到了但生成没答好"。最该先看的是检索命中率——准备一批测试问题,每条标注答案应来自哪个文档,看检索结果里有没有包含它。命中率低,说明问题出在检索,这时反复改 prompt 是徒劳的,prompt 再好也救不回没被检索到的内容。

先把"坑 1"落地——一个带拒答约束的完整 RAG 问答流程,串起前面所有环节:

SYSTEM_PROMPT = (
    "你是知识库问答助手。只能依据用户给出的「已知资料」回答,"
    "禁止使用资料之外的任何知识。若资料中没有相关信息,"
    "必须直接回答「根据现有资料无法回答此问题」,不要猜测、不要编造。"
)


def answer(client, store: VectorStore, query: str) -> str:
    """完整 RAG 流程:检索 -> 阈值过滤 -> 重排 -> 拼接 -> 生成。"""
    # 1) 检索 + 阈值过滤:把明显不相关的候选先挡掉
    hits = search_with_threshold(store, query, top_k=8, min_score=0.35)

    # 2) 检索为空 —— 直接拒答,绝不进入生成环节让模型自由发挥
    if not hits:
        return "根据现有资料无法回答此问题。"

    # 3) 重排,从候选里精选出最相关的几段
    top_docs = rerank(query, [text for text, _ in hits], top_n=3)

    # 4) 拼成带编号的上下文,交给模型作答
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": build_context(query, top_docs)},
        ],
    )
    return resp.choices[0].message.content

再把"坑 4"落地——一个评估检索命中率的函数,它是 RAG 调优的指南针:

def evaluate_retrieval(store: VectorStore,
                       testset: list[dict]) -> float:
    """评估检索命中率:每条测试问题都标注了答案应来自哪个文档,
    检查检索结果里有没有包含它。命中率上不去,RAG 一定答不好。"""
    hit = 0
    for case in testset:
        results = store.search(case["question"], top_k=4)
        retrieved = [text for text, _ in results]
        # 标准答案所在的文档,有没有出现在检索结果里
        if any(case["expect"] in text for text in retrieved):
            hit += 1
    rate = hit / len(testset)
    print(f"检索命中率:{hit}/{len(testset)} = {rate:.1%}")
    return rate


# RAG 调优要"先治检索":命中率低时,先去调切块、调检索策略,
# 而不是反复改 prompt —— prompt 再好,也救不回没被检索到的内容。

下面这张图把 RAG 从文档索引到生成答案的完整链路串起来,注意"检索为空就拒答"那个分叉:

关键概念速查

概念 / 手段 说明
RAG 检索增强生成;不改模型,提问时检索相关资料拼进 prompt 让模型开卷作答
索引阶段 离线:文档切块、算 embedding、连同原文存入向量库
查询阶段 在线:问题算 embedding、检索、拼上下文、生成答案
embedding 把文本映射成向量,语义相近的文本向量也相近
切块 chunking 把文档切成小块,切大塞噪声、切小丢上下文,直接决定检索质量
重叠窗口 相邻块保留一段重叠,防一句话被切点拦腰斩断
top-k 检索返回的候选数,太小漏召回、太大引入噪声
相似度阈值 低于阈值的候选视为不相关,全被丢弃则判定为"检索不到"
rerank 重排 用 cross-encoder 给召回的候选精确打分,先粗筛召回再精排
拒答 检索为空时让模型明确说"无法回答",而非用旧知识硬编

避坑清单

  1. RAG 回答质量的上限由检索决定,生成无法修复一次糟糕的检索;答不好先查检索,别先改 prompt。
  2. 切块切太大,一块里混入太多无关内容,检索精度被稀释;切太小,一句话脱离上下文,检索和使用都对不上。
  3. 固定长度切块必须带重叠窗口,否则切点会把完整语义拦腰截断,相邻两块都不完整。
  4. 优先按文档结构(段落、标题)切块,在自然语义边界处断开,每块自身的意思才完整。
  5. 向量"相似"不等于内容"相关";纯向量检索对专有名词、型号、编号不敏感,要叠加关键词检索做混合。
  6. top-k 不是越大越好,过大会把只沾边的不相关块也塞进上下文,反而干扰模型。
  7. 必须设相似度阈值;库里没有答案时检索仍会返回低分结果,阈值是判定"检索不到"和拒答的依据。
  8. 用两阶段检索:embedding 快速召回较多候选,再用 cross-encoder 重排精选,先粗筛再精排。
  9. 检索为空时要在代码里直接拒答,别进生成环节,否则模型会绕开资料、用过时知识一本正经地编。
  10. 索引和查询必须用同一个 embedding 模型,换模型要全量重建索引;RAG 要分开评估检索命中率与生成质量。

总结

回头看那次"AI 答得头头是道却全是错的"的事故,最该记住的不是某个切块函数或某个阈值,而是我上线前那个想当然的假设——"把文档喂给 AI,它自己会查、会答"。这句话把一个本该由我设计的检索系统,当成了模型自带的能力。而真相是:模型不会"查",它只会"读"——读你递给它的那几段材料。RAG 这个词里,Generation(生成)是模型的事,但 Retrieval(检索)从头到尾是你的事。你递对了材料,再普通的模型也能答得又准又有据;你递错了,再强的模型也只是把错误答得更流畅、更像真的。

所以做 RAG,真正的工程重心是回答一连串关于"检索"的问题:文档该怎么切,才能让每一块既不臃肿又不残缺?问题和文档怎么匹配,才能既抓住语义又不漏掉专名?召回的一堆候选怎么精排,才能把最相关的顶上来?库里根本没答案时,怎么让系统老实承认而不是编?这篇文章其实就是顺着这条链展开的:先认清 RAG 是给模型的一场开卷考试、流程分索引和查询两阶段;再深入最被低估的切块;然后是检索——相似不等于相关、要设阈值、要混合关键词;接着是两阶段的重排;最后是拒答、溯源、模型一致、评估这几个工程坑。

你会发现,RAG 和传统的搜索引擎工程,骨子里是同一件事。一个搜索引擎好不好用,从来不取决于结果页那个排版多漂亮,而取决于召回和排序准不准。RAG 只是把"结果页"换成了"大模型生成的一段话"——它把检索质量包装得更不容易被一眼看穿了,因为模型总能把任何材料都说得通顺。但通顺不等于正确。一个 RAG 系统真正的内功,依然是那套古老的检索功夫:怎么切、怎么召回、怎么排序、怎么知道自己没召回到。模型让"生成"这一步变得几乎免费,于是你省下来的精力,理应全部投到"检索"上。

最后想说,RAG 做没做好,差距同样不会在 Demo 阶段暴露——Demo 时你问的都是自己挑过、知道库里有答案的问题,怎么切都答得漂亮。它只在真实用户、问题五花八门、其中很多库里根本没有答案的时候才显形。那时候它一次性给你三类账单:答旧数据(检索没捞到最新的那块)、答非所问(召回了不相关的噪声)、以及最伤信任的一种——库里没有却硬编。所以别等用户拿着一个离谱答案来质问你,在你写下第一行切块代码的时候就该想清楚:我的检索,捞得准吗?捞不到的时候,我的系统会诚实地闭嘴,还是会自信地撒谎?想清楚了,你的 RAG 才不只是 Demo 里那几个漂亮的问答,而是一个用户敢拿它的答案去做决定的系统。

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

WebSocket 完全指南:从一次"消息莫名丢失、连接悄悄断开"看懂实时通信

2026-5-21 18:23:09

技术教程

Redis 缓存三大杀手完全指南:从一次"缓存挂了数据库被打垮"看懂穿透、击穿、雪崩

2026-5-21 18:36:13

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