首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从零构建一个 RAG 文档问答助手:思路、代码与踩坑记录

从零构建一个 RAG 文档问答助手:思路、代码与踩坑记录

原创
作者头像
搜weiranit.fun
发布于 2026-11-08 14:52:25
发布于 2026-11-08 14:52:25
1070
举报

一、RAG 到底在做什么

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

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

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

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

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


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

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

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

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


三、核心实现

3.1 文档切分

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

代码语言:javascript
复制
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 模型的调用即可,逻辑完全一致。

代码语言:javascript
复制
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 拼接上下文并生成

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

代码语言:javascript
复制
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。但它真正的难度在于后半程:切分策略、混合检索、重排序、评估体系,每一项都影响最终体验。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、RAG 到底在做什么
  • 二、整体架构:四步流水线
  • 三、核心实现
    • 3.1 文档切分
    • 3.2 向量化与检索
    • 3.3 拼接上下文并生成
  • 四、几个容易踩的坑
  • 五、什么时候不该用 RAG
  • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档