───────────────────────
故事是这样的。
一个同学上个月来找我,说他做了个内部知识库,接了RAG,跑起来了,演示时候还挺顺,结果一上线就被各种反馈说答案乱套,文档里明明有的内容查不到,查到的又是风马牛不相及的段落。
他当时挺崩溃,觉得是大模型的问题,换了几个不同的LLM,还是一样,然后开始怀疑是不是向量数据库有问题,最后直接把问题甩给了我。
我翻了他的代码,看了大概二十分钟,问他,你文档是怎么切的?
他说按字数,每五百个字一刀。
我当时没说什么,但心里已经知道问题在哪了。
───────────────────────
这两年做大模型应用开发的同学越来越多,RAG几乎是必过的一关。原理听起来很朴素,先检索相关文档片段,再把这些片段塞给LLM当参考资料,让它根据这些资料回答问题,避免大模型凭空乱编。
理论上确实简单。
但实际落地的时候,卡点会接连出现,而且很多问题不是查API文档能解决的,是靠踩坑才能长进的那种。
我见过不少同学拿RAG项目去面试,被问到文档切分策略时直接答不上来,或者说就是用LangChain的默认切分,然后就没了。面试官一般不会当场刁难,但印象分就这样没了。
这篇文章我就来聊聊RAG开发里那几个最容易翻车的地方,以及真正值得写进简历的技术点。
───────────────────────
坑一,文档切分
按字数机械切割,是RAG项目里最普遍的错误。
问题的核心在于,文档的语义边界和字数完全不对齐。你有一份数据库故障处理手册,里面记录了MySQL主从切换的前置检查、执行步骤和回滚方案,这三个部分之间有逻辑关系,但如果你按五百字一刀,可能刚好把前置检查的第三步和执行步骤的第一步切到了两个不同的chunk,用户问一个完整的操作流程,检索回来的两个片段各讲半截,大模型拿这个来回答,答案自然就乱了。
真正靠谱的切分,是要尊重文档自身的结构。如果是Markdown文档,按标题层级来划分是更合理的做法,同时给每个chunk加上来源路径,比如「MySQL主从切换 > 前置检查」,这样既能保留上下文语义,检索回来的片段也更容易让大模型读懂。
class SmartDocumentSplitter:
"""语义感知的文档切分器"""
def init(self, max_chunk_size=800, min_chunk_size=100):
self.max_chunk_size = max_chunk_size
self.min_chunk_size = min_chunk_size
def split_markdown(self, text: str) -> list:
chunks = []
current_headers = {1: '', 2: '', 3: ''}
lines = text.split('\n')
current_content = []
for line in lines:
header_match = re.match(r'^(#{1,3})\s+(.+)$', line)
if header_match:
if current_content:
chunk_text = '\n'.join(current_content).strip()
if len(chunk_text) >= self.min_chunk_size:
chunks.append({
'content': chunk_text,
'context_path': self._build_context_path(current_headers)
})
current_content = []
level = len(header_match.group(1))
title = header_match.group(2)
current_headers[level] = title
current_content.append(line)
else:
current_content.append(line)
if len('\n'.join(current_content)) > self.max_chunk_size:
chunks.append({
'content': '\n'.join(current_content).strip(),
'context_path': self._build_context_path(current_headers)
})
current_content = []
return chunks
def buildcontext_path(self, headers: dict) -> str:
path_parts = [h for h in [headers[1], headers[2], headers[3]] if h]
return ' > '.join(path_parts) if path_parts else '未分类'
这个切分器做了两件事,一是按标题结构来感知文档的边界,二是给每个chunk生成上下文路径。后者很多人会忽略,但它对检索精度的提升很明显。
───────────────────────
坑二,语义鸿沟
文档里写的是「MySQL服务异常」,用户问的是「数据库挂了咋办」。
向量模型会把这两句话各自编码成向量,计算相似度,表面上是语义搜索,但在这种口语和书面语差异很大的场景下,纯向量检索会频繁失准。
靠谱的做法是两阶段检索,先用向量召回二十个候选,再用重排序模型精排出五个。
BGE系列模型在这块做得不错,BAAI/bge-reranker-base在中文语境下效果还可以,重排之后相关性会明显比只用向量检索准。
class EnhancedRetriever:
"""两阶段检索,向量粗筛加重排序精排"""
def retrieve_with_rerank(self, query: str, initial_top_k=20, final_top_k=5):
# 第一阶段,向量检索召回更多候选
initial_results = self.vector_store.search(query, top_k=initial_top_k)
candidates = [{'content': hit.content, 'vector_score': hit.score}
for hit in initial_results]
# 第二阶段,重排序
rerank_scores = self._compute_rerank_scores(
query, [c['content'] for c in candidates]
)
for i, score in enumerate(rerank_scores):
candidates[i]['rerank_score'] = score
candidates.sort(key=lambda x: x['rerank_score'], reverse=True)
return candidates[:final_top_k]
还有个进阶技巧是查询扩展,用大模型把用户的问题改写成三到五个不同表述的变体,分别检索再合并去重,覆盖口语化表达的时候效果会更好。
下面是整体的RAG Pipeline流程:
用户提问
│
▼
┌──────────────┐
│ 意图识别 │ 区分知识库问答/闲聊/代码
└──────┬───────┘
▼
┌──────────────┐
│ 查询扩展 │ LLM改写生成多个变体
└──────┬───────┘
▼
┌──────────────┐
│ 向量检索 │ 召回20个候选
└──────┬───────┘
▼
┌──────────────┐
│ 重排序 │ BGE精排取Top5
└──────┬───────┘
▼
┌──────────────┐
│ Prompt构建 │ 按问题类型动态调整
└──────┬───────┘
▼
┌──────────────┐
│ LLM生成 │ 流式输出
└──────┬───────┘
▼
答案 + 来源引用
这套流程跑通之后,日均查询两百多次,平均响应两秒多,首字节延迟控制在零点八秒左右,用户满意度从最初的一塌糊涂慢慢爬到了七成以上。
───────────────────────
这里插个福利。如果你现在正在找工作,或者想提前准备大模型应用开发方向,需要简历优化、面试辅导、求职陪跑相关的帮助,可以在置顶动态里找到联系方式,找云老师聊聊。很多同学改完简历之后,面试邀约率和通过率提升都挺明显的。
───────────────────────
坑三,Prompt写烂了
大模型回答问题时会编造,这不是RAG的问题,是大模型的底层特性。
但很多人搭完RAG之后,Prompt还是一句话,「请根据以下资料回答用户问题」,然后就把文档片段全粘上去了。大模型当然可以回答,但它没被告知资料里如果没有该怎么办,也没被引导给出信息来源,回答质量就看大模型当天心情。
有效的Prompt需要做三件事,明确告诉模型只使用参考资料里的信息,告诉它没有时要说没有而不是编造,让它标注每句话对应的是哪份资料。
另外更进一步的做法是根据问题类型动态调整Prompt格式。用户在问操作流程和在问概念定义,回答的结构应该是不一样的,操作类问题用分步骤格式,概念类问题先给核心定义再展开,故障排查类问题先列可能原因再给解决方案。
这个优化做完,用户觉得「答案看起来不对」的反馈会减少很多。
───────────────────────
聊聊这些坑在简历上怎么写
这些踩坑经历,很多同学在写简历的时候要么完全不写,要么就写「使用LangChain搭建了RAG系统,实现了智能问答」,这样的描述拿去面试,面试官基本上不会有什么反应。
差异化的写法是把你解决了什么具体问题,用什么技术手段解决的,最终效果如何量化出来,写进项目里。
比如这样,「针对企业内部知识库问答场景,基于BGE Embedding + Milvus向量数据库搭建两阶段检索系统,通过语义分块策略解决文档语义割裂问题,结合重排序模型将检索相关性提升约四十个百分点,系统上线后用户满意度达到七成以上」。
这样的描述有场景,有技术,有数据,面试官看到能直接问你问题,你也能答上来,双向都不尬。
目前2026年AI开发岗位对RAG经验的考察已经不只是「你有没有用过」,而是「你踩过什么坑,你怎么解决的」,这个层次的考察,靠背文档是回答不了的,只有真正下场做过才知道怎么讲。
───────────────────────
这是做大模型应用开发最真实的一件事,走捷径的代价,就是在面试时哑口无言。
真正的积累,是从每一个搜不到的chunk、每一次大模型编造的答案、每一个不好使的Prompt里一点一点攒出来的。