别再给 AI 画饼了:一个 RAG 踩坑一年的血泪总结
RAG 系统从搭建到上线,半年调 chunk 半年治幻觉。本文总结了一个 RAG 项目的真实踩坑经验。
# 别再给 AI 画饼了:一个 RAG 踩坑一年的血泪总结
去年开始搞 RAG 项目,我以为就是把文档丢进向量库,写个 retrieval 然后丢给 LLM。结果呢?半年时间在调 chunk size,又半年在治 hallucination。
这篇文章把踩过的坑都摊开,希望能让后来者少掉几根头发。
Chunk 大小:没有银弹,只有权衡
这是 RAG 里最大的玄学。
文档分割太细,上下文丢失严重——你切到单句级别,语义完整性直接崩盘。切太粗,噪声太多,embedding 质量暴跌。
我们初期用 500 token 的固定 chunk,召回率看着还行,但 answer 质量差得离谱。LLM 拿到一堆碎片信息,根本拼不出完整逻辑。后来换到 1000 token + 重叠 100 token,情况才好转。
但 1000 也不是万能值。对于技术文档,代码块和自然语言混在一起,固定 chunk 会把代码截断在中间,embedding 完全失效。
**我的解法:语义感知分割**
import re
def smart_split(text, max_tokens=1000):
# 先按代码块分割
code_pattern = r'(``[\s\S]*?``)'
parts = re.split(code_pattern, text)
chunks = []
for part in parts:
if part.startswith('```'):
# 代码块整体保留,不分割
chunks.append(part)
else:
# 自然语言按段落分割
paragraphs = re.split(r'\n\n+', part)
current = ""
for para in paragraphs:
if len(current) + len(para) > max_tokens * 3:
chunks.append(current)
current = para
else:
current += para + "\n\n"
if current:
chunks.append(current)
return chunks
代码块单独处理是关键。代码的语义边界就是 `` 标记,不要动它。自然语言部分按段落切,比按固定长度切靠谱得多。
Embedding 模型选型:越大越好?
刚开始我用 text-embedding-3-small,成本低,响应快。召回率也就那样,但能接受。
后来项目要求提高,换到了 text-embedding-3-large。效果确实提升明显,但成本翻了 4 倍,延迟也上来了。
中间还试过 Cohere 的 embed-english-v3.0,在特定领域(技术文档)的召回率比 OpenAI 的还好一些。
**建议:先用 small 跑通 pipeline,确认瓶颈在召回质量再换模型。**
很多人一上来就砸大模型,结果发现瓶颈根本不在 embedding 质量,而在 chunk 策略或者检索逻辑。
混合检索:BM25 + 向量,才是正道
纯向量检索有个致命问题:对精确匹配无能为力。
比如你搜 "TypeError: undefined is not a function",向量库可能给你召回一堆语义相近但关键词不匹配的结果。而 BM25 对这种精确关键词匹配很敏感。
我们把两种检索结果融合,用 Reciprocal Rank Fusion(RRF)合并:
def rrf_fuse(vector_results, bm25_results, k=60):
"""Reciprocal Rank Fusion 合并"""
scores = {}
for i, doc in enumerate(vector_results):
doc_id = doc['id']
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + i + 1)
for i, doc in enumerate(bm25_results):
doc_id = doc['id']
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + i + 1)
# 按融合分排序
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
RRF 的好处是不需要调权重参数,两种检索结果天然融合。实测下来,混合检索的 NDCG@10 比纯向量检索高了 15-20%。
提示词里的 RAG 套路
检索到文档后,怎么让 LLM 正确使用这些信息才是关键。
一个经常被忽视的细节:**让 LLM 说出它的信息来源**。
根据以下参考资料回答问题。如果资料中没有相关信息,
请明确说明"资料中未找到",不要编造答案。
回答时请标注信息来源的文档编号。
参考资料:
{context}
问题:{question}
这段 prompt 做了两件事:
2. 要求标注来源,方便后续追溯
我们的经验是,加上"标注来源"这个要求后,LLM 的幻觉率明显下降。因为它知道自己要 accountability,会更谨慎。
最后说几点踩坑体会
2. **不要迷信重排序**。Reranker 确实有效,但成本也高。先确保基础检索质量好,再加 reranker 事半功倍。
3. **监控比调优更重要**。记录每次检索的召回结果、LLM 的回答质量,建立反馈闭环。没有监控的 RAG 就是在黑盒里瞎猜。
4. **chunk 策略要适配场景**。技术文档、产品手册、FAQ,每种内容的最佳 chunk 策略都不一样。别指望一套参数打天下。
RAG 不是一个"装好就能用"的系统,它需要持续迭代。但只要你踩够足够的坑,它确实能work。