金融RAG的幻觉难题:用原子知识单元让模型"说实话"

一句话总结:把回答拆成最小知识单元,逐个验证正确性,用细粒度奖励训练模型不再胡编乱造。


📖 为什么金融RAG这么容易"说胡话"

如果你问大模型"贵州茅台2025年一季度的每股收益是多少",它可能会回答:"截至2025年5月15日,贵州茅台基本每股收益为70.86元"。

听起来很专业,对吧?但问题是:检索文档里明确写着"截至2025年3月31日",模型把时间搞错了。

这就是金融RAG的典型幻觉问题:模型生成了与检索文档相矛盾的内容。在金融领域,这种错误的代价很高——把财报日期搞错可能导致严重的投资决策失误。

RAG(检索增强生成)技术原本是为了解决大模型知识过时的问题,让模型可以"开卷考试"。但在实际应用中,即使把正确答案摆在面前,模型仍然会"抄错"。论文中的图1直观展示了这个问题:

图1:金融RAG中的幻觉案例

图1:模型回答中的幻觉问题——虽然检索文档明确标注了正确的时间(3月31日),但模型生成的回答却错误地关联到了5月15日。这种"看着答案抄错"的现象在金融领域尤为危险。


🔍 现有方法的两个硬伤

方法一:人工标注参考答案

传统做法是让人工标注每个问题的标准答案,然后用模型生成的回答和标准答案对比。

问题:金融数据更新频繁,人工标注成本极高。财报数据每天都在变,你不可能为每个时间点都标注一套答案。

方法二:粗粒度二元奖励

另一种方法是用另一个模型判断生成的回答是否忠实于检索文档,给出一个"正确/错误"的二元标签作为奖励信号。

问题:这种"一刀切"的奖励太粗糙了。比如模型生成了5个知识点,其中4个正确、1个错误,粗粒度奖励会给整个回答打0分。这样模型学不到"哪个知识点错了",优化方向模糊。

论文的图2对比了粗粒度和细粒度奖励的区别:

图2:粗粒度vs细粒度奖励对比

图2:粗粒度奖励(上)vs 细粒度奖励(下)。粗粒度奖励只给整体打分,模型不知道错在哪;细粒度奖励逐个验证每个知识单元,提供更精确的优化信号。


🧠 核心方法:RLFKV框架

论文提出了RLFKV(Reinforcement Learning with Fine-grained Knowledge Verification)框架,核心思路是:把回答拆成最小的"知识原子",逐个验证,给每个原子打分。

什么是"原子知识单元"

金融领域的信息通常可以用四元组表示:(实体, 指标, 数值, 时间)

比如: - (贵州茅台, 基本每股收益, 70.86元, 截至2025年3月31日) - (苹果公司, 营收, 948亿美元, 2025财年Q1)

这种四元组就是一个"原子知识单元"——不能再拆分的最小知识颗粒。

这让我想到了量子力学里的"原子"概念:物质可以无限细分,但到了原子层面就是最小单位了。同样,一段金融回答可以分成多个句子,每个句子可以分成多个知识点,到了四元组这个层面,就是知识的"原子"。

框架整体流程

RLFKV框架包含两个核心步骤:

第一步:原子知识单元分解与验证

  1. 用另一个LLM(论文用Qwen3-32B)把模型回答拆成四元组
  2. 对每个四元组,让验证模型检查它与检索文档是否一致
  3. 生成二元验证分数:1=正确,0=错误

第二步:计算奖励并优化

根据验证结果计算两个奖励信号:

忠实奖励(Faithfulness Reward): $\(r_f = \frac{1}{e^{\eta \cdot \min(\text{error\_count}, \gamma)}}\)$

这个公式的设计很巧妙: - 错误单元数越多,奖励越低(指数衰减) - 但设置了上限γ,防止奖励变成0,保持梯度稳定 - η是温度参数,控制惩罚力度

信息量奖励(Information Reward): $\(r_i = \begin{cases} 1 & \text{if } k \geq k_0 \\ 0 & \text{otherwise} \end{cases}\)$

其中k是当前回答的单元数,k₀是基础模型生成的单元数。

为什么要加这个奖励?为了防止"奖励黑客"。如果只用忠实奖励,模型可能学会一个取巧策略:只生成一个简单正确的句子,这样错误数为0,忠实奖励最高。但这样的回答毫无信息量。

这个设计让我想到考试:如果只看正确率,学生可能只答最简单的题;但如果要求"至少答够N道题",就能保证答题量。

总奖励是两者的平均值,然后用GRPO算法优化策略模型。

GRPO算法简介

GRPO(Group Relative Policy Optimization)是DeepSeek团队提出的强化学习算法,和传统PPO的主要区别是:不需要训练一个价值网络(Critic Model)

传统PPO需要一个价值网络来估计"当前状态的价值",这相当于需要一个"老师"告诉模型"你这个回答大概值多少分"。训练价值网络需要大量显存和计算资源。

GRPO的思路是:对同一个问题生成多个候选回答,用奖励模型的均值和标准差来归一化,高分的给正奖励,低分的给负奖励。这样就不需要额外的价值网络了。

用考试来类比: - 传统PPO:有个老师预估你这次能考80分,你考了85分就算进步 - GRPO:你同一张卷子做5遍,自己跟自己比,选最好的那次

这种"组内相对比较"的方式大大降低了训练成本,让大模型的强化学习训练更加高效。


📊 实验结果:细粒度确实更有效

数据集

论文用了两个数据集:

数据集 规模 内容
FDD 1,461样本 股票描述
FDD-ANT(新提出) 2,000样本 股票、基金、宏观经济指标

FDD-ANT是论文新提出的真实业务数据集,覆盖更广的金融领域,更具挑战性。

主要结果

表1展示了不同方法在两个数据集上的表现:

表1:主要实验结果

表1:在FDD和FDD-ANT数据集上的忠实度(Faith.)和信息量(Info.)对比。RLFKV在两个数据集上都取得了最佳效果,忠实度提升约3个百分点。

关键发现:

  1. RLFKV效果稳定:在两个数据集上,忠实度都提升了约3个百分点(从86.5%→89.5%,从90.2%→93.3%)

  2. 信息量奖励必不可少:消融实验显示,去掉信息量奖励后,忠实度变化不大,但信息量显著下降。这证明了防止"奖励黑客"的必要性。

  3. 细粒度优于粗粒度:图3对比了两种奖励方式的训练曲线:

图3:细粒度vs粗粒度训练对比

图3:细粒度奖励(蓝色)vs 粗粒度奖励(橙色)。细粒度奖励不仅最终效果更好,而且收敛更快(约2000步达到稳定),训练更稳定。

错误分析:方法还不够完美

论文还对模型的错误进行了分类(图5):

图5:错误类型分析

图5:即使使用RLFKV,仍存在三种主要错误类型。时间相关错误占比高达83%,是未来改进的重点方向。

三种错误类型: 1. 时间遗漏(55%):检索文档有时间信息,但回答里忘了提 2. 时间不准确(28%):相对时间表达、财年与日历年转换错误 3. 数值错误(17%):主要是四舍五入不精确

这个分析很有价值:时间问题是金融RAG的最大痛点。金融数据的时间敏感性极强,"昨天涨停"和"今天涨停"含义完全不同。


💡 我的观点和启发

技术创新点

这篇论文的核心贡献不是某个全新的模型架构,而是一个工程化的问题拆解思路:把复杂的"幻觉问题"拆解成可验证的最小单元。

这种思路在很多领域都适用: - 代码审查:把PR拆成多个独立的改动点,逐个review - 产品测试:把用户路径拆成多个步骤,逐个验证 - 知识问答:把长回答拆成多个事实,逐个核实

工程落地思考

如果要实际部署这个方法,有几个工程问题需要考虑:

  1. 验证模型的准确性:论文用Qwen3-32B做验证,但验证模型本身也可能出错。如果验证模型把正确的单元判为错误,会给错误的训练信号。可能需要用更强的模型(如GPT-4)做验证,或者用ensemble方式提高鲁棒性。

  2. 四元组抽取的边界情况:金融文本有些复杂表达不太适合四元组。比如"该公司近三年营收复合增长率为15%",时间、实体、指标都好提取,但"数值"是"15%"还是"复合增长率"?

  3. 训练成本:虽然GRPO省掉了价值网络,但每次都要生成多个候选回答、逐个验证,计算量不小。论文没有详细讨论训练成本,这对实际部署很重要。

和其他工作的关联

这篇论文的"原子知识单元"思路让我想到另一个工作:Atomic Fact Decomposition for Attributed Question Answering。那篇论文也是把回答分解成原子事实,然后逐个归因。不同之处在于: - RLFKV关注的是"验证正确性",用于训练模型 - Atomic Fact Decomposition关注的是"找到来源",用于归因报告

这两个方向其实是互补的:验证→归因→修正,构成完整的幻觉治理流程。

局限性

论文坦诚地指出了一个局限:时间问题仍然难以解决。55%的错误是"时间遗漏",这说明模型学会了"说正确的话",但还没学会"说完整的话"。

这可能是因为: 1. 时间信息在金融文本中通常不是核心焦点,容易被模型忽略 2. 四元组结构对时间的表达不够丰富(只有timestamp字段,没有"时间上下文"的概念)

未来方向

基于论文的错误分析,我认为可以探索几个方向:

  1. 时间感知的奖励设计:给时间相关的单元更高的权重,或者在奖励函数中加入时间覆盖率的考量

  2. 结构化时间推理:不把时间当作普通字段,而是引入时间推理模块,专门处理"同比增长"、"环比下降"、"近三年复合增长"等时间表达

  3. 多粒度验证:除了四元组级别的细粒度验证,还可以加入句子级别、段落级别的验证,确保信息的完整性


🔧 代码示例:如何实现原子知识单元验证

下面是一个简化的Python示例,展示如何实现论文中的原子知识单元验证逻辑:

from dataclasses import dataclass
from typing import List
import math

@dataclass
class AtomicKnowledgeUnit:
    """原子知识单元:金融四元组"""
    entity: str      # 实体(如:贵州茅台)
    metric: str      # 指标(如:每股收益)
    value: str       # 数值(如:70.86元)
    timestamp: str   # 时间(如:2025年3月31日)

    def __str__(self):
        return f"({self.entity}, {self.metric}, {self.value}, {self.timestamp})"

def decompose_to_units(response: str, decomposer_model) -> List[AtomicKnowledgeUnit]:
    """
    将模型回答分解为原子知识单元
    实际实现需要调用LLM进行结构化抽取
    """
    prompt = f"""请将以下金融文本分解为(实体, 指标, 数值, 时间)四元组:

文本:{response}

输出格式:每行一个四元组,用逗号分隔
"""
    # 调用模型进行分解(这里省略具体实现)
    units = decomposer_model.generate(prompt)
    return units

def verify_unit(unit: AtomicKnowledgeUnit, retrieved_doc: str, verifier_model) -> bool:
    """
    验证单个原子知识单元与检索文档是否一致
    返回True表示正确,False表示错误
    """
    prompt = f"""请判断以下知识单元是否与参考文档一致:

知识单元:{unit}
参考文档:{retrieved_doc}

只回答"正确"或"错误"
"""
    result = verifier_model.generate(prompt)
    return "正确" in result

def compute_faithfulness_reward(error_count: int, eta: float = 1.0, gamma: int = 5) -> float:
    """
    计算忠实奖励
    公式:r_f = 1 / exp(eta * min(error_count, gamma))
    """
    score = min(error_count, gamma)
    return 1.0 / math.exp(eta * score)

def compute_information_reward(current_units: int, baseline_units: int) -> int:
    """
    计算信息量奖励
    当前单元数 >= 基线单元数 则奖励1,否则0
    """
    return 1 if current_units >= baseline_units else 0

def compute_total_reward(
    units: List[AtomicKnowledgeUnit],
    verification_results: List[bool],
    baseline_unit_count: int
) -> dict:
    """
    计算总奖励
    """
    error_count = sum(1 for r in verification_results if not r)

    faithfulness_reward = compute_faithfulness_reward(error_count)
    information_reward = compute_information_reward(len(units), baseline_unit_count)
    total_reward = (faithfulness_reward + information_reward) / 2

    return {
        "faithfulness_reward": faithfulness_reward,
        "information_reward": information_reward,
        "total_reward": total_reward,
        "error_count": error_count,
        "unit_count": len(units)
    }

# 使用示例
if __name__ == "__main__":
    # 模拟数据
    response = "截至2025年3月31日,贵州茅台基本每股收益为70.86元。同期净利润为287.8亿元。"
    retrieved_doc = "贵州茅台2025年一季报显示:截至2025年3月31日,基本每股收益70.86元,净利润287.8亿元..."

    # 假设分解和验证结果
    units = [
        AtomicKnowledgeUnit("贵州茅台", "基本每股收益", "70.86元", "2025年3月31日"),
        AtomicKnowledgeUnit("贵州茅台", "净利润", "287.8亿元", "2025年3月31日")
    ]

    verification_results = [True, True]  # 两个单元都正确
    baseline_units = 2  # 基线模型生成的单元数

    rewards = compute_total_reward(units, verification_results, baseline_units)
    print(f"奖励计算结果:{rewards}")
    # 输出:{'faithfulness_reward': 1.0, 'information_reward': 1, 'total_reward': 1.0, ...}

📚 参考资料


📝 总结

这篇论文提出了一种简单但有效的思路来解决金融RAG的幻觉问题:把回答拆成最小知识单元,逐个验证,用细粒度奖励训练模型

核心洞察是:幻觉不是"全对全错"的问题,而是"部分对部分错"的问题。粗粒度的奖励信号让模型不知道错在哪,细粒度奖励则能精准定位问题。

当然,方法还不完美——时间相关问题占错误的83%,这是未来需要重点攻克的方向。但论文提供了一个清晰的框架,后续工作可以在此基础上继续优化奖励设计、改进验证机制、加强时间推理能力。

对于金融领域的RAG系统来说,这篇论文的价值在于:它把一个模糊的"幻觉问题"变成了可量化、可优化的技术问题。这比提出一个新的模型架构更有实用价值。