M004_基于Langchain+RAG的银行智能客服系统设计与开发
做银行智能客服这个项目,前后折腾了三个多月。刚接到需求那会儿,团队里不少人第一反应是“这不就是接个大模型API嘛,套个Prompt不就完事了”。真做起来才发现,银行场景和互联网To C客服完全是两码事——知识库更新频率、答案准确性要求、合规审计约束、私有化部署限制,每一项都在逼着你放弃“裸奔大模型”的偷懒路线。最终落地选型是LangChain + RAG(检索增强生成),用向量检索把银行内部的业务制度、产品说明、操作手册先捞出来,再让大模型基于检索结果组织回答。这套方案解决的核心痛点,就是让模型在不额外微调的前提下,尽量少“编答案”,把答案来源钉在内部知识库上。适合正在做企业级客服、知识问答类项目,或者刚接触RAG但想了解生产环境落地细节的开发者参考。
1. 整体设计与方案选型拆解
1.1 为什么是RAG而不是微调
项目启动前,我们内部做了一轮技术选型讨论,争论最集中的就是RAG和微调两条技术路线到底怎么选。当时团队里有人认为银行场景专业术语多、问答格式要求严格,微调才能让模型真正“懂行”。但仔细分析后发现,银行的业务知识有一个关键特点:更新极其频繁。存款利率可能按月调整,理财产品说明书隔一阵就换一批,信用卡积分规则说改就改,这些内容如果用微调方式跟进,意味着每次业务变更都要重新走一遍数据准备、训练、评估、上线的全流程,一个版本没跑完下一波变更又来了,根本追不动。
RAG的优势恰恰在于把“知识”和“推理”解耦。知识放在向量数据库里,随增随改,模型只需要负责理解问题和组织语言。业务侧改一份文档,索引更新一下,客服的回答立刻就能跟上,不需要重新训练模型。从项目排期看,微调一条路走通至少需要GPU资源、训练工程师、数据标注人力,而RAG的工程重点在数据处理和检索链路,团队现有的后端开发能力就能覆盖。成本上,RAG因为不需要为每个业务场景单独训练模型,长期维护成本也更可控。
当然RAG也不是银弹。如果你的场景对格式模板要求极其死板(比如必须按固定字段输出报表),或者领域知识极度封闭、公开语料完全覆盖不到,微调仍然是必要的补充。我们最终选了个折中路线:底层基座模型不做微调,但对检索出的知识片段做了大量后处理规则,用工程手段把“格式合规”这件事兜住。这个思路后面在回答策略章节会详细展开。
1.2 系统整体架构与技术栈
整个系统按模块划分,大致可以拆成五层:
- 接入层:对接银行的在线客服渠道,统一接收用户问题,同时做渠道适配和会话管理
- 问答编排层:基于LangChain的Agent和Chain机制,负责任意路由、检索触发、回答生成的组织
- 知识处理层:涵盖离线文档解析、清洗、切片、向量化、入库的全流程
- 检索增强层:向量检索 + 关键词检索的双路召回,配合重排模型做精排
- 服务与审计层:负责接口封装、权限校验、日志留存和回答溯源
技术选型上,LangChain作为编排框架承担了Chain组织、Prompt管理、文档加载器和向量存储对接的职责。向量数据库选的FastAPI + Faiss的组合——起初我们评估过Milvus和Elasticsearch,但考虑到项目初期数据量在几十万片级别,Faiss单机部署完全够用,而且不需要额外维护分布式集群。后来数据量增长后迁移到了Milvus,接口层面因为LangChain做过封装,迁移成本并不高。Embedding模型用的是国产的开源embedding模型,具体地说,是从 HuggingFace 下载的中文向量模型,选择它主要是因为它对中文金融术语的支持效果好,并且支持私有化部署,不依赖外部API调用。
检索链路分两条路并行:一条是向量检索,用Embedding模型把用户问题转成向量后在Faiss里做相似度搜索;另一条是BM25关键词检索,用来兜底一些专业术语和精确匹配场景。两路结果汇合后,进入重排阶段——这也是我们在实践里发现对最终回答质量影响最大的一个环节,具体做法后文会详细写。
1.3 为什么用LangChain做编排
市面上有不少RAG框架,比如LlamaIndex也做得不错,但结合银行项目的实际情况,LangChain有两个不可替代的点。第一是它的抽象层次刚好合适,从文档加载器、文本分割器、向量存储到Chain,每个环节都留了接口让你替换自己的实现,而不是把整套方案焊死。比如官方默认的文本分割器对中文支持一般,我们直接自己写了一个针对银行文档结构的分割逻辑,通过LangChain的TextSplitter基类接入,其他链路代码一行没改。
第二是它的工具生态在检索问答这个场景上最成熟。像RetrievalQA、ConversationalRetrievalChain这些高层封装,配合ChatPromptTemplate做Prompt管理,开发效率确实高。当然LangChain的版本迭代很快,API经常变动,我们在项目里做了一个策略:核心业务逻辑自己包一层Service,不直接散落调用LangChain的API,这样后续升级LangChain版本时改动可控。
2. 银行知识库建设与语料处理
2.1 数据来源与特征分析
知识库建设是RAG项目里最脏最累但又最决定上限的环节。数据不行,后面检索、Prompt整得再花哨都白搭。银行客服涉及的知识来源比一般企业复杂得多,我们第一期整理下来主要有这几类:
- 产品制度类文档:存款、贷款、理财、信用卡等产品的业务制度和操作规程,多为Word和PDF格式,经常有大段法律条文式表述
- 操作手册类内容:网银、手机银行、自助设备的操作指引,往往带有步骤编号和界面截图说明
- 高频问答对:客服团队过往积累的标准问答库,结构相对规整,但字段格式不统一
- 公告与通知:利率调整、系统升级、产品停售等时效性强的信息,对新鲜度要求高
这些数据有一个共同的问题:格式混乱。同一个产品在不同文档里可能表述不一致,有的文档扫描件夹杂在Word里,有的PDF翻录后文字带大量空格和错位。如果原样入库,检索效果会非常差。我们统计过,第一版直接入库后,部分问题的检索召回准确率不到六成,根因就是源头数据噪声太大。
2.2 文档清洗与切片策略
清洗阶段我们沉淀了一套固定流程。首先是格式统一,所有Word和PDF转成纯文本,扫描件走OCR服务识别,识别结果里常见的“囗”“口”混用、断行错乱用规则批量修一遍。然后做内容去重,同一份制度文件可能存在多个版本,按发布时间保留最新,同时把附件、落款、历史修订记录这些页面噪声剔除。
切片是知识库质量的关键一步,也是很多人容易忽视的地方。我们踩过的坑是:直接用固定字符数切分,比如每500字一刀切,结果一个完整的产品规则被掰成两三截,检索时经常只召回其中一部分,回答就残缺不全。后来我们改用“结构感知切分”,大致逻辑是:
- 先识别文档的段落结构,针对银行制度文件里常见的“第X章”“第X条”“一、二、三”层级做标记
- 切分时优先保持章节和条目的完整性,一个条目内容过长时才按段落二次切分
- 相邻切片增加少量重叠(overlap),避免语义边界被切断
语义完整性保住了,检索的命中率明显改善。另外切完的每一片都保留元数据,包括来源文档名、章节路径、更新时间,这些信息在回答溯源和权限过滤时都要用到。
2.3 Embedding模型选型与向量化
Embedding是整个检索链路的地基,选型时对比了市面上几款主流中文Embedding模型。测试集是从银行客服真实对话里抽样出的两百条问题,按“能否检索到正确答案所在的文档片段”这个指标卡出来的结果,效果排名前列的分别是BGE-M3、BGE-large-zh-v1.5和M3E。最终选了BGE系列模型,理由是它在中文长文本上的表现稳定,而且对金融术语的语义理解明显比其他通用模型强。
向量化过程中还有几个工程细节值得说。第一是查询侧和文档侧可以使用不同的向量化策略:文档入库时用完整的描述性语句生成向量,而用户查询往往很短,直接用原句向量化就行,不需要额外做Query扩展。第二是向量维度与检索性能的权衡,BGE-large-zh-v1.5是1024维,在Faiss里索引两百万条向量毫无压力,但当数据量再上一个量级,降维或者换用M3E这类768维模型可以作为备选方案。第三是增量更新,每天凌晨定时扫描新增和变更的文档,增量向量化后更新索引,保证知识库新鲜度。
3. 基于LangChain的问答链路实现
3.1 会话管理与问题理解
银行客服场景的对话和单轮问答最大的区别在于上下文依赖。用户经常先问“信用卡怎么申请”,接着问“需要什么材料”,这个问题单独看是不完整的,必须结合上一轮才能理解。我们基于LangChain的ConversationalRetrievalChain做扩展,实现了一个轻量级的会话管理模块。
具体做法是把历史对话压缩成两部分的记忆:一部分是原始对话记录,用于大模型理解指代关系;另一部分是系统抽取出的“当前任务摘要”,防止多轮之后上下文太长把关键信息冲掉。两个部分拼接后作为问题理解的上下文输入,再做检索。这里有一个经验:不要把全部历史对话都塞进Prompt,一方面浪费Token,另一方面检索时的噪声也更大,通常保留最近三到五轮就够了,更早的内容通过摘要保留核心意图。
遇到需要个人化回答的问题(比如查积分、查账单),系统会抽取用户ID,拿到会话上下文中存储的账户信息和权限范围,再决定回答策略。涉及具体账户金额的问题,我们不会让大模型直接回答,而是触发后端接口实时查询后,把结果按模板组装成答案——这也是银行场景里对准确率要求最严格的一类问题。
3.2 Prompt设计与LLM调用封装
Prompt设计在RAG链路里的重要性,怎么强调都不过分。我们第一版Prompt非常简单,大概就是“基于以下资料回答用户问题”,结果模型经常把检索出来的无关信息也揉进答案,甚至自己发挥补充一些知识库没有的内容。后来经过多轮迭代,沉淀出一套相对稳定的Prompt结构:
system_template = """ 你是银行智能客服助手,请严格按照以下规则回答用户问题: 1. 只能依据“参考资料”中的内容回答问题,严禁编造参考资料中不存在的信息。 2. 如果参考资料不足以回答用户问题,请直接回复“抱歉,这个问题我暂时无法回答”,不要尝试猜测。 3. 如果用户问题涉及账户、额度、利率等个人化信息,提示用户通过人工客服或自助渠道查询。 4. 回答时保持简洁、专业,避免使用模型自身经验补充无关内容。 参考资料: {context} """这套Prompt看起来简单,但背后的设计是有讲究的。第一点“严禁编造”实际上是给模型划了边界,配合“无法回答就直接说”,能有效把回答的幻觉率压下来,这点后面评测章节有数据支撑。第二点是针对银行合规要求的,个人化查询必须转人工,防止模型基于模糊数据给出错误账户信息。Prompt里的{context}就是检索重排后选出的最相关文档片段,通常取三到四段,每段限制在三百字左右,太长会让模型抓不住重点,太短则信息可能不够。
LLM调用层我们做了统一封装,通过LangChain的llm接口代理,底层可以切换不同的基座模型。第一期用的开源模型,参数量级别大概在7B到14B之间,部署在内网GPU服务器上。对延迟的要求是单次回答控制在3秒以内,因此Prompt的Token数做了严格控制,检索结果过多时先做一遍截断和去重,再进入模型。
3.3 检索增强的完整编排
整个问答编排的核心代码结构大致是这样的:
# 向量检索 vector_retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 10} ) # 关键词检索 keyword_retriever = BM25Retriever.from_documents(documents) # 双路召回合并 ensemble_retriever = EnsembleRetriever( retrievers=[vector_retriever, keyword_retriever], weights=[0.7, 0.3] ) # 重排 compressor = CrossEncoderReranker( model_name="bge-reranker-large", top_n=4 ) # 组装问答链 qa_chain = RetrievalQA.from_chain_type( llm=llm, retriever=compress_retriever, chain_type="stuff", return_source_documents=True )EnsembleRetriever做双路召回合并,权重配比上向量检索占七成、关键词检索占三成,这个比例是在两百多条测试问题上逐步调出来的。纯向量检索对近义表达和口语化问题更友好,比如用户说“我卡里钱少了”,靠语义能关联到“账户余额变动”相关文档;但遇到“U盾”这种专业名词,向量召回反而不如关键词精确匹配。合并后的结果再用交叉编码器重排,这里用的模型专门针对“文档片段与问题的相关性”做打分,效果比向量相似度再排序要准不少。
chain_type我们选的stuff,意思是把检索结果都塞进Prompt一起让模型回答。这个模式在小规模检索结果下效果最好,因为模型能看到全部上下文,不会因为遗漏某一篇而产生偏差。如果检索片段数量太多,可以改用map_reduce或者refine模式,但回答质量和延迟都会有所牺牲。项目一期检索结果控制在四段以内,stuff完全够用。
4. 检索质量优化与效果调优实践
4.1 双路召回与重排的实战效果
检索质量是RAG系统回答准确率的前置条件,检索都找不到正确内容,模型再聪明也无济于事。我们在调优过程中记录了一组数据:第一版单独用向量检索时,两百道测试题中有大约七成能正确命中答案所在的文档片段;加上BM25关键词召回做合并后,命中率提升到八成左右;再加上重排模型精选Top结果,最终“答案准确率”稳定在九成以上。
这个提升链条说明一个道理:每一层优化都是在补上一层漏掉的东西。向量检索解决语义相似问题,但有专业术语时不灵;关键词检索解决精确匹配问题,但口语化表达不灵;两者合并覆盖的面更全,但多路召回会引入噪声,把不相关但字符相似的内容混进来。重排模型就是干这个活的,把两路结果合并后统一做相关性打分,低质量的片段在最后一步被过滤掉。
重排模型的选择上,我们用了交叉编码器架构的专用重排模型。它的特点是把问题和文档片段拼接在一起输入模型,可以捕捉更细粒度的语义匹配,比向量相似度这种“先各自编码再算距离”的方式更准。代价是推理速度慢,所以只对双路召回后的少量候选(十到二十条)做重排,而不是在全量索引上做,这样精度和性能都能兼顾。
4.2 问题改写与查询扩展
银行客服场景里,用户提问的口语化程度比我们预想的要高得多。比如用户说“我那个贷款为啥没批下来”,系统如果直接拿这句话去检索,向量机虽能匹配到一些相关文档,但准确率不太稳定。我们的解决方案是在检索前加一个“问题改写”步骤:让大模型把口语化问题转成更正式的检索式表达。
rewrite_prompt = """ 请将用户的问句改写为适合知识库检索的正式查询语句。 要求:保留核心意图,去掉口语化表达,补充必要的业务术语。 例如: 用户问:我那个贷款为啥没批下来 改写为:个人贷款审批未通过原因 用户问:信用卡怎么提额快 改写为:信用卡额度提升方法 用户问:{question} 改写为: """这一招对检索质量提升非常明显。主要原因是Embedding模型训练的语料偏向正式文本,口语化表达在向量空间里的分布和正式术语不在一个区域,改写后再检索更容易命中知识库里那些“一本正经”的制度文档。当然问题改写会引入额外一拍延迟,我们只在检索前做一次,实测增加的时间大约几百毫秒,在可接受范围内。
4.3 针对银行专业场景的检索调优
通用RAG教程教的是标准做法,真落到银行场景有几处要单独调。一是同义词和简称的问题,“对公账户”和“单位银行结算账户”指的是同一件事,知识库里可能两种表述都有,但检索时不一定都能召回。我们的办法是在索引写入时额外扩展一组同义词字段,把文档涉及的核心业务实体别称一起存进去,检索时多一个匹配通道。
二是时效性加权。银行文档里“现行利率”“最新优惠”这类信息,知识库里旧文档和新文档可能同时存在,纯相似度检索可能会召回旧版本。我们的做法是给切片元数据里增加发布时间字段,重排打分时做轻微的时间衰减加权,让新文档排名略靠前,避免回答“已经过期的存款利率”这种低级错误。
三是安全过滤前置。部分文档属于内部制度,只有特定角色权限的客服人员才能查看,在检索阶段就要根据当前用户的权限范围对候选文档做过滤,而不是等回答生成后才发现越权。这个操作要在检索之前做,因为一旦包含敏感内容的片段被塞进了Prompt,即使模型不直接输出,也存在数据泄露的风险。
5. 评测体系与闭环迭代
5.1 分类评测指标设计
RAG系统上线前如果只靠人工点几条问题看看效果,基本等于没测。我们基于银行客服的实际场景,搭建了一套分类评测体系,主要分三个维度:答案准确性、拒答合理性和格式合规性。
- 答案准确性:模型回答的核心信息是否和知识库原文一致,是否出现事实错误或编造
- 拒答合理性:当知识库信息不足时,模型是否正确拒答,而不是强行给答案
- 格式合规性:涉及金额、日期、利率等数字时格式是否正确,是否存在“大概”“左右”等模糊表达
评测集是从真实客服日志里抽样出的三百条问题,覆盖高频咨询、边缘案例和易错场景,每轮迭代后在这个固定集上跑一遍,记录三个维度的通过率变化。这样每次改动Prompt还是重排逻辑,都能看到明确的量化反馈,而不是凭感觉说“好像效果好了点”。
5.2 评测结果与调优过程
第一版系统跑完评测,答案是“惨不忍睹”。答案准确率不到七成,拒答率又过高——有一半以上的问题模型都回复“无法回答”,里面相当一部分其实是知识库里有答案但检索没找出来。当时对照bad case一个个分析,归纳出了几类主要问题:
第一类是切分不合理导致的召回失败,产品规则被切碎后,检索命中的只是一个残缺片段,模型看了半天形不成完整答复,只好拒答。这直接推动了章节2里结构感知切分策略的落地。第二类是Prompt边界不清晰,模型对“哪些能答、哪些不能答”的判断摇摆,造成拒答率偏高。我们把Prompt里“无法回答”的触发条件写得更具体,比如“仅在参考资料完全未提及的情况下拒答”,通过率明显改善。第三类是重排把正确答案挤掉了,有时候重排模型偏好语义更流畅的段落,反而把包含明确答案的段落排到后面,后来通过调整Top N数量和重排权重缓解了这个问题。
三轮迭代后,答案准确率从六成多提到了九成左右,拒答合理率也恢复了正常区间。这个过程让我深刻体会到一件事:RAG项目的调优重心,大部分时间不在模型上,而在数据和检索链路上,模型本身反而不用怎么动。
_5.3 线上监控与人工反馈闭环
系统上线后,评测不能停,但要换个形式。线上运行会不断产生新的提问类型,知识库也可能出现新的盲区。我们设计了一个“bad case回流”机制:客服系统里增加了一个反馈按钮,当用户对回答不满意或转人工时,这条会话自动打标进入待分析队列。每周抽取一批bad case,人工标注出问题原因(是检索漏召回、重排排错,还是知识库缺内容),汇总后进入优化清单。
这个闭环是系统持续变好的关键。比如上线第二周就发现,用户经常问“营业网点在哪里”,这类问题和知识库里的文档内容关联度低,检索效果很差。后来我们在知识库里补建了一个网点和营业时间的数据源,对应问题从bad case里慢慢消失。没有这个反馈闭环,很多问题你根本不知道用户在问什么,系统迭代就成了闭门造车。
6. 部署实践与避坑总结
6.1 私有化部署与资源规划
银行场景的信息安全要求决定了系统必须私有化部署,所有模型和数据都在内网环境,不能调用外部API。整个系统包含四大块资源:GPU服务器(跑Embedding模型和LLM推理)、CPU服务器(跑检索服务和后端接口)、向量数据库存储、对象存储(保存原始文档和处理后的文本切片)。
GPU服务器的显存规划要提前算好。Embedding模型参数量不大,CPU也能跑,但我们实测GPU推理速度快三四倍,高峰期检索并发上来后CPU方案会明显拖慢响应,所以还是给Embedding模型单独分了一块GPU。LLM推理选择的基座模型参数量约14B,用FP16量化部署时大约需要30GB显存,单张48GB显卡可以承载。这里给个计算经验:推理显存大约等于模型参数量乘以2倍(FP16)再留出KV Cache的余量,不用卡得太死。
向量数据库初期用Faiss单机部署完全够用,索引加载到内存后检索是毫秒级。但要注意增量更新的策略,我们遇到的问题是频繁更新索引会导致内存碎片化,检索偶尔卡顿,后来改成“夜间全量重建 + 日内增量缓存”的模式才稳定下来。
6.2 内容安全与合规审计
银行对客服回答的合规性要求极其严格,该系统架构中专门设计了内容安全过滤和合规审计两层机制。第一层是回答内容过滤,模型生成答案后,增加规则引擎检测敏感信息和不合规表达,比如不承诺“保本保息”,不使用“绝对没问题”这类话术,涉及投诉建议会自动转给人工处理。第二层是全链路审计日志,从用户问题、检索命中的文档切片、模型生成的原始回复到最终发送给用户的答案,全程记录下来,留待后续合规审查和溯源。
在合规审计方案之外,还有一项独立的事实一致性校验环节。对涉及金额、时间、利率等关键数字的答案,系统增加了一个可用API层调用的校验服务,将模型回复中的关键数字和检索到的原文做比对,不一致时触发告警并转人工。这一层校验对降低风险帮助很大,也是RAG系统在银行场景里能走通的关键因素。
6.3 踩坑清单与实操建议
把项目里印象比较深的几个坑列出来,希望对后来者有帮助。
坑一:Tokenizer差异导致的切片计数失真。我们最初用字符数控制切片长度,后来发现大模型Tokenize中文时,不同模型的Token化比例不一致,导致“看起来长度合适”的切片输入模型后Token数超限。后来所有切片长度统一改用Token数计算,按模型实际Tokenizer的容量定上限,这类问题就再没出现过。
坑二:文档去重不彻底导致的回答漂移。同一份产品制度被多个部门上传过,内容略有差异,检索时多个版本一起命中,模型有时综合出前后不一致的回答。现在入库前会用SimHash对切片做一轮去重,相似度超过阈值的只保留最新版。
坑三:LangChain版本升级带来的接口变更。项目期间LangChain从旧版本升到新版本,不少API都变了。我们的应对是前面提过的Service隔离策略,业务代码不直接调LangChain接口,统一收口封装,升级时只改封装层。
坑四:高峰期检索超时。并发量上去后,检索链路偶尔出现超时,原因是向量检索做了全量扫描,内存索引查询时间不稳定。后来加了缓存层,把高频问题直接缓存答案,实测大部分重复问题的响应时间降到几百毫秒,压力显著缓解。
坑五:Embedding模型更新后向量空间不一致。中期换过一次Embedding模型版本,结果新旧向量在同一个索引里语义空间断裂,检索效果时好时坏。教训是Embedding模型版本一旦确定就尽量保持稳定,如果必须升级,需要全量重新向量化再重建索引。
我个人在整个项目里最大的体会是,RAG系统真正难的并不是LangChain怎么用、向量库怎么配,而是数据质量和评测闭环这两件“笨功夫”。第一版系统从搭建到跑通只花了两周,但后续六周时间几乎都花在清洗数据、调检索、看bad case、改Prompt的循环里。效果的自然语言真实提升,正是这些看似不起眼的重复劳动一点一点堆出来的。如果你正准备做类似的项目,我的建议是:先把知识库的数据治理做好,先把评测集建起来,再去追求更花哨的模型和框架。地基打牢了,上面盖什么都稳。