CSDN 原文镜像
本文为作者 CSDN 博客的全文镜像,原文发布于 2026-06-10。为适配本站结构,仅补充了站内元数据与来源说明,正文主体保持原文内容。
- 原文链接:https://blog.csdn.net/m0_63309778/article/details/161868156
- 站内分区:RAG / RAG 文档分块

RAG 优化实践:如何解决 Markdown 表格截断、代码块丢失和图片语义缺失问题
很多人做 RAG 优化时,第一反应是调参数:chunk_size 要不要大一点?top_k 要不要多召回几个?embedding 模型要不要换?rerank 要不要加?
这些当然重要,但在真实项目里,我越来越感觉到一个问题:很多 RAG 效果差,并不是模型不够强,也不是向量库不够好,而是文档在进入向量库之前,结构已经被破坏了。
尤其是 Markdown、PDF 转 Markdown、Word 转 Markdown 这类复杂文档,经常会出现几个典型问题:
- Markdown 表格被截断,表头和数据行分离;
- 代码块被切成两半,函数逻辑不完整;
- 图片只剩一个 OSS 地址,没有任何语义;
- 标题、正文、表格、图片之间的上下文关系丢失;
- 检索命中了片段,但真正回答时缺少完整证据。
这篇文章想聊的不是某一个具体框架,而是一个更工程化的问题:复杂文档进入 RAG 系统之前,应该如何做结构保护、语义增强和多表示检索。
一、为什么普通分块会破坏 Markdown 文档?
很多 RAG 系统一开始都会使用固定长度分块,比如按照 500、800、1000 tokens 切分。这种方式对普通段落文本还算可用,但对 Markdown 文档来说非常危险。
比如下面这个表格:
| 字段 | 类型 | 说明 |
|------|------|------|
| user_id | string | 用户唯一标识 |
| created_at | datetime | 创建时间 |
| status | int | 用户状态 |如果分块器按照字符数一刀切,很可能切成这样:
| 字段 | 类型 | 说明 |
|------|------|------|
| user_id | string |另一个 chunk 里是:
用户唯一标识 |
| created_at | datetime | 创建时间 |
| status | int | 用户状态 |这样一来,问题就很明显了:
第一,表头和数据行可能分离。
第二,某些单元格语义不完整。
第三,embedding 时表格语义变弱。
第四,检索命中了片段,但 LLM 看不到完整上下文。
第五,回答时容易出现“看起来引用了资料,但其实资料不完整”的幻觉。
所以复杂文档的 RAG 优化,第一步不是调大 chunk_size,而是先问一个问题:
当前分块方式有没有破坏原始文档的结构?
二、哪些内容应该被保护?
在 Markdown 文档里,有一些内容不能被普通分块器随意切开。我把这类内容称为:
Atomic Semantic Block,原子语义块。
所谓原子语义块,就是一旦被切断,语义就会明显受损的内容。
常见类型包括:
| 类型 | 为什么要保护 | 推荐处理方式 |
|---|---|---|
| Markdown 表格 | 表头、字段、行列关系一旦断开,语义就残缺 | 小表整体保留,大表按行分组但重复表头 |
| 代码块 | 函数、类、SQL、JSON 被截断后难以理解 | fenced code block 整体保护 |
| 图片 OSS 地址 | URL 本身没有语义,但图片可能包含关键证据 | 提取图片上下文,生成图片摘要 |
| Mermaid / PlantUML | 流程图关系不能被切断 | 整体保存,并生成流程摘要 |
| JSON / YAML | 层级结构被切断会导致字段含义丢失 | 按对象层级切分 |
| SQL | join、where、group by 等逻辑断开会误导模型 | 按完整 SQL 或逻辑段切分 |
| 法条 / 合同条款 | 条款上下文连续性强 | 按条款编号和标题层级切分 |
| 公式 / 配置项 | 单独一部分通常没有意义 | 尽量整体保留 |
这类内容不能简单按长度切。它们需要先被识别出来,然后作为独立 block 进入后续处理流程。
三、表格分块的关键:不是绝对不切,而是结构保真
表格是 RAG 里最容易出问题的内容之一。
很多人会说:“那我把表格整体保留不就行了吗?”
这只适合小表格。真实企业文档里的表格可能非常长,比如项目清单、风险清单、预算明细、人员名单、设备清单、合同条款对照表。如果一个表格几千行,全部塞进一个 chunk 并不现实。
所以表格分块的核心不是“不切”,而是:
切完以后,仍然保留表格的语义结构。
可以分成几种情况处理。
1. 小表格:整体保留
如果表格本身不大,最好的方式就是完整作为一个 block 保存。
表格标题:用户字段说明表
| 字段 | 类型 | 说明 |
|------|------|------|
| user_id | string | 用户唯一标识 |
| created_at | datetime | 创建时间 |
| status | int | 用户状态 |这种情况下,表头、字段、说明都在一起,检索和生成都比较稳定。
2. 中等表格:整体保存原文,同时生成摘要
中等表格可以完整保存 raw table,同时额外生成一份语义摘要。
原始表格用于回答,摘要用于检索。
例如原始表格是:
| 指标 | 2022 | 2023 | 2024 |
|---|---:|---:|---:|
| 营收 | 1200 | 1500 | 2100 |
| 毛利率 | 28% | 31% | 35% |可以生成摘要:
该表展示了公司 2022 年至 2024 年的核心经营指标,包括营收和毛利率。
营收从 1200 增长到 2100,呈持续增长趋势;毛利率从 28% 提升到 35%,说明盈利能力同步增强。
适合回答关于营收变化、毛利率变化、经营趋势、财务表现的问题。这样用户问“公司近三年经营趋势如何”时,不一定能直接命中原始表格,但很容易命中这段摘要。
3. 超长表格:按行分组,但每个子表重复表头
如果表格太长,可以按行分组切分,但每个子表都要重复表头、标题和必要说明。
比如:
表格标题:项目风险清单
字段说明:风险编号、风险类型、风险描述、影响范围、整改建议
| 风险编号 | 风险类型 | 风险描述 | 影响范围 | 整改建议 |
|---------|---------|---------|---------|---------|
| R001 | 权限风险 | ... | ... | ... |
| R002 | 数据风险 | ... | ... | ... |下一个 chunk 不应该只保留数据行,而应该继续重复上下文:
表格标题:项目风险清单
字段说明:风险编号、风险类型、风险描述、影响范围、整改建议
| 风险编号 | 风险类型 | 风险描述 | 影响范围 | 整改建议 |
|---------|---------|---------|---------|---------|
| R003 | 流程风险 | ... | ... | ... |
| R004 | 合规风险 | ... | ... | ... |这样即使只召回其中一个子表,模型也知道这些数据属于哪个表,每列代表什么含义。
4. 复杂表格:不要只靠 RAG,要结构化入库
如果表格涉及计算、筛选、排序、多表关联,就不应该完全依赖普通向量检索。
比如用户问:
2024 年预算超过 100 万的项目有哪些?
这类问题本质上是结构化查询,不是普通文本问答。
更好的方式是:
简单表格:Markdown 原文 + 摘要
中等表格:转 JSON / CSV,再交给 LLM 理解
复杂表格:入库为 SQL / DataFrame,通过工具查询后再让 LLM 解释也就是说:
文本解释类问题 → RAG
表格查数类问题 → RAG + SQL / Pandas
表格计算类问题 → 查询工具 + LLM 解释四、代码块也不能被随意截断
Markdown 里经常会有代码块:
<span class="token keyword">def</span> <span class="token function">chunk_markdown</span><span class="token punctuation">(</span>text<span class="token punctuation">)</span><span class="token punctuation">:</span>
blocks <span class="token operator">=</span> parse_markdown_blocks<span class="token punctuation">(</span>text<span class="token punctuation">)</span>
<span class="token keyword">return</span> merge_blocks<span class="token punctuation">(</span>blocks<span class="token punctuation">)</span>如果代码块被截断,模型可能只看到一半函数,既无法理解完整逻辑,也无法回答实现细节。
代码块建议遵循几个原则:
- fenced code block 整体识别;
- 短代码块整体保存;
- 超长代码按函数、类、SQL 语句、配置段切分;
- 保存语言类型,比如 Python、Java、SQL、YAML;
- 额外生成代码摘要,用于语义检索。
比如可以为代码块生成这样的摘要:
该代码实现了 Markdown 文档的结构化分块逻辑,主要用于识别并保护 fenced code block、Markdown table、image URL 等不可切分结构,避免普通字符分块破坏语义完整性。为什么要给代码块生成摘要?
因为用户的问题往往是自然语言,比如:
这个系统是怎么防止表格被切断的?
而原始代码里不一定包含“防止表格被切断”这几个字。如果只对代码本身做 embedding,未必能召回。给代码块加一层自然语言摘要后,检索效果会明显更好。
五、图片 OSS 地址不能只是一个 URL
企业文档里经常有这样的内容:
如果直接把这个 URL 放进向量库,它几乎没有语义价值。embedding 模型看到的只是一个字符串,而不是图片内容。
但是这张图片可能非常重要。它可能是系统架构图、流程图、网络拓扑图、页面截图、合同扫描件、设备照片、审计证据图片。
所以图片应该被处理成一个 image block:
<span class="token punctuation">{<!-- --></span>
<span class="token string-property property">"type"</span><span class="token operator">:</span> <span class="token string">"image"</span><span class="token punctuation">,</span>
<span class="token string-property property">"raw_url"</span><span class="token operator">:</span> <span class="token string">"https://xxx.oss-cn-shanghai.aliyuncs.com/arch.png"</span><span class="token punctuation">,</span>
<span class="token string-property property">"alt_text"</span><span class="token operator">:</span> <span class="token string">"系统架构图"</span><span class="token punctuation">,</span>
<span class="token string-property property">"caption"</span><span class="token operator">:</span> <span class="token string">"图 3-1 智能审计系统总体架构"</span><span class="token punctuation">,</span>
<span class="token string-property property">"surrounding_text"</span><span class="token operator">:</span> <span class="token string">"本系统采用前后端分离架构……"</span><span class="token punctuation">,</span>
<span class="token string-property property">"image_summary"</span><span class="token operator">:</span> <span class="token string">"该图展示了智能审计系统总体架构,包括用户层、应用层、AI 能力层、数据层和基础设施层。"</span>
<span class="token punctuation">}</span>图片类内容至少应该保存四类信息:
1. 图片 URL / OSS key
2. 图片标题 / alt / caption
3. 图片前后正文上下文
4. 多模态模型生成的图片摘要如果后续支持多模态问答,可以把原图 URL 传给多模态模型;如果暂时只做文本 RAG,也至少可以通过图片摘要回答“这张图大概表达了什么”。
六、复杂块要做语义摘要索引
表格、代码块、图片、流程图都有一个共同特点:
原始内容适合回答,但不一定适合检索。
比如表格里可能全是数字,没有“增长趋势”“经营情况”“风险变化”这类自然语言表达。
代码里可能全是函数名和变量名,没有“这个函数用来解决什么问题”的描述。
图片里可能只有一个 OSS 地址,没有任何语义。
所以对这些复杂 block,建议引入大模型生成语义摘要。检索时检索摘要,回答时返回原始内容。
这就是一种典型的“双层结构”:
检索层:summary / keywords / hypothetical questions / metadata
证据层:raw table / raw code / raw image url / original markdown可以把每个 block 存成类似结构:
<span class="token punctuation">{<!-- --></span>
<span class="token string-property property">"block_id"</span><span class="token operator">:</span> <span class="token string">"doc_001_table_003"</span><span class="token punctuation">,</span>
<span class="token string-property property">"type"</span><span class="token operator">:</span> <span class="token string">"table"</span><span class="token punctuation">,</span>
<span class="token string-property property">"parent_title"</span><span class="token operator">:</span> <span class="token string">"三、经营数据分析"</span><span class="token punctuation">,</span>
<span class="token string-property property">"raw_content"</span><span class="token operator">:</span> <span class="token string">"原始 Markdown 表格"</span><span class="token punctuation">,</span>
<span class="token string-property property">"summary"</span><span class="token operator">:</span> <span class="token string">"该表展示了公司近三年营收、毛利率和成本变化趋势……"</span><span class="token punctuation">,</span>
<span class="token string-property property">"keywords"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"营收"</span><span class="token punctuation">,</span> <span class="token string">"毛利率"</span><span class="token punctuation">,</span> <span class="token string">"经营指标"</span><span class="token punctuation">,</span> <span class="token string">"增长趋势"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
<span class="token string-property property">"hypothetical_questions"</span><span class="token operator">:</span> <span class="token punctuation">[</span>
<span class="token string">"公司近三年的营收变化如何?"</span><span class="token punctuation">,</span>
<span class="token string">"毛利率是否有提升?"</span><span class="token punctuation">,</span>
<span class="token string">"经营数据反映了什么趋势?"</span>
<span class="token punctuation">]</span><span class="token punctuation">,</span>
<span class="token string-property property">"metadata"</span><span class="token operator">:</span> <span class="token punctuation">{<!-- --></span>
<span class="token string-property property">"source"</span><span class="token operator">:</span> <span class="token string">"xxx.md"</span><span class="token punctuation">,</span>
<span class="token string-property property">"page"</span><span class="token operator">:</span> <span class="token number">12</span><span class="token punctuation">,</span>
<span class="token string-property property">"section_path"</span><span class="token operator">:</span> <span class="token string">"经营分析/财务指标"</span><span class="token punctuation">,</span>
<span class="token string-property property">"oss_urls"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>这里有几个关键字段:
raw_content:原始证据,用于最终回答;summary:自然语言摘要,用于语义检索;keywords:关键词,用于 BM25 或混合检索;hypothetical_questions:可能命中的用户问题;metadata:来源、页码、章节路径、文件 ID 等可追溯信息。
这样检索时不只依赖原文,还可以依赖摘要、关键词、假设问题等多个入口。
七、多表示检索:不要只给一个 chunk 一个向量
传统做法通常是:
一个 chunk → 一个 embedding → 存入向量库但复杂文档更适合:
一个原始 block → 多个语义表示 → 多个 embedding → 指向同一个 block_id比如一个表格,可以有这些表示:
1. 原始表格向量
2. 表格摘要向量
3. 关键词向量
4. 假设问题向量
5. 标题路径向量检索时,这些向量都可以被召回,但它们最终指向同一个原始表格。
也就是说:
summary 被召回
↓
找到 block_id
↓
回填 raw_content
↓
把完整表格传给 LLM这样做的好处是:
- 摘要更容易被自然语言问题命中;
- 原文保留事实细节;
- block_id 保证摘要和原文能对应;
- 最终回答不会只依赖摘要,减少信息损失。
一句话总结就是:
用摘要提高召回,用原文保证答案可靠。
八、父子块回填:小块负责召回,大块负责回答
除了多表示检索,还有一个很重要的设计是父子块回填。
很多时候,小 chunk 更适合检索,因为它语义集中;但大 chunk 更适合回答,因为它上下文完整。
所以可以把文档拆成两层:
父块:章节级内容,保留完整上下文
子块:段落、表格、代码块、图片摘要,用于精确检索检索时命中子块,但最终返回父块。
例如:
用户问题:系统的数据同步机制是什么?
命中子块:
“该代码实现了基于消息队列的数据同步任务……”
回填父块:
“第四章 数据同步设计”整个章节,包含架构说明、流程图、代码块、异常处理策略。这样既避免了大 chunk 召回不准,也避免了小 chunk 上下文不够。
生产上可以这样设计:
child_chunk_id → parent_chunk_id → document_id检索链路:
用户问题
↓
检索 child chunks
↓
rerank
↓
根据 parent_chunk_id 回填父块
↓
构造 prompt
↓
生成答案九、一个推荐的 RAG 优化流水线
结合上面的思路,一个更完整的复杂文档 RAG 流水线可以是:
文档输入
↓
Markdown / PDF / Word 解析
↓
结构识别:标题、段落、表格、代码块、图片、列表、公式
↓
Atomic Block 保护
↓
结构化分块:父块 / 子块 / 特殊块
↓
LLM 语义增强:摘要、关键词、实体、假设问题
↓
多表示索引:raw chunk、summary、keywords、questions
↓
混合检索:向量检索 + BM25 + metadata filter
↓
重排序:reranker / LLM rerank
↓
父子块回填:返回完整原文证据
↓
答案生成:带引用、带来源、可追溯这套流程的重点不是“切块技巧”,而是完整的 RAG 数据治理链路。
在真实项目里,RAG 不是把文档丢进向量库就完事了,而是要考虑:
- 文档结构有没有被保留;
- 复杂内容有没有被摘要;
- 摘要和原文有没有映射关系;
- 检索结果是否能回填完整证据;
- 回答是否可追溯到原始来源。
十、保护型 Markdown Chunker 的设计思路
如果要自己实现一个保护型 Markdown Chunker,可以按这个思路做:
第一步:扫描 Markdown,识别 fenced code block
第二步:识别 Markdown table
第三步:识别 image syntax 和 OSS URL
第四步:识别 Mermaid / PlantUML 等流程图
第五步:识别标题层级,建立 section path
第六步:把特殊块替换成 placeholder
第七步:普通正文正常分块
第八步:把 placeholder 还原成完整 block
第九步:对特殊 block 生成 summary
第十步:写入 vector store + doc store示意:
## 系统架构
本系统采用前后端分离架构……
```mermaid
graph TD
A[前端] --> B[后端服务]
B --> C[向量数据库]
B --> D[大模型服务]| 模块 | 说明 |
|---|---|
| 检索层 | 负责召回相关知识 |
| 生成层 | 负责组织答案 |
处理后可以拆成:
```text
section_block_001:系统架构正文
diagram_block_001:Mermaid 流程图,完整保留
table_block_001:模块说明表,完整保留
summary_block_001:系统架构摘要这里最关键的是:特殊块不是被字符串切分器切开的,而是先被识别、保护、摘要,再进入索引。
十一、检索阶段建议:并行检索 + 合并去重 + rerank
复杂文档场景下,不建议只用单一路径检索。
更稳的方式是并行检索:
用户问题
↓
Query Rewrite / Query Decomposition
↓
并行检索:
- 原文 chunk 向量
- 摘要向量
- hypothetical questions 向量
- BM25 关键词检索
- metadata filter
↓
结果合并去重
↓
rerank
↓
根据 block_id 找 parent/raw block
↓
构造上下文
↓
LLM 生成答案其中有两个细节很重要。
第一,摘要可以参与检索,但不要只用摘要回答。
因为摘要本身是大模型生成的,可能会压缩、遗漏甚至轻微误读。
第二,召回后一定要回到原始证据。
比如命中的是表格摘要,最终上下文里应该放原始表格;命中的是图片摘要,最终上下文里应该放图片说明、图片 URL、前后正文;命中的是代码摘要,最终上下文里应该放完整代码块。
十二、评估 RAG,不要只看最终回答
很多人在评估 RAG 时,只看最终回答对不对。但这还不够。
因为最终回答对了,不代表检索链路是健康的;最终回答错了,也不一定是模型问题,可能是分块阶段已经把证据切坏了。
建议至少评估四层:
1. Block Integrity,块完整性
检查表格、代码块、图片、公式有没有被切断。
比如:
表格是否保留表头?
代码块是否保留完整 fenced code?
图片 URL 是否和 caption、上下文绑定?2. Retrieval Recall,召回完整性
用户问题对应的证据是否被召回。
比如用户问“毛利率变化趋势”,是否能召回对应表格或表格摘要。
3. Context Completeness,上下文完整性
召回结果是否包含足够回答的信息。
比如只召回了某一行表格,但没有表头,就算召回到了也不算合格。
4. Answer Faithfulness,答案忠实度
最终答案是否严格基于召回证据,是否出现编造、遗漏、误读。
这四层里,前两层经常被忽略,但它们恰恰决定了 RAG 的上限。
十三、总结:RAG 优化的本质是文档结构治理
很多 RAG 效果差,并不是因为模型不够强,而是因为知识进入系统时已经被破坏了。
表格被截断,模型看不到表头。
代码块被切断,模型看不到完整逻辑。
图片只剩 OSS 地址,模型不知道图片表达了什么。
标题和正文分离,模型失去章节语境。
摘要和原文没有映射,检索命中了也无法回填证据。
所以 RAG 优化的第一步,不是盲目换 embedding 模型,也不是简单调大 chunk size,而是先做好文档结构治理:
识别结构
保护结构
增强语义
多表示检索
回填原文证据
可追溯生成答案对于 Markdown、PDF、Word 这类复杂文档,真正有效的 RAG 优化应该是:
用结构保护保证知识不被切坏,用语义摘要提高召回能力,用父子块回填保证回答依据完整。
一句话总结:
RAG 的效果上限,不只取决于模型和向量库,更取决于知识进入系统时是否保持了原有结构。
写这篇文章的目的,不是为了证明某个技术有多先进,而是希望把一个真实问题拆开,讲清楚它背后的设计逻辑、工程取舍和落地路径。
AI 应用开发正在从“会调用模型”进入“会设计系统”的阶段。
模型只是起点,真正决定效果的,往往是数据、流程、工具、上下文、检索、评估、工程架构,以及持续迭代的能力。
我会继续围绕这些方向做系统化分享:
Agent、RAG、LLM 工程化、企业 AI 应用、私有化部署、系统架构与真实项目复盘。
希望这里不仅是一个技术博客,也能逐渐成为一个聚集 AI 应用开发者、产品实践者和工程落地者的交流空间。
dd-y 的技术博客:把想法落地,把技术讲透。
欢迎关注,一起把 AI 从 Demo 做到真正可用。
如果你也在关注 AI 应用落地、Agent 开发、RAG 系统、LLM 工程化、企业知识库、私有化部署 等方向,欢迎扫码加入我的技术交流群。
这里不会只聊概念,更希望一起交流真实项目中的问题、方案、踩坑经验和落地思路。
备注:如果二维码过期,可以私信我拉你进群。