多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

RAG效果优化:JSON格式为何优于Markdown?

RAG效果优化:JSON格式为何优于Markdown? 提升RAG效果为何 JSON 格式远胜 Markdown做过RAG项目的朋友应该都有这种体验——知识库里的文档明明解析出来了向量也存进去了可是提问的时候结果总是差那么点意思。很多人第一反应是调embedding模型换rerank改prompt甚至重训微调但很少有人回头看最基础的问题喂给分块器的原始格式到底是Markdown还是JSON这个选择直接决定了我后面百分之八十的调优工作。今天想聊的不是那种用什么库、跑什么模型的入门教程而是我自己在真实项目里反复折腾之后的一个核心结论当知识库中的数据结构化程度足够高时JSON格式在RAG全链路上的表现明显优于Markdown。不是说Markdown一无是处而是它的自由在RAG里恰恰是检索质量不稳定的根源。这篇文章会从解析、分块、检索、生成四个环节逐一拆解并给出可以直接抄作业的落地指南。1. 先搞懂RAG调优到底在调什么1.1 RAG全链路里最容易被忽视的一环RAG系统的链路从表面上看很清晰文档加载 → 文本分块 → 向量化入库 → 用户提问 → 检索召回 → 拼接上下文 → 生成回答。但真正做过的人都知道这条链路的性能瓶颈往往不在模型而在最前面的文档加载和文本分块这两个环节。道理很简单如果原始内容在分块阶段就被切得语义破碎后面无论用多强的embedding模型、多贵的rerank都很难把丢失的上下文找回来。这就像做菜食材在备菜阶段就已经切坏了后面的刀工和火候再讲究也无济于事。我自己早期的项目就是在这一步吃了大亏花了大把时间在召回率、重排序上做参数搜索结果发现换了一个更合理的分块输入格式之后效果提升远比调参来得明显。做个最直观的对比。同样是处理一份包含产品手册、FAQ、接口文档的企业知识库用Markdown分块后一个块里可能既有表格说明、又有代码示例、还夹杂着段落正文而把同一内容转成JSON结构后每个字段都有明确的边界和语义归属。前者在向量化时会被迫把不相关的语义混在一起后者则可以精确地按照字段粒度进行向量化。这一进一出检索的精确度差距就拉开了。1.2 从一次客服问答系统的翻车说起我接手过一个企业智能客服项目知识库里有几百份产品文档和工单记录。最初方案很简单把PDF和Word转成Markdown按固定字符数切开丢进向量库完事。上线后发现一个特别典型的问题——用户问退款周期是几个工作日系统召回回来的内容往往是整篇的售后政策正文里面包含了大量无关描述最终生成的回答非常啰嗦还动不动把退货条件和退款时限混为一谈。这个问题表面上像是embedding模型不够聪明但实际排查后我发现根子就在分块。原始文档转成Markdown后退款政策、退货条件、物流时效都在一个章节里甚至一个段落里固定长度切分时完全无法保证块内的语义纯净度。当我尝试把同样的文档改造成JSON结构——把退款政策、退货条件、物流时效拆成独立的键值对和嵌套对象——分块器就能沿着JSON的边界精确切分每个块里的信息都是高度内聚的。同样的embedding模型召回准确率起来了回答的幻觉也少了很多。这个项目经验让我养成了一个习惯在优化RAG之前先审视知识库的底层格式。如果你的RAG现在效果不好不要急着换大模型或调参数先去看一眼知识库里到底是什么结构。2. Markdown在RAG里的三个老大难2.1 层级嵌套的语义被切得七零八落Markdown用#号表示标题层级但它的层级关系在纯文本层面非常脆弱。比如一个三级标题下面的内容可能在分块时被硬生生地拦腰截断导致半个条目留在上一个块半个条目进到下一个块两块都没法独立表达完整含义。我用一个极简例子来说明## 退款政策说明 ### 七天无理由退货 消费者在签收后七天内可以申请无理由退货。退货商品应当保持完好不影响二次销售。 ### 退款时限 退货审核通过后退款将在3-5个工作日内原路返还。如果分块器恰好把七天无理由退货的段落切进前一个块而把退款时限开头的标题切到后一个块那么前一个块里就会出现一个没头没尾的退款政策说明后一个块则会带着退款时限的标题却把退货商品应当保持完好这半句留在了上一块。向量化之后两个块都变成语义残缺的垃圾。这里的问题本质在于Markdown的嵌套层级是通过行首符和缩进视觉上表达的而不是通过语法上的强约束。解析器在处理扁平字符串时很难精确判断一个块是否应该在此处结束。一旦分块尺寸和文档的自然段落错位层级语义就必然被切碎。2.2 表格解析在向量化时容易水土不服Markdown的表格是通过管道符和分隔行描述的这在人眼看来很清晰但对embedding模型来说却是另一回事。一个三行五列的表被转成Markdown文本后每一行的管道符和空格都会进入向量计算的token序列。举个例子一个员工信息表姓名部门职位入职时间状态张三技术部后端工程师2023-04-01在职李四市场部运营专员2022-11-15在职转成Markdown后字符串里充满了|和---这种符号。如果分块器没有特殊的表格处理逻辑这些符号就会混入文本语义导致向量化时注意力被分散。更重要的是表格的行列对应关系在扁平字符串里完全丢失——张三和技术部的关联只靠管道符位置暗示模型很难稳定地学到这种对应关系。而JSON格式下同一个表可以变成数组对象{ employees: [ {name: 张三, department: 技术部, title: 后端工程师, join_date: 2023-04-01, status: 在职}, {name: 李四, department: 市场部, title: 运营专员, join_date: 2022-11-15, status: 在职} ] }键值对天然建立了字段与值之间的联系embedding模型在编码时能明确感知department是部门含义、status是状态含义。这种结构信息对语义理解是极大的帮助。2.3 代码块与段落混排拖累检索精度技术类知识库里经常出现一段说明文字后面跟着一段代码示例的情况。Markdown里代码块用三个反引号包裹看似边界清晰但嵌入向量时代码和说明文字会被当成一段连续的文本进行编码。说明文字的语言风格是自然语言代码的语言风格是编程语法两者混在一起会让embedding模型的输出空间变得很尴尬它既不能很好地对齐自然语言语义也不能准确表征代码结构。用户如果问这段代码里的logger配置是什么意思检索系统很可能把整段说明文字和代码一起召回回答时又不得不把大量无关的代码片段塞进上下文既浪费token又增加幻觉风险。JSON则能优雅地拆开这两类内容{ topic: logger配置, explanation: logger用于记录系统运行日志level字段控制日志输出级别。, code_sample: { language: python, content: import logging\nlogging.basicConfig(levellogging.INFO) } }explanation和code_sample成为独立字段分块器可以只对explanation做语义向量化把代码示例作为引用内容附带存储。需要代码时单独检索代码字段需要解释时单独检索解释字段互不干扰。3. JSON结构化在RAG里的底层优势3.1 边界可控分块器第一次有了指路牌我用过一个很直观的类比来解释JSON和Markdown的区别Markdown像一篇没有标点的小说读者只能靠段落和换行来猜语义边界JSON像一份填写完整的表格每个单元格都有自己的地址。在RAG分块阶段这个地址有多重要我实测过同样的文档内容Markdown分块后一个块的平均主题漂移程度明显高于JSON分块。所谓主题漂移就是一块里混入的无关主题数量。JSON分块器可以沿着对象边界切开比如按数组元素切、按键值对切、按嵌套层级切每一块都是完整且有明确主题的。这种可控性还体现在对复合文档的处理上。一份企业规章制度可能包含报销流程、请假规定、绩效考核如果全都平铺在Markdown里分块器很难分清边界。但转成JSON后{ document: { title: 员工手册, sections: { reimbursement: {...}, leave_policy: {...}, performance_review: {...} } } }我可以精准地让每个sections字段成为一个独立的检索单元。用户问报销需要几张发票时只召回reimbursement字段对应的内容块而不是把整个员工手册都捞出来。3.2 细粒度检索让查得准从源头开始RAG的核心指标不外乎两个查得准精度和查得全召回。大多数调优方案的思路是在检索阶段用各种技巧提高准确度。但如果你在数据入库时就能让每个向量对应一个明确的语义单元那检索精度从源头就已经有保障了。JSON格式天然支持这种原子级粒度。比如一个商品知识库每个商品有名称、价格、库存、描述、评价等字段。Markdown模式下一个商品可能被切成好几个块价格信息和评价内容散落在不同块里JSON模式下向量化可以精细到字段级别用户查询某某商品还有货吗直接命中stock字段查询某某商品好不好用命中reviews字段。实际项目中我还发现JSON格式对属性组合查询特别友好。比如用户问2000元以内、支持无线充电、续航超过两天的手机Markdown文档很难把这三个属性通过一个分块同时覆盖而JSON里这三个属性本来就在同一个商品对象的不同字段里只要分块粒度设计合理一个向量就能包含全部关键信息。3.3 数据清洗与转换的零成本福利做RAG项目的人都知道知识库里的原始数据往往是非常脏的。PDF转出来的文本可能有乱码、页眉页脚混入正文、段落顺序错乱。这些问题在Markdown格式下极难自动修复只能靠人工清理。JSON格式有一个隐蔽的福利它强制数据经过了结构化校验。只要你能把内容转成合法的JSON说明你已经经过了成对括号合法字符串类型正确等多重检查。数据在这一步就已经被清洗过一轮了。更重要的是JSON数据可以用非常成熟的工具链进行二次处理和校验。我用jq做字段提取和结构验证用JSON Schema来校验字段类型和必填项这些工具生态远比Markdown处理工具成熟。一旦数据格式合法后续无论是改字段名、增加嵌套层级还是筛选特定类型的内容都是一行命令的事不需要写一堆脆弱的正则表达式去匹配Markdown符号。3.4 故障可观测性出问题能快速定位在RAG系统上线后维护阶段最痛苦的不是模型效果波动而是不知道问题出在哪一环。如果知识库是Markdown格式你很难判断某个回答不准确究竟是原始文档信息缺失还是分块切坏了还是embedding编码出了问题——因为Markdown本身没有结构约束你在任何一个环节都找不到一个清晰的锚点来定位问题。JSON格式给了一个非常好的诊断抓手。假设用户提问后系统回答有误我可以直接回溯到JSON中的对应对象检查该对象的分块是否合理、字段是否完整、向量化的内容是否包含噪音。例如我可以快速筛选出包含refund_policy字段但没有amount字段的商品记录定位到数据缺失的具体位置。这种结构级Debug能力在维护多知识库、多文档的复杂RAG系统时非常实用。4. 实操复盘如何设计一个高可用的JSON分块器4.1 数据源改造把Markdown转成JSON如果知识库里的内容已经是Markdown第一步要做的是结构化改造而不是直接上分块。我这里提供一个我常用的预处理路径针对的是最常见的场景知识库内容存在数据库或CMS系统里但其正文是以Markdown格式存储的。在这类场景下我第一时间想到的是利用已有的元数据和层级关系做反转。比如数据库里如果已经存储了标题分类发布日期正文这些字段就不要偷懒把这些字段拼成一个Markdown文档而应该直接把字段作为JSON对象的键{ article_id: A001, title: 退款政策说明, category: 售后服务, published_at: 2025-01-15, content_blocks: [ {type: paragraph, text: 消费者在签收后七天内可以申请无理由退货。}, {type: list, items: [商品保持完好, 不影响二次销售]}, {type: code, language: python, content: def refund(): ...} ] }这一步看似只是格式转换但其本质是在做语义单元划分。content_blocks数组里的每个元素都对应一个独立语义单元后续分块器只需要决定几个元素拼成一个块而不需要担心语义被切开。这里我强烈建议利用好已有的数据库元数据而不是试图从Markdown文本里感知结构——从文本里反推结构是吃力不讨好的做法。如果确实只能拿到纯Markdown文本也没有别的元数据可用我的建议是用一款支持Markdown AST解析的工具把文档解析成语法树AST再按照标题层级将AST节点分组成JSON对象。这里的要点是转换的边界必须落在标题层级上能拆到多细就拆到多细后续的分块才有足够的灵活性。注意分块粒度不是越细越好而是能够在字段层面自包含最好比如返回退款规定这种完整主题对象而不是把退款和规定拆开。4.2 分块算法的核心要点如果你要自己实现一个JSON分块器核心思路非常简单递归遍历JSON遇到叶子节点时收集内容遇到对象边界时考虑是否成块。我给出一个轻量级的Python实现示例方便你直接改造成自己的逻辑。import json def recursive_json_splitter(data, max_chars800): blocks [] buffer [] def flush(): if buffer: blocks.append(\n.join(buffer)) buffer.clear() def walk(obj, path): if isinstance(obj, dict): for key, value in obj.items(): walk(value, f{path}.{key} if path else key) elif isinstance(obj, list): for idx, item in enumerate(obj): walk(item, f{path}[{idx}]) else: text f{path}: {obj} if len(\n.join(buffer) \n text) max_chars: flush() buffer.append(text) walk(data) flush() return blocks这个实现的核心逻辑是把每个键值对转成路径 内容的文本形式。比如{name: 张三}会变成name: 张三。当一个缓冲区里的内容加起来超过max_chars时就把缓冲区的内容作为一个块输出。这个方案的优势在于键路径天然携带了语义上下文向量化时employees[0].name: 张三比单纯的张三信息量更大。同时块的切分点永远落在某个叶子节点的边界上不会出现切掉半个值的情况。实际使用时我通常会把max_chars设为300-1000之间具体取决于你的embedding模型最大token数。比如OpenAI的text-embedding-3-small原生支持8191个token那这个值就可以放宽到2000字符左右。但如果用的是sentence-transformers的mini模型建议控制在500字符以内。我的经验是与其依赖Embedding模型处理长文本不如把分块控制在1000字符以内并保证语义内聚这样检索精度会稳定很多。4.3 向量化时的字段级策略有了JSON结构之后向量化不再需要一把抓而是可以做字段级的差异化处理。以商品对象为例{ product_id: P1001, name: 无线充电器, price: 199, description: 支持Qi协议最大功率15W, specs: { weight: 80g, color: 白色, input: USB-C } }对于这种结构我会把name description specs拼接成一个向量因为这三者共同决定了商品的语义表示而price通常会单独存成一个filter字段不参与向量化或只在数值查询时使用。这样设计的好处是当用户问支持Qi协议的充电器时向量检索能精确命中description和specs的内容不会被价格数字干扰。如果你的JSON里嵌套层级特别深我还建议在向量化时做一次结构压缩——把多层嵌套的对象拍平成两级。因为embedding模型对深度嵌套的语义理解能力有限过度嵌套反而会让向量表示变得模糊。拍平方法很简单把嵌套对象的完整路径作为前缀拼进文本即可。5. 工程落地时最容易踩的六个坑说完了方法论这部分我讲讲自己在实际项目中踩过的坑基本都能对号入座。5.1 failed to deserialize the json body解析失败高发场景我先说一个最常见的报错failed to deserialize the json body into the target type: input: missing field。这个错误字面上看是说JSON解析时缺少了某个字段但绝大多数情况并不是真的字段缺失而是你的JSON字符串本身有语法问题——多余的逗号、不成对的引号、类型不匹配等。从我经验看最容易触发这个问题的场景有三个从Excel导出的数据自动转JSON时空单元格变成了null程序却期望一个字符串。手工拼接JSON字符串时忘了转义特殊字符比如HTML标签里的。从API拿数据时返回了带BOM头的编码格式解析器不识别开头字节。避坑建议不要在业务代码里手写JSON解析逻辑尽量用成熟的反序列化库让编译器自己去处理类型映射。同时所有进入知识库的JSON数据都应该先用JSON Schema校验一遍字段类型和必填项不合格的直接进待修复队列而不是强行塞进RAG管线。这里也补一个排查技巧遇到deserialize错误时先打印原始JSON字符串的前后50个字符看看有没有隐藏的特殊字符、中文引号、全角冒号这类隐形杀手。我遇到过不下三次都是因为某份文档是从网页复制过来的引号和冒号被自动替换成了全角版本肉眼完全看不出来解析器却立刻报错。5.2 非法JSON注入导致的分块崩溃JSON的语法要求比Markdown严格得多——不定时地出现一个多余的逗号或未转义的引号整个解析就会失败。Markdown里哪怕格式乱一点人眼还是能看懂内容但JSON不行一个非法字符就能让整块数据无法被加载。这个问题的典型触发场景是知识库内容是从用户提交的表单、评论或工单里采集的天然携带大量非结构化的脏文本。把这类文本塞进JSON字段时如果不做转义处理很容易产生非法JSON。我的解决方案是在入库前增加一道清洗层所有文本字段统一做html.unescape和json.dumps转义。字段值里不允许出现的控制字符如\x00直接剥离。遇到过长或明显异常的字段值自动截断并打上告警标记。这一步会消耗一些计算资源但能避免在线RAG服务在运行时突然崩溃。5.3 JSON设计得太业务化导致检索粒度错位这是我自己走过的一段弯路。最初设计知识库JSON时我完全按照业务表格的结构来设计字段比如把订单表的所有列都原样塞进JSON。这样做的结果是一个订单对象有几十个字段向量化时所有字段被揉在一起用户问这个订单的物流单号是多少系统却把整单的金额、商品、发票信息全部召回。后来我意识到JSON的字段设计必须服务于检索需求而不是服务于业务存储需求。我的调整思路是把高频检索字段如订单号、物流单号、状态单独、扁平地暴露在JSON顶层把低频细节字段如扩展属性、附属备注收敛到嵌套对象里并在向量化时只对检索型字段做embedding其他字段作为过滤条件使用。这么一改单次查询的召回准确率提升了20%以上。5.4 Chunking策略与Embedding最大长度不匹配Embedding模型都有最大输入长度限制如果你的JSON路径文本太长嵌入时可能出现截断。举例来说一个对象嵌套了五层路径是store.employees[3].contact_info.phone再加上字段值本身文本长度很容易超过embedding模型的512 token限制。这里我分享两个规避办法对超长文本字段做摘要再向量化原始值存到单独的详情字段里检索时先命中摘要再读取详情。控制JSON嵌套深度不超过3层。嵌套越深文本拼接时的前缀越长信息密度反而降低。5.5 混合检索时关键字与向量的权重失衡在JSON结构化知识库中做混合检索向量检索 关键词检索时很容易遇到一个问题关键词检索出来的结果全是某个字段的碎片而向量检索的结果又过于整体。两者融合不足时最终结果反倒不如单一检索模式。我的经验是关键词检索在JSON场景下应该针对字段名而不是全文。例如用户搜退款时限关键词只匹配到refund_deadline这个键而不是匹配到退款 时限在全文中的任意位置。向量检索负责语义扩展关键词检索负责精确锚定两者各司其职融合后的准确性才是最稳的。5.6 数据更新时没有做增量重向量化JSON结构化之后如果某个字段的值发生了变化但你不重新向量化那旧的向量就会跟新的内容打架。我在维护知识库时踩过这个坑有一次把退款政策里的时限从7天改成了15天但因为只更新了字段文本、没有触发重新向量化用户问退款多久到账时仍然命中旧向量回答了错误的7天。后来我引入了一个简单的版本号机制每个JSON对象里加一个update_version字段数据更新时递增。增量更新任务只对版本号变化的对象重新向量化未变化的则直接跳过。这既保证了数据时效性也避免了全量重建带来的计算开销。6. 避坑备忘可直接复用的JSON-RAG速查表如果你正打算把知识库从Markdown迁移到JSON下面这份速查表是我结合多个项目经验整理的检查清单建议在动手之前逐项过一遍。检查项最佳实践可能遇到的问题数据来源优先从数据库/API取结构化字段非结构化文本需要清洗和转义JSON字段设计高频检索字段放顶层低频细节字段嵌套业务字段过度嵌套导致检索粒度错位嵌套深度控制在3层以内超过3层拍平嵌套过深导致embedding前缀过长信息密度降低字符转义入库前统一做html.unescape json.dumps全角引号、BOM头等隐形字符导致解析报错分块大小300-1000字符视embedding模型而定块太短丢失上下文块太长超出token限制混合检索关键词匹配字段名向量匹配语义权重失衡导致精确性和泛化性互相干扰增量更新增加update_version字段仅重向量化变更对象旧向量未更新导致回答过期故障排查记录JSON路径和分块映射关系问题难定位无法回溯到具体结构6.1 从Markdown切换到JSON的迁移路径如果你现在手里已经有一套基于Markdown的RAG系统担心大改伤筋动骨我的建议是不需要推翻重来而是渐进式迁移。第一阶段先把知识库里结构化程度最高的文档如产品参数、政策条款、FAQ转成JSONMarkdown格式只保留给博客文章、新闻报道这类叙事性内容。第二阶段在检索层加一个格式标签——JSON类内容走结构化分块检索Markdown类内容继续走原来的固定长度分块。两套结果做融合后再统一进rerank。第三阶段观察两类来源的检索命中率、回答准确率用数据判断是否需要把更多内容迁移到JSON。我建议不要追求一步到位这类改动如果一次覆盖范围太大出问题时排查成本会高到让人崩溃。6.2 不同体量项目的配置参考针对不同体量的项目我整理了三套可以直接套用的配置方案方案A轻量级个人博客、小型知识库数据量少于1000条JSON对象分块策略直接使用recursive_json_splittermax_chars设为500向量化任意开源embedding模型存储SQLite sqlite-vec或直接存内存List这套方案的特点是实施简单不需要额外基础设施最多半天就能跑通。方案B中型企业部门级知识库数据量1万-20万条JSON对象分块策略按字段分组向量化区分description字段与metadata字段向量化text-embedding-3-small级别存储pgvector或Milvus检索向量检索 关键词检索混合用RRF算法融合分数这套方案我实际验证过多次是性价比最高的组合兼顾了效果和运维成本。方案C大型多业务线、多知识域数据量百万级以上分块策略细粒度原子块 父子块结构父块存上下文子块做检索向量化根据内容语言、领域训练多个embedding模型做多向量路由存储分布式向量库分片 副本检索多路召回 rerank 业务过滤规则这套方案适合做agentic RAG让agent在不同知识域之间做路由把用户的query先路由到正确的JSON子集再进行联合检索。它的调优难度会更高但从架构上已经是生产级形态。7. 写在最后从能用到好用的那一步回看自己做过的所有RAG项目最后悔的往往不是选错了模型或参数调得不到位而是在一开始就没有认真思考喂给RAG的数据到底应该长什么样。很多人包括曾经的我默认拿Markdown文档就直接开干因为它简单、通用、人眼看着舒服。但RAG系统的服务对象不只是人还有向量模型和检索算法——它们对结构远比人敏感得多。JSON格式之所以在RAG场景里远胜Markdown说到底就一句话它让语义边界从人眼的直觉变成了机器的强约束。向量检索没有了模糊边界召回结果自然更稳定。如果你现在刚好在优化RAG效果我真心建议你先别急着换模型和调参花半天时间把知识库里最核心的几十条内容改造成JSON对比一下检索质量的变化大概率你会发现之前折腾了那么久的调优其实都绕了远路。
返回列表