RAG 进阶:父子切片机制与多重防幻觉架构落地
前言
在构建垂直领域的 RAG 智能体时,往往会面临两个生产级痛点:一是传统定长切片导致“检索到了碎片,但丢掉了前置上下文”;二是检索质量差时,模型开始“一本正经地胡说八道”。
本文不谈虚无缥缈的概念,我直接从工程落地角度拆解解决方案:如何通过 父子切片解耦检索与生成,以及如何通过 防御性编程 在 Prompt 侧、Tool 侧和生成侧构建三道硬熔断闸门,彻底扼杀 RAG 的幻觉风险。
一、 为什么传统的单层切片不够用?
在传统的 RAG 架构中,我们通常使用固定 Size切分文档。这会引发一个两难困境:
Chunk 太小:向量语义聚焦,检索精准度高,但丢失了上下文。
Chunk 太大:上下文给足了,但稀释了关键信息的语义密度,导致向量检索的召回率大幅下降。
为了破解这个矛盾,必须引入 父子切片 机制。其核心逻辑是:子块负责高精度检索,父块负责上下文渲染。
二、 父子切片的推荐实践策略
在工程落地中,不建议纯粹按固定字数切分父块。最佳的切分策略是:结构切父块 + 递归切子块。
+-------------------------------------------------------------+
| Parent Chunk: 按 Markdown #/## 标题或自然段拆分, 800-1500字 |
| (存入 DocStore / Redis, 不参与向量计算, 负责上下文生成) |
+-------------------------------------------------------------+
│
┌──────────────┴──────────────┐
▼ ▼
+----------------------------+ +----------------------------+
| Child Chunk 1 (150字, 20重叠) | | Child Chunk 2 (150字, 20重叠) |
| (向量化入 VectorStore, 检索) | | (向量化入 VectorStore, 检索) |
+----------------------------+ +----------------------------+
1. 父块:按结构切分
利用 Markdown 标题(#, ##)、HTML 标签(<h1>, <div>)或段落换行符(\n\n)将文档拆分为独立的业务逻辑块(800~1500 字)。
作用:确保父块是一个完整的独立话题,天然包含完整的语义与前置条件。
2. 子块:按递归字符切分
针对每一个父块,使用 RecursiveCharacterTextSplitter 进行进一步拆解:
Python
from langchain_text_splitters import RecursiveCharacterTextSplitter
child_splitter = RecursiveCharacterTextSplitter(
chunk_size=150,
chunk_overlap=20,
separators=["\n\n", "\n", "。", "!", "?", " ", ""]
)
作用:高语义密度,控制在 150 字左右,并保持 20 字的滑动窗口重叠,防止跨句断意。
3. 存储与检索映射
子块:生成 Vector,存入 Vector Database,仅用于向量相似度计算。
父块:保留原始文本,存入 KV 数据库,并维护
child_id -> parent_id的映射表。运行时:用户问题命中子块后,图引擎通过映射关系提取对应的父块文本,作为 Context 喂给 LLM。
三、 多重防幻觉架构:用代码硬约束控制确定性
解决完数据切片,下一步就是解决大模型的幻觉问题,靠防御性编程设下多重闸门。
整个防幻觉体系在架构上分为三层:
[用户输入] ──> [1. Supervisor Node 意图隔离 + JSON 强约束]
│
▼
[2. Tool 侧 Score 硬熔断与空召回拦截]
│
▼
[3. LLM 最终生成侧 Strict Prompt 兜底] ──> [安全输出]
1. 输入端:意图隔离与格式硬约束(Supervisor Node)
在路由节点上做两件事:隔离业务边界 与 限制输出自由度。
Python
SUPERVISOR_PROMPT = SystemMessage(content=(
"你是一个意图分类助手。请根据用户的最新输入,只返回对应的分类名称:\n"
"- tool_expert: 咨询xx相关问题。\n"
"- ask_clarification: 用户输入过于模棱两可,无法判断意图。\n\n"
"请严格按 JSON 格式输出:{\"next\": \"节点名称\", \"clarification_question\": \"反问内容(仅在ask_clarification时填)\"}"
))
边界隔离:阻止模型跨界强行回答不属于当前业务域的问题。
JSON 强约束:通过结构化输出挤压模型的自由发挥空间。
2. Tool 层:代码级硬熔断(防幻觉的最强闸门)
这是整套防幻觉架构中最硬核的部分。拒绝将低置信度数据投喂给 LLM:
Python
@tool
async def search_knowledge_base(query: str) -> str:
"""异步检索私有知识库,内置混合检索 + 重排序精刷。"""
rag = get_rag_service()
docs = await rag.advanced_retrieve(query)
if not docs:
return "知识库中未检索到相关内容,请尝试其他问题或联系人工客服。"
top_score = docs[0].metadata.get("rerank_score", -100)
if top_score < -2.0:
return "未找到高度匹配的内容,建议咨询官方渠道。"
return format_docs(docs)
空召回截断 :未查到数据,代码层直接拦截返回提示,直接切断 LLM 基于无数据源盲目编造的路径。
分值熔断:通过 Reranker 模型计算相似度。即使召回了文档,只要得分低于硬性阈值,依然判定为无效召回,终止大模型推理。
3. 回答层:边界兜底
在 Agent Node 接收到 Tool 结果后,于 System Prompt 层下达死命令:
Python
if messages and messages[-1].type == "tool":
system_prompt = SystemMessage(
content="你是XX。请根据检索到的参考资料回答用户问题。若资料中没有请坦诚说明。"
)
response = await self.llm.ainvoke([system_prompt] + messages, config=config)
如果 Tool 层触发了硬熔断,LLM 在该提示词约束下会老老实实向前端汇报“未找到”,完成防幻觉闭环。
四、 总结与架构思考
大模型工程落地不是搞学术研究,能用代码解决的确定性问题,就绝不交给概率模型去猜。
切片层:结构切父块保证上下文完整,递归切子块保证检索精度。
路由层:Supervisor 节点做好意图隔离与 JSON 结构化收敛。
工具层:代码级拦截,宁可拒答也不喂废料。
生成层:Strict Prompt 死扣边界,提供最后的容错兜底。
将防幻觉拆解为这三层锁链,RAG 系统在真实生产环境中的可用度与稳定性才能得到根本保证。