1. 项目背景:为什么非要用7B模型做金融智能体
做金融智能体这个项目,是从一次需求评审开始的。业务那边提了个很实际的需求:让投研人员用自然语言直接查公司财报、算财务指标、对比行业数据,不用再手动翻PDF和Excel。听起来不复杂,但落到技术选型上,我们纠结了很久。最大的约束不是模型能力,而是数据合规——内部研报和持仓数据不能出内网,所有依赖外部API的方案第一轮就被否了。剩下的路只有一条:本地部署开源模型,自己搭一套智能体。
模型选型阶段,我们在多个开源模型上做了评测。7B这个参数量级的模型,通用对话还过得去,但金融场景里的表现一开始真让人捏把汗。当时摆在我们面前的有两条路:一是换更大的本地模型,比如13B甚至70B,但这意味着更高的显存成本和更长的推理延迟;二是坚持用7B,靠架构设计把模型短板补掉。最终我们选了后者。核心判断是:金融问答里的大部分错误,其实不是模型"不懂",而是它在"硬算"和"硬编"。只要把计算、检索、组织答案这些环节从模型的自由发挥中剥离出来,小模型也能交出稳定可靠的结果。
整个项目从第一版到最终稳定上线,经历了三次明显的架构演进:
| 版本 | 核心方案 | 主要问题 |
|---|---|---|
| V1 | 朴素RAG:文档切块+向量检索+上下文生成 | 数字错误频繁、跨段落推理几乎为零 |
| V2 | RAG+代码解释器 | 数值计算准确率大幅提升,但职责混乱、链路笨重 |
| V3 | 多Agent+Supervisor统一编排 | 职责清晰、过程可追踪、支持人工兜底,达到生产可用 |
这个演进过程挺有代表性,基本覆盖了金融智能体最常见的三个坑:检索不可信、计算不可靠、协作不可控。下面按实际推进的顺序,把每一步的关键细节和踩坑记录拆开讲。
2. RAG层重构:检索质量决定了幻觉的下限
2.1 文本切分:金融文档的表格才是重点
做RAG的第一步是切文档。这一步我踩过最大的坑,就是用固定长度硬切。研报这类文档,信息密度最高的地方恰恰是表格——利润表、资产负债表摘要、行业对比,全是数字。按固定长度切,一张宽表被切成好几片,检索时要么丢了表头,要么只召回半张表。模型拿到残缺的表格数据,给出的答案怎么可能对。
后来我们改成按文档结构做语义切分:先解析标题层级,以"章节→小节→段落"为基本单位组织内容块;遇到表格,先把表格区域单独提取出来,转成带字段名的结构化文本,比如"净利润|2024Q3|12.35亿元|同比+15.2%"。这样每个chunk都是语义完整、自包含的,检索命中之后,模型拿到的是完整上下文,而不是四处拼凑的碎片。
切完之后我们又加了一个校验环节:用自动化脚本逐chunk检查字段名与数值是否对齐、有没有缺行漏列。这个步骤看起来土,但效果很直接,能拦掉大量脏数据,避免脏数据一路流到模型输出里。强烈建议RAG上线之前,先花时间把这类校验脚本写好,后面能省下无数排查时间。
2.2 召回策略:向量检索远远不够
第一版我只用了向量检索,在真实业务场景里很快露馅。金融文档里大量出现公司简称、股票代码、专业术语,这些内容用向量去匹配,容易召回一堆语义相近但不相关的片段。用户问"某公司2024年经营现金流",向量检索可能把"现金流折现模型"的分析段落也捞回来,因为语义上它们离得太近了。
后来我改成混合检索:向量召回和BM25关键词召回并行跑,再用RRF把两路结果合并。逻辑不复杂,就是个加权倒数融合:
# 混合检索:向量召回 + BM25 关键词召回,RRF 融合排序 def hybrid_retrieve(query, top_k=20): vec_hits = vector_index.search(query, top_k=50) bm25_hits = bm25_index.search(query, top_k=50) score_dict = {} for rank, hit in enumerate(vec_hits): score_dict[hit.doc_id] = score_dict.get(hit.doc_id, 0) + 1 / (60 + rank) for rank, hit in enumerate(bm25_hits): score_dict[hit.doc_id] = score_dict.get(hit.doc_id, 0) + 1 / (60 + rank) ranked_ids = sorted(score_dict.items(), key=lambda x: x[1], reverse=True) return [doc_id for doc_id, _ in ranked_ids[:top_k]]为什么两路互补?向量擅长语义相似,BM25擅长精确匹配股票代码、公司名这类"死词",一个管理解,一个管命中。混合之后,召回准确率提升非常明显,尤其对带数字和代码的查询,效果立竿见影。这个改动花费不大,但对整体幻觉水平的压制作用相当可观。
2.3 重排序与引用约束:给模型戴上"紧箍咒"
粗排之后我又加了一层重排序,用交叉编码器模型对候选chunk做精排,只把相关度最高的几个送进生成上下文,默认保留5个。这一步增加的计算开销不大,但对最终回答质量影响很大。7B模型上下文塞得越杂,越容易被无关内容带偏,所以宁可少给,也要给准。
另外一个我强烈推荐的做法是强制引用。我们在生成prompt里明确要求:回答中的每个关键数字,必须标注来自哪个chunk编号。后处理环节再写一个校验器,解析模型输出的引用编号,如果某个数字没有对应任何chunk,就判定为"无法证实",不允许出现在最终回复里。这相当于在输出端设了一道关卡。它不能彻底消灭幻觉,但至少能让幻觉不再以"看起来很笃定"的形式出现在用户面前。金融场景里,"知道不知道"比"假装知道"安全得多。
3. 代码解释器:让模型学会"算账"
3.1 7B模型的计算短板与破解思路
小模型的计算能力有多弱,没有亲自测过的人很难想象。让7B模型算"去年营收52.3亿元,同比增长约18%,求今年营收",它能给你算出好几个版本。这不是个别模型的毛病,而是自回归生成机制的本质短板——它本质上是在预测下一个token,而不是在执行精确的算术步骤。
破解思路其实很多团队都在用:既然模型不擅长计算,就不要让它算。我给系统加了一个代码解释器工具,把模型的任务从"算出答案"改成"写出计算逻辑"。模型只需要根据检索到的数据,生成一段Python代码描述计算过程,剩下的交给执行器。对7B模型来说,把"两个数的比值"表达成代码,比自己在token空间里心算可靠得多。这个改动是整个项目里性价比最高的一次升级。
3.2 沙箱设计:安全与可控缺一不可
能执行任意Python代码的工具,必须考虑安全问题。我这里用的是受限子进程方案:代码在一个独立的Python进程里跑,禁掉网络和文件读写,只能import白名单内的库,设置最大执行时间和资源上限,超时直接杀掉。返回结果只能是JSON对象,不允许写任何中间文件。
# 代码解释器受限执行:白名单依赖、无网络无文件、超时强杀 import subprocess SAFE_PREFIX = "import numpy as np, pandas as pd, math\n" def run_code_interpreter(code: str, timeout: int = 15) -> dict: wrapped = SAFE_PREFIX + f"print({code})" try: proc = subprocess.run( ["python", "-c", wrapped], capture_output=True, text=True, timeout=timeout, env={"PATH": "/usr/bin"}, ) if proc.returncode != 0: return {"status": "error", "message": proc.stderr[-500:]} return {"status": "ok", "result": proc.stdout.strip()} except subprocess.TimeoutExpired: return {"status": "timeout", "message": "execution timeout"}这个沙箱方案在内部工具场景里性价比最高,部署简单、开销小。但如果未来把解释器能力开放给外部用户,一定要升级成容器级隔离,每个请求一个一次性容器,用完销毁,把风险彻底隔离掉。
3.3 从"生成答案"到"生成代码再执行"的流程改造
接入解释器不是简单挂个工具就能跑通,我做了三处改造才算真正落地。第一,检索结果先预处理成"可计算的素材",把召回内容里与问题相关的数值字段提取成结构化列表,带上年份标签和单位一起传给模型。第二,在prompt里明确要求:数值计算必须先调用工具,不允许凭上下文猜测。第三,在工具返回后加合理性校验,比如计算出的利润率超过100%或者为负,直接标记异常,让模型重新审视数据来源。
这三步跑通之后,数值类问题的准确率提升非常大。代价是延迟变高,一次计算要等解释器执行完才能返回。但金融场景里,准确性的优先级远高于响应速度,这个代价完全值得。实测下来,我们内部标注的数值问答准确率从最初的不到七成,提升到了九成以上。
4. Supervisor模式:多智能体的编排核心
4.1 为什么需要Supervisor
有了RAG和代码解释器,单Agent版本已经能解决不少问题,但用久了会发现一个尴尬的情况:一个Agent既要检索、又要算数、还要组织回答,任务一复杂就手忙脚乱。比如用户问"对比A和B两家公司的偿债能力,并结合最新研报观点给出分析",这里包含数据检索、指标计算、研报观点归纳三个环节,单Agent会把所有逻辑混在一条生成链路里,互相干扰,生成质量就崩了。
这个阶段我决定拆成多Agent,并引入Supervisor统一调度。Supervisor不负责具体干活,只做三件事:看懂用户想问什么、决定派哪个Agent执行、汇总结果成最终答案。子Agent各管一摊,各司其职。这种拆分让每个Agent的prompt都变得简洁,7B模型在单一职责下的表现,比在一个万能Agent里的表现好得多。架构上看起来多了一层,实际上每个环节的稳定性都上来了。
4.2 子Agent拆分与职责边界
拆分多Agent最容易犯的错是边界不清。比如"财务分析Agent"和"研报观点Agent"都可能被问到"这家公司怎么样",如果两边都答,内容重叠还互相矛盾,用户体验很差。我的做法是给每个Agent定义严格的任务说明和禁止事项:财务分析Agent只能呈现客观数据和指标,禁止引用研报主观观点;研报观点Agent只能引用原文观点,禁止自行计算指标。边界划清楚,Supervisor的路由才有意义。
另一个容易被忽略的细节是上下文隔离。每个子Agent只接收与当前任务相关的检索结果和用户原始意图,不接收其他Agent的中间输出。刚开始团队的共识是"上下文共享越多越好",实操之后才明白,对7B模型来说,上下文越长越容易串味,最小必要上下文才是多Agent稳定工作的关键。这个经验后来也被我们用在了所有Agent的prompt设计里。
4.3 路由逻辑与状态传递
Supervisor的路由,我采用意图分类加规则兜底的双保险。先让模型对用户意图做分类,类别映射到对应Agent;同时叠加关键词规则,比如用户提到"市盈率""市值""净利润",不管模型分类结果如何,都强制路由到财务数据Agent。测试下来,这种"模型+规则"的混合路由比单纯依赖模型稳定得多。
# Supervisor 路由逻辑:意图分类 + 关键词规则兜底 def route(query: str, intent: str) -> str: if any(k in query for k in ["市盈率", "市值", "净利润", "毛利率"]): return "financial_agent" if any(k in query for k in ["研报", "观点", "展望", "评级"]): return "research_agent" if "对比" in query or intent == "compare": return "compare_agent" if "合规" in query or "风险" in query: return "compliance_agent" return "general_handler"状态传递方面,我维护了一个全局状态对象,记录用户问题、检索结果、各Agent输出和最终答案。Supervisor每次路由前更新状态,子Agent完成后把结果写回状态。流程控制上加了两道保险:最大路由轮次设5次,超过就停止并把已有结果返回;每个子Agent执行完必须给出明确的"完成"标记,没有标记就按异常处理,避免Agent之间互相踢皮球陷入死循环。
4.4 Human-in-the-loop与合规兜底
金融场景还有一个硬要求:不能全自动。涉及投资建议、业绩预测这类结论,哪怕系统答出来了,也必须有人工确认环节。我给Supervisor的最终输出增加了一个"置信度"字段。当置信度低于阈值,或者问题命中合规敏感词表时,系统不直接返回答案,而是进入人工审核队列,由业务人员在界面上确认或修改后再对外展示。
这个设计初看多了一步,但对业务接受度的提升非常关键。金融业务方天然不信任一个纯自动生成的内容系统,有了人审兜底,他们才真正愿意把系统用起来。对生产系统来说,让用户信任,有时候比让用户觉得"智能"更重要。这一步做完之后,系统才算是真正从Demo变成了可交付的产品。
5. 实战复盘:那些踩过的坑和排查记录
5.1 幻觉问题排查实录
即使架构迭代到多Agent,幻觉也没有被彻底消灭,只是从高频降成了低频偶发。记录下来的典型残留幻觉主要有三类,这里整理成表格方便对照:
| 残留幻觉类型 | 典型表现 | 应对方案 |
|---|---|---|
| 数据冲突 | 上下文有两个年份的同名指标,模型选错年份 | prompt中强制"以最新披露日期为准" + 字段抽取时带时间标签 |
| 过度外推 | 模型根据已有数据编造一个无来源的中间值 | 强制引用校验,无来源数值不允许输出 |
| 张冠李戴 | 把A公司的财务特征安到B公司头上 | 检索环节做实体对齐,公司名/代码不一致的chunk直接丢弃 |
其中张冠李戴最隐蔽,排查难度也最大。这类问题通常发生在用户问A公司、但相似段落来自B公司研报的场景。我们最终的应对是在检索阶段增加实体对齐校验:先抽取问题主体实体,再与候选chunk的实体标签做匹配,不匹配的直接过滤。这一步做完,误答率又下降了一截。
另一个排查心得是:一定要把模型的完整运行轨迹记录下来,包括检索命中了哪些chunk、模型调用了哪个工具、解释器执行了什么代码、最终引用了哪些依据。这些日志在问题定位时价值极大,尤其是金融场景,任何一次误答都要有据可查。我们最初没有全量记录,结果一次线上误答排查花了好几个小时,后来补上完整日志,同类问题基本分钟级定位。
5.2 工具调用失败的典型场景
代码解释器上线初期,失败率一度高得吓人。最常见的错误是模型生成的Python代码有语法错误,或者引用了不存在的变量。一开始我以为是模型写代码能力不行,后来发现根因在数据格式上——我传给模型的结构化素材字段名不稳定,有时候叫"净利润",有时候叫"净利润(万元)",模型照着示例写代码自然就乱套了。解决办法是提供明确的字段schema和示例代码模板,让模型做填空式生成,而不是自由发挥。这个改动之后,一次执行成功的比例大幅上升。
另一个高频问题是超时。涉及大量数据聚合的计算,比如"统计过去五年所有季度的营收",数据量一大解释器就容易超时。我的处理是把任务拆小,必要时让Agent自己把任务拆成多步执行,而不是让一行代码跑完全部。宁可多调用几次工具,也要保证每次执行都在可控时间内完成。
5.3 性能与成本平衡
7B模型的最大优势是便宜、可控,但多Agent架构会让模型调用次数成倍增长,延迟和算力开销都上来了。我做了两个优化。一是给简单问题设置快速通道:不涉及多环节分析、不涉及多源数据对比的问题,直接走单Agent轻量流程,不经过完整Supervisor编排。二是精简子Agent的prompt,去掉不必要的系统提示和示例,这能明显缩短生成时长。实测下来,整体平均响应时间比最初版本快了不少,算力成本也控制在可接受范围。
这里提醒一句:多Agent不是越多越好,每多一个Agent就多一次模型调用、多一个出错的可能。我们最后保留下来的Agent只有四个,每个都有明确的业务价值,那些"锦上添花"的Agent全部砍掉了。
6. 经验总结
这套系统从第一版朴素RAG到多Agent架构,迭代了好几轮。真要说哪条经验最值得分享,我总结成一句话:模型能力不够的时候,不要在prompt层硬扛,要在架构层去补。7B模型的幻觉、算术、推理短板,分别被强制引用、代码解释器、Supervisor路由补上了,最终系统才稳定跑在生产环境里。如果当初我们只想着优化prompt,大概率还在跟幻觉打持久战。
最后再分享一个小技巧:无论做RAG还是Multi-Agent,务必把中间过程全部记录下来,检索命中记录、工具调用记录、代码执行记录,这些日志是使用模型类系统时最容易被忽略、却又最救命的东西。我们当初也嫌全量日志麻烦,直到一次线上误答排查吃了大亏才补上。对金融这类对准确性要求极高的场景来说,这套"过程留痕"的机制,可能比模型本身更值得投入。这一步千万别省。