Featured image of post RAG 失败复盘手册:一张流程图 + 一段代码,把问题定位到检索/生成/数据

RAG 失败复盘手册:一张流程图 + 一段代码,把问题定位到检索/生成/数据

很多 RAG 系统的问题,表面看起来是“模型不行”,但真正的根因往往在更前面:数据切分、索引构建、检索策略、拼接截断、或后置校验。

这篇文章我给你一套可复用的排障流程

  • 一张“从 Query 到日志”的流程图(你可以贴到团队 wiki)
  • 一段最小可用的 Python 代码:把一次请求的关键中间产物都打出来(便于复盘)

1) 先统一语言:RAG 失败到底分哪几类?

我把 RAG 的失败分成三类(按排查优先级):

  1. 检索失败:检索出来的内容不相关 / 证据不足
  2. 拼接失败:检索对了,但上下文被截断、重复、排序错误
  3. 生成失败:证据足够,但模型没按证据回答(提示词/格式/温度等问题)

你只要能把一次失败明确归类,后面的优化就不会“凭感觉”。

2) 一张流程图:把排障步骤固定下来

下面这张图是我做 RAG 排障时的默认流程:

你可以把它当作 checklist:每次线上出现“答非所问/胡说八道/延迟突然变大”,就按这个顺序走。

3) 一段最小可用代码:把一次请求的关键中间产物都记录下来

下面这段代码示例做三件事:

  • 记录规范化后的 query
  • 记录检索结果(文档 id、score、片段)
  • 记录最终 prompt(以及截断信息)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
from dataclasses import dataclass
from typing import List, Dict, Any
import time

@dataclass
class Chunk:
    doc_id: str
    score: float
    text: str


def normalize_query(q: str) -> str:
    # 你可以在这里做:全角半角、大小写、同义词、实体标准化…
    return " ".join(q.strip().split())


def retrieve(q: str, topk: int = 5) -> List[Chunk]:
    # 示例:这里替换成你的 BM25/向量检索
    # 返回 doc_id/score/text,便于后续定位“到底检索到了什么”
    return [
        Chunk(doc_id="doc:pricing", score=0.78, text="..."),
        Chunk(doc_id="doc:limits", score=0.74, text="..."),
    ][:topk]


def build_prompt(q: str, chunks: List[Chunk], max_chars: int = 6000) -> str:
    context = "\n\n".join(
        f"[source:{c.doc_id} score={c.score:.2f}]\n{c.text}" for c in chunks
    )
    prompt = (
        "你是一个严谨的助手。只允许基于给定的 sources 回答,并在结尾列出引用。\n\n"
        f"Question:\n{q}\n\n"
        f"Sources:\n{context}\n\n"
        "Answer:\n"
    )
    truncated = len(prompt) > max_chars
    if truncated:
        prompt = prompt[:max_chars] + "\n\n[TRUNCATED]"
    return prompt


def rag_once(question: str) -> Dict[str, Any]:
    t0 = time.time()

    q = normalize_query(question)
    chunks = retrieve(q, topk=8)
    prompt = build_prompt(q, chunks, max_chars=6000)

    # 这里替换成你的 LLM 调用
    answer = "(mock) ..."

    return {
        "latency_ms": int((time.time() - t0) * 1000),
        "query": q,
        "top_docs": [{"doc_id": c.doc_id, "score": c.score} for c in chunks],
        "prompt_chars": len(prompt),
        "answer": answer,
    }


if __name__ == "__main__":
    result = rag_once("你们套餐的价格和限制是什么?")
    print(result)

3.1 这段代码你应该怎么用

  • 在你真实服务里,把 rag_once 的输出写进一次请求的 trace/log
  • 线上出现 badcase 时,你能立刻回答三个问题:
    1. query 进来后被改成了什么?
    2. 检索到底检到了哪些 doc?score 如何?
    3. prompt 有没有被截断?

4) 结尾:把“能复盘”当成 RAG 的第一优先级

RAG 的优化不是玄学。

只要你能把一次失败的链路完整记录下来,下一步该改数据、改检索、改提示词,结论会非常清晰。

build with Hugo, theme Stack, visits 0