我的 RAG 知识库问答总是答非所问、要么答不全要么牛头不对马嘴,模型和向量库都没问题,我对着文档切分的 chunking 排查了大半天的复盘
那是我做的一个企业知识库问答:把公司文档切片、做向量化存进向量库,用户提问时检索相关片段、喂给大模型生成答案(经典的 RAG)。模型选的是好模型,向量库也没问题,可问答质量就是差强人意:有时答案残缺不全(明明文档里写得很清楚,它却只答了一半);有时牛头不对马嘴(检索出来的片段跟问题压根不相关);有时一段话被拦腰截断,模型读到的是半句话。我换了更强的模型、调了检索参数,效果都没本质改善。排查了大半天,我才意识到:问题不在模型、也不在检索算法,而在那个最容易被忽略的、最上游的环节——文档切分(chunking)。这篇就把这场"chunking 拖垮 RAG"的事故,从头复盘一遍。
故障现场:答案残缺、片段不相关、句子被截断
先看现场。问题的根子,藏在我那段"简单粗暴按固定长度切"的代码里:
# 我的文档切分: 简单粗暴, 按固定字符数切
def split_document(text, chunk_size=500):
chunks = []
for i in range(0, len(text), chunk_size):
chunks.append(text[i:i + chunk_size]) # ✗ 每500字符硬切一刀
return chunks
# 然后把每个 chunk 向量化存库, 检索时返回最相关的几个 chunk。
# 这种"按固定长度硬切"导致的问题:
# 问题1: 在句子/段落中间切断 —— 语义被切碎
# "...公司的退款政策规定, 商品签收后7天内可" | "无理由退货, 但需满足..."
# ↑ 在这里硬切一刀!
# → 一个完整的句子/规定, 被切成了两个 chunk。
# → 检索时可能只命中前半个 chunk(残缺)→ 模型读到半句话, 答不全。
# 问题2: chunk 太大 —— 一个 chunk 里塞了多个不相关的主题
# 一个500字的chunk里, 可能同时有"退款政策""配送说明""会员等级"。
# → 用户问"退款", 检索到这个chunk(因为含退款), 但里面一大半是无关内容,
# → 噪声干扰模型、稀释了相关信息 → 答得不准。
# 问题3: chunk 太小 —— 上下文不完整, 丢失语境
# 切成100字的小chunk: "需满足商品完好、吊牌未剪。"
# → 这句话脱离了上文"退款政策"的语境, 检索/理解都困难,
# 模型不知道这是"退款"的条件还是"换货"的条件。
# 问题4: 关键信息跨 chunk —— 检索召回不全
# 一个完整的答案需要chunk A(政策)+ chunk B(例外情况), 但检索
# 只返回了A → 答案缺了B那部分 → 不完整。
# 现象拼图:
# - RAG 的质量 = 检索质量 × 生成质量; 而检索质量, 高度依赖"chunk 切得好不好"。
# - 我用"按固定长度硬切": 切碎语义、混入噪声、丢失语境、割裂关联。
# - 喂给模型的"原料"(检索到的chunk)本身就是残缺/混乱的,
# 再强的模型也"巧妇难为无米之炊"。
# - ★ 根因: 我把 RAG 的精力全放在了"模型"和"检索算法"上, 却忽略了
# 最上游的"chunking"—— 而 chunk 的质量, 从根上决定了 RAG 的上限。
看清真相后,我才明白问题出在最上游。问题的根源,是我用"按固定字符数硬切"的方式切分文档,导致了一连串质量问题。问题一:在句子/段落中间切断——一个完整的规定被切成两个 chunk,检索只命中前半个就答不全;问题二:chunk 太大——一个 chunk 塞了多个不相关主题,无关内容成了噪声、稀释相关信息;问题三:chunk 太小——脱离语境,模型不知道这句话属于哪个主题;问题四:关键信息跨 chunk——一个完整答案需要 A+B 两个 chunk,检索只返回 A 就不完整。核心认知是:RAG 的质量 = 检索质量 × 生成质量,而检索质量高度依赖"chunk 切得好不好";我用固定长度硬切,切碎了语义、混入了噪声、丢失了语境、割裂了关联,喂给模型的"原料"本身就是残缺混乱的,再强的模型也巧妇难为无米之炊。根因是:我把精力全放在"模型"和"检索算法"上,却忽略了最上游的 chunking——而 chunk 的质量,从根上决定了 RAG 的上限。
第一件事:搞懂 chunking 为什么是 RAG 的根基
要解决它,得先理解 chunking 在 RAG 流程里的位置,以及它为什么这么关键。
chunking: RAG 流程的"地基"
# 一、RAG 的完整流程(chunking 在最上游):
# 文档 →【切分 chunking】→ 每个chunk向量化 → 存向量库
# ↓
# 用户提问 → 问题向量化 → 检索最相关的几个chunk → 喂给LLM → 生成答案
# ★ chunk 是"被检索、被喂给模型"的【最小单位】, 它的质量决定一切下游。
# 二、为什么 chunk 的质量至关重要?
# - 检索是"按 chunk 检索"的: chunk 切得好不好, 直接决定"能不能检索到
# 完整、相关、干净的信息"。
# - 喂给模型的是 chunk: chunk 残缺/含噪声/丢语境, 模型拿到的原料就差。
# - 一句话: chunk 是 RAG 的"原材料", 原材料质量, 决定成品上限。
# 再好的模型(厨师)、再好的检索(选料), 也救不了烂原料(烂chunk)。
# 三、好的 chunk 应该是什么样?
# 1. 语义完整: 不要在句子/段落/语义单元中间切断。
# 2. 主题单一: 一个 chunk 尽量只讲一个主题/一件事(便于精准检索)。
# 3. 大小适中: 不太大(避免混入噪声)、不太小(保留足够上下文)。
# 4. 保留语境: 必要时带上"它属于哪个章节/主题"的信息。
# 5. 关联不割裂: 强相关的内容(如政策+其例外)尽量在一起或有重叠。
# 四、chunking 的核心矛盾(要权衡):
# - chunk 大: 上下文完整, 但可能混入噪声、检索不精准、占更多token。
# - chunk 小: 主题单一、检索精准, 但可能丢失上下文、关联被割裂。
# → 没有"完美大小", 要根据文档类型和问答场景, 权衡 + 用更聪明的切分策略。
# 核心: chunk是RAG里"被检索和喂给模型的最小单位", 是原材料, 质量决定RAG上限;
# 好chunk要语义完整/主题单一/大小适中/保留语境/不割裂关联; 大小是核心权衡。
想透 chunking 在 RAG 里的位置,就明白它为什么是根基了。一、RAG 的完整流程:文档 → 切分 chunking → 每个 chunk 向量化 → 存库;用户提问 → 检索最相关的几个 chunk → 喂给 LLM → 生成答案;chunk 是"被检索、被喂给模型"的最小单位,它的质量决定一切下游。二、为什么 chunk 质量至关重要?——检索是按 chunk 检索的、喂给模型的是 chunk;chunk 是 RAG 的"原材料",原材料质量决定成品上限,再好的模型(厨师)、再好的检索(选料),也救不了烂 chunk(烂原料)。三、好的 chunk 是什么样?——语义完整(不在句子中间切)、主题单一(便于精准检索)、大小适中、保留语境、关联不割裂。四、chunking 的核心矛盾:chunk 大则上下文完整但易混噪声、检索不精准;chunk 小则主题单一、检索精准但易丢上下文、割裂关联;没有完美大小,要权衡 + 用更聪明的切分策略。
第二件事:正解——按语义切分 + 重叠 + 保留结构
搞懂了原理,正解就清晰了:按语义边界(段落/句子)切、chunk 间留重叠、保留文档结构信息、根据文档类型选策略。
# ====== 正解一: 按"语义边界"切, 别在句子/段落中间硬切 ======
# 优先按 段落 → 句子 的边界切, 保证每个 chunk 是完整的语义单元。
import re
def split_by_semantic(text, max_size=500):
# 先按段落分(\n\n), 段落太大再按句子(。!?)分
paragraphs = text.split("\n\n")
chunks, current = [], ""
for para in paragraphs:
if len(current) + len(para) <= max_size:
current += para + "\n\n"
else:
if current: chunks.append(current.strip())
current = para + "\n\n"
if current: chunks.append(current.strip())
return chunks
# → 在自然的语义边界切, 不会把一句话/一段话切碎。
# ====== 正解二: chunk 之间留"重叠(overlap)" ======
# 相邻 chunk 重叠一部分, 避免"关键信息正好在切口处被割裂"。
def split_with_overlap(text, chunk_size=500, overlap=100):
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunks.append(text[start:end])
start = end - overlap # ★ 下一个chunk往回退overlap个字符, 形成重叠
return chunks
# → 重叠让"跨切口的信息"在相邻chunk里都有, 检索更不容易漏。
# (实践中: overlap 取 chunk_size 的 10%~20% 常见)
# ====== 正解三: 用成熟的切分库(别自己造轮子)======
# LangChain 的 RecursiveCharacterTextSplitter(最常用):
# from langchain.text_splitter import RecursiveCharacterTextSplitter
# splitter = RecursiveCharacterTextSplitter(
# chunk_size=500, chunk_overlap=100,
# separators=["\n\n", "\n", "。", "!", "?", " ", ""]) # 按优先级递归切
# → 它会优先按段落切, 不行再按句子, 再不行才按字符, 尽量保语义完整。
# ====== 正解四: 保留文档结构/元信息 ======
# 给每个chunk带上"它来自哪个文档/章节/标题"的元信息:
# chunk = {
# "content": "需满足商品完好、吊牌未剪。",
# "metadata": {"doc": "退款政策", "section": "退货条件"} # ★ 语境!
# }
# → 检索时能利用元信息, 模型也知道"这段话属于退款政策的退货条件"。
# (进阶: 在chunk内容前拼上标题, 如"【退款政策-退货条件】需满足...")
# ====== 正解五: 根据文档类型选切分策略 ======
# - Markdown/有标题结构: 按标题(#)层级切, 天然语义边界。
# - 代码: 按函数/类切。
# - 表格/结构化数据: 整行/整表作为一个单元, 别切断。
# - QA/FAQ: 每个"问答对"作为一个chunk。
# → 没有万能策略, 按文档的"天然结构"来切, 效果最好。
# 核心: 按语义边界(段落/句子)切别硬切 + chunk间留重叠防割裂 + 保留章节标题等元信息语境
# + 按文档类型选策略(Markdown按标题/代码按函数/FAQ按问答对); 用成熟切分库别造轮子。
修复的核心,是"按语义而非按长度切,并通过重叠和结构信息,保住 chunk 的完整与语境"。正解一:按"语义边界"切——优先按段落 → 句子的边界切,保证每个 chunk 是完整的语义单元,不把一句话切碎。正解二:chunk 之间留"重叠(overlap)"——相邻 chunk 重叠一部分(常取 chunk_size 的 10%~20%),避免关键信息正好在切口处被割裂。正解三:用成熟的切分库——LangChain 的 RecursiveCharacterTextSplitter 会按"段落→句子→字符"的优先级递归切,尽量保语义完整,别自己造轮子。正解四:保留文档结构/元信息——给每个 chunk 带上"来自哪个文档/章节/标题"的元信息(甚至在内容前拼上标题),让检索能利用、模型也知道语境。正解五:根据文档类型选策略——Markdown 按标题层级切、代码按函数切、表格整行整表不切断、FAQ 按问答对切;按文档的"天然结构"切效果最好。归根结底:按语义边界切别硬切 + chunk 间留重叠防割裂 + 保留章节标题等元信息 + 按文档类型选策略;用成熟切分库别造轮子。
第三件事:RAG 质量的全链路
修这个问题时我意识到,chunking 只是 RAG 质量链条的一环。我把整条链路梳理了一遍。
RAG 质量全链路(每一环都影响最终质量)
# RAG 的质量, 是【整条链路】每一环的乘积, 任何一环差都拖垮全局:
# 1. 文档预处理: 清洗(去页眉页脚/乱码)、格式转换。脏数据 → 烂chunk。
# 2. chunking(本文): 切分粒度/策略。切不好 → 检索不到完整相关信息。
# 3. embedding: 用什么向量模型(中文要用支持中文的, 见embedding不一致篇)。
# 问答和入库要用【同一个】embedding模型!
# 4. 向量检索: top-k(取几个chunk)、相似度阈值。
# - top-k 太小: 漏掉相关chunk(答不全); 太大: 混入噪声。
# 5. 重排序(rerank): 对检索出的chunk再用更精的模型排序, 提升相关性(进阶)。
# 6. prompt 组装: 怎么把chunk拼进prompt、怎么引导模型"基于资料答"。
# 7. 生成: 模型能力、temperature、是否要求引用(见幻觉篇)。
# 一个重要认知: RAG 不是"接个向量库 + 大模型"就完事
# - 它是一条有很多环节的"流水线", 每一环都有优化空间和坑。
# - 效果不好时, 要【逐环排查】是哪一环的问题:
# * 检索出来的chunk相关吗? (不相关→chunking/embedding/检索的问题)
# * chunk内容完整吗? (残缺→chunking的问题)
# * chunk都对, 但模型答错? (生成/prompt的问题)
# - 别一上来就换模型(那通常是最后才动的), 先看"喂给模型的资料对不对"。
# 核心: RAG质量是全链路(预处理/chunking/embedding/检索top-k/rerank/prompt/生成)的乘积,
# 任一环差都拖垮全局; 效果差要逐环排查(先看检索的chunk相不相关、全不全), 别一上来换模型。
修这个问题让我看到了更完整的图景:chunking 只是 RAG 质量链条的一环。RAG 的质量是整条链路每一环的乘积:文档预处理(清洗,脏数据→烂 chunk)、chunking(本文,切不好→检索不到完整信息)、embedding(中文要用支持中文的,且问答和入库要用同一个模型)、向量检索(top-k 太小漏召回、太大混噪声)、重排序 rerank(进阶,对检索结果再精排)、prompt 组装、生成(模型/temperature/要求引用)。一个重要认知:RAG 不是"接个向量库 + 大模型"就完事,而是一条有很多环节的流水线,每环都有坑;效果不好时要逐环排查——检索出来的 chunk 相关吗(不相关→chunking/embedding/检索)、chunk 内容完整吗(残缺→chunking)、chunk 都对但模型答错(生成/prompt);别一上来就换模型(那通常最后才动),先看"喂给模型的资料对不对"。下面这张图,是这次 chunking 拖垮 RAG 的成因与解法:
第四件事:chunk 大小的权衡速查
这次踩坑后,我把 chunk 大小、重叠、策略的权衡整理成一张表,调 RAG 时对照着选。
| 维度 | chunk 偏大 | chunk 偏小 |
|---|---|---|
| 上下文完整性 | ✓ 完整 | ✗ 易丢语境 |
| 检索精准度 | ✗ 易混噪声 | ✓ 主题单一更精准 |
| 关联保留 | ✓ 相关内容易在一起 | ✗ 易割裂关联 |
| token 成本 | ✗ 每次喂更多token | ✓ 省token |
| 适用文档 | 叙述性/逻辑连贯的长文 | FAQ/条目式/结构化 |
| 经验值 | 800~1500字(长文) | 200~500字(问答/条目) |
这张表,把 chunk 大小的权衡讲清了。核心结论是:没有"万能的 chunk 大小",它取决于文档类型和问答场景——叙述性长文用大 chunk(保完整)、FAQ/条目式用小 chunk(保精准);经验值长文 800~1500 字、问答条目 200~500 字,配 10%~20% 重叠。它给我的最大启发是:chunk 大小是一个需要"实验和调优"的参数,而不是一个"查到一个推荐值就一劳永逸"的常量。因为它的最优值,高度依赖你的"具体文档"和"具体问题类型";别人的推荐值只是起点,真正合适的值,要靠用你自己的真实数据和真实问题去实验、评估、调整。这让我领悟到一个做 RAG(乃至一切机器学习/数据相关系统)的态度:这类系统的效果,往往不是"一次设计就完美"的,而是"建立一个能评估效果的方法,然后不断实验、迭代、调优"出来的。所以做 RAG,最该先建立的,是一套"评估问答质量"的方法(一组标准问答对、人工或自动打分),有了它,你才能客观地判断"调了 chunk 大小/换了策略后,到底变好还是变差了",从而有依据地迭代;否则就是凭感觉瞎调。
第五件事:RAG 效果不好时的排查顺序
这次走了弯路(一上来就换模型),我把"RAG 效果差该怎么排查"的正确顺序梳理了一下。
| 排查步骤 | 看什么 | 问题指向 |
|---|---|---|
| 1. 看检索结果 | 检索出的chunk相关吗 | 不相关→embedding/检索/chunking |
| 2. 看chunk内容 | chunk完整吗、有噪声吗 | 残缺/混乱→chunking |
| 3. 看召回数量 | 该有的信息都检索到了吗 | 漏了→top-k太小/chunking割裂 |
| 4. 看prompt | chunk怎么拼进prompt的 | 引导不当→prompt |
| 5. 看生成 | 资料都对但答错 | 模型/temperature→最后才动 |
这张表,纠正了我"一遇到效果差就换模型"的错误习惯。正确的排查顺序是从上游到下游、从"喂给模型的资料"到"模型本身":先看检索出的 chunk 相不相关、完不完整、有没有漏,再看 prompt 怎么拼的,最后才考虑模型/参数。它给我的最大启发是:当一个由"多个环节串联"的系统(RAG)出问题时,排查要"顺着数据流,从上游往下游查"——因为上游的问题会污染下游,你在下游看到的"症状"(答错),根子可能在上游(chunk 残缺)。我之前的错,正是"头痛医头":看到答案差(下游症状),就去换模型(下游),而没有顺着数据流往上游追"到底是哪一环把数据搞坏的"。这让我领悟到一个排查复杂流水线问题的通用方法:对于"数据经过多个环节加工"的系统,排查时要在每一环的"出口"处检查数据质量,定位到"数据是从哪一环开始变坏的";而不是只盯着最终的输出、并想当然地去优化最后一环。顺着数据流定位"污染源"——这种"沿链路逐环检查中间产物"的排查法,是搞定一切数据流水线问题(RAG、ETL、数据处理管道)最可靠的思路。
第六件事:做 RAG 知识库时,我现在的 chunking 决策
现在做 RAG,我不再一上来按固定长度切,而是按这张图先想清楚 chunking 策略:
这张图的精髓,是"按文档类型选切分策略,并用评估集驱动调优"。第一问 "文档是什么类型":Markdown 按标题切、FAQ 按问答对切、代码按函数切、叙述长文按段落→句子递归切+重叠。无论哪种,都给 chunk 带上章节标题等元信息、配合适的大小和重叠、用成熟切分库。而最后两步是我现在的关键习惯:建一个评估集(一组标准问答对),实验不同的 chunk 大小/策略,用评估集打分对比、选最优的(这次的坑正是因为没有评估集、全凭感觉调,根本不知道改动是变好还是变差)。这套决策,让我做 RAG 时,从"固定长度一切了之"变成了"按文档结构切分、用数据驱动调优"——核心始终是:chunk 质量决定 RAG 上限,按文档类型用语义切分,并用评估集客观地迭代调优。
我立下的几条规矩
这场"chunking 拖垮 RAG"的事故,换来了我做 RAG 时,刻进骨子里的几条铁律:
- chunk 质量决定 RAG 上限。它是喂给模型的原材料,切不好再强的模型也救不了。
- 按语义边界切,别按固定长度硬切。别在句子/段落中间切断,保语义完整。
- chunk 间留重叠。10%~20% overlap,防关键信息正好在切口处被割裂。
- 按文档类型选策略。Markdown 按标题、FAQ 按问答对、代码按函数、长文递归切。
- 给 chunk 带元信息。章节标题等语境,让检索和模型都知道它属于什么。
- chunk 大小要实验调优。没有万能值,建评估集用真实问答对客观对比。
- 效果差先查上游。顺数据流从检索/chunk 往下游查,别一上来就换模型。
附:一个语义切分 + 重叠 + 元信息的 chunking 实现
口说无凭。下面把"按标题/段落语义切 + 重叠 + 带元信息"合到一个可用的 chunking 实现里:
import re
def smart_chunk(text, doc_name, max_size=800, overlap=150):
"""语义感知的文档切分: 按标题/段落切, 带重叠, 带元信息。"""
chunks = []
# 1. 先按 Markdown 标题切成"带标题的小节"(保留章节语境)
# 匹配 # / ## / ### 标题, 把文档分成 (标题, 正文) 的小节
sections = re.split(r'\n(?=#{1,3}\s)', text)
for section in sections:
# 提取这一节的标题(作为元信息)
title_match = re.match(r'#{1,3}\s*(.+)', section)
section_title = title_match.group(1).strip() if title_match else "正文"
# 2. 小节太大, 再按段落切, 并控制大小 + 重叠
if len(section) <= max_size:
chunks.append(_make_chunk(section, doc_name, section_title))
else:
paragraphs = section.split("\n\n")
current = ""
for para in paragraphs:
if len(current) + len(para) <= max_size:
current += para + "\n\n"
else:
if current.strip():
chunks.append(_make_chunk(current, doc_name, section_title))
# ★ 重叠: 新chunk带上上一个chunk的尾部, 防关联被割裂
tail = current[-overlap:] if len(current) > overlap else current
current = tail + para + "\n\n"
if current.strip():
chunks.append(_make_chunk(current, doc_name, section_title))
return chunks
def _make_chunk(content, doc_name, section_title):
content = content.strip()
# 3. ★ 关键: 在chunk内容前拼上"文档名-章节标题", 给模型和检索补充语境
enriched = f"【{doc_name} - {section_title}】\n{content}"
return {
"content": enriched, # 实际入库/喂模型的内容
"metadata": { # 元信息, 检索时可用
"doc": doc_name,
"section": section_title,
"length": len(content),
}
}
# 用法:
# chunks = smart_chunk(open("退款政策.md").read(), doc_name="退款政策")
# for c in chunks:
# embedding = embed(c["content"]) # 向量化(含语境的内容)
# vector_db.add(embedding, c["content"], c["metadata"])
# 核心: 先按标题切保章节语境 → 大节再按段落切控大小 → chunk间带重叠防割裂 →
# 内容前拼"文档名-章节"补语境 + 带metadata; 比固定长度硬切, RAG质量天差地别。
这个 smart_chunk,把这篇文章的 chunking 思路,落成了一个可以直接用的实现。它把好几个关键技巧编织在一起:先按 Markdown 标题切成"带章节语境的小节"(保留结构)、小节太大再按段落切并控制大小、chunk 之间带重叠(防关联割裂)、最关键的是在每个 chunk 内容前拼上"文档名-章节标题"(给检索和模型补充语境)、并附带 metadata。尤其那个"在内容前拼上章节标题"的细节——它让一个原本脱离语境的 chunk(如"需满足商品完好"),变成了带语境的"【退款政策-退货条件】需满足商品完好",无论是向量检索的相关性,还是模型理解的准确性,都会大幅提升。这,正是我想用这段代码,留给每个做 RAG 的人的最后一课:RAG 的"魔鬼"和"提升空间",大量地藏在 chunking 这种"看起来很基础、很不起眼"的数据预处理环节里。很多人(包括曾经的我)以为做 RAG 的功夫在"调模型、调检索"这些"高大上"的地方,却低估了"把数据切好、组织好、补充好语境"这种"朴实的数据工程"对最终效果的决定性影响。这也再次印证了那条朴素的真理:在数据驱动的系统里,"数据/输入的质量",往往比"算法/模型的精妙"更能决定成败;把基础的数据处理(清洗、切分、组织、补语境)做扎实,常常是性价比最高的优化。与其追逐更炫的模型,不如先把喂给它的数据,认认真真地切好、理好——这,是我从这场"RAG 答非所问"的事故里,带走的、最实在的领悟。
写在最后
回头看,这场由"chunking 切不好"引发的、RAG 答非所问的事故,真正教给我的,远不止"文档要按语义切"这一套技巧。它让我对"系统的瓶颈到底在哪"有了更清醒的认识。我排查时走的弯路,典型地反映了一个普遍的认知误区:当一个系统效果不好时,我们总是本能地去优化那个"最显眼、最'高级'、最让我们觉得'有技术含量'"的环节——对 RAG 来说,就是"模型"和"检索算法"。我花大力气换更强的模型、调更精的检索参数,却始终没去看那个最朴素、最上游、最不起眼的"文档切分"。可真相是:我的系统瓶颈,恰恰就在这个最不起眼的环节。这让我领悟到一个关于"优化"的深刻道理:一个系统的瓶颈,常常不在那些"看起来最复杂、最核心"的地方,而在那些"最基础、最上游、最容易被忽略"的地方;而我们的注意力,却总是不成比例地被那些"高级"的环节吸引,忽略了"地基"。这其实也呼应了一个经典的工程原则:"垃圾进,垃圾出(Garbage In, Garbage Out)"——如果你喂给系统的原材料(数据/输入)本身就是差的,那么无论你把下游的处理(模型/算法)做得多精妙,也产不出好的结果。所以,当一个系统效果不好时,我现在会先克制住"去优化高级环节"的冲动,转而问自己:"我喂给它的最原始的输入/数据,质量到底怎么样?会不会问题根本就出在最上游、最基础的地方?"——把目光投向那些不起眼的地基,常常能找到那个真正卡住一切的瓶颈。这,是我用一次"RAG 答非所问"的事故,换来的、关于 AI、也关于"瓶颈常在不起眼的上游"的、最朴素也最深刻的领悟。如果这篇复盘,能让你在 RAG 效果不好时,先去看一眼"检索出来的 chunk 到底切得怎么样",那我对着那些残缺混乱的检索结果熬的这大半天,就值了。
—— 别看了 · 2026