闪学it-百战程序员 - AI应用与智能体开发|零基础可学|企业级项目

0 阅读5分钟

一、RAG 到底在做什么

一句话概括:先检索,再生成。

用户提问 → 从知识库里找出最相关的几段文字 → 把这些文字和问题一起交给大模型 → 模型基于"给定材料"作答。

它把"记忆"这件事从模型参数里拆了出来,交给一个可随时更新的外部知识库。好处是显而易见的:

  • 知识可以随时更新,改文档即可,不用重新训练;
  • 答案有出处,可追溯,降低幻觉;
  • 成本低,一次检索加一次调用就完成了。

代价是:检索质量决定了上限。检索不到,再强的模型也答不对。


二、整体架构:四步流水线

一个最小可用的 RAG 系统只有四个环节:

  1. 切分(Chunking) :把长文档切成适合检索的小片段;
  2. 向量化(Embedding) :把每个片段转成向量存起来;
  3. 检索(Retrieval) :把用户问题也转成向量,找最相似的片段;
  4. 生成(Generation) :把片段拼进提示词,调用大模型作答。

听起来很唬人,实际上核心代码不到一百行。下面我们逐一实现。


三、核心实现

3.1 文档切分

切分的目标是"每块语义完整、长度适中"。太小会丢失上下文,太大会稀释相关性。实践中 300~800 字符是比较稳妥的区间。

def split_text(text, chunk_size=500, overlap=50):
    """按字符切分,overlap 让相邻块有重叠,避免语义被切断"""
    chunks, start = [], 0
    while start < len(text):
        end = start + chunk_size
        chunks.append(text[start:end])
        start = end - overlap          # 回退一段,保证连续性
    return [c for c in chunks if c.strip()]

一个细节:中文按字符切、英文按词切,效果差异很大。如果文档结构清晰(有标题、段落),优先按段落切,再用长度做兜底。

3.2 向量化与检索

这里为了不引入重型依赖,我们用最朴素的方式演示——真实项目里把 embed() 换成 OpenAI、BGE 或任意 embedding 模型的调用即可,逻辑完全一致。

import numpy as np

def embed(text: str) -> np.ndarray:
    """示意:真实场景替换为 embedding API 调用"""
    vec = np.array([hash(text[i:i+3]) % 1000 for i in range(0, min(len(text), 300), 3)],
                   dtype=float)
    return vec / (np.linalg.norm(vec) + 1e-9)

def retrieve(query, chunks, k=3):
    q = embed(query)
    scored = []
    for c in chunks:
        v = embed(c)
        n = min(len(q), len(v))
        scored.append((float(np.dot(q[:n], v[:n])), c))   # 余弦相似度
    scored.sort(reverse=True, key=lambda x: x[0])
    return [c for _, c in scored[:k]]

生产环境中,向量库(FAISS、Chroma、Milvus 等)负责管理索引和近似最近邻搜索,这里用列表循环是为了让流程一眼看清。

3.3 拼接上下文并生成

这一步的关键是提示词设计:明确告诉模型"只依据材料回答,找不到就说不知道"。这一句话能挡掉大部分幻觉。

def ask(query, chunks, llm):
    context = "\n---\n".join(retrieve(query, chunks))
    prompt = f"""请仅根据下面的资料回答问题,资料中没有的信息请回答"资料中未提及"。

资料:
{context}

问题:{query}
"""
    return llm(prompt)

至此,一个最小 RAG 就闭环了。llm 可以是任何模型接口,换成官方 SDK 的 chat.completions.create 调用即可。


四、几个容易踩的坑

跑通之后,真正的挑战才开始。以下几个问题几乎每个项目都会遇到:

1. 检索不准,答非所问。
最常见的原因是切分不合理。标题和正文被切开、表格被打散、上下段落失去关联,都会让检索失效。解决办法是给每个片段加上来源标题作为"上下文前缀",检索命中的概率会明显提升。

2. 相似度高分,但内容无关。
纯向量检索对"关键词精确匹配"不敏感,比如产品型号、人名。实践中常用混合检索:向量召回 + BM25 关键词召回,再融合排序,效果比单一路径稳定得多。

3. 上下文塞太多,反而变差。
不是召回越多越好。无关片段会干扰模型判断,还挤占上下文窗口。一般 3~5 个片段足够,宁可精不要多。

4. 多轮对话丢失上下文。
用户追问"那第二步呢",单独拿这句话去检索必然失败。需要先让模型把追问改写成完整问题,再进入检索。

5. 没有评估,全靠感觉。
建议准备 20~30 条真实问题作为测试集,记录"检索命中率"和"答案正确率"。没有评估,优化就是在盲猜。


五、什么时候不该用 RAG

RAG 不是万能钥匙。如果你的任务是改变模型的表达风格、语气或固定格式,那更适合微调;如果知识总量很小(几页纸),直接塞进提示词反而更简单可靠;如果需求是复杂的多步推理、跨文档计算,单轮检索就不够了,需要考虑 Agent 或工作流编排。

判断标准其实很简单:问题是"缺知识"还是"缺能力" 。缺知识用 RAG,缺能力用微调或更强的模型。


结语

RAG 的门槛比想象中低——四个环节、不到百行代码就能跑通一个 demo。但它真正的难度在于后半程:切分策略、混合检索、重排序、评估体系,每一项都影响最终体验。

如果你准备动手,建议的路径是:先用几十行代码跑通最小闭环,拿真实文档试一遍,感受一下"哪里答得不对",然后再针对性地优化某一个环节。比起一上来就搭复杂架构,这种"先跑起来再打磨"的方式,往往走得更远。