基于LightRAG搭建法律智能问答系统实战指南

基于LightRAG搭建法律智能问答系统实战指南 最近我在折腾法律文书的智能问答试了不少RAG框架最后把LightRAG用在了法律问答系统上跑通之后的效果让我相当满意。这个项目本质上是一个轻量级的检索增强生成RAG应用把法律条文、司法解释等文档丢进去系统自动抽取实体关系、建立索引之后用户提问时它先检索相关法律条文再交给大模型生成带依据的回答。整个过程不需要微调模型不需要GPU训练一台普通开发机就能跑对想快速搭建垂直领域问答系统的朋友来说LightRAG是个很值得试的方案。这篇文章我会从方案选型、环境准备、完整代码实现、参数调优到踩坑排查把我实际操作中的经验完整记录下来。标题说5分钟搭建指的是在环境就绪后从启动服务到完成第一次法律问答整个链路可以压缩在几分钟内跑通。我会把每一步的细节都交代清楚包括我踩过的坑和排查思路方便你照着复现。1. 为什么我用LightRAG而不是传统RAG来做法条问答1.1 法律问答场景的三个特殊要求法律问答和通用知识问答有个本质区别用户要的不是一个大概正确的答案而是有明确法条依据的确定性回答。比如用户问合同纠纷的诉讼时效是几年系统必须检索到民法典第一百八十八条而不是泛泛地说一般是三年。这就对RAG系统的检索精度提出了很高要求。我最初用的是传统的向量检索方案把法律文本切成块用embedding模型转成向量存进向量数据库查询时算相似度。这种方式跑通很快但遇到两类问题就抓瞎了。第一类是长条文、跨条款的问题比如租赁合同里承租方擅自转租出租方该怎么维权答案散落在合同法编的多个条款里向量检索往往只召回其中一段生成的回答就不完整。第二类是专业术语的语义鸿沟用户口语里说房东赶人法条里写的是承租人返还租赁物这种词汇层面的差异纯向量检索经常匹配不到。1.2 LightRAG的设计思路与传统RAG的差别LightRAG的做法和传统RAG不一样它在向量检索之外引入了一层图结构索引。简单说系统在导入文档时会做实体抽取和关系识别把承租人出租方租赁合同这些实体以及转租需经同意这种关系抽出来构建成一张知识图谱。查询的时候系统先做关键词提取再做向量检索和图检索然后把两种检索结果融合起来交给大模型。这个设计非常适合法律场景因为法律条文本身就带有很强的结构化特征一个法条里包含行为模式、适用条件、法律后果这些要素之间的逻辑关系用图来表达比用连续文本块表达要清晰得多。实测下来处理多法条联合回答的问题时LightRAG的回答完整度明显高于纯向量方案。1.3 和GraphRAG对比LightRAG优势在哪可能有人会问既然要做图增强检索为什么不用微软的GraphRAG我在选型时两个都测过。GraphRAG的思路是把整个文档集做全局社区检测索引过程非常重一份几百页的法律文档构建索引可能要跑很久调参也复杂。LightRAG的设计取向就是轻它保留了图结构增强检索的核心收益但把索引流程简化了几兆的语料几分钟就能建完图而且支持增量更新——新增一部法律只需要把新文档插入它只对新增部分做索引不用全量重建。对于个人开发者或者中小团队来说LightRAG这种够用且足够轻的路线更实际。我当时的服务器只有8G内存没有GPU跑LightRAG完全没压力。2. 环境准备从零开始装好LightRAG2.1 Python环境与依赖安装先交代一下我的环境Ubuntu 20.04Python 3.108G内存的普通云服务器。LightRAG对Python版本要求是3.10以上建议直接上3.10或3.11太老的版本会有依赖冲突。安装其实就一条命令pip install lightrag-hku注意包名是lightrag-hku不是lightrag。我第一次就装错了装了错误的包导致导入报错找了一圈才发现问题。装完可以验证一下版本python -c from lightrag import LightRAG; print(LightRAG.__name__)能正常输出就说明装好了。如果你要连接OpenAI兼容接口需要装一下openai库如果用Ollama本地模型装ollama库即可。这两个我后面都会讲到。我建议再装一个fastapi uvicorn后面要把问答服务包成HTTP接口这两个库能直接帮你省掉写Web框架的时间。2.2 准备法律语料整理民法典文本LightRAG支持txt、markdown、pdf等格式但为了减少解析问题我把法律文本统一处理成纯文本文件。这一步看起来简单实际有讲究。我用的语料是民法典全文从网上找的公开文本。拿到之后我做了三个预处理操作一是去掉页码、页眉页脚等无关内容二是统一换行符Windows的\r\n要转成\n否则有些解析逻辑会出问题三是按编分段把第一编 总则第二编 物权等大章节拆成多个txt文件每个文件控制在几百KB以内。这里分享一个经验语料质量直接决定问答效果。我第一次直接用了带广告和乱码的网页抓取版结果系统在回答里引用了广告里的企业名称非常离谱。后来我换成校对过的公开文本并且做了一遍人工抽查效果立刻好了很多。文件命名也有技巧我用的是民法典_第一编_总则.txt这种格式因为LightRAG在抽取实体时会把文件名作为上下文之一清晰的文件名有助于实体识别。2.3 配置大模型两种接入方式LightRAG本身不含大模型它需要调用外部大模型来完成实体抽取、关键词生成和最终回答。我实测了两种接入方式都跑通了。第一种是OpenAI兼容接口。现在很多国内模型服务商都提供OpenAI兼容的API只需要设置环境变量export OPENAI_API_KEY你的密钥 export OPENAI_API_BASEhttps://api服务商地址/v1然后LightRAG初始化时指定llm_model_func为openai_complete_if_cache再指定模型名称比如gpt-4o-mini或国产的qwen模型。这种方式响应快效果好但要留意API调用费用因为索引阶段每一步都要调用大模型做实体抽取文本量大的时候API账单涨得很快。第二种是Ollama本地模型。如果你不想花钱或者对数据隐私敏感可以用Ollama跑本地模型。我用的模型是qwen2.5:7b在8G内存的机器上跑得动就是生成速度慢一点。初始化时指定ollama_complete函数再把base_url指向http://localhost:11434。这里有个坑Ollama默认只监听本机地址如果想在远程服务器上用需要启动时设置OLLAMA_HOST0.0.0.0。embedding模型我建议单独指定。LightRAG默认支持OpenAI的text-embedding-3-small但如果你走本地方案可以配Ollama的nomic-embed-text模型或者其他兼容OpenAI embedding接口的服务。embedding模型直接影响向量检索的召回质量法律文本里很多专业术语太弱的embedding模型会召回一堆不相关的片段。3. 核心代码实现一个最小可复用的法律问答服务3.1 初始化LightRAG参数逐行说明写代码之前先把关键概念理清楚。LightRAG的核心类是LightRAG初始化时需要传三类东西存储目录、大模型回调函数、embedding函数。下面这个示例是OpenAI兼容接口的配置import os import asyncio from lightrag import LightRAG, QueryParam from lightrag.llm.openai import openai_complete_if_cache, openai_embedding # 环境变量时在终端配置的也可以在代码里临时指定 os.environ.setdefault(OPENAI_API_KEY, sk-xxx) os.environ.setdefault(OPENAI_API_BASE, https://api.example.com/v1) async def llm_model_func(prompt, system_promptNone, history_messagesNone, **kwargs): return await openai_complete_if_cache( gpt-4o-mini, prompt, system_promptsystem_prompt, history_messageshistory_messages, api_keyos.environ[OPENAI_API_KEY], base_urlos.environ[OPENAI_API_BASE], **kwargs ) async def embedding_func(texts: list[str]) - list[list[float]]: return await openai_embedding( texts, modeltext-embedding-3-small, api_keyos.environ[OPENAI_API_KEY], base_urlos.environ[OPENAI_API_BASE] ) rag LightRAG( working_dir./legal_rag_data, llm_model_funcllm_model_func, embedding_funcembedding_func )working_dir参数指定数据存储目录LightRAG会把图结构、向量索引、文档切块都持久化到这个目录里。这个目录要选在一个有足够磁盘空间的位置我一开始放在/tmp下结果服务器重启后索引全没了被迫重建。后来改成项目目录下的稳定路径再也没出过这个问题。如果你用Ollama本地模型只需要把llm_model_func换成ollama_completeembedding_func换成Ollama的embedding接口其他代码完全不用动from lightrag.llm.ollama import ollama_complete, ollama_embedding async def llm_model_func(prompt, system_promptNone, history_messagesNone, **kwargs): return await ollama_complete( qwen2.5:7b, prompt, system_promptsystem_prompt, history_messageshistory_messages, base_urlhttp://localhost:11434, **kwargs )3.2 文档导入全量索引与增量更新文档导入是项目里核心的一步。LightRAG的insert方法接收纯文本会异步执行切块、实体抽取、关系构建和向量化。官方推荐用ainsert异步方法避免阻塞事件循环。我的导入脚本长这样import asyncio from pathlib import Path async def load_documents(): folder Path(./law_texts) for file in folder.glob(*.txt): text file.read_text(encodingutf-8) print(f正在导入: {file.name}, 长度: {len(text)}) await rag.ainsert(text) print(f完成: {file.name}) asyncio.run(load_documents())这段代码看起来简单但导入过程会调用大模型做实体抽取耗时会比较长。我导入了民法典全编加若干司法解释大概几万条文本跑了大约二十分钟。期间要注意API调用频率限制如果服务商限流可以在openai_complete_if_cache参数里加上自定义的max_retries或者在LightRAG初始化时设置llm_kwargs{temperature: 0.1}来控制生成参数。LightRAG支持增量更新这是它一个很实用的特性。如果某部法律出台了新修订你只需要把修订后的全文重新insert一遍系统会在后台对比处理不需要把整个知识库删掉重建。我在实际项目中用这个功能更新过一次劳动法相关条文旧索引完全没有受到影响。3.3 四种查询模式naive、local、global、hybridLightRAG提供了四种查询模式这是用起来最需要理解的一个概念。它们的差别在于检索策略naive模式就是传统的向量检索直接对用户问题做embedding然后找最相似的文档片段。速度最快但效果和纯向量RAG没区别。local模式在向量检索的基础上增加了对查询相关实体的邻居节点搜索。比如用户问承租人擅自转租系统抽取出承租人转租等实体然后顺着知识图谱找到和这些实体直接相连的条文片段。这种方式对单点事实类问题的召回很准。global模式走的是图社区检索路径它会把知识图谱划分成多个社区从全局视角检索与查询相关的社区摘要。这个问题适合总结类问题比如民法典中关于合同解除的情形有哪些。hybrid模式是前三种的结合先并行做向量检索、实体邻居检索和社区检索然后把结果融合去重。理论上最全面但检索耗时长一点对API的调用次数也更多。我在代码里的实测经验是法律问答优先用hybrid。因为法律回答最怕遗漏关键法条hybrid模式召回最全面。如果对响应速度要求高比如做在线客服再用local模式做降级方案。from lightrag import QueryParam query 房屋租赁合同期间房东把房子卖了租客还能继续住吗 param QueryParam(modehybrid, top_k10) result asyncio.run(rag.aquery(query, paramparam)) print(result)3.4 用FastAPI封装一个可调用的问答服务命令行能跑通只是第一步实际使用中我们需要一个HTTP接口。我用FastAPI封装了一个轻量服务代码量很小但很实用from fastapi import FastAPI, HTTPException from pydantic import BaseModel import asyncio from lightrag import LightRAG, QueryParam app FastAPI(title法律问答服务) class QueryRequest(BaseModel): question: str mode: str hybrid class QueryResponse(BaseModel): answer: str mode: str app.post(/api/legal-qa, response_modelQueryResponse) async def legal_qa(req: QueryRequest): if not req.question.strip(): raise HTTPException(status_code400, detail问题不能为空) try: param QueryParam(modereq.mode, top_k10) answer await rag.aquery(req.question, paramparam) return QueryResponse(answeranswer, modereq.mode) except Exception as e: raise HTTPException(status_code500, detailf处理失败: {str(e)}) app.get(/health) async def health_check(): return {status: ok} if __name__ __main__: import uvicorn uvicorn.run(app, host0.0.0.0, port8000)启动服务python legal_qa_api.py然后你就可以用curl来测试了curl -X POST http://localhost:8000/api/legal-qa \ -H Content-Type: application/json \ -d {question: 买到假货可以要求三倍赔偿吗, mode: hybrid}返回的answer字段会包含完整的回答和它引用的法条依据。我在实际项目里还加了一个功能在返回结果中附带检索到的相关法条原文这样用户可以直接核对系统回答是否准确这个功能对法律场景很重要因为用户需要验证AI的引用是不是真的存在。4. 实操实录从原始法条到准确回答4.1 数据清洗的细节处理前面说了语料准备这里补充一些我在实操中觉得影响很大的细节。法律文本经常有第X编第X章第X条这类结构标记我在导入前特意保留了这些标记没有把它们当噪音去掉。原因是LightRAG的实体抽取器会把这些结构标记作为命名实体的一部分有助于它区分不同层级的法条。另一个细节是数字的格式化。法律文本里的日期、年龄、金额都是数字加单位比如十五日三倍第五百六十三条。如果文本里有全角数字和半角数字混用的情况建议统一转成半角否则实体抽取的一致性会受影响。我自己写了个简单的正则替换import re def normalize_number(text: str) - str: full_to_half str.maketrans(, 0123456789) text text.translate(full_to_half) text re.sub(r[ \t], , text) return text.strip()还有一点不同编辑版本的法律文本对的之等虚词的使用略有差异这是正常的不影响检索。但如果你从多个网站拼凑语料建议检查一下有没有重复条文。我刚开始就遇到了同一个法条在两个文件里各出现一次的情况LightRAG的图结构会把重复实体合并掉一部分但向量的embedding还是会重复计入既浪费存储又可能稀释检索精度。4.2 实测不同查询模式在法律场景下的表现我在同一份民法典语料上用三个典型问题测了四种查询模式的效果。这里把实测结果列出来供参考。第一个问题是事实类民法典规定租赁合同的租赁期限不得超过多少年这个问题的答案固定在第二十一年属于单点事实。四种模式都能答对local模式最快回答时引用了第七百零五条。第二个问题是多条款联合类承租人未经出租人同意转租出租人可以解除合同吗这个问题涉及第七百一十六条、第七百一十八条等多个条文。naive模式只引用了第七百一十六条回答得不算完整local模式引用了两条hybrid模式把几个相关条文都召回了回答中明确区分了未经同意转租和经同意转租两种情形解释更细致。第三个问题是概括类民法典合同编的立法目的是什么这种问题其实不太适合法律问答系统的日常使用场景但一定要测一下。global模式回答得最全面因为它从图社区里抽取了合同编的整体摘要naive模式就答得比较散。hybrid模式也还可以但输出里混入了一些不直接相关的条文引用。给一个省流结论日常使用用hybrid追求速度用local做概括总结再用globalnaive其实没太大必要单独用。4.3 回答质量调优温度、提示词与引用溯源很多人在配LightRAG的时候只关注retrieval部分容易忽略大模型生成参数的影响。在法律问答场景中我认为最关键的是把大模型的temperature调低。我实测设置temperature0.1回答的确定性明显提高模型不再自由发挥编造不存在的法条编号。如果设置太高比如默认的0.7模型会在引用法条时出现张冠李戴的情况把相近条文编号搞混。LightRAG允许你在初始化时设置llm_kwargs或者在每次query时覆盖。我的推荐配置是rag LightRAG( working_dir./legal_rag_data, llm_model_funcllm_model_func, embedding_funcembedding_func, llm_kwargs{temperature: 0.1, max_tokens: 2000} )设置max_tokens也很重要。法律回答经常包含大量条文引用如果max_tokens设得太小回答会被截断。我之前设成512结果回答在引用关键法条时被硬生生切断了排查了很久才发现是这个原因。另外LightRAG也支持自定义查询提示词。你可以在初始化时传入system_prompt告诉模型输出格式。我设置的提示词是你是专业法律顾问回答必须基于提供的法律条文逐条引用条文编号和原文关键句禁止编造不存在的法条。这个提示词有效减少了模型胡编法条编号的情况。关于引用溯源LightRAG的返回结果本身就包含上下文。但实际上它的回答中不会主动列出引用文档列表你需要把QueryParam的return_context设为True才能拿到本次回答依据了哪些文本片段param QueryParam(modehybrid, top_k10, return_contextTrue)这样返回的context字段里就有每个检索片段的原文和来源信息你可以直接展示给用户作为参考依据。5. 踩坑记录与排查速查表5.1 高频报错与解决办法我在这套系统上踩过的坑不算少挑几个典型的列出来。第一个坑是lightrag导入报错。这基本是装错包的问题正确的安装命令是pip install lightrag-hku。装完之后如果还有报错检查一下Python版本和依赖冲突用pip install --upgrade先把基础依赖更新一遍。第二个坑是中文编码问题。Windows环境下尤其容易遇到因为默认编码可能是GBK。处理办法是在导入脚本最前面加上import sys sys.stdout.reconfigure(encodingutf-8)文本文件的读写也必须显式指定encodingutf-8否则Windows下会出现乱码或UnicodeDecodeError。我建议所有开发调试都在Linux或macOS下进行能省去很多编码烦恼。第三个坑是working_dir目录没权限或空间不足。我遇到过一次存储目录所在分区磁盘写满的情况结果LightRAG在写入图数据时静默失败但进程没有退出之后所有查询都报错。排查方法很简单定期检查磁盘空间df -h du -sh ./legal_rag_data第四个坑是embedding API的限流。法律文本导入时LightRAG会对每个文本块调用embedding接口并发量高了之后有些云服务商要求必须传encoding_format参数否则返回400错误。解决方法是给embedding函数加上这个参数async def embedding_func(texts: list[str]) - list[list[float]]: return await openai_embedding( texts, modeltext-embedding-3-small, api_keyos.environ[OPENAI_API_KEY], base_urlos.environ[OPENAI_API_BASE], encoding_formatfloat )5.2 内存与存储优化实践LightRAG的存储机制是图结构加向量索引默认情况下图数据放在NetworkX里向量数据用numpy数组存。这带来两个优化点。第一点是控制文档切块大小。LightRAG默认的chunk_token_size是1024切得太大会让实体抽取的上下文过于冗长切得太小又会丢失跨句关系。我实测法律文本用800到1200比较合适。修改方式是在插入文档前先初始化chunk_token_sizefrom lightrag import LightRAG, QueryParam from lightrag.lightrag import LightRAGConfig config LightRAGConfig(chunk_token_size900, entity_extract_max_gleaning2) rag LightRAG( working_dir./legal_rag_data, llm_model_funcllm_model_func, embedding_funcembedding_func, lightrag_configconfig )第二点是及时调用rag.cleanup_cache()清理临时缓存。LightRAG在索引过程中会产生大量中间数据不清理的话跑完几部法律后存储目录会膨胀到几个G。我在每次导入完成后会主动清理一次实测存储空间能减少30%到50%。5.3 关于增量更新的一个坑发布新文档的时候如果直接调用ainsert插入一个包含大量重复内容的文件比如一部法律的新旧版本都在语料里旧版本并不会被自动删除。这会导致回答中出现新旧法条混用的情况对法律问答来说是致命的。我的处理办法是对于需要替换整部法律的场景干脆把该法律相关文件从working_dir中剔除然后重新导入新版本。最稳妥的做法是定期重建索引或者一开始就把不同法律版本放在独立的working_dir中管理。我在实际项目里就是给民法典劳动合同法司法解释各建了一个working_dir需要更新哪部就重建哪个目录互不影响。LightRAG没有提供现成的删除单篇文档的API这是它的一个局限。如果你对文档管理有很严格的要求建议在一开始就规划好目录隔离策略而不是指望后期能精确删除。5.4 回答不准确时的排查思路如果遇到回答不准确的情况先别急着调模型参数按这个顺序排查。先看检索召回的结果对不对。把return_contextTrue打开看系统实际召回的是哪些文本片段。如果召回片段里根本没有相关条文说明是检索环节的问题重点排查embedding模型是否太弱、文档切块大小是否合理、知识图谱是否成功构建了关键实体关系。如果召回片段里有相关条文但回答结果仍然不对说明是生成环节的问题。检查temperature是否过高提示词是否明确要求引用条文以及max_tokens是否足够生成完整回答。我之前碰到一个典型案例用户问离婚时夫妻共同财产怎么分割系统给出的回答看起来头头是道但引用的是已经被民法典取代的旧婚姻法的条文。排查后发现是语料库里混入了一份旧法文本系统把新旧条文都召回了。删掉旧法文本并重建对应目录的索引后问题彻底解决。这也是我反复强调语料质量的原因。RAG系统的天花板基本由知识库决定大模型只是把知识组织成回答知识库里没有正确的内容再好的模型也答不对。最后说几点我的使用体会这套法律问答系统从搭建到跑通整体花了我一个周末的时间大部分时间都耗在语料整理和参数调优上LightRAG本身的上手成本比我预想的低很多。我觉得它很适合作为垂直领域问答系统的起步框架尤其是你手头有一批领域文档、想要快速验证AI问答可行性的场景。如果后续想扩展我建议优先考虑这几个方向一是把前端页面加上做一个简单的聊天界面二是加入用户反馈机制对回答质量做人工评分然后根据反馈微调提示词三是针对高频问题做缓存减少大模型调用次数节省API费用。这几个方向我目前正在做后面有进展会继续分享。另外提醒一句用法律问答系统回答用户问题时要加免责声明明确本回答仅供参考不构成法律意见具体法律事务请咨询专业律师。技术能帮人快速检索和整理信息但法律的最终判断还是要交给专业的人来做。