检索做好了,输出还在裸奔
为什么散文式回答不够用
RAG 团队的优化重心通常压在检索侧,输出侧默认「模型写一段流畅的散文」就完事。但在真实业务里,散文式输出经常是不合格的交付物:客服系统要把答案填进工单字段;运维助手要把告警处置步骤吐给自动化脚本消费;数据分析场景要的是「结论 + 支撑数据」的表格,而不是一段文学性描述。检索层千辛万苦捞回来的结构化事实(数值、日期、条款编号、对比项),经过生成模型一层「散文化」,结构信息全被抹平了。
更现实的问题是下游无法消费。散文答案要靠另一个模型或人工再做一次信息抽取才能进业务流程,这一步抽取本身又是一个错误率来源。我们在客服场景统计过:散文答案 → 二次抽取 → 工单字段的链路,字段错误率是「直接生成结构化答案」的三倍多。结论很直接:如果下游需要结构,就在生成时给结构,中间的每一步转换都是误差放大器。
还有一类常被忽视的诉求:结构化是防幻觉的手段。字段化的答案(每个字段带来源编号)让错误无处藏身——散文里一句含糊的「一般来说」,在结构化输出里必须落到具体字段和具体引用上,模型没法用模糊修辞蒙混过关。
结构化输出的三层实现
Prompt 约束:便宜但不保险
第一层是纯提示词约束:在系统提示里给出目标 JSON 的示例和字段说明,要求模型「只输出 JSON」。这是最轻的做法,适合原型期。它的失败率不可忽视:模型偶尔在 JSON 前后加上解释文字、字段名与示例不完全一致、嵌套结构层级出错、长枚举值拼错。我们的经验数据:用强模型加精心设计的少样本示例,格式合法率能到九成以上,但字段级完全正确(每个字段类型、取值范围都对)大约八成——对流程自动化来说,两成的失败率意味着这条链路不可用,因为下游每一条都要设防。
提示词约束还有一个隐藏陷阱:schema 越复杂,格式错误率越高。三个字段的平坦 schema 几乎不出错,五层嵌套加联合类型(「这个字段要么是字符串要么是带类型的对象」)时,即使强模型的格式失败率也会显著抬头。schema 设计本身要为生成友好性服务:能平坦就平坦,能用枚举就不用自由文本,能用固定键就不用动态键。
Function Calling 与 JSON Schema 约束解码
第二层是平台级结构化输出。Function calling 是最早的形态:模型被约束为输出符合工具签名的参数 JSON。2024 年起各家的 constrained decoding(约束解码)把这件事推到了更底层——推理时对 logits 做掩码,只允许输出能被目标语法(JSON Schema 编译成的状态机/文法)接受的部分,从生成机制上杜绝格式错误。开源侧 Outlines、llama.cpp 的 GBNF 语法、vLLM 的 guided decoding 都是这条路线的工程实现。
约束解码解决了「格式合法」,但没有解决「内容正确」。schema 保证每个字段是字符串,保证不了日期字段的值是真实日期,保证不了枚举值选得合理,保证不了数字字段之间逻辑自洽(结束日期晚于开始日期)。所以平台级方案之上必须叠内容校验——这就是第三层。
校验-修复循环
第三层是校验与修复的外循环:生成结果先过静态校验(JSON Schema 校验、Pydantic 类型检查、枚举值白名单、业务规则如日期先后、金额非负),不合格的结果带着具体错误信息回炉重生成——修复提示词里只附上「哪个字段、什么错」,而不是重发全量上下文。修复循环的深度设二到三层封顶,超过即降级(输出原始文本 + 告警,转人工)。
def generate_structured(query: str, contexts: list[Context],
schema: dict, max_repair: int = 2) -> dict:
result = llm.generate(query, contexts, schema=schema) # 约束解码
for _ in range(max_repair):
errors = validate(result, schema) # 静态校验
errors += business_rules(result) # 业务规则
if not errors:
return result
result = llm.repair(result, errors, schema=schema) # 定向修复
raise OutputQualityError(result, errors) # 降级人工
校验规则的质量决定这个循环的价值上限。我们把校验规则分成三档:结构档(schema/type,必须全过)、事实档(枚举值、格式、范围)、语义档(字段间一致性、与引用片段的一致性)。前两档可以全自动;语义档里最有价值的一条是字段-引用一致性检查:答案里每个关键断言应能在其标注的来源片段里找到文本依据,这条检查的实现是轻量的 NLI 或嵌入相似度判定,它能在「格式全对但内容编造」的案例里再拦下一批。
flowchart LR
Q["查询 + 检索片段"] --> G["约束解码生成 JSON"]
G --> V["三档校验:结构 / 事实 / 语义"]
V -- "全过" --> CITE["字段-引用对齐检查"]
V -- "有错" --> R["定向修复(≤2 层)"]
R --> V
CITE -- "通过" --> OUT["结构化答案交付下游"]
CITE -- "漂移" --> R
R -. "超层降级" .-> FB["散文输出 + 告警转人工"]
温度、采样与重试的工程参数
结构化输出有两个容易被忽略的采样层参数。温度:生成 JSON 时温度不是越低越好——温度 0 输出最稳定,但遇到歧义 schema 或复杂枚举时会在同一个错误上反复跌倒(修复循环里尤其明显);我们的配比是首次生成用 0 到 0.2 保稳定,修复重试用 0.3 到 0.5 换运气,让重试真正提供「不同的尝试」而不是复刻上一次的失败。停止条件与截断:长 schema 的输出可能被 max tokens 截断,截断的 JSON 过不了校验、走进修复循环纯属浪费,正确做法是约束解码下预估输出长度(按 schema 字段数估),把 max tokens 给足,并在流式接收侧检测截断直接重生成而不是送校验。
还有一条分布式场景的细节:结构化输出服务化之后(作为内部接口供多个业务方调用),修复循环的重试要带上幂等键,同一请求的重试在网关层合并,避免业务方超时重发把修复循环放大成调用风暴。这类「评估指标看不见、账单看得见」的坑,只有上了量才会暴露,提前设计好。
引用对齐:结构化与溯源的结合
让每个字段带上出处
结构化输出和引用溯源的结合方式,决定了这份结构化数据能不能被信任。我们的 schema 设计约定:每个「事实型字段」带伴生的 source_ids 字段,值是检索片段的编号;校验层检查两件事——每个事实字段都有非空来源、来源片段确实包含与字段值语义一致的文本。
这个设计直接把「引用对齐」从页面装饰升级成了数据契约。下游拿到结构化结果,可以按 source_ids 回查原文渲染引用悬浮框;审计场景可以自动核对每个字段的出处;评测场景可以按「字段错误但引用正确」(生成读错了原文)和「引用本身错」(检索/溯源错了)拆分错误归因。一个伴生字段,三处受益。
工程细节上有一个坑值得记录:模型偶尔把编号引到「最像答案但其实是背景信息」的片段上(引用漂移)。抑制手段是在生成提示词里要求「引用编号必须在填写该字段时逐一确认来源片段的原文」,加上校验层的语义一致性抽查,漂移率能压到个位数百分比。100% 消除做不到,审计上要求高危字段(金额、日期、法规编号)必须二次程序核对——这些字段有固定的格式与可回查的原始结构化数据,程序核对比模型核对可靠。
生成表格:从要点到 Markdown 表
表格输出是结构化转换的另一形态,常见于对比类和列举类查询。它比 JSON 更难一档:JSON 有 schema 兜底,Markdown 表格没有强约束,列错位、单元格换行、分隔行缺失都会破坏渲染。工程要点:
- 先定列、再填行。让模型第一步只输出表头列名(可以列名枚举约束),第二步按列填数据。两步生成的表格结构错误率显著低于一步到位。
- 单元格原子化。禁止单元格内长文本和换行,长内容引导为「摘要 + 详见正文」;比较类表格的每行锚定一个被比较对象,列锚定比较维度,行列语义在提示词里显式声明。
- 程序化兜底渲染。模型输出后用 Markdown 解析器校验表格合法性(列数一致、分隔行存在),不合法走修复循环。表格的校验器比 JSON 的难写,但对渲染崩溃的防御价值更高。
「Let Me Speak Freely」(Tam 等,2024)这篇研究给过一组提醒:格式约束(尤其是严格 JSON)在某些推理型任务上会拉低模型表现——模型把「智力」花在了迎合格式上。我们的应对是把结构与推理解耦:需要推理的复杂问题,允许模型先自由推理、最后一步单独产出结构化结论(两段式生成);简单事实抽取则直接约束解码一步到位。什么时候解耦、什么时候耦合,按任务类型分流,不要一刀切。
工程实践
落地半年的经验按优先级排:第一,schema 是产品接口,要像 API 设计一样对待——版本化、向后兼容、字段注释完整,下游按版本消费,schema 升级走公告期;第二,校验规则库是资产,每产生一次线上事故就沉淀一条校验规则,半年积累的上百条业务规则是这个模块真正的护城河;第三,监控要对齐业务——格式失败率、修复成功率、字段级错误率、引用对齐率四个指标分层看板,字段级错误率才是业务方关心的数字;第四,永远保留「降级为散文 + 告警」的出口,结构化输出链路的所有失败最终都应该落到一个人类可读、可兜底的形态,而不是 500 错误。
成本账也要算清楚:约束解码本身几乎零成本(推理侧掩码),校验修复循环每多一层是一次 LLM 调用。我们的配比是格式约束全量开、修复循环深度二、语义档校验只对高危字段开启,单次请求的平均额外成本控制在生成成本的五分之一以内。
小结
RAG 的输出侧改造遵循三层递进:提示词约束(原型期,格式不可靠)、Schema 约束解码(机制上杜绝格式错误,但管不了内容)、校验-修复循环(静态校验 + 业务规则 + 字段-引用一致性检查,把「格式对但内容错」也拦下来)。结构化与溯源的结合点是字段级引用伴生——它让结构化数据从展示品升级为可审计、可归因的数据契约。
表格输出走「先定列再填行」的两步生成加程序化校验;推理型任务与格式约束的冲突用「推理与结构化解耦」的两段式生成来调和。工程化的核心是把 schema 当产品接口管理、把校验规则当资产沉淀,并永远给失败留一个降级出口。检索决定答案的上限,输出结构决定这份答案能否进入业务流水线——两者都做到位,RAG 才算交付。
读者留言
COMMENTS 暂无还没有留言,来说第一句?