大模型RAG实战,从被骂不靠谱到成为部门MVP,这是我的踩坑全记录
大模型RAG实战,从被骂不靠谱到成为部门MVP,这是我的踩坑全记录
先交代背景:半年前,领导让我给公司内部知识库做一个“智能问答机器人”。当时我信心满满——不就是调API吗?结果第一个demo演示时,模型对着我们的产品手册一本正经地胡说八道,领导当场脸色铁青,说了一句让我至今难忘的话:“这玩意儿还不如用Ctrl+F搜索。” 后来我才明白,不是模型不行,是我压根没搞懂RAG的正确打开方式。今天就把我从“被嘲讽”到“被同事求着要部署”的完整踩坑过程写出来,全是血泪经验。## 一、最初我犯的致命错误:把文档“整坨”塞给模型当时我的第一版代码长这样:pythonfrom openai import OpenAIclient = OpenAI(api_key="your-key")def naive_qa(question): # 把整个知识库文档读进来,塞进prompt with open("knowledge_base.txt", "r", encoding="utf-8") as f: docs = f.read() response = client.chat.completions.create( model="gpt-4", messages=[ {"role": "system", "content": "你是客服助手,请基于以下文档回答。"}, {"role": "user", "content": f"文档内容:{docs[:3000]}\n\n问题:{question}"} ] ) return response.choices[0].message.content这个代码有两个致命伤:1. Token爆炸:文档一长,直接截断,后半部分内容模型根本看不到。2. 上下文污染:不相关的内容会干扰模型判断,导致它“自由发挥”编造答案。结果就是:用户问“退货政策”,模型回答“我们支持免费退换”,但文档里其实写的是“仅限未拆封商品”。这不是模型笨,是我喂的料有问题。## 二、第一次顿悟:检索-增强-生成,三步走后来看了LangChain的文档,才明白RAG的核心是:先检索,再生成。不是让模型读所有内容,而是先从知识库里找出和问题最相关的片段,只把这些片段喂给模型。于是第二版我用了向量检索:pythonfrom sentence_transformers import SentenceTransformerimport chromadb# 1. 加载编码模型和向量数据库encoder = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')client = chromadb.Client()collection = client.create_collection("kb")# 2. 把知识库切成小块(chunk)并向量化存储def chunk_text(text, chunk_size=500, overlap=50): words = text.split() chunks = [] for i in range(0, len(words), chunk_size - overlap): chunks.append(" ".join(words[i:i+chunk_size])) return chunksknowledge_base = load_from_file() # 读取你的文档for i, chunk in enumerate(chunk_text(knowledge_base)): collection.add( ids=[f"chunk_{i}"], embeddings=[encoder.encode(chunk).tolist()], documents=[chunk] )# 3. 查询时先检索,再交给LLMdef rag_answer(question): # 第一步:检索最相关的3个片段 results = collection.query( query_embeddings=[encoder.encode(question).tolist()], n_results=3 ) context = "\n\n".join(results['documents'][0]) # 第二步:增强生成 response = client.chat.completions.create( model="gpt-4", messages=[ {"role": "system", "content": "只根据提供的上下文回答,不要编造。如果上下文没有,就回答'我不知道'。"}, {"role": "user", "content": f"上下文:\n{context}\n\n问题:{question}"} ] ) return response.choices[0].message.content这版效果好了不少,但依然被同事吐槽:“有时候答案是对的,有时候又答非所问,不稳定。” 我开始怀疑:是不是我切的chunk有问题?## 三、踩坑重灾区:Chunk切分和Embedding选择后来我统计了失败case,发现80%的问题出在chunk边界上。比如文档里“退货政策”和“退款时间”写在同一段,但我切chunk时把它们拆到了不同块,检索时只找到了“退货政策”,丢了“退款时间”,模型自然答不完整。踩坑教训:- 不要用固定字数切chunk,要按语义段落(比如标题、换行、列表)来切。- 使用带重叠(overlap)的滑动窗口,避免信息断层。- Embedding模型要选领域适配的。我一开始用通用英文模型,对中文技术文档效果很差,后来换成了BAAI/bge-large-zh-v1.5,检索准确率直接提升40%。另外,检索到的chunk数量不是越多越好。我试过n_results=5,结果噪声太多,模型反而被干扰。最终调成3个,再配合相关性阈值过滤,效果最稳。## 四、终极优化:让模型“知道”自己不知道这是让我从“被骂”到“被夸”最关键的一步。之前模型经常在上下文不足时强行编造,后来我做了两件事:1. 在Prompt里强制约束:如果没有确切答案,必须回答“抱歉,知识库中未找到相关信息”。2. 加一个置信度判断:如果检索到的最高相似度分数低于某个阈值(比如0.7),直接不调用LLM,返回预设话术。pythondef safe_rag_answer(question): results = collection.query( query_embeddings=[encoder.encode(question).tolist()], n_results=3, include=["documents", "distances"] ) # 如果最相似的chunk距离太远(相似度低),说明知识库里没有相关信息 if results['distances'][0][0] > 0.8: # 阈值调优 return "抱歉,我暂时没有学会这个问题的答案,建议联系人工客服。" # 正常检索增强生成 context = "\n\n".join(results['documents'][0]) response = client.chat.completions.create( model="gpt-4", messages=[ {"role": "system", "content": "严格基于上下文回答,禁止编造。上下文没有的内容,就回答'不知道'。"}, {"role": "user", "content": f"上下文:\n{context}\n\n问题:{question}"} ] ) return response.choices[0].message.content这个改动上线后,我们部门“答非所问”的投诉率下降了90%。领导甚至在周会上点名表扬,说我这套系统“终于像个正经AI了”。## 五、总结回顾这半年的实战,我最大的感悟是:RAG不是“模型+文档”的拼接,而是一个检索系统与生成系统的深度耦合工程。 核心踩坑点就三个:- 检索质量决定上限:chunk切分、Embedding选择、相似度阈值,每一个都值得花时间调优。- Prompt约束决定下限:明确告诉模型“不知道就说不知道”,能极大减少幻觉。- 永远要加兜底策略:别让LLM在信息不足时自由发挥,置信度过滤是保命符。现在这套系统每天被公司内部调用上千次,我也成了部门里“懂大模型应用”的红人。但每次有人夸我,我都会想起那个被领导当场打脸的下午——技术不踩坑,永远不知道坑有多深。希望这篇记录能让你少走一些弯路,如果你也在做RAG,欢迎在评论区交流你的踩坑经历。
更多推荐
所有评论(0)