尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

金融RAG精度提升:条款森林架构与vLLM性能优化实践

金融RAG精度提升:条款森林架构与vLLM性能优化实践 1. 从“条款森林”说起金融RAG的精度困境与架构瓶颈最近在折腾一个金融领域的智能问答项目核心需求是让大模型能准确回答关于复杂金融合同、监管文件、产品说明书里的具体条款。这听起来是个典型的RAG检索增强生成应用场景对吧但真做起来你会发现传统的RAG架构在这里简直是“水土不服”。我们最开始走的也是经典路线把PDF文档切块、向量化、存进向量数据库用户提问时做语义检索把最相关的几个文本块扔给大模型去生成答案。在通用知识库上这套流程跑得还行。但一遇到动辄上百页、条款间引用关系错综复杂的金融协议问题就全暴露了。最典型的就是“碎片化检索”和“上下文割裂”。比如用户问“这份贷款协议中关于‘交叉违约’条款的具体执行条件是什么” 向量检索可能会返回几个包含“交叉违约”字样的片段但这些片段可能只是定义部分而具体的“执行条件”——比如违约金额阈值、宽限期、通知义务——很可能散落在协议的其他章节甚至被其他条款如“定义与解释”、“违约事件”所引用和限定。模型拿到的只是几个孤立的“树叶”根本看不到整棵“树”的结构更别说理解条款之间的逻辑森林了。这就是我们引入“条款森林”Forest of Clauses, FoC这个概念的初衷。它不是一个现成的工具而是一种针对金融、法律等强结构化文档的RAG架构设计思想。其核心是把文档从“一袋词向量”还原成“一棵有逻辑的树”甚至是一片相互关联的“森林”。简单说FoC试图在检索前先对文档进行深度结构化解析识别出所有的条款、子条款、定义、引用关系并构建一个图状的知识网络。当用户提问时系统不是去向量库捞几个相似的片段而是先在这个“条款森林”里进行精准的“路径查找”和“子图抽取”把与问题相关的完整逻辑链而不仅仅是文本片段找出来再喂给大模型。这样模型得到的上下文是连贯、完整且具备逻辑性的答案的准确性和可靠性自然大幅提升。然而理想很丰满现实却很骨感。当你真的尝试构建并查询这片“森林”时性能问题立刻成为拦路虎。文档解析、图构建都是计算密集型操作基于图的检索虽然精准但比简单的向量相似度计算要复杂得多更别提最终生成答案的大模型本身就是个“吞金兽”。如何在引入FoC带来精度提升的同时不把系统响应时间拖到用户体验不可接受的程度就成了架构演进中必须解决的“性能压榨”难题。这也是为什么我们会把目光投向vLLM这类高性能推理引擎试图从模型服务层挤出每一毫秒的性能。接下来我就结合我们趟过的坑聊聊从传统RAG到集成FoC的架构演进之路以及我们是如何在各个环节上“压榨”性能的。2. 架构演进从“向量检索”到“图检索向量过滤”的混合模式完全抛弃向量检索全部转向基于图的条款森林查询听起来很纯粹但在工程上几乎是自杀。金融文档的图结构可能非常庞大一次全图遍历的代价极高。我们的架构演进实际上是一个从“纯向量”到“图主向量辅”再到“混合检索”的务实过程。2.1 第一代纯向量检索的“无力感”最初的架构非常简单也是大多数RAG项目的起点文档处理使用LangChain的RecursiveCharacterTextSplitter或MarkdownHeaderTextSplitter尝试按标题分割。但对于格式不统一的PDF效果很差常常把一条完整的条款拆得七零八落。检索使用ChromaDB或Qdrant存储文本块的向量。检索时用问题向量去数据库里做similarity_search返回top-k个块。生成将检索到的文本块拼接成上下文调用 OpenAI API 或部署一个开源模型如Qwen-7B进行生成。遇到的典型问题答案不完整如上文所述检索到的块不包含全部必要条件。引用缺失模型会基于不完整的上下文“脑补”可能错误地解释或遗漏关键引用关系。幻觉加剧当关键信息缺失时大模型更容易胡编乱造。2.2 第二代引入条款森林FoC作为“预处理器”我们意识到必须在检索之前增加一个“理解文档结构”的环节。这就是FoC的核心层。这一代的架构变成了双路并行路径A图检索深度解析使用经过微调的NER模型或基于规则/格式的解析器对于标准化程度高的文档识别文档中的所有条款单元Clause Unit。每个单元包含条款编号如Section 3.1、标题、正文内容、类型定义、义务、条件、例外等。关系抽取通过规则如正则匹配“见第X条”、“根据前述规定”或轻量级关系抽取模型建立单元之间的引用关系references、is_defined_by、amends。图构建将单元作为节点关系作为边构建一个有向图。这就是“条款森林”。我们会把这个图序列化如用NetworkX生成GraphML存储起来。图查询当用户提问时首先使用一个轻量级的文本分类模型或关键词匹配确定问题可能涉及的核心条款节点。然后从这个节点出发在图上游走例如遍历其引用的节点、定义它的节点、它修改的节点抽取出一个相关的子图。将这个子图的所有节点内容按逻辑顺序如广度优先遍历顺序拼接形成“图检索上下文”。路径B向量检索 与传统方式类似对所有文本块可以是原始的也可以是条款单元的内容做向量化存储和检索得到“向量检索上下文”。融合将“图检索上下文”和“向量检索上下文”拼接送入大模型。此时图上下文提供了主干逻辑向量上下文补充了细节和周边信息。这一代的进步与代价进步答案的准确性和逻辑连贯性有显著提升。模型能清楚地看到“交叉违约”的定义、触发条件、例外情况之间的关联。代价处理延迟文档解析和图构建是离线过程但即使在线查询阶段的图遍历也比向量检索慢一个数量级。架构复杂需要维护两套检索系统和一套融合逻辑。冷启动问题新文档必须先经过耗时的FoC构建流程才能被高质量检索。2.3 第三代混合检索与向量化图嵌入第二代架构虽然有效但图检索的延迟在实时问答中仍然敏感。我们进一步优化走向了“图引导的向量检索”即混合检索模式。核心思想是利用FoC提供的丰富元数据和结构信息来增强向量检索的表示和排序而不是完全依赖图遍历。具体实现构建增强的向量单元不再简单切割文本。我们将FoC解析出的每个“条款单元”作为一个基本的向量化单元。但在为这个单元生成向量embeddings时我们不仅仅使用其自身的文本。上下文注入在编码前我们会把该单元的“图上下文”信息以特定格式拼接到文本中。例如一个单元的文本可能是“[定义] 交叉违约指借款人... [引用] 参见第5.2条违约事件[父条款] 第3条违约条款”。这样向量本身就蕴含了结构信息。元数据过滤为每个向量单元附加丰富的元数据clause_id,clause_type,section_hierarchy如3.1.2referenced_by哪些条款引用了它。混合检索流程Step 1: 粗筛图引导用户提问Q。首先用一个非常快的关键词提取或小模型从Q中识别出最可能涉及的1-3个核心条款类型或编号例如识别出“交叉违约”可能对应clause_type: default和section_hierarchy: 3.*。这步很快相当于在森林入口做了个路标识别。Step 2: 向量检索元数据过滤使用问题Q的向量在向量数据库中进行检索。但这次我们带上第一步得到的元数据作为过滤器。例如在Qdrant或Weaviate中我们可以构建这样的查询“查找与Q向量最相似的top-k个点但必须满足clause_type为 ‘default’ 且section_hierarchy以 ‘3.’ 开头”。这极大地缩小了搜索范围提升了精度。Step 3: 重排序与上下文构建检索到的单元已经自带结构信息。我们可以根据它们在原始文档中的逻辑顺序通过clause_id或section_hierarchy排序进行重排并自然拼接。如果需要还可以根据单元间的引用关系动态补入少数被引用但未在top-k中的关键单元通过查询图数据库快速完成。这一代的优势性能与精度的平衡检索主体仍是高效的向量搜索延迟可控。图结构信息以元数据和增强向量的形式发挥作用指导检索方向避免了昂贵的全图遍历。灵活性可以灵活调整“图引导”的强度。对于简单问题可以弱化过滤对于复杂问题可以加强元数据约束。与现有生态兼容主流向量数据库都支持基于元数据的过滤集成成本低。这个架构演进的过程本质上是将FoC从一种独立的“检索器”转变为赋能传统向量检索的“增强器”。它不再替代向量检索而是让它变得更聪明。3. 性能压榨实战vLLM如何成为生成环节的“涡轮增压器”架构优化解决了检索的精度和部分延迟问题但生成环节——大模型推理——依然是耗时大户。尤其是在金融场景下我们可能无法使用太小的模型如7B需要至少14B或32B级别的模型来保证对复杂逻辑的理解和生成质量。这时推理引擎的效率直接决定了用户体验。我们对比了多种方案最终vLLM以其卓越的吞吐量和极低的延迟脱颖而出成为了我们的核心推理引擎。3.1 为什么是vLLM不仅仅是PagedAttention很多人知道vLLM是因为它的PagedAttention算法它像操作系统管理内存一样管理KV Cache解决了传统自回归模型解码时因序列长度变化和批处理带来的大量内存浪费问题从而能支持更大的批处理大小batch size。这对于高并发场景至关重要。但在我们的压榨实践中vLLM的优势是全方位的连续批处理Continuous Batching这是vLLM在实时场景下吊打传统静态批处理的关键。传统方式要等一个批次里所有请求都生成完毕才能处理下一批快慢取决于最慢的那个请求。vLLM的连续批处理允许它动态地将新请求加入正在运行的批次中并让已完成的请求提前退出极大提高了GPU利用率。对于金融问答这种输入输出长度差异可能很大用户问题短模型生成的答案可能很长的场景收益巨大。极简的部署与Servingvllm serve命令一行启动一个高性能的、兼容OpenAI API协议的服务就起来了。这省去了我们基于Transformers库自封装服务时在并发、队列、批处理上踩的无数个坑。强大的生态与优化对FlashAttention的良好支持、与AWQ/GPTQ等量化工具的紧密集成让我们能轻松部署量化模型进一步降低显存占用和延迟。3.2 我们的vLLM部署与调优流水线以下是我们从模型准备到服务调优的完整步骤其中包含了不少踩坑后总结的经验。步骤一模型准备与量化我们选择Qwen1.5-14B-Chat作为基座模型因为它对中文金融文本理解较好且社区活跃。# 1. 使用AWQ进行量化在精度损失和速度提升间取得较好平衡 # 安装 autoawq pip install autoawq # 执行量化生成 qwen1.5-14b-chat-awq 目录 python -m autoawq.llm_awq \ --model /path/to/Qwen1.5-14B-Chat \ --w_bit 4 \ # 4比特量化 --q_group_size 128 \ # 分组大小 --zero_point True \ # 使用零点量化 --version GEMM \ # 使用GEMM版本兼容性好 --output_path ./qwen1.5-14b-chat-awq注意量化不是无损的对于金融条款中一些非常细微的措辞差异如“应当” vs “可以”低比特量化可能导致模型敏感度下降。我们做过A/B测试对于我们的任务4-bit AWQ带来的轻微精度损失在可接受范围内而推理速度提升超过2倍显存占用减少60%性价比极高。步骤二vLLM服务部署# 2. 使用vLLM启动服务 # 指定量化后的模型路径使用Tensor并行在2张A100上分布 vllm serve ./qwen1.5-14b-chat-awq \ --tensor-parallel-size 2 \ --max-model-len 8192 \ # 模型支持的最大长度根据需求调整 --gpu-memory-utilization 0.9 \ # GPU内存使用率给系统留点余量 --enforce-eager \ # 对于某些模型或环境启用eager模式可能更稳定 --port 8000关键参数解析与调优--max-model-len这是最重要的参数之一。它定义了vLLM为每个请求预留的KV Cache的最大长度。设置过小长文档上下文会被截断设置过大会浪费显存降低并发能力。我们的经验是根据你的检索系统返回的上下文平均长度加上问题长度和答案预留长度再乘以一个安全系数如1.5来设定。我们设置为8192足以覆盖绝大多数场景。--gpu-memory-utilization默认0.9。在显存紧张时可以调低如0.85给PagedAttention的内存调度留更多空间避免OOM。如果显存充足可以保持默认或略调高。--enforce-eager在vLLM早期版本或某些特定模型上关闭CUDA Graph即启用eager模式可以避免一些诡异的内存错误或输出不一致问题。如果遇到vllm serve输出不一致比如相同输入得到不同输出的情况首先尝试加上这个参数。步骤三客户端调用与性能测试服务启动后我们使用一个简单的Python脚本进行性能和效果测试。import openai import time import statistics client openai.OpenAI( api_keytoken-abc123, # vLLM服务不需要真实token任意非空字符串即可 base_urlhttp://localhost:8000/v1 ) # 模拟一个典型的金融问答上下文和问题 test_context [此处插入一段由FoC混合检索系统返回的、关于“交叉违约”的完整结构化上下文可能包含多个条款单元约3000字符] test_question 请总结一下触发交叉违约的具体条件有哪些以及债权人需要履行哪些通知义务 prompt f你是一个专业的金融合同分析助手。请严格根据以下提供的合同条款上下文来回答问题。如果上下文没有明确提及请回答“根据所提供信息无法确定”。 上下文 {test_context} 问题 {test_question} def test_latency_and_output(): latencies [] responses [] for i in range(10): # 测试10次取平均 start time.time() response client.chat.completions.create( model./qwen1.5-14b-chat-awq, # 模型名与服务启动时一致 messages[{role: user, content: prompt}], max_tokens500, temperature0.1, # 金融场景要求确定性温度设低 top_p0.9, ) end time.time() latencies.append(end - start) responses.append(response.choices[0].message.content) print(fRequest {i1} latency: {latencies[-1]:.2f}s) # print(fResponse: {responses[-1][:200]}...) # 可选打印部分回答 avg_latency statistics.mean(latencies) p95_latency statistics.quantiles(latencies, n20)[18] # 计算95分位数 print(f\n 测试结果 ) print(f平均延迟: {avg_latency:.2f}秒) print(fP95延迟: {p95_latency:.2f}秒) # 检查输出一致性对于确定性要求高的场景 if all(r responses[0] for r in responses): print(输出一致性: 完美 (10次输出完全相同)) else: print(警告: 输出存在不一致) # 可以进一步diff分析 if __name__ __main__: test_latency_and_output()通过这样的测试我们能够量化vLLM服务在真实负载下的表现。在我们的硬件2xA100和量化模型下平均生成延迟TTFT Token生成时间稳定在1.5-2.5秒之间P95延迟在3秒以内满足了金融业务对实时性的基本要求。3.3 遇到的坑与解决方案vllm serve输出不一致这是最令人头疼的问题。相同输入两次请求得到不同答案。除了上文提到的使用--enforce-eager参数还需要检查温度temperature和随机种子seed确保在请求中明确设置了temperature0.1和seed42。vLLM的默认温度可能不是0。模型本身有些量化模型可能存在不稳定性。尝试换用不同的量化方法如GPTQ或调整量化参数如q_group_size。vLLM版本更新到最新稳定版很多此类问题在后续版本中被修复。长上下文下的性能衰减当max-model-len设置很大如32K即使实际序列很短vLLM初始的内存分配也会很大影响吞吐。我们的策略是按需分配。部署两个vLLM服务实例一个max-model-len4096处理简单问答另一个max-model-len16384处理需要超长上下文如整份协议分析的复杂任务。通过网关路由请求。海光/昇腾等国产GPU适配vLLM官方主要支持NVIDIA CUDA。对于海光DCU或昇腾Ascend需要寻找社区移植版或厂商提供的定制分支。关注vllm.engine.model_loader中的权重加载逻辑。对于昇腾可能需要修改vllm.worker.model_runner中关于torch.cuda的调用替换为torch.npu并处理自定义的AscendModel类。这是一个深度定制工作需要较强的工程能力。通过将vLLM作为生成引擎我们成功将端到端的响应时间检索生成控制在了3-5秒的范围内其中生成环节的耗时占比从早期的70%以上降到了50%以下性能压榨效果显著。4. 系统集成与全链路调优让FoC和vLLM高效协同单独的组件再优秀如果集成不当整个系统也会效率低下。将FoC混合检索系统与vLLM推理服务组合起来并确保全链路稳定高效是最后一道关卡。4.1 异步化与流水线设计金融问答请求的处理流程可以看作一个流水线用户请求 - 意图识别/FoC图引导 - 向量检索元数据过滤 - 上下文构建与重排序 - 大模型生成 - 后处理与返回为了压榨性能我们必须让这个流水线尽可能并行。异步客户端我们使用aiohttp或httpx的异步客户端来调用vLLM的API避免在等待模型生成时阻塞整个服务进程。Python的asyncio是关键。检索与生成的解耦检索阶段图引导、向量查询是CPU密集型或I/O密集型访问数据库而生成是GPU密集型。我们将它们部署为独立的微服务通过消息队列如Redis Streams或gRPC进行通信。这样检索服务可以持续处理新请求而无需等待上一个请求的生成完成。上下文构建的优化在将检索结果拼接成最终Prompt时我们做了一些优化长度预估与截断在发送给vLLM前先预估Prompt的token长度使用tiktoken或transformers的tokenizer。如果超过max_model_len - max_tokens为答案预留空间则启动智能截断优先保留FoC子图中核心路径上的节点内容剔除冗余或相关性较低的向量检索结果。Prompt模板工程设计清晰的Prompt模板用XML标签或Markdown符号明确区分“系统指令”、“上下文”、“问题”。例如我们用clause id3.1这样的标签包裹每个条款单元有助于模型理解结构。我们发现结构清晰的Prompt能略微提升模型的遵循指令能力和答案质量。4.2 监控、限流与降级高并发下系统必须有自我保护能力。监控指标我们监控几个核心指标vLLM服务GPU利用率、显存使用情况、请求队列长度、每秒处理的Token数Tokens/s、请求延迟P50, P95, P99。检索服务平均检索时间、向量数据库/图数据库查询耗时。全链路端到端响应时间、业务成功率。限流在API网关层对用户请求进行限流。同时对vLLM服务的并发请求数也做了限制防止过多的请求压垮GPU导致所有请求都超时。我们使用了令牌桶算法。降级策略当vLLM服务响应过慢或不可用时系统可以降级到“仅检索”模式即只返回最相关的原始文本片段而不进行生成。或者可以路由到一个更小、更快的备用模型如部署在CPU上的Qwen-1.8B模型虽然质量下降但能保证服务可用。4.3 效果评估与迭代闭环金融场景容错率低我们必须持续评估系统效果。自动化评估我们构建了一个测试集包含数百个从真实合同中提炼的QA对。每次模型更新或架构调整后都会自动运行评估计算答案精确匹配EM、F1分数基于关键词或命名实体以及人工可接受率。Bad Case分析所有失败案例都会进入分析池。我们发现很多错误并非生成模型的问题而是源于上游FoC解析错误NER模型没能正确识别出一个关键条款的边界。关系抽取遗漏两个关联条款的引用关系没有被捕捉到导致检索上下文不完整。向量检索偏差增强向量的表示方式可能让某些不相关但文本相似的条款排名过高。 针对这些我们建立了数据飞轮用Bad Case去微调NER/关系抽取模型或调整向量化时上下文注入的模板。经过全链路的打磨我们的系统最终形成了一个相对稳定的状态FoC负责提供精准的“地图”混合检索负责高效地“定位”vLLM负责基于完整信息进行“精炼输出”。三者各司其职共同在金融RAG这个对精度和性能都有严苛要求的赛道上跑出了一条路。5. 总结与展望架构演进的本质是权衡回顾从传统RAG到引入FoC再到集成vLLM的整个过程本质上是一系列针对“精度-性能-复杂度”这个不可能三角的权衡。纯向量RAG复杂度低性能尚可但精度在复杂领域不足。引入FoC精度大幅提升但复杂度激增原始性能下降。FoC混合检索 vLLM通过架构设计混合检索和底层引擎优化vLLM在可接受的复杂度内找回了性能并保持了高精度。这个演进路径可能不是唯一的但它反映了我们在处理专业领域RAG问题时的一种务实思路不要追求理论上最完美的方案而要寻找工程上最可行的平衡点。未来还有一些方向值得探索更智能的图引导能否用一个小型语言模型来学习用户问题与FoC元数据之间的映射替代基于规则的关键词提取vLLM与检索的更深集成vLLM正在快速发展未来是否可能将检索器的某些计算如重排序卸载到GPU上进一步降低延迟端到端训练也许未来的“检索-生成”模型能够直接接受文档图结构作为输入进行端到端训练从根本上统一两个阶段。对于我们这些在一线折腾的工程师来说架构没有银弹。今天分享的这套“条款森林混合检索vLLM”的组合拳是我们当前阶段应对金融RAG挑战的答案。它可能不完美但足够有效。如果你也在类似领域摸索希望这些踩坑经验和实操细节能给你带来一些启发。记住最重要的不是用了多酷的技术而是你的系统是否真的解决了业务问题并且用户愿意用。
返回列表