ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于RAG的智能法律问答系统:从混合检索到极简实现指南

基于RAG的智能法律问答系统:从混合检索到极简实现指南 简介该压缩包面向法律科技开发者、NLP初学者及智能问答系统设计者是一个基于RAG架构的智能法律问答系统完整项目。项目针对传统人工法律咨询难以应对法规数量庞大、更新频繁的痛点采用检索增强生成方式先从法律文档库检索相关知识片段再结合深度学习生成专业回答兼顾准确性与响应速度。包内共217个文件、约2.35MB含178个txt法律文本构成知识库、9个Python脚本实现检索与生成核心逻辑以及7套HTML/CSS/JS前端页面另附yaml配置、说明文档与许可证内容预览显示已覆盖用户注册登录、知识库管理、文件上传等完整业务模块。目前已有92人学习下载适合快速搭建法律问答原型或进行课程设计通过该项目可完整掌握从法律语料构建、向量索引到生成回答的RAG工程落地链路并易于替换语料扩展至其他专业领域。1. 法律问答用直接LLM会翻车RAG架构才是能落地的解法如果有人让你做一个法律咨询问答系统直觉反应是接一个大模型API把法条喂进去让它背下来。真这样做过的人都知道后果模型记不住更新后的司法解释回答里自己编法条把废止的条款当有效依据。法律场景不允许这种幻觉回答错了不是扣分是误事。所以这类项目在架构上几乎统一收敛到同一种做法——基于RAG架构的智能法律问答系统用检索增强生成替代模型背书。这个项目标题里带“极简说明”说明它不是一个复杂平台而是一套可复现的实现思路先把民法典、劳动合同法这类文本拆成可检索的段落向量化入库用户提问时先从库里召回相关条文再把这些条文拼进提示词交给LLM生成答案。好处是答案有出处法条更新只需增量重建索引模型换掉也不影响系统主体。这篇笔记适合三类人正在做法律信息化或企业法务系统的人、想给律所搭知识库的开发者、以及只听过RAG想找一个真实落地场景的初学者。接下来我会按“文本切分→混合检索→RAG管道→评估机制→落地排错”的顺序把能直接抄走的方案讲完整。2. 法律文本的RAG分层检索单元怎么切决定了回答上限2.1 把法律文本切成可检索的最小单元条文级切分与元数据设计RAG的第一件事不是选模型而是决定“检索粒度”。法律文本不同于通用网页文本它有天然边界——第几条、第几款、附录、生效日期。常见的错误是拿通用文本分块器按500字一块硬切结果一块里横跨三个条文检索时命中“噪音”比命中“有效内容”还多。我一般建议在“条文级别”做切分。对法律条文用正则按“第X条”或“Article X”切分对裁判文书或判例按案号、事实、裁判理由、判决结果分段。每条文本附带结构化元数据来源法律名称、章节、条文编号、生效日期、效力状态现行有效/已废止/已被修改。这些元数据后面会用于时效过滤也会用于回答时的引用标注。给一张参数表按文本类型选切分策略文本类型切分策略分块上限重叠长度元数据示例法典条文按“第X条”边界切分每条的完整原文可稍长0source劳动法, article第三十六条司法解释先按“第X条”切再按条款号分段单条不超过800字0source司法解释, doc_id2023-05裁判文书按文书结构切案情/争议/判决每段不超过1200字128字符case_no, court, judgment_date法律术语表/释义语义句段切分512字64字topic, definition切分这里有几个细节值得说明。第一条文和条文之间不要做重叠重叠会把相邻法条混进同一个块检索时明明问A条却被召回B条判例文本则要有少量重叠因为案情描述和裁判理由是连续叙述的硬切会切断因果关系。第二分块完成后记录它在原文中的绝对偏移量这样回答生成后可以反查原文做“引用定位”这一步在验收时特别有用。2.2 混合检索为什么法律问答不能只靠向量召回向量检索擅长语义匹配但法律场景有两类查询它处理不了一类是精确编码检索用户直接报“劳动法第三十六条”这不是语义问题是ID精确匹配另一类是案号检索如“2023京01民终587号”汉字加数字加括号向量很容易把“终”和“民”的语义扯进去导致召回偏移。因此要做双路召回一路是BM25或ES的keyword检索负责精确匹配法条编号、案号、关键术语另一路是Embedding向量检索负责把用户的口语化问题映射到相关条文。两条结果按权重融合通常的做法是keyword结果占0.3、向量结果占0.7再按融合分数取top_K。权重不是玄学它取决于你的语料结构如果语料以判例为主keyword权重可以适当提高因为案号、法院名称这类精确字段非常可靠如果语料以连续叙述的科普解释为主向量权重应该更高。融合层要注意归一化。BM25的分数量纲和向量余弦相似度不在一个量级不能直接相加。常见做法是各自在本次召回的候选集内做Min-Max归一化再按权重相加。这样既保留两路各自的排序又避免某一方的极端分数主导最终结果。2.3 检索后重排让法条、判例、司法解释按证据强度排序双路召回得到的结果是“候选集”但候选集内部顺序未必符合法律逻辑。比如用户问“被公司辞退有什么补偿”召回结果里可能同时有《劳动合同法》第四十六条、第四十七条和一个相关判例但向量排序可能把判例排在法条前面。法律回答的正确结构是先指法律依據再讲适用条件最后给判例参考因此候选集必须重排。两种重排方案可以按资源情况选。方案一是交叉编码器Cross-Encoder重排把“问题召回文本”拼起来一次性过模型打分比双塔式Embedding更准但推理成本高适合候选集在20条以内的小规模场景方案二是LLM重排直接把候选条文列表交给LLM让它按与问题的相关性、效力位阶、是否现行有效三个维度排序优点是灵活缺点是延迟高适合离线重排或候选集较小的时候。重排之后还要做一步“证据过滤”召回文本中如果带有“已废止”“已被修改”元数据直接降权或剔除。这一步必须在重排之后做不能在召回阶段做因为用户可能故意问“劳动合同法第三十条废止了吗”这时候被废止的条文恰恰是正确答案的上下文提前过滤会丢失这类查询的线索。3. 基于RAG架构的智能法律问答系统从零跑通一个极简可用的实现3.1 选型为什么用LangChain向量库而不是自研检索管道法律问答的RAG管道并不复杂核心就四段加载、切分、入库、检索生成。不必自己从零写向量索引和prompt拼接逻辑现有框架足够稳。常见做法是用LangChain做编排Chroma做向量存储SentenceTransformer或BGE系列做中文Embedding再外接一个LLM API。选LangChain不是因为它最好而是因为它的文档加载器和检索器接口覆盖了大部分法律文本格式Markdown、PDF、Docx都能加载省掉很多文本解析的体力活。向量库选型上如果文档总量在几十万条以内Chroma或FAISS足够如果超过百万条或者要支持并发写入再考虑Milvus这类服务化向量库。法律问答项目大多数起步阶段几十万条文已经不小不需要一上来就上分布式架构。标题里的“极简”也提醒我们先把管道跑通再考虑扩展。整个系统跑在我自己的2C4G云主机上也比较从容因为Embedding模型可以本地跑只有生成阶段需要调用LLM API如果你的数据量小于五万条这不是一个高负载系统。3.2 最小实现加载、切分、入库、检索、生成整条管道用Python实现按“准备→入库→问答”三个脚本组织即可。下面是核心代码和逻辑说明。第一步加载法律文本并按条文切分。以劳动法为例import re from langchain_community.document_loaders import TextLoader from langchain_core.documents import Document # 加载原始文本 raw_docs TextLoader(labor_law.txt, encodingutf-8).load() # 按“第X条”切分法律条文 def split_by_article(text: str, law_name: str): # 匹配“第三十六条”这类中文条文编号 pattern re.compile(r(?Particle第[一二三四五六七八九十百零]条)) pieces [] # 记录当前条文内容和起始位置 current_article None current_pos 0 for match in pattern.finditer(text): if current_article: # 抽取条文正文记录来源与条文编号 pieces.append(Document( page_contenttext[current_pos:match.start()].strip(), metadata{ source: law_name, article: current_article, offset: current_pos, effective: True } )) current_article match.group(article) current_pos match.start() # 处理最后一条 if current_article: pieces.append(Document( page_contenttext[current_pos:].strip(), metadata{source: law_name, article: current_article, offset: current_pos, effective: True} )) return pieces docs split_by_article(raw_docs[0].page_content, 劳动法)逻辑说明用正则匹配“第X条”作为边界把长文本切成独立条文。这里刻意没做重叠避免相邻条文混进同一个检索单元。offset字段记录条文在原文中的绝对偏移将来要做引用定位或展示原文时可以直接按这个偏移回溯。参数说明这条正则只匹配中文大写数字条文编号。不同法律的条文编号格式会有差异比如“第一条”和“1.”混排建议在切分前先人工抽看20条再定正则不要盲目通用化。第二步生成向量并写入向量库from langchain_huggingface import HuggingFaceEmbeddings from langchain_chroma import Chroma # 中文法律文本推荐用BGE系列效果比通用Embedding更稳 embedding_model HuggingFaceEmbeddings( model_nameBAAI/bge-large-zh-v1.5 ) # 构造向量库并写入切分后的条文 vectorstore Chroma.from_documents( documentsdocs, embeddingembedding_model, persist_directory./legal_db, collection_namelaw_articles )逻辑说明from_documents会逐条做向量化并持久化到本地目录。BGE模型在中文法律文本上明显好于通用英文模型它本身在大规模中文语料上训练条文里的长句、术语召回更稳。Chroma落盘到legal_db目录后续问答复用时直接load不用重新embedding。参数说明collection_name是逻辑集合名同一个向量库可以按法律类型分多个collection比如“labor_law”“civil_code”到检索时按collection过滤省去metadata过滤的开销。第三步召回与生成这是RAG的核心from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI # 双路召回向量检索 关键词检索 retriever vectorstore.as_retriever( search_typesimilarity, # 向量相似度召回 search_kwargs{k: 6} # 召回6条候选 ) # 关键词检索走BM25与向量结果融合此处简化为直接合并去重 bm25_results keyword_search(question) # 假设已实现 vector_results retriever.invoke(question) candidates dedupe_and_merge(bm25_results, vector_results, top_k6) # 按相关度重排实际项目中可用交叉编码器重排 reranked rerank_by_llm(question, candidates) # 假设已实现 # 拼装prompt要求模型必须引用来源且只能依据条文回答 prompt ChatPromptTemplate.from_messages([ (system, 你是一名法律顾问。仅基于提供的法律条文回答 不要引入条文外的知识每条结论后用【来源法律名-第X条】标注。 如果条文不足以回答问题直接说“检索到的条文无法完整回答”。), (user, 相关条文\n{context}\n\n用户问题{question}) ]) llm ChatOpenAI(modelgpt-4o-mini, temperature0.1) chain prompt | llm answer chain.invoke({context: reranked, question: question})逻辑说明第36行到38行做了两路召回关键词补足精确匹配向量补足语义匹配合并去重后重排最后把重排后的条文放prompt里生成回答。强制在prompt里要求“仅基于条文回答”和“标注来源”是法律RAG和通用RAG最大的区别。参数说明k6是召回条数法律问答不建议太猛。条数太少怕丢掉关键法条线太多会让prompt上下文变长模型容易被不相关条文干扰6条是一个经验平衡点。temperature0.1是生成参数法律回答要稳定温度越高越容易发挥过头。3.3 关键参数表一套能直接上手的初始配置参数初始值调整方向分块大小条文级切分不设固定字数释义类文本可用512字上限重叠长度0条文/ 128字判例连续性说明文本适当加大召回top_K6回答空洞时增大到8噪音多时降到4融合权重keyword 0.3 / vector 0.7精确编码常被问时提高keyword权重Embedding模型BAAI/bge-large-zh-v1.5效果不够换bge-m3但推理更慢LLM温度0.1偏解释型问答可升到0.2禁高于0.4最大输出token512长解答调到1024代价是响应变慢这套参数我跑过劳动法、民法典、交通事故司法解释几类语料效果稳定。值得提醒的是参数不要照搬你的用户问法如果偏口语vector权重反而应该提高偏术语keyword权重提高。4. 法律回答的准确性从哪来检索评估与引用兜底4.1 用hit rate和MRR验证检索质量先别急着聊RAG很多团队把RAG系统上线后凭“感觉回答变好了”来验收这是把系统当黑匣子。RAG的回答质量先取决于检索质量检索召回都不对生成再好也白搭。所以要先建立一个评估集至少准备50条典型问答。每一条包含问题、真实答案、应当召回的法条编号。然后用三个指标打分hit rate命中率正确条文是否进入top_K候选、MRR倒数排名正确条文排在第几位、引用准确率生成答案里标注的来源是否真实存在。hit rate看着简单但有一个隐蔽问题如果正确条文在库里两处重复比如总则和分则都收录了模型可能召回另一处但语义相同导致标注“正确”的负反馈。所以评估集里的“正确法条”要允许一个列表不只支持单条。这个细节不处理好指标会误导你反复调参浪费时间。MRR相对严格它计算1/rank。正确条文排第1得1分排第3得0.33。一般法律RAG做到MRR 0.7以上可以判断检索层基本过关。如果MRR低于0.5优先检查切分颗粒度和embedding模型不要直接调生成prompt生成prompt再漂亮也救不回调错的法条。4.2 生成阶段约束强制引用与“检索不到就拒答”检索质量过关之后还要在生成这一侧做两道约束。第一道约束是强制来源标注prompt中明确要求每个结论后面都带【来源法律名-第X条】。不要只让模型“尽量”要给它规定输出格式最好让它输出结构化JSON方便程序校验和前端展示。第二道约束是拒答机制。很多问答系统翻车不是因为答案错而是因为不知道什么时候该说“不知道”。检索回来的top_K列表如果整体相似度低于阈值说明知识库完全没有覆盖这个问题这时应当拒绝回答而不是让模型硬编。法律场景宁可说“这个问题我暂时查不到对应法条”也不要给一个模糊建议。在实际系统中我会在召回后加一段简单评估如果排名第一的向量相似度低于0.5直接走拒答分支不再调用LLM。这个阈值因embedding模型而异不要照抄要在自己的评估集上跑一遍找分界点。4.3 从RAG到Agentic RAG什么时候该升级别让架构先飞热词“agentic RAG”这两年很火它的做法是让模型决定检索策略先查什么、再查什么、不够时要不要换关键词重查。这个方向确实能解决复杂问题比如用户问“深圳的产假是多少天”需要先定位到《广东省人口与计划生育条例》再结合《女职工劳动保护特别规定》跨文档组装答案。这类多步检索问题普通单轮RAG确实会丢信息。但法律问答的“极简”方案里我建议先别上Agentic。原因很简单Agentic RAG的每一步决策都依赖LLM法律场景要求每一步可回溯如果检索路径不固定出了问题很难向使用者解释为什么走到某一条法条上。更现实的落地路径是单轮RAG先跑通积累足够多的用户问题日志再统计有多少比例的问题需要多步检索如果确实超过30%再考虑升级。架构选择跟着问题走不要跟着热点走这是我做过几个RAG项目后的血泪经验。5. 法律RAG常见问题与避坑五个翻车现场和它的解法5.1 切分把相邻法条混进同一条引用张冠李戴现象用户问“试用期工资”回答引用了《劳动合同法》第二十条但引用文本同时还带上了第二十一条试用期不得解除合同的内容生成结果前后矛盾。原因用通用文本分块器按固定字数切分零上下文重叠一条分块跨越两个条文。法律条文天然是独立单元混在一起会让一次检索返回“半条”生成模型误以为这些内容同属一条。解决改按条文边界切分用正则定位“第X条”的位置并在那里断开不设固定字数上限。从流程上保证“一个检索单元完整包含至少一个条文”单个条文即使很长有的条有四款数百字也让它保持完整。5.2 新旧法版本混在黑匣子里回答引用了已废止条款现象用户问“劳动法里关于产假的规定”系统返回的条文与现行规定不一致进一步排查发现知识库里同时存在1995年版本和2018年版本向量检索把两个版本都召回了生成模型选择了旧版。原因入库时没有按版本隔离也没有给每个Document写入effective是否有效元数据。解决在入库元数据中加入版本号和生效状态检索阶段必须按“effectivetrue”过滤重排阶段对已废止条文直接降权。更稳妥的做法是同一个法律的不同版本分不同的collection存从源头避免向量空间里的混淆。5.3 案号检索被语义召回带偏判例怎么都检不到现象输入“2023京01民终587号”向量检索返回一堆“民事判决”“终审”相关内容精确案号反而没进top_K。原因Embedding模型对数字和括号的处理能力偏弱案号中的汉字民终主导了语义相似度数字串被当噪声忽略。解决对这类精确标识采用keyword检索单独召回并按精确匹配加分。匹配到完整案号时直接置顶不需要经过语义排序。如果使用ES案号字段单独建keyword索引与正文向量检索走两路最后融合。5.4 模型回答丢掉了来源标注审计时无从回溯现象回答内容完整有理有据但模型忘记了promt里“标注来源”的要求答案里找不到任何一条条文编号。原因prompt里的来源约束被淹没在过长的上下文里另一个常见原因是模型版本对中文指令遵循能力弱。解决把来源标注要求放到user消息的末尾并规定输出格式为JSON数组每个结论带source字段。生成之后再做一次程序校验——用正则抽取所有“第X条”标识到知识库反查是否存在。校验不通过就让系统拦截输出这比反复调prompt可靠得多。5.5 长裁判文书超出上下文窗口回答内容支离破碎现象输入一份上百页的一审判决书切分后某段超过2000字嵌入后与其他内容互相干扰生成的回答只引用了其中一小段还遗漏了关键判项。原因没有按文书结构切分而是简单按长度硬切导致“事实认定”和“裁判结果”相距很远同时单个Document内容过长向量表达被稀释。解决裁判文书先按结构性标题切分“当事人信息”“诉讼请求”“事实与理由”“本院认为”“判决如下”再对“本院认为”这种关键段落做二次细分。必要时采用父子分块——父块是完整文书子块是段落检索命中子块后回传父块整体给生成模型保证上下文完整。6. 让回答真正可用的三个进阶习惯引用校验、时效过滤与问答日志先说一个我踩坑后养成的习惯每次调整prompt或分块参数第一件事是跑一遍评估集上的引用准确率而不是看几条“感觉不错”的示例回答。这里有个不起眼但很实用的校验手段——生成结果里抽取来源编号再到知识库反查确认编号确实存在并且归属于正确的法律文件。这段程序不复杂却是法律RAG最关键的“后悔药”它能在问题流到用户之前截住错误。第二个习惯是时效过滤前置。法律文本的版本问题前文已经提到但更完整的做法是每个检索单元都带生效日期和失效日期用户提问时取当前日期作为检索基准只召回生效日期在当天之前、失效日期在当天之后的文本。比如2021年生效的民法典、2024年修正的司法解释各自在对应时间区间内有效。这个字段需要在入库时人工清洗如果可能尽量用结构化数据源导入避免从PDF里抽日期那份文本质量太不稳定。第三个习惯是记录问答日志并定期评估。把每次问答后用户是否有追问、是否有纠错行为记录下来积累两周以后就可以统计出哪些问题命中了知识盲区哪些问题持续触发相似度低分。我一般每两周跑一次日志分析把高频未命中问题补进评估集这样检索质量不会随时间退化。日志里至少记录四个字段问题原文、召回条文top5、LLM回答、用户反馈有/无/纠错。运行这个方案一段时间之后我最大的心得是RAG法律问答系统并不依赖最强的大模型恰恰相反模型反而可以选小一号的把预算花在数据清洗和检索评估上。BGE加一个普通量级LLM加上可靠的召回链路就能支撑起一个能实际服务的法律咨询场景。这套组合跑稳之后再去尝试Agentic RAG、GraphRAG这些更复杂的架构也不迟先让基础管道具备可解释性再谈智能化。希望这些经验和排坑记录帮到你。本文还有配套的精品资源点击获取
返回列表