ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LightRAG用图结构替代Query改写:本地部署实践与原理深度解析

LightRAG用图结构替代Query改写:本地部署实践与原理深度解析 我第一次在本地项目里接入 LightRAG 时第一反应是要不要在它前面加一个 query 改写模块过去用传统向量 RAG 习惯了总担心用户问得太口语化、指代不清导致向量召回根本命中不了。后来翻了 LightRAG 的源码和检索链路才意识到这个框架压根不是靠把问题改写得更像原文来兜底的它直接换了一套打法——用图结构把关系查询这件事从语义相似度猜测变成了图上连通性匹配。这也就是社区里总在讨论LightRAG 为什么不用 query 改写的真正原因。这篇文章我想把三件事讲透LightRAG 到底解决了什么痛点、它的检索机制为什么不需要 query 改写、以及我在 Windows 上用 Python 3.10 Ollama 把它完整跑起来的过程和踩坑记录。如果你是做本地知识库、企业文档问答、或者想摆脱云端 API 依赖的 RAG 开发者这篇应该能直接给你省下不少试错时间。1. 先聊聊传统 RAG 的痛为什么我要换 LightRAG1.1 一次让我印象深刻的教训多跳问题整体翻车我之前做过一个内部文档问答系统需求很简单把几十份项目文档丢进去让员工用自然语言提问。刚开始用的是经典的向量检索 大模型生成方案也就是把文档切块、做 embedding、查询时用余弦相似度召回 top-k 文本块。表面上跑通了但一上真实问题就露馅。用户问A 部门提出的预算方案最后被谁批准了这里涉及两层关系先要找到预算方案这个实体再沿着提出人 - 审批人的关系去找答案。向量检索的做法是拿整个问题去做相似度匹配它找到的往往是包含预算方案字样的文本块但审批人可能出现在另一份会议纪要里两层关系不在同一个 chunk 里召回自然就断了。这类问题有个专门的名字叫多跳查询multi-hop query。传统向量 RAG 在处理这种实体之间有明确关系路径的问题时本质上是不擅长的因为文本块之间的关联信息在切块时就已经被切碎了。我当时还尝试过加 query 改写用 LLM 把问题展开成几个子查询再分别去检索。效果有一些提升但代价是每次查询多花一次甚至好几次 LLM 调用延迟和 token 成本都上去了而且改写本身也会引入新的噪声——有时候改写出来的问题比原问题还要抽象。1.2 LightRAG 的定位用图结构补上关系这一课后来我注意到 HKUDS 团队开源的 LightRAG这个项目的定位很有意思。它不是一个传统的向量检索增强框架而是把知识图谱和 RAG 结合到了一起先让 LLM 从文档里抽取实体和实体之间的关系构成一张图查询的时候除了做向量召回还会从图上沿着关系路径去找上下文。这样一来前面那个预算方案 - 审批人的问题就不再依赖两个文本块恰好挨在一起了而是看图里预算方案这个节点连着哪些人。LightRAG 还有一个特性是轻。GraphRAG 这个概念其实之前就有微软的 GraphRAG 也做了类似的事但它的全量索引流水线很重从构建社区、做层级聚类到生成知识报告跑一遍大文档集需要消耗大量 LLM 调用。LightRAG 的做法更务实只做完实体抽取和关系抽取不加一堆额外加工环节并且支持增量更新新增文档只需要抽取新实体和关系并合并进图里不用全量重建。这一点对我这种需要持续往里加文档的场景非常关键。2. LightRAG 内部是怎么跑的抽取、建图、检索与更新2.1 文本进图实体和关系的抽取环节LightRAG 的索引流程可以分成三步。第一步是把原始文档切块切块逻辑和传统 RAG 一样有一个 chunk_token_size 参数控制块大小默认大约是 1200 token块之间还有 overlap 避免切断语义。第二步是实体与关系抽取。LightRAG 对每一个文本块调用一次 LLM让模型从里面抽取出实体比如人名、组织名、项目名、专有名词以及实体之间的关系比如提出、审批、隶属于。这一步得到的结构会被写入图存储。LightRAG 默认使用 NetworkX 这样的内存图结构来做持久化数据存在 working_dir 目录下如果你有更强的关系型查询需求也可以接 Neo4j 这类专业图数据库。第三步是对文本块本身做 embedding保存成向量索引。也就是说LightRAG 是图 向量双通道的索引结构。图负责记录实体间的关系向量负责记录原文内容的语义。两条腿走路跟传统 RAG 只靠向量有本质区别。这里有一个很值得注意的细节实体抽取的 prompt 是固定的但模型可以不同。实际使用中模型质量直接决定图的质量。如果用一个很小的模型去抽可能会出现实体漏抽、关系张冠李戴的问题如果模型指令遵循能力弱还可能出现输出 JSON 格式不合法导致解析失败。LightRAG 内部做了容错但抽取质量最终还是模型能力的真实体现。2.2 检索不是只有一路local、global、hybrid 的区别插入文档之后查询阶段就进入重头戏了。LightRAG 的 QueryParam 里有一个 mode 参数最常用的几个值分别是naive、local、global和hybrid。naive模式其实就是传统向量检索拿问题直接做 embedding召回最相似的文本块不做任何图操作。这个模式适合当基线用来对比图增强到底带来了多少收益。local模式会先提取问题中的关键词映射到图上的实体节点然后沿着这些节点的边向外扩展一跳或者两跳把这些邻居实体的描述、关系描述、对应的原始文本块拼起来作为上下文。这个模式天然适合回答A 和 B 是什么关系、这个项目涉及哪些人这类关系型问题。global模式则是把关键词映射到全局的实体和关系上做更宏观的聚合适合回答整个文档集在讨论什么话题、哪些领域被提到最多这类全局性问题。hybrid模式是把 local 和 global 的结果合并起来再一起交给 LLM 生成答案。官方推荐用hybrid作为默认模式因为它兼顾了局部关系和全局信息。我自己实际跑下来也是 hybrid 效果最稳定后面有一节会放具体对比。2.3 增量更新最打动我的一点传统 RAG 更新文档的方式通常是重新切块、重新 embedding、重新建索引如果文档集很大每次更新都是一次全量工程。LightRAG 的增量更新策略是新的文档进来时只对新文本块抽取实体和关系然后 merge 进已有的图里。如果新实体出现了就添加节点如果出现了新的关系描述就给已有节点之间添加边。这个设计对实际项目的意义很大。我现在的文档库是每周更新一次的如果每次都要全量跑一遍索引光 LLM 调用就是一大笔开销。增量更新把成本压缩到只处理新增部分而且图结构天然支持这种渐进式扩展。当然增量更新也有它的代价如果老文档本身抽取质量差后期想修复老实体就会比较麻烦因为图上已经存在错误节点。所以在索引阶段尽可能把抽取质量做高比后期再修补要省事得多。3. 为什么它不做 Query 改写拆开源码看答案3.1 LightRAG 的查询入口关键词先走一步很多人第一次用 LightRAG 都会有这个困惑现在主流 RAG 方案不是都应该先做 query 改写、HyDE、多查询扩展吗怎么 LightRAG 没有这一步实际上 LightRAG 的查询链路里也有关键词提取环节但它的作用和传统 query 改写完全不同。它在拿到用户问题后会从问题中抽取关键词然后用这些关键词去匹配图里的实体节点。关键词匹配发生之后检索就直接进入了图遍历阶段沿着实体节点的边去取邻居和关系。这一步的设计思路是问题本身经过关键词提取后已经变成了一个实体入口。比如用户问预算方案被谁批准了关键词提取得到的核心实体是预算方案它直接对应到图上的一个节点。接下来只要沿着这个节点的关系边找审批关系就能定位到审批人。整个过程不需要把问题改写得更丰富、更适合 embedding 匹配因为图检索本来就不是靠向量相似度打天下的。LightRAG 之所以能做到这一点还有一个前提是实体抽取阶段的信息密度够高。它在索引阶段已经把谁提出了什么、谁批准了什么、哪个部门负责什么这类三元组关系全部记录下来了所以查询阶段只需要负责对号入座。3.2 图关系匹配为什么比改写后再相似度召回更稳传统 query 改写解决的问题是用户问题表达方式与文档原文差异太大导致向量相似度匹配失败。解决思路是让 LLM 把问题扩写成多个版本提高与原文撞上的概率。这个方法在纯文本检索体系里是合理的但它的本质还是在文本空间里猜相似。而 LightRAG 已经把问题的答案路径抽象成了图上的关系路径它就不再需要靠改写来扩大召回。举个例子用户问甲公司的合作伙伴有哪些如果走改写路线得先猜原文里可能用了合作、伙伴、携手等不同表达方式一个个去匹配而 LightRAG 在抽取阶段就已经把甲公司 -合作关系- 乙公司这条边建好了查询时直接沿边扩展就行不管原文是用合作还是携手写的。这种精确匹配还有一个好处就是可解释性更强。hybrid 模式返回的答案可以回溯到具体的图路径从哪个实体出发、经过哪条关系边、命中了哪个文本块。传统 query 改写后的检索中间过程是一个黑盒出了问题很难排查。另外query 改写是有成本的。多查询扩展一次查询会变成三到五个子查询LLM 调用量成倍增加本地部署时这个延迟会非常明显。LightRAG 把检索重心放到图上之后查询开销主要来自关键词提取和图遍历图遍历本身不消耗 LLM token整个查询链路的成本更可控。3.3 什么情况下可以自己做一点实体归一化话虽如此我在实际使用中也遇到过 LightRAG 处理不好的情况主要是实体指代问题。比如用户问那家做数据库的公司怎么样了数据库公司是一个口语化描述不是文档里的精确实体名关键词提取阶段可能匹配不到图节点。这种情况下传统 query 改写确实有帮助但你要做的不是文本扩展而是实体归一化——在进 LightRAG 之前先用一个轻量 LLM 把口语化表达还原成文档里的标准实体名称。比如把那家做数据库的公司归一化成XX数据库科技有限公司。我建议把这种归一化放在外层作为前置步骤而不是去修改 LightRAG 的检索流程。原因很简单LightRAG 的核心能力是精确的图关系匹配加太多文本扩展反而会把实体入口搞模糊。前置归一化只是在入口处做一次纠偏能保留图匹配的精确性又不牺牲对口语化问题的兼容性。4. Windows 本地部署实录Python 3.10 Ollama4.1 装环境Python 3.10 虚拟环境与依赖Windows 上部署 LightRAG我强烈建议用 Python 3.10。原因很实际LightRAG 的一些依赖比如涉及 onnxruntime、numpy 的包在旧版本 Python 上可能有预编译 wheel 缺失的问题而 3.12、3.13 又太新个别包可能还没跟进。3.10 是一个各方面兼容性都比较稳的版本。第一步创建虚拟环境避免把系统 Python 环境搞乱。python -m venv .venv .venv\Scripts\activate第二步安装 LightRAG 官方包。注意包名是lightrag-hku不是lightrag。后者是一个老早以前的第三方包功能完全不同装错了后面全乱套。pip install lightrag-hku如果下载速度比较慢可以临时切换 pip 镜像源pip install lightrag-hku -i https://pypi.tuna.tsinghua.edu.cn/simple除了 LightRAG 本体还需要 OpenAI Python 客户端因为我们后面要通过 OpenAI 兼容接口去连 Ollamapip install openai numpy安装完成后可以验证一下python -c import lightrag; print(lightrag.__version__)如果你能打印出版本号说明环境基本就绪了。4.2 准备 Ollama模型选择与启动验证Ollama 在 Windows 上属于下载即用型工具。安装完成后会自动作为后台服务运行默认监听 11434 端口并且自带 OpenAI 兼容接口。这个兼容接口很重要它让 LightRAG 可以像调用 OpenAI API 一样调用本地 Ollama 模型不需要额外写适配层。模型选择上我推荐两件套一个生成模型一个 embedding 模型。生成模型我用的最多的是qwen2.5:7b-instruct-q4_K_M它在中文实体抽取和指令遵循上都比较稳7B 量化版体积大约 4.7GB8GB 显存的卡勉强能跑纯 CPU 也能跑但速度慢一些。如果你的机器配置有限可以降到qwen2.5:3b速度快很多抽取质量会略降。embedding 模型我用的是bge-m3维度 1024中文语义理解能力属于第一梯队。如果你磁盘空间紧张也可以用nomic-embed-text维度 768体积更小但中文效果不如 bge-m3。拉取模型ollama pull qwen2.5:7b-instruct-q4_K_M ollama pull bge-m3验证 Ollama 的 OpenAI 兼容接口是否正常可以在 PowerShell 里执行Invoke-RestMethod -Uri http://127.0.0.1:11434/v1/models -Method Get如果返回了模型列表说明接口就绪。4.3 最小可运行代码把第一段文档插进去接下来是最关键的部分。LightRAG 的插入和查询都是异步接口我们需要自己写一个 async 函数用 asyncio.run 来跑。下面是我在 Windows 上实测可用的最小代码。import asyncio import numpy as np from openai import AsyncOpenAI from lightrag import LightRAG, QueryParam from lightrag.utils import EmbeddingFunc # 1. 用 Ollama 的 OpenAI 兼容接口封装 LLM 调用 llm_client AsyncOpenAI( base_urlhttp://127.0.0.1:11434/v1, api_keyollama, # Ollama 不校验 key但接口要求非空 ) async def llm_model_func(prompt, system_promptNone, history_messagesNone, **kwargs): messages [] if system_prompt: messages.append({role: system, content: system_prompt}) if history_messages: messages.extend(history_messages) messages.append({role: user, content: prompt}) resp await llm_client.chat.completions.create( modelqwen2.5:7b-instruct-q4_K_M, messagesmessages, temperaturekwargs.get(temperature, 0.1), max_tokenskwargs.get(max_tokens, 2048), ) return resp.choices[0].message.content # 2. 封装 embedding 函数LightRAG 要求返回归一化二维数组 embedding_client AsyncOpenAI( base_urlhttp://127.0.0.1:11434/v1, api_keyollama, ) async def embedding_func(texts: list[str]): resp await embedding_client.embeddings.create( modelbge-m3, inputtexts, ) arr np.array([item.embedding for item in resp.data], dtypefloat32) norm np.linalg.norm(arr, axis1, keepdimsTrue) return arr / norm # 3. 初始化 LightRAG rag LightRAG( working_dir./lightrag_data, llm_model_funcllm_model_func, embedding_funcEmbeddingFunc( embedding_dim1024, max_token_size8192, funcembedding_func, ), chunk_token_size1200, ) async def main(): # 4. 插入文档 await rag.ainsert(公司决定在2025年启动数据中台项目由张伟担任项目负责人李婷负责预算审批。) await rag.ainsert(数据中台项目包含数据采集、数据治理和数据分析三个子模块预算总额为500万元。) # 5. 查询 resp await rag.aquery( 数据中台项目由谁负责, paramQueryParam(modehybrid), ) print(resp) if __name__ __main__: asyncio.run(main())这里有一个细节值得说明LLM 函数里的 temperature 我设成了 0.1因为实体抽取任务需要稳定输出温度太高会导致每次抽取结果抖动很大。LightRAG 内部会在不同阶段调用同一个 llm_model_func包括关键词提取、实体抽取、最终答案生成我统一用低温保证抽取稳定性最终生成部分也不会太离谱。working_dir会在当前目录下创建一个lightrag_data文件夹图数据、向量数据、原始文本分块都会存在这里。以后重启程序直接复用这个目录不需要重新插数据。4.4 跑查询naive 和 hybrid 的实测对比插入几条测试文本后我分别用 naive 和 hybrid 模式跑了一下同一个问题数据中台项目由谁负责。naive 模式返回的内容大致是先从向量召回中找了一段提到项目负责人的文本然后拼给 LLM 回答。因为两句话里确实都有数据中台项目这几个字所以也能答对。但如果文档再复杂一些第一句提到张伟担任项目负责人第二句只有该项目预算总额500万元而没提数据中台四个字naive 就很可能召回不到两句之间的关联。hybrid 模式在同样的数据上表现就不一样。关键词提取命中数据中台项目之后local 检索会沿着图边找到张伟 - 项目负责人和李婷 - 预算审批两个实体关系再把相关的原文片段带回给 LLM。它不仅能回答谁负责还能顺带把李婷负责预算审批这个关系也暴露出来信息密度明显更高。所以我实际项目里默认都用 hybrid。只有当你想跟传统 RAG 做对照组实验时我才会建议跑一下 naive 模式看差距。5. 部署后必须知道的资源与调优细节5.1 内存、显存和磁盘这些数字你最好心里有数LightRAG 做本地部署很多人只关注模型能不能跑忽略了索引过程中的资源占用。我实测下来有几个数字可以参考。生成模型qwen2.5:7b-instruct-q4_K_M加载到内存/显存大约需要 5GB 左右。如果你有独立显卡且显存大于等于 8GBOllama 会优先把模型放进显存如果显存不够就会退到 CPU 推理速度会明显下降但不会报错。embedding 模型bge-m3体积约 1.2GB量级较小。它的计算主要发生在插入文档阶段查询阶段 embedding 计算量不大。LightRAG 的索引数据会持久化到工作目录。我插入了大概 200 份中长文档每份 3000-5000 字之后工作目录大约占用了 50MB 左右。图数据以 JSON 形式存储随着实体增多会逐渐膨胀但在一万级实体以内普通机械硬盘都毫无压力。如果你用的是纯 CPU 机器完整插入 200 份文档的耗时可能在几十分钟到几个小时不等大头全在 LLM 实体抽取上。这个耗时会让你真正理解为什么前面说增量更新很重要。5.2 让抽取更准的调参思路实体抽取是整个图质量的源头这里值得多花一点心思。第一个调节点是 chunk_token_size。官方默认 1200但这不是万能的。如果你的文档里实体密度很高比如法律合同、技术方案这类文本1200 的块会让一次抽取包含太多实体LLM 容易漏抽。我把 chunk_token_size 调到 800 左右之后抽取准确率有明显改善。反过来如果文档内容比较稀疏调大 chunk 反而能让关系更完整。第二个调节点是 embedding 批量大小。LightRAG 提供了 embedding_batch_num 参数默认好像是 32控制每次送入 embedding 模型的文本条数。如果你的 embedding 模型跑在 CPU 上批次太大会导致内存峰值飙升适当调小可以避免卡顿。第三个调节点是 LLM 的 max_tokens。实体抽取阶段需要输出较长的 JSON尤其当一个文本块里实体很多时输出长度可能超过默认值。建议设置到 4096避免输出被截断导致解析失败。不过我这里要提醒一句如果你用的是小内存模型max_tokens 过大也会拖慢生成速度量力而行。5.3 Windows 上我踩过的五个坑这里整理几个我在 Windows 上实际踩过、而且很可能你也会踩的坑。第一个坑包名装错。pip install lightrag装出来的是一个废弃的第三方包跟 LightRAG 完全不是一回事。必须用pip install lightrag-hku。我最初就是没注意折腾了半天才发现装错对象。第二个坑Python 版本太高。我在 Python 3.12 上装过一次LightRAG 运行时出现了某些 C 扩展编译兼容性问题。换到 Python 3.10 的虚拟环境后一路顺畅。所以如果你遇到莫名其妙的底层报错先检查 Python 版本。第三个坑Ollama 没有真正启动。Windows 上 Ollama 安装后通常自动常驻但如果你手动改过服务或者安装了多个版本11434 端口可能没有监听起来。表现就是请求超时或连接拒绝。排查方式很简单浏览器访问http://127.0.0.1:11434/v1/models有返回就说明启动正常。第四个坑工作目录用了中文或带空格的路径。LightRAG 在 Windows 上对路径的容忍度一般中文路径偶尔会触发编码问题。保险起见工作目录一律用纯英文路径。第五个坑insert 大文件时一次性塞太多内容。我试过一次插入一整本几十万字的电子书结果内存直接飙到接近 8GB系统差点卡死。LightRAG 虽然支持批量插入但建议每次传入一个文档列表控制单个文档大小分多次插入。6. 写在最后我现在的使用习惯和一些小建议LightRAG 我已经在内部文档问答系统上跑了两个多月整体体验比我预期的好。它并不是一个完美无缺的框架比如实体抽取质量完全依赖底层模型小模型在专业领域里还是会漏抽图存储默认走文件形式文档量到几十万级以后查询速度会肉眼可见地下降。但对于中小规模的知识库场景它把关系型问题这个传统 RAG 的软肋补得非常扎实。我现在的使用习惯是底层生成模型用 qwen2.5 系列embedding 用 bge-m3查询模式固定 hybrid插入文档时按批次控制大小每周增量更新一次。如果是专业性很强的文档我还会在抽取阶段额外给 LLM 提供该领域的实体示例让抽取更贴合业务语言。如果你刚接触 LightRAG我的建议是先别急着上复杂功能。把最小的 Python 3.10 Ollama 环境跑通插入十几条测试文档分别用 naive 和 hybrid 对比一下你就知道它的价值在哪里了。等确认它对你的场景有用再考虑接 Neo4j、做 API 服务化、加前置实体归一化这些进阶操作。本地化、图增强、增量更新这三个特性组合在一起让我觉得它值得在 RAG 技术方案里占一个位置。
返回列表