简介:该压缩包面向法律科技开发者、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条”边界切分 | 每条的完整原文,可稍长 | 0 | source=劳动法, article=第三十六条 |
| 司法解释 | 先按“第X条”切,再按条款号分段 | 单条,不超过800字 | 0 | source=司法解释, doc_id=2023-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", encoding="utf-8").load() # 按“第X条”切分法律条文 def split_by_article(text: str, law_name: str): # 匹配“第三十六条”这类中文条文编号 pattern = re.compile(r"(?P<article>第[一二三四五六七八九十百零]+条)") pieces = [] # 记录当前条文内容和起始位置 current_article = None current_pos = 0 for match in pattern.finditer(text): if current_article: # 抽取条文正文,记录来源与条文编号 pieces.append(Document( page_content=text[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_content=text[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_name="BAAI/bge-large-zh-v1.5" ) # 构造向量库并写入切分后的条文 vectorstore = Chroma.from_documents( documents=docs, embedding=embedding_model, persist_directory="./legal_db", collection_name="law_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_type="similarity", # 向量相似度召回 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_k=6) # 按相关度重排(实际项目中可用交叉编码器重排) reranked = rerank_by_llm(question, candidates) # 假设已实现 # 拼装prompt:要求模型必须引用来源,且只能依据条文回答 prompt = ChatPromptTemplate.from_messages([ ("system", "你是一名法律顾问。仅基于提供的法律条文回答," "不要引入条文外的知识;每条结论后用【来源:法律名-第X条】标注。" "如果条文不足以回答问题,直接说“检索到的条文无法完整回答”。"), ("user", "相关条文:\n{context}\n\n用户问题:{question}") ]) llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.1) chain = prompt | llm answer = chain.invoke({"context": reranked, "question": question})逻辑说明:第36行到38行做了两路召回,关键词补足精确匹配,向量补足语义匹配,合并去重后重排,最后把重排后的条文放prompt里生成回答。强制在prompt里要求“仅基于条文回答”和“标注来源”,是法律RAG和通用RAG最大的区别。
参数说明:k=6是召回条数,法律问答不建议太猛。条数太少怕丢掉关键法条,线太多会让prompt上下文变长,模型容易被不相关条文干扰,6条是一个经验平衡点。temperature=0.1是生成参数,法律回答要稳定,温度越高越容易发挥过头。
3.3 关键参数表:一套能直接上手的初始配置
| 参数 | 初始值 | 调整方向 |
|---|---|---|
| 分块大小 | 条文级切分,不设固定字数 | 释义类文本可用512字上限 |
| 重叠长度 | 0(条文)/ 128字(判例) | 连续性说明文本适当加大 |
| 召回top_K | 6 | 回答空洞时增大到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 |
| 最大输出token | 512 | 长解答调到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(是否有效)元数据。
解决:在入库元数据中加入版本号和生效状态,检索阶段必须按“effective=true”过滤,重排阶段对已废止条文直接降权。更稳妥的做法是同一个法律的不同版本分不同的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这些更复杂的架构也不迟,先让基础管道具备可解释性,再谈智能化。希望这些经验和排坑记录帮到你。
本文还有配套的精品资源,点击获取