拓冰建站拓冰建站
首页 / 资讯中心 / 正文

法律知识图谱问答系统实战:RAG与向量检索融合方案

简介本资源是一套面向法律科技领域开发者与NLP研究者的法务智能知识图谱实践项目聚焦法律问答与资讯检索场景融合知识图谱构建、案由预测、问题分类及自动问答四大核心能力。项目基于20万条真实法务问答与法律资讯数据完成案由知识库、法务咨询对话知识库及法律资讯知识图谱的构建并提供对应模型训练与服务接口代码适用于法律AI产品原型开发、司法辅助系统验证及高校法律信息学课程实践。压缩包为33.88MB的ZIP文件含Python源码含图谱构建、BERT微调、Neo4j交互模块、结构化数据集JSON/CSV格式及配置说明文档目录层次清晰便于快速定位知识抽取、图谱存储与问答服务模块。目前已有674人学习下载读者可直接复用完整流程代码、调试预置模型、拓展知识节点或对接自有法律语料显著降低法律垂直领域知识图谱落地门槛。 去年年底我接手了一个挺有意思的法务问答项目要做一个基于法务智能知识图谱的问答系统手里有20万条法务问答数据还要带上法律资讯问答功能。客户明确要求含码源也就是整个系统要能交付可跑的代码不能只给个demo。当时团队内部讨论了好几次核心纠结点就是到底用纯向量检索做RAG还是用知识图谱最终我们定了知识图谱为主、向量库为辅的混合方案跑完以后效果确实比纯向量检索稳不少。这篇就把从数据清洗、图谱构建、问答链路到服务化部署的完整过程捋一遍代码结构和核心片段也都会拆开讲给同样在做法律领域知识问答的朋友一个可参考的落地样板。先交代一下背景。20W法务问答数据来源主要是公开的裁判文书摘要、法律咨询平台的匿名问答、法条和司法解释的结构化文本。这里面有大量的人名、机构名、案由、法条引用而且很多问题不是简单的什么是型而是劳动者被辞退后经济补偿金怎么算这种多实体、多条件约束的复合查询。如果只用向量库把文档切成块存起来检索到的可能是包含相关关键词的段落但很难准确回答根据《劳动合同法》第四十七条工作年限不满六个月支付一个月工资这种需要精确引用条款的问题。知识图谱的优势在于它能把法条、案件事实、裁判规则之间的复杂关系显式建模查询时可以沿着关系精准命中答案。整个项目我们落地成了几个大块图谱构建、问答引擎、资讯问答服务、FastAPI接口层。下面我会按这个顺序展开每个环节都带上踩坑记录。1. 项目缘起与整体思路1.1 为什么选知识图谱而不是纯向量检索纯向量检索在通用领域问答中表现很好但在法律这种高精度领域有个致命短板回答必须有法可依、有据可查而向量相似度检索返回的是语义相近的文本片段它不理解逻辑关系。举个例子用户问试用期被辞退有赔偿吗一段纯文本可能反复提到试用期辞退但并没有真正回答赔偿标准。而知识图谱可以设计成劳动关系-辞退行为-经济补偿金这样的路径每个节点指向具体法条检索时直接沿路径取答案。当然知识图谱也有构建成本高、覆盖不全的问题所以我们后来引入向量检索作为兜底。简单说能走图查询的就走图图查不到再走向量召回最后都交给LLM组织答案。这个图谱优先、向量兜底的策略是整套系统的灵魂。1.2 项目要解决的真实痛点客户手上的数据虽然量大但并没形成业务价值。原始的问答记录是半结构化的有的问题多轮对话混在一起有的答案引用法条但没写条文内容。传统的全文检索只能做关键词匹配查经济补偿金就漏掉N1赔偿金这类同义表达。而要做大模型微调20万条数据对7B模型来说又不够充分而且法律文本的更新会造知识过时微调一次成本太高。所以真正的解决方案不是堆模型而是把知识结构化。我们先把20W问答里包含的实体和关系抽出来构建成图谱再在这个图谱上做问答。这样不仅现有问题都能答还能推理出新的组合问题。比如数据里没有孕期被辞退怎么赔偿这种具体问法但图谱里有孕期保护条款、辞退行为、赔偿计算方式系统就能组合出来。1.3 技术选型全景技术栈如下模块技术方案选型理由图谱存储Neo4j Community Edition 5.x成熟稳定Cypher查询方便支持APOC插件做实体解析向量召回ChromaDB / Milvus二选一数据量不大时用ChromaDB足够20W文档切块后约50W向量ChromaDB完全扛得住LLM推理llama.cpp Qwen2-7B-Instruct本地化部署避免API调用费用和数据合规问题服务框架FastAPI异步支持好配合uvicornhttpx并发表现不错关系抽取规则 DeepKE轻量序列标注模型法律实体有强模式规则能解决80%模型兜底部署Docker Compose一键拉起Neo4j、向量库、推理服务这里重点说一下为什么用llama.cpp跑Qwen2-7B。法律数据敏感客户要求所有数据不出内网。用llama.cpp量化Qwen2-7B到Q4_K_M在单张消费级显卡上比如RTX 3090推理速度能到每秒20~30 token虽然不如API快但对问答场景够用了。而且llama.cpp的gguf格式对CPU也有不错的支持没有GPU也能跑只是慢一点。后面我会专门说怎么部署。另外FastAPI几乎没什么争议。异步接口、OpenAPI自动文档、参数校验这些对快速交付太重要了。后续所有接口都用Pydantic模型做请求校验前端传错参数直接返回400省了很多联调时间。2. 知识图谱构建从法条到实体的结构化之路2.1 数据来源与清洗20W问答数据的来源比较杂有文本抽取的有JSON导出的还有一部分是PDF转的。第一步不是急着抽取而是统一格式。我们定义了一个中间结构{ id: QA_000001, question: 公司被收购后员工拒绝转签新公司是否有经济补偿, answer: 根据《劳动合同法》第四十六条用人单位被合并的原劳动合同继续有效……, tags: [劳动纠纷, 经济补偿], law_citations: [劳动合同法-第四十六条], facts: [公司被收购, 员工拒绝转签] }清洗规则有几条比较关键去重用MinHash计算文本相似度相似度高于0.85的只保留一条。20W去重后剩17W左右去掉了大量咨询平台互相转载的内容。空值和噪声处理答案长度少于30字的直接丢弃因为这种回答没有实际参考价值。法条引用归一化纯文本里的《劳动合同法》第46条、劳动合同法第四十六条、第四十六条规定全部统一成劳动合同法-第四十六条这种标准ID格式。这里我们写了50多条正则法律文本的表述相对固定正则可以做得比较干净。2.2 实体关系抽取规则模型的混合策略实体和关系抽取决定了图谱的上限。如果只靠通用NLP模型法律文书的当事人诉讼请求法院认为这些结构很难识别出来。我们最终用的是三层抽取管线规则层基于关键词和句法模式抽取。法条实体正则匹配书名号内条文比如《劳动法》第[一二三四五六七八九十百千0-9]条时间实体\d{4}年\d{1,2}月\d{1,2}日金额实体\d(\.\d)?万元行为实体维护一个行为词表如辞退、裁员、未签合同、未缴社保、拖欠工资……模型层用DeepKE训练了一个序列标注模型识别原告、被告、法院、案由、赔偿金额等有监督实体。训练数据自己标注了3000条效果基本够用。关系规则实体有了之后根据共现关系和位置模式抽关系。比如如果一句话里同时出现辞退和经济补偿金且辞退在主语位置就会建立辞退-触发-经济补偿的关系。最终我们抽取出来20种实体类型和32种关系类型。这是图谱构建的核心资产比模型本身值钱。2.3 Neo4j图模型设计图谱的节点和关系设计直接影响查询效率。我们设计的是这样一个核心模式(:Law {id, name, chapter, content}) // 法条节点 (:LegalDocument {id, title, content}) // 法律文书节点 (:Question {id, text, user_type}) // 用户提问节点 (:Answer {id, text, source}) // 答案节点 (:Entity {id, name, type}) // 通用实体行为、期限、金额等 (Question)-[:HAS_ANSWER]-(Answer) (Question)-[:INVOLVES_ENTITY]-(Entity) (Question)-[:CITES_LAW]-(Law) (Answer)-[:BASED_ON]-(Law) (Answer)-[:REFERENCES_DOC]-(LegalDocument) (Law)-[:RELATED_TO]-(Law)这里有几个设计细节值得一说Question节点和Answer节点是分离的因为一个问可能对应多答一个答也可能覆盖多问分离开查询更灵活。Law节点存条文全文而不是只存第X条这个编号。LLM在生成回答时需要引用内容图里直接取全文就不用再回查文档。RELATED_TO关系用于法条之间的关联。比如劳动合同法-第四十六条和劳动合同法-第四十七条存在引用关系这个是从法律知识整理中提前配好的。Neo4j的索引也用了不少。节点至少500万量级没有索引的话一个动作查询轻松卡死。我们给Entity.name建有唯一约束索引给Law.id建唯一约束给Question.id建普通索引。代码示例建索引和约束CREATE CONSTRAINT law_id IF NOT EXISTS FOR (l:Law) REQUIRE l.id IS UNIQUE; CREATE CONSTRAINT entity_name IF NOT EXISTS FOR (e:Entity) REQUIRE e.name IS UNIQUE; CREATE INDEX question_id_idx IF NOT EXISTS FOR (q:Question) ON (q.id); CREATE INDEX law_content_idx IF NOT EXISTS FOR (l:Law) ON (l.content);注意唯一约束会自动创建索引但普通索引一定要手动加否则大批量写入时查询会慢到无法接受。2.4 码源结构说明项目代码结构是标准的模块化legal_kg_qa/ ├── app/ │ ├── api/ # FastAPI路由 │ │ ├── answer.py # 法务问答接口 │ │ ├── news.py # 法律资讯问答接口 │ │ └── health.py # 健康检查 │ ├── core/ │ │ ├── config.py # 配置 │ │ ├── neo4j_client.py # Neo4j驱动封装 │ │ └── llm_client.py # llama.cpp推理客户端 │ ├── graph/ │ │ ├── builder.py # 图谱构建主流程 │ │ ├── extractor.py # 实体关系抽取 │ │ └── loader.py # 批量写入Neo4j │ ├── retrieval/ │ │ ├── graph_retriever.py # 图谱查询器 │ │ ├── vector_retriever.py # 向量检索器 │ │ └── fusion.py # 融合排序 │ ├── llm/ │ │ └── generator.py # 答案生成 │ └── schemas/ │ └── request.py # Pydantic模型 ├── data/ # 原始数据与导出数据 ├── models/ # gguf模型文件 ├── scripts/ │ ├── build_graph.py │ ├── load_vectors.py │ └── test_api.py └── requirements.txt码源能跑的关键在于配置文件和客户端封装都做得很薄适配不同环境只需要改core/config.py里的连接参数。3. 问答系统实现RAG与图谱问答的融合3.1 基础问答链路整个问答链路跑起来是这样接收用户问题。先用NER模型抽取问题里的实体和意图。根据实体生成Cypher查询优先走图谱检索。如果图谱检索结果不足N条比如少于3条则用向量库做补充召回。将图谱结果和向量结果合并按得分排序。拼接成Prompt交给Qwen2-7B生成最终答案要求LLM必须基于检索到的内容回答做不到就回答根据现有资料无法确认。这套链路的两个关键点实体抽取的准确率直接决定图谱查询的准确率。我们使用了一个轻量级规则模型的混合NER准确率大概在90%左右对常见法务问题够用。法务问题的实体往往是劳动纠纷经济补偿工伤认定等长尾词我们把这些词维护成一个词表再对每个词关联到图谱节点ID这样只要实体识别命中词表就能直接跳到节点。图谱和向量检索的融合排序并不是简单拼接。我们设计了一个简单的打分函数final_score 0.7 * graph_score 0.3 * vector_score。graph_score根据路径长度和实体匹配数计算路径越短、实体匹配越多分越高。vector_score用向量相似度。实验证明这个加权方式比纯取max效果好因为图谱命中的答案更精准给它更高权重是合理的。3.2 图查询到LLMCypher生成与约束这里有个容易踩坑的地方用LLM直接生成Cypher非常不可控。我们一开始尝试让Qwen2-7B直接根据问题写Cypher结果在测试集上语法正确率只有70%左右一旦生成错误语句整个查询就崩了。最终我们放弃让LLM写Cypher改成模板化图查询 LLM结合约束生成。具体做法是根据实体类型预定义若干查询模板。比如识别出行为实体后使用模板MATCH (e:Entity {name: $behavior})-[:INVOLVES_ENTITY]-(q:Question) MATCH (q)-[:HAS_ANSWER]-(a:Answer) RETURN q.text, a.text LIMIT 5再比如识别出法条实体后使用模板MATCH (l:Law {id: $law_id})-[:CITES_LAW]-(q:Question) MATCH (q)-[:HAS_ANSWER]-(a:Answer) RETURN q.text, a.text LIMIT 5LLM在这里只做一个工作从用户问题中抽取实体并映射到模板参数。这比直接写Cypher简单可靠得多。我们把合法的模板参数做成白名单每个参数在查询前校验只有匹配^[\u4e00-\u9fa5a-zA-Z0-9\-()]{1,50}$才允许执行防止注入。当然模板化也限制了灵活性无法处理特别复杂的多条件问题。比如怀孕女职工在试用期被辞退可以要双倍赔偿吗这种同时涉及孕期保护试用期辞退赔偿四个实体的问题单个模板装不下。我们的方案是多模板并行查询每种实体组合生成一个子查询最后取所有子查询结果的并集。实测下来多模板并行的召回率远高于单个模板。3.3 向量数据库引入弥补图谱覆盖不足图谱再大也覆盖不完所有知识特别是资讯类内容。20W问答里有些问题比较冷门在图谱中匹配不到足够多的邻居节点。这时就需要向量检索兜底。向量检索的流程不复杂但有几个细节要注意。我们用中文法律文本训练了一个BGE-M3模型做Embedding后来替换成bge-large-zh-v1.5切块策略是按法律文件结构切一个法条一个块一个问答对一块。这样切块的好处是每块语义完整不会出现前一句话在A块、后一句话在B块的情况。向量库选择ChromaDB因为20W问答对切块后约50W向量单机部署很轻松。存储时带上元数据来源文档ID、法条ID、标题、时间戳。查询时先向量召回TopKK20然后用一个裁判模型用一个轻量分类器判断相关性重排取Top5。这个重排模型我们用的是BGE-reranker-base法律领域虽然没微调但效果比纯相似度好很多。3.4 基于llama.cppQwen2-7B的本地推理部署LLM推理这块是交付过程中客户最关心的毕竟涉及隐私数据。llama.cpp部署Qwen2-7B-Instruct的步骤我直接贴出来git clone https://github.com/ggerganov/llama.cpp cd llama.cpp make -j4 LLAMA_CUBLAS1 # 下载Qwen2-7B-Instruct的gguf格式文件 # 建议使用Q4_K_M量化显存占用约6.5GB ./build/bin/llama-server \ -m /models/qwen2-7b-instruct-q4_k_m.gguf \ --host 0.0.0.0 \ --port 8080 \ -c 8192 \ -ngl 35 \ --api启动了llama-server后FastAPI通过HTTP请求调用它。这里有一个很重要的参数-c 8192上下文长度。如果设为默认的2048Prompt长一点就会截断答案生成质量明显下降。我们实测Qwen2-7B在8192上下文下能处理包含3~5个检索片段的问题生成答案较完整。-ngl 35是把35层模型层放到GPU需要根据显存调整。如果显存只有8GB建议层数设少一点或者改用Q3_K_S量化。还有一些参数需要在FastAPI调用时设置payload { prompt: final_prompt, temperature: 0.1, top_p: 0.9, max_tokens: 512, stop: [|im_end|] }temperature设0.1而不是0是为了让答案在稳定基础上保留一点点多样性。如果完全设为0虽然稳定性好但多个相似问题可能输出一字不差的回答维权类场景会显得奇怪。stop参数必须设置否则模型可能一直续写到max_tokens上限。下面简单贴一个FastAPI调用本地推理服务的核心代码import httpx class LLMClient: def __init__(self, base_url: str http://127.0.0.1:8080): self.base_url base_url async def generate(self, prompt: str, max_tokens: int 512) - str: payload { prompt: prompt, temperature: 0.1, top_p: 0.9, max_tokens: max_tokens, stop: [|im_end|] } async with httpx.AsyncClient(timeout60) as client: resp await client.post(f{self.base_url}/completion, jsonpayload) data resp.json() return data[content]调用llama-server的/completion接口直接拿到生成文本。注意这里的timeout要设置长一些因为7B模型在较长的Prompt下生成速度并不快我遇到过需要45秒的场景。4. 法律资讯问答功能的独立设计4.1 资讯数据的采集与结构化除了20W问答系统还需要具备法律资讯问答能力。所谓资讯问答不是回答什么是正当防卫这种百科型问题而是回答2024年最高人民法院发布了哪些劳动争议典型案例或者最新的个税专项附加扣除政策是什么。这类问题时效性强且答案可能随时间变化。资讯数据我们通过合规渠道采集了公开的法律新闻、政策解读、典型案例发布稿去重后差不多5W篇。存入系统前每篇资讯要做结构化抽取标题、发布时间、来源、正文。用正文的TF-IDF关键词生成标签。提取正文中引用的法条和案例名称。每篇资讯存成两个节点一个是News节点保存原始正文另一个是NewsSummary节点保存摘要和关键信息点。为什么分开因为正文一般很长如果都作为属性存在图里写入慢查询也慢。摘要节点方便快速匹配正文则存到文档数据库或干脆以文件形式保存图里只存引用ID。4.2 时效性处理与答案引用资讯问答的难点在于时效性。法律政策一变如果系统回答的还是旧内容会误导用户。我们给资讯节点加了一个effective_date和expired_date属性。查询时加条件MATCH (n:News) WHERE n.effective_date date() AND (n.expired_date IS NULL OR n.expired_date date()) RETURN n.title, n.summary LIMIT 10这样过期资讯就不会被检索到。此外在Prompt中明确告诉LLM你只能使用当前有效的信息回答如果检索到的信息包含过期内容请忽略。对于最新类问题比如最新的劳动法解释我们还需要一个时间排序逻辑。在向量检索和关键词检索之后如果问题中包含最新近期2024等时间词就对结果按发布时间倒序排序再取TopK。这里有一个小技巧如果用户没有指定时间但答案库中同一问题的多个答案发布时间不同应该优先采用发布时间较新的答案。我们在答案生成Prompt中把时间信息带进去以下是检索到的相关资讯请根据这些内容回答问题 1. 标题xxx发布时间2024-05-01内容xxx 2. 标题xxx发布时间2022-01-01内容xxx 用户问题xxx 请优先采用发布时间较新的信息并在回答末尾标注信息来源及发布日期。这样能有效降低拿旧法条回答新问题的风险。5. FastAPI服务封装与工程化落地5.1 API设计与并发模型全部功能最终通过FastAPI对外提供接口只有三个方法路径功能POST/api/v1/legal/ask法务知识图谱问答POST/api/v1/news/ask法律资讯问答GET/api/v1/health健康检查legal/ask的请求体{ question: 公司拖欠工资三个月我主动离职有经济补偿吗, user_id: u_12345, from_source: app }响应体{ code: 0, answer: 根据《劳动合同法》第三十八条和第四十六条用人单位未及时足额支付劳动报酬的劳动者可以解除劳动合同并有权要求用人单位支付经济补偿金。, sources: [ { type: law, id: 劳动合同法-第三十八条, content: 用人单位有下列情形之一的劳动者可以解除劳动合同…… } ], confidence: 0.92 }FastAPI最大的优势是异步支持。我们在调用LLM时用httpx.AsyncClient这样多个用户同时提问时一个请求在等待LLM时不会阻塞其他请求。配合uvicorn的多worker实测单机QPS能达到20左右对内部系统够用了。5.2 缓存与批量更新策略20W问答的查询结果有很强的重复性。比如试用期工资怎么算这个问题100个用户问的可能是差不多的。我们在Redis里做了三层缓存完全文本匹配缓存完全相同的问题直接返回。实体组合缓存如果问题解析出的实体组合与之前某个问题一致直接复用答案。路径缓存图谱查询结果缓存对应Cypher语句的hash相同查询不用再跑图。缓存时间设置为24小时。法律答案变化慢不需要实时更新但资讯类答案缓存时间要短我们设了30分钟保证时效性。批量更新这块也很重要。图谱不是一次性建完就不管了法律数据会更新。我们做了每日增量任务新到的问答对先做实体抽取然后批量写入Neo4j。写入要用UNWIND批量提交一次1000条比一条条CREATE快10倍以上。另外Neo4j写库时要注意事务大小事务太大会内存溢出太小又太慢1000条是平衡点。UNWIND $batch AS row MERGE (q:Question {id: row.q_id}) SET q.text row.q_text MERGE (a:Answer {id: row.a_id}) SET a.text row.a_text MERGE (q)-[:HAS_ANSWER]-(a)注意这里的MERGE不是CREATE防止重复写入。如果要更新现有节点属性用SET即可但关系只能用MERGE保证唯一性。6. 常见问题与排查实录6.1 实体抽取过粗导致图查询失败我们一开始的NER训练集里只有赔偿金补偿金这种粗粒度实体结果用户问经济补偿金时实体识别成了补偿金图谱里匹配不到节点。后来把训练标签细化区分经济补偿金赔偿金违约金等同时维护了一个同义词表经济补偿金-经济补偿金|补偿金|N1|赔偿经济补偿查询前先做一次输入归一化。这个方法见效很快图谱查询命中率从68%提升到82%。6.2 LLM回答不稳定如何约束LLM在生成答案时偶尔会自由发挥尤其是法律场景这是大忌。我们加了两个约束在Prompt中明确写如果检索到信息不足以回答问题必须回复根据现有资料无法确认不要编造。在后处理时检查答案中是否包含检索内容的高频关键词。如果完全不相关就返回默认话术。还有一个技巧给LLM的检索片段不要超过5个太多反而容易混淆。而且每条片段前面要加标签比如【法条】、【问答】、【资讯】LLM能更清楚信息来源。6.3 Neo4j查询超时Neo4j默认查询超时是30秒我们有些复杂路径查询会超过这个时间。可以从两个方向解决一是简化查询模式减少深度二是给Neo4j配置加超时时间或者在应用层用CALL apoc.util.sleep配合超时控制。更实用的是在查询之前先做一次EXPLAIN确认有没有走索引。我遇到过几次因为忘了加索引导致全库扫描的情况加上索引后查询时间从8秒降到200毫秒。6.4 20W数据量下的性能优化20W在Neo4j里不算大但如果设计不当也会变慢。我们一个核心优化是把Question.text重复内容做归一化存储同一问题的变体统一映射到标准问题ID。另一个优化是给关系加类型属性比如HAS_ANSWER关系带source属性查询时过滤掉不需要的来源减少数据扫描。向量库的优化则体现在批量写入时调大批大小ChromaDB默认写入速度较慢批量写入设置成256个documents性能提升明显。6.5 知识图谱与向量数据库如何选择这个也是社区里经常讨论的问题。我的经验是如果数据中实体关系密集且需要精确逻辑推理必须上图谱如果只是大量文本的语义匹配向量库更高效。法律领域两者都很需要所以我们的方案是图谱当主干向量当兜底。在实际部署中两者是互补关系不是二选一。RAG相关网络热词里也一直在讨论图谱和向量的融合这个方向我认为是法律AI落地最靠谱的一条路。7. 实操心得与后续扩展项目交付到现在跑了快半年线上效果基本稳定。我个人的体会是法律问答系统最难的不是模型而是知识结构化程度。数据抽出高质量实体关系比换个更大的LLM重要得多。如果你也想做类似系统建议先从1000条高频问答开始把图谱模型和问答链路跑通再扩大到20W千万不要一上来就把全部数据灌进去不然调试成本极高。几个具体建议版本管理图谱构建脚本和抽取规则一定要用Git管理改坏了可以回滚。我们的实体抽取规则迭代了十几版每次改动都会重新构建增量图没有版本控制会乱套。监控统计每天不能回答的问题定期人工review把新问题补进图谱。这个闭环才是系统越用越聪明的原因。扩展方向目前只做了文本问答后续可以做多轮对话和意图反查。也可以把图谱推理能力加进去比如自动计算经济补偿金的具体金额而不只是引用法条。不过那需要更多的业务规则参与等下一阶段再说。最后分享一个小技巧如果你也在用llama.cpp做本地推理建议开启--mlock参数锁住内存避免swap导致推理变慢。我们线上服务器的吞吐量因此提升了差不多一成。这套系统不依赖任何商业API全部代码可以自己掌控我觉得这也是法务场景最稳妥的做法。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门