☰
从纯RAG到Multi-Agent:7B模型金融问答幻觉治理实践
2026/10/21 3:34:16 网站建设 项目流程

去年年初,我接了一个金融数据团队的活儿:基于某个开源 7B 参数模型,给内部投研系统做一个问答助手。一开始我们觉得这事很简单,RAG 都烂大街了,向量库一接、提示词一拼、模型一部署,能跑就行。结果一上真实数据,问题全来了——用户问"去年三季度营收增速",模型把两个名字高度相似的公司的财务数字直接混在一起,一本正经地给了个错的。那一刻我就意识到,金融场景下的智能体,不是"能不能生成"的问题,而是"能不能对着数字负责"的问题。这篇复盘,就是记录我们怎样从一个纯 RAG 链路,一步步演进到带代码解释器、带 Supervisor 调度的 Multi-Agent 架构,解决 7B 模型幻觉并沉淀下来的完整路径。适合正在做金融、政务、财务分析这类高精度领域大模型应用,又受限于小模型算力预算的团队参考。

1. 为什么纯 RAG 撑不住金融问答:第一版方案的失控现场

1.1 最初的设计:向量库 + 关键词检索 + 提示词拼接

第一版方案在架构上没有任何花哨:文档切片后做向量化,存进开源向量数据库,查询时先用关键词检索(BM25 类算法)和向量召回做双路召回,合并去重后取 Top-K 片段,拼进提示词,丢给 7B 模型生成答案。

挑这个路线的原因很实际。金融数据团队的预算有限,GPU 资源只够跑 7B 级别模型,不可能为每个场景上几百亿参数的大模型。私有化部署是硬要求,内部研报、持仓数据、交易流水不能出内网,所以云端 API 基本不用考虑。这两个约束凑在一起,就决定了我们必须在"检索质量"和"生成可靠性"上挖潜力。

切片策略参考了常规实践:研报按章节切,公告按段落切,每个片段控制在 500 字左右,重叠 50 字。向量模型用的是通用中文 embedding 模型,没有针对金融语料做二次训练,因为当时觉得通用模型"够用"。现在回头看,这个"够用"的判断本身就埋了雷。

1.2 在真实调用中暴露的四类问题

系统上线两周之后,我们做了 200 条真实问题的回归测试,发现纯 RAG 在金融场景下的失败模式非常集中,而且每一类都很致命:

第一类是数字张冠李戴。金融文档里数字密度极高,一个段落可能同时出现"营收""净利润""同比""环比"四组数据。用户问"A 公司三季度应收账款周转率",7B 模型很容易从上下文里挑一个数值最大的数字作为答案,而完全不管这个数字到底对应哪个字段。这不是模型笨,是生成式模型天然缺少"列名到数值"的强约束。

第二类是语义相似文本干扰。A、B 两家公司如果都做光伏组件,研报里大量句式几乎一样:"公司营收为XX,同比增长XX%"。向量召回经常把两家的片段同时捞进来,两个数字在上下文里打架,7B 模型直接困惑,最终输出往往是夹在中间的错误值。

第三类是多轮对话上下文漂移。用户先问"海外的毛利率",再问"那国内的对比一下呢",系统需要把"那"指代到上一轮的主体。7B 模型原生只处理单轮,我们在提示词里拼接历史对话,拼长了之后模型分不清哪段是历史、哪段是当前问题的检索结果,答案质量断崖式下跌。

第四类是最要命的——需要计算的推理问题完全没法答。用户问"把 Q1 和 Q2 的营收加总,再除以总股本,求每股营收",纯 RAG 链路里模型只能靠内部算术能力硬算,7B 模型的数学能力本就有限,两位数乘法都可能出错,多层计算的准确率惨不忍睹。

这四类问题放在一起,结论很清晰:不是调调提示词、换换切片大小能解决的。我们需要把"翻书找答案"和"按规则算答案"分开,让不同的智能体干各自擅长的事。

2. 7B 模型幻觉的根因拆解与可量化的评测基线

2.1 幻觉不是"模型笨",而是生成机制与检索上下文的错位

要解决幻觉,首先得理解幻觉在 7B 模型上为什么异常频繁。我们当时做了一个小实验:把检索到的上下文从提示词里完全去掉,只给模型一个问题,看它怎么答。结果模型给出的是"编造但语法通顺"的答案,比如问你某公司毛利率,它真的会编一个"约 23.5%"。这个数字在训练语料里可能存在过,但根本不是当前文档里的答案。

问题出在生成机制上:7B 模型的本质是基于条件概率逐字预测下一个 token,它没有"查表校验"的能力。当上下文里有多个候选数字时,模型会倾向选择条件概率最高的那个 token 序列,而"条件概率高"不代表"正确"。金融文本的语言模式高度雷同,模型可能在学习阶段见过太多"毛利率:XX%"的句式,生成时便沿用了这种句式,数字却被随机填充了。

另一个重要机制是注意力分配的稀释。当上下文超过 2000 token,7B 模型的注意力权重会明显分散。检索回来的片段越长、噪声越多,模型越难集中在关键数值上。我们在实验中对比了 Top-3 和 Top-6 片段的生成效果,发现 Top-6 不仅没提升答案质量,反而让数字错误率上升了 12 个百分点。信息太多反而有害,这对金融问答尤其明显。

2.2 幻觉评测:召回指标之外的答案可信度度量

光感觉"答得不对"是不够的,我们需要一套能复现、能对比的评测方法。我们的做法是构造了 200 条评测集,覆盖三类题型:数值抽取类(从指定段落中提取具体数值)、数值对比类(比较两个及以上财务指标的大小关系)、数值计算类(需要加减乘除才能得出答案)。

评分维度不是简单的"内容相不相似",而是三个硬标准:

  • 数字准确率:答案中的每个数字必须与来源文档完全一致。不一致即判错,容忍区间直接设为 0。
  • 归因可溯性:答案末尾必须给出片段来源标识,人工核查时能否定位到具体段落。定位不到,判错。
  • 逻辑一致性:对比类题型中,模型不能只说结果,需要复述比较的两个数值并给出明确的大小判断。

改造前这 200 条测试集的数字准确率只有 61.5%。也就是说,将近四成的问题,模型给了一个我们不信任的数字。这个数字击穿了内部底线——金融场景里,一个错误的"小数点位"可能意味着完全不同的投资结论。后续所有架构改造,都以这套测试集为基准,每次调整都必须跑一遍全量回归。

2.3 为什么不是"换个更大模型"这么简单

团队里当时有人提议,干脆上 14B 或 72B 的模型,幻觉不就没了吗。这个方案不是没有考虑过,但三条现实约束把它否了:

一是推理成本。7B 模型在单张卡上可以跑,换成 72B 就得至少 4 张卡,而且并发一高就会成为瓶颈。我们的查询量集中在开盘后两小时,流量峰值是均值的五倍,大模型在这个窗口期根本撑不住延迟。

二是私有化数据环境。金融内网对代码执行、外部依赖、模型热更新的管控非常严格,大规模模型的下发和更新流程冗长,小团队没有足够的运维人力去扛。

三是核心矛盾没解决。即使模型换成 72B,"无法保证输出数字来自给定文档"这个机制问题仍然存在。大模型照样会编数字,只是编得更逼真而已。真正的解法是改变信息通路:不让模型"回忆"数字,而是让模型"读取"数字。这就是我们后续引入工具调用与代码解释器的出发点。

3. 架构分水岭:从单次 RAG 链路走向多智能体协作

3.1 中间态尝试:把检索、推理、计算拆成三个独立节点

既然一个模型干不了所有事,那就拆开。我们做的第一版多智能体改造,是把原来的"检索->生成"单链路换成三个独立节点:

  • 检索智能体(Retriever Agent):负责关键词与向量双路召回,只负责找出候选文档片段,不承担生成任务。
  • 推理智能体(Reasoner Agent):负责阅读检索结果、判断哪段与问题相关、抽取关键数值,但只做"理解",不做"计算"。
  • 计算智能体(Calculator Agent):只负责执行数学运算,不再从上下文中硬猜答案。

每个节点通过一个编排层串起来:编排层先调用检索智能体拿到候选片段,再把片段交给推理智能体做字段级定位,最后如果需要计算,编排层把定位好的数值和公式一起交给计算智能体。

这个改造的第一个直接收益,是数值抽取准确率的提升。因为推理智能体不再需要一边找数字一边算数字,任务变得纯粹,数字准确率从 61.5% 提到了 72.3%。但问题也接踵而至。

节点之间的通信格式没有统一。推理智能体输出的结果是一个自然语言自然段,计算智能体需要从中解析出"数值和公式",解析过程非常脆弱。有时候推理智能体输出"Q1 营收为 12.3 亿,Q2 营收为 14.5 亿,二者相加得到 26.8 亿",看起来没问题,但一旦推理智能体决定用"约""大概"这类词修饰数字,或者中途多了一句解释,计算节点的解析就崩了。

3.2 工具调用协议:用结构化 JSON 连接节点间通信

解决节点通信问题的关键,是定义一套严格的结构化协议。我们借鉴了业界常用的"函数调用"范式,让智能体的输出不是一个自然段,而是一个可以被程序直接执行的 JSON 结构。

具体做法是给每个节点绑定一个工具注册表。注册表里预置了可用的计算函数,比如 sum_values、ratio_between、year_over_year 等。推理智能体在阅读检索片段后,不再是"自己想答案",而是输出一个明确的计算请求:

{ "intent": "calculate_gross_margin", "fields": { "gross_profit_value": [23.4, 28.1], "revenue_value": [100.2, 115.6], "period": ["2024Q1", "2024Q2"] }, "formula": "gross_margin = gross_profit / revenue", "source_ids": ["doc_001", "doc_002"] }

能走结构化流程的问题,一律走流程。模型负责的,只是"从检索到的段落里挑出正确的数字填进字段"以及"选择正确的公式"。这是一次决定性转变:模型从"答案的生成者"变成了"信息的搬运工和决策者"。数字不再经过模型的 next-token 采样,而是通过代码程序直接计算。

协议化改造让数字准确率又往上走了 8 个百分点。更关键的是,整个流程可以追踪了:每个数字从哪个片段来、用了什么公式、结果是多少,全部有结构化记录。这对金融场景下的审计要求来说是刚需。

3.3 代码解释器:为什么"写代码"比"直接算"可靠

在协议化基础上,我们往前再走了一步:把"预设公式"从有限的注册表扩展为动态代码生成。

注册表方式的问题是覆盖不全。金融分析的公式五花八门,光是一个"调整后 EBITDA"就有多种口径,预置函数永远滞后于业务需求。我们最终采用的方法是:让推理智能体生成一段可执行的 Python 代码片段,交给独立的代码解释器沙箱运行,沙箱返回计算结果。

背后的原理其实很简单:公式的正确性由解释器保证,而不是由模型保证。人类写代码都会犯低级错误,但代码一旦通过运行得到结果,这个结果的正确性至少不受模型采样干扰。模型要做的只是把正确的字段名、正确的数值、正确的运算符写进代码里——这比让模型直接心算出结果靠谱得多。

我们的代码解释器跑在独立的沙箱进程里,屏蔽了文件系统、网络、环境变量这些敏感能力,只开放了一个极其受限的基于 Python 的求值环境,并且做了超时控制(单次执行不超过 5 秒)。为了效率,沙箱进程常驻内存,通过进程间通信接收代码、返回结果,避免了每次调用都拉起新进程的巨大开销。

一个典型的问题是"计算某公司 2024 全年毛利率"。推理智能体会输出这样的代码片段:

# 来自检索片段 doc_003 与 doc_004 revenue_2024 = 456.2 # 单位:亿 gross_profit_2024 = 98.7 # 单位:亿 gross_margin = gross_profit_2024 / revenue_2024 result = {"gross_margin": gross_margin, "source_ids": ["doc_003", "doc_004"]}

沙箱运行这段代码后返回{"gross_margin": 0.2163, "source_ids": [...]},编排层再把这个数值连同来源标识一起填入最终回复模板。整个过程,7B 模型没有生成过一个数字 token。幻觉的命门,就这样被绕开了。

4. Supervisor 模式:多智能体的调度中枢与任务仲裁

4.1 为什么需要 Supervisor:并行工具调用与上下文串扰

多智能体拆到第四个版本时,我们面临了一个新问题——并行带来的管理失控。

早期每个请求都走同一条流水线:检索->推理->计算。但真实场景根本不是线性的。用户可能问一个综合问题"比较 A 和 B 公司的偿债能力,并给出近两年的趋势判断"。这类任务既要检索 A 公司的年报,又要检索 B 公司的年报,还要计算各自的流动比率、速动比率,最后再汇总对比。如果全部串行执行,延迟会累积到 10 秒以上,用户完全没法接受。

所以下一步自然就是并行化:检索阶段同时发起两路召回,计算阶段同时跑两个公式。并行化之后,问题又换了副面孔——

多个工具同时返回结果时,上下文里塞满了各种来源的片段和中间数据,7B 模型在汇总成最终答案时再次发生串扰。上一秒 A 公司的流动比率还是 1.4,下一秒任务里出现 B 公司的数值 2.1,7B 模型在最终回复里把两个数字的位置互换,张冠李戴的问题以"高层级"的形式回来了。

我们意识到,并行本身不是问题,缺乏统一的调度和任务追踪才是问题。谁的任务先跑完?结果放到哪里?归谁汇总?以什么顺序参考?这些问题没有答案,多个智能体的协作就是一团乱麻。我们需要的,是一个能让这些智能体有序协同的调度中枢。

4.2 状态机与任务队列:Supervisor 的核心工作机制

最终我们采用了一个自研的轻量编排模块,或者叫它 Supervisor(监督者)。它在文档里不长这个样子,没有引入复杂的外部框架,核心就是两件事:任务状态机和任务队列。

任务状态机的模型如下:每个用户请求进来后,Supervisor 拆分成若干子任务,子任务在状态机里有五个互斥状态:pending(等待执行)、running(执行中)、waiting_review(需要再次确认)、completed(已完成)、failed(执行失败)。

Supervisor 不会傻等所有子任务全部完成才开始下一步,它会做"部分汇总":比如三个子任务里已经有两个成功返回,第三个还在跑,它会先让推理智能体基于已有的两个结果做初步整合,等第三个结果返回后只做增量更新。这个设计在金融场景下特别有用,因为用户问题经常存在多个可选答案,我们需要"边等边呈现"而不是"全部就绪才回答"。

任务队列则负责管理执行顺序。同类的检索任务会被聚合,比如同一个来源文档的多段内容一次并行召回,而不是循环调用多次检索接口。这个小小的聚合优化,就把平均响应时间从 8 秒降到了 4.5 秒。

4.3 任务路由与冲突消解:Supervisor 最核心的设计决策

Supervisor 最需要拿捏的是两个决策问题:任务怎么分,结果冲突了怎么办。

任务拆分不是枚举规则,而是由一个小模型做意图识别。我们训练了一个极小的分类器(几千条标注数据),把用户请求划分为五类:

  • simple_fact:单一事实查询,不需要计算。走轻量链路:检索->生成。
  • calculation:需要数值运算。走检索->推理->代码解释器链路。
  • comparison:对比型问题。必须双路检索+独立计算后再汇总。
  • trend_analysis:趋势分析。检索历史区间片段,做多轮摘要。
  • ambiguous:语义模糊。不直接执行,先追问澄清。

路由决策的核心原则是"能窄则窄,不能窄则宽"。simple_fact类的请求绝不上代码解释器,省掉不必要的延迟;calculation类请求绝不让模型口头作答,强制走计算管道。这个规则的收益其实很大——我们把 200 条测试集里所有"需要计算的题目"单独拎出来算准确率,从纯 RAG 的 41.2% 提升到 Multi-Agent 改造后的 87.6%,提升幅度巨大。

冲突消解则参考了"多重验证"的思想。当两路检索分别给出不同的数值时,Supervisor 不会直接取其一,而是发起一轮佐证请求:把两个数值对应的原始片段重新交给推理智能体,要求输出"哪个数值与问题的时期、主体、指标匹配度更高",并附带匹配理由。如果系统判定置信度不够,就直接向用户坦白"检测到信息冲突,请确认时间周期或公司主体"。实践证明,与其让模型猜,不如承认矛盾。这个"承认矛盾"的兜底逻辑,反而提升了用户对系统的信任度。

冲突消解的另一个常用策略是时间优先。金融文档按日、季、年发布,旧数据和新数据天然存在版本冲突。如果问题没有明确指定时间窗口,我们默认采用发布时间较新的来源,并提示用户"基于 XX 报告"。这也是金融场景独有的细节。

4.4 降级路径:模型超时、解析失败、工具异常时的兜底逻辑

架构设计得再漂亮,不上生产环境永远不知道底线在哪里。我们给 Supervisor 的每条链路都配了降级路径,这条路径用真实线上事故喂出来的经验特别多。

第一个降级点是模型超时。7B 模型在 GPU 上一般几十秒内能出结果,但偶发排队会导致推理超时。Supervisor 的应对策略是:把超时请求转移到另一个低优先级的推理队列重试;如果二次重试仍然超时,直接放弃模型生成,改为返回"检索到的原始片段摘要",让用户自己查看来源文档。

第二个降级点是结构化输出解析失败。模型偶尔会输出不符合 JSON 规范的文本,比如在 JSON 前多加了一句解释。我们的 parser 不会直接报错,而是做宽松解析:先尝试原样解析,失败则提取最接近的 JSON 片段再尝试。如果仍然失败,就退回纯文本模式,把检索结果原样返回。这个策略听起来简单,但在上线初期每天能拦截掉 30% 以上的请求崩溃。

第三个降级点是计算异常。代码解释器可能遇到除零错误、字段缺失、单位不一致之类的问题。Supervisor 捕获异常后,会把错误消息传给推理智能体,让它重新审视输入字段。很多情况下,模型会发现"把毛利率当成了增长率"之类的问题,重新提取字段后再次计算就成功了。只有当同一子任务连续失败三次,才决定向用户显示错误提示。

降级路径的核心逻辑是:"宁可给一个可追溯的保守回答,也绝不给出一个高置信度的错误答案。"经过多轮压测,最终系统的服务可用性稳定在 99.2%,这在金融内部应用中属于可接受的水平。

5. 全流程复盘:基准测试对比、资源消耗与上线建议

5.1 改造前后的指标对比:一份我们内部的评测记录

整个改造历时约两个月,涉及检索、编排、模型、缓存、前端五个模块。我把核心指标的变化整理成了一张内部复盘表:

指标纯 RAG 基线多智能体 v1(协议化)多智能体 v2(Supervisor)
数字准确率(200 条测试集)61.5%72.3%89.4%
计算类问题单独准确率41.2%78.9%87.6%
平均响应时间(秒)2.13.44.5(并发场景 8.0)
归因可溯率无来源标识90.2% 带来源97.8% 带来源
无法作答率(系统主动承认不会)0%12.5%8.2%

其中有两个数字值得单独说明。

"无法作答率"从 0% 升到 12.5%,看起来是"变差了",但实际上是"诚实了"。纯 RAG 链路碰到答不了的问题时会硬胡编一个答案,所以它的"有答案率"是 100%,但这个有答案率没有意义。Supervisor 版本里有 8.2% 的问题会明确告诉用户"我算不出来"或"信息不足",用户虽然得不到答案,但也不会因为错误答案而做出错误决策。在金融这个场景里,这个取舍非常值。

平均响应时间从 2.1 秒增加到 4.5 秒,是唯一一个"开倒车"的指标。但如果我们只看体现了计算复杂度的题目,时间增加是合理的:检索多路并发、代码解释器额外执行、冲突消解阶段二次确认,这些步骤在纯 RAG 架构里根本不存在。如果只看 simple_fact 类问题,响应时间仍然控制在 2 秒以内,并没有全盘膨胀。

5.2 资源消耗与控制:7B 模型如何在预算内跑完任务

很多同行一听到"多智能体"就会联想到"负载爆炸"。实际上我们在资源层面做了非常精准的核算,总体开销远比想象可控。

7B 模型单卡运行,最大并发 8 个请求。通过 Supervisor 的任务队列,我们把请求排队等待,确保 GPU 利用率维持在 70% 左右。代码解释器沙箱用的是 CPU 资源,单次执行平均 200 毫秒,最多占用 2 核 CPU,几乎可以忽略不计。

向量数据库用的是内存索引,200GB 的索引数据全部加载到内存,查询 P95 延迟 30 毫秒左右。这部分资源是固定成本,不管并发高与低都在那里。

真正需要控制的是模型推理次数。我们的思路是"能复用缓存就坚决不二次推理":相同的问题(语义相似度超过 0.92)直接命中答案缓存;检索结果一样的相似问题也试图复用中间的计算结果。这个缓存策略上线后,模型调用量下降了大约三成,效果显著。

有一个坑必须提醒:代码解释器沙箱常驻会有内存泄漏的风险。我们的 Python 沙箱跑了三天后内存占用会缓慢爬升,后来加了一个"每处理 500 个请求重启沙箱进程"的定时任务才把这个问题压住。类似这种线上才会遇到的小细节,常规教程真的不会讲。

5.3 可复用的三条核心经验:不是技术,是方法论

当这套架构在内部稳定运行了几个月后,复盘时我们提炼出三条对后续项目最有用的经验:

第一条:先定义"错误",再定义"正确"。我们最开始做评测时,只看"生成的答案和标准答案像不像",这个指标根本没法发现问题。后来改成"数字必须逐位一致、来源必须可追溯",所有 bug 才真正暴露在阳光下。无论是金融还是其他高精度领域,先定义什么样的答案不可接受,比追求准确率重要得多。

第二条:模型越弱,架构越要补位。7B 模型的短板是客观存在的,与其花大力气微调或换大模型,不如把它的短板用工具和流程包起来。让模型少做点不擅长的事,它擅长的(理解意图、抽取字段)反而能做得更好。这套思路后来我们用在一个政务问答项目上,同样有效。

第三条:不要迷信"一步到位"。如果一开始就直接照搬 Multi-Agent 的完整架构,我们大概率会在排查问题时晕头转向。正是因为经历了"纯 RAG 出错 -> 拆节点 -> 协议化 -> 并行 -> Supervisor"的层层演进,每一步的收益和代价都清清楚楚。一个复杂系统,最好是在解决真实问题的过程中长出来,而不是设计出来的。

写在最后的一点个人体会:

这套架构上线三个月后,有一次某投研同事跟我说:"它算的数字我们敢直接用,但它说'不确定'的时候我们也得认。"这句话我觉得是对这次改造最精准的注脚。架构级金融智能体的价值,不在于让 7B 模型变得无所不知,而在于让它明确知道自己不知道什么,并把这层边界变成可以被审计、被信任的系统能力。如果你也在做类似的高精度领域应用,建议不要急着上大规模模型,先把"如何让模型不为数字负责"这层架构问题想清楚,后面的事都会顺很多。

如果你对 Supervisor 的路由策略细节、代码解释器沙箱的开源实现方案,或者那套评测集的构造方法感兴趣,随时可以接着聊,我可以把更细的实现翻出来展开讲。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询