Appearance
2.2.3 结构化数据和非结构化数据在 RAG 里分别怎么处理?
先给结论:结构化数据和非结构化数据都可以服务 RAG,但它们通常不应该用完全同一种方式处理。
因为这两类数据承载知识的方式不一样。
什么是结构化数据
结构化数据通常指有明确字段和模式的数据,比如:
- 数据库表
- CRM 记录
- 工单记录
- 订单数据
- 指标表
它的特点是:
- 字段明确
- 记录边界清楚
- 过滤条件容易表达
这类数据更像“可查询的事实集合”。
什么是非结构化数据
非结构化数据通常指自然语言文档或半结构化内容,比如:
- 网页正文
- 手册
- Wiki
- 会议纪要
- 产品说明
它的特点是:
- 语义主要在长文本里
- 结构边界较弱
- 更依赖切块和语义检索
这类数据更像“可阅读的知识材料”。
结构化数据常见的处理方式
对结构化数据来说,核心通常不是“先切块再语义检索”,而是:
- 保留字段含义和记录边界。
- 给记录建立明确的主键、时间、租户、权限等元数据。
- 优先使用过滤、查询、聚合等方式定位结果。
- 必要时再把查询结果转成模型可读上下文。
也就是说,结构化数据很多时候更接近:
- 检索增强查询
- 工具调用
- SQL 或条件过滤
而不只是普通向量检索。
更实际一点说,结构化数据最怕被错误地“文本化”。一旦你把字段、条件、时间范围、主外键关系都压成一段自然语言,再去做普通语义召回,就很容易损失:
- 精确过滤能力
- 数值比较能力
- 聚合统计能力
- 实时查询能力
所以结构化数据的重点往往不是 embedding 做得多漂亮,而是查询语义有没有被稳定保留下来。
非结构化数据常见的处理方式
对非结构化数据来说,重点通常是:
- 正确提取正文。
- 按合适粒度切块。
- 给每个块补充来源和结构信息。
- 建立语义检索和关键词检索能力。
- 在召回后做排序、去重和上下文构造。
这类链路更符合人们平时说的“经典 RAG”。
非结构化数据这条链路里,最关键的通常也不是“能不能检索”,而是:
- 抽出来的是不是真正文
- 切块边界是不是合理
- 来源、标题、章节、版本有没有保留下来
因为只要这些前提没站稳,后面的语义检索就很容易把错误、噪声和残片一起召回。
为什么两类数据不能简单混在一起
如果把结构化数据完全当长文本处理,会丢失很多本来很强的查询能力。
如果把非结构化数据完全当结构化记录处理,又会丢掉文本中的细节语义和上下文。
所以更好的思路通常是:
- 对结构化数据,优先保留查询能力
- 对非结构化数据,优先保留文本语义
然后在回答阶段把两类结果统一组织给模型。
如果强行混在一起,常见后果通常是:
- 结构化数据失去精确约束,只剩模糊解释
- 非结构化数据失去上下文,只剩碎片字段
- 用户问“有多少”时系统给出一堆说明文档
- 用户问“为什么”时系统只吐出几个字段值
这些现象本质上都说明:问题类型和数据链路没有对齐。
一个更实用的问题路由思路
很多真实问题,并不是一眼就能归到结构化或非结构化,而是可以先看它更偏哪种需求:
更偏结构化的问题
通常长这样:
- 有多少订单发生了退款
- 本周哪些工单超时
- 某个客户当前状态是什么
- 哪个区域的转化率更高
这类问题的核心是查事实、查状态、查条件、查聚合。
更偏非结构化的问题
通常长这样:
- 退款规则具体怎么规定
- 某个流程为什么这样设计
- 某项制度的适用范围是什么
- 某个问题的常见处理经验有哪些
这类问题的核心是解释、说明、背景和规则文本。
混合型问题
通常长这样:
- 这个月退款率为什么升高
- 哪些客户投诉最多,常见原因是什么
- 某个功能使用率下降,研发和运营文档里提到过哪些原因
这类问题往往就需要“先查事实,再补文本解释”。
一个常见的混合场景
比如用户问:
“最近一个季度华东区退款率上升,常见原因是什么?”
这个问题里往往同时需要两类数据:
- 结构化数据:订单、退款率、区域、时间
- 非结构化数据:工单描述、客服备注、问题归因文档
只靠其中一类,回答都可能不完整。
这时更实际的系统设计往往是:
- 先查结构化数据,确定事实范围。
- 再检索相关文档和文本材料。
- 最后把结构化事实和文档证据一起交给模型组织答案。
这类设计真正想避免的是两种极端:
- 只查数据库,最后只能给出冷冰冰的数字
- 只查文档,最后只能给出模糊的经验解释
成熟一点的系统,通常会让结构化数据负责“把事实钉住”,让非结构化数据负责“把原因和背景补齐”。
一个混合处理的示意代码
python
def query_orders(region: str, quarter: str) -> dict:
return {
"refund_rate": "4.2%",
"top_reason_codes": ["late_delivery", "size_issue"]
}
def retrieve_reason_docs(reason_codes: list[str]) -> list[str]:
doc_map = {
"late_delivery": "华东区 2026Q1 工单分析:延迟配送是退款上升的重要原因。",
"size_issue": "服饰类商品尺码偏差导致退款申请增加。"
}
return [doc_map[code] for code in reason_codes if code in doc_map]
def answer_question(region: str, quarter: str) -> str:
facts = query_orders(region=region, quarter=quarter)
docs = retrieve_reason_docs(facts["top_reason_codes"])
context = "\n".join(docs)
return (
f"{region} {quarter} 退款率为 {facts['refund_rate']}。\n"
f"相关说明:\n{context}"
)这段示意代码体现的是一种很常见的现实做法:
- 用结构化查询先拿到“事实范围”
- 再用文本检索补充“原因解释”
- 最后把两类结果一起组织成回答
你可以先这样理解
结构化数据擅长回答:
- 有多少
- 哪一条
- 哪个时间段
- 满足什么条件
非结构化数据擅长回答:
- 为什么
- 具体怎么规定
- 背景上下文是什么
- 相关说明在哪
成熟的 RAG 往往不是二选一,而是让两种数据各做自己最擅长的部分。
如果你还想记得更牢一点,可以把它理解成:
- 结构化数据更像“可计算、可过滤、可验证的事实层”
- 非结构化数据更像“可解释、可引用、可补背景的知识层”
RAG 做得越往后,你越会发现:真正稳的系统,通常不是让一种数据包打天下,而是先分清哪一层该由谁负责。