1. 项目概述从零构建文本知识图谱的本地化实践最近在整理个人知识库时我一直在寻找一种比传统关键词搜索和向量检索更深入、更能揭示内容内在关联的方法。相信很多从事内容分析、研究或者单纯想深度消化一本电子书的朋友都有过类似的困扰我们读了很多材料但知识点是孤立的难以形成体系化的认知。这正是知识图谱可以大显身手的地方。这个项目qianniuspace/llm_notebooks中的一个子模块提供了一个非常接地气的解决方案利用本地运行的大语言模型LLM将任意文本语料库如PDF、TXT文档自动转化为可视化的知识图谱Knowledge Graph。简单来说它就像给你的文档内容做了一次“CT扫描”不仅列出所有重要的“器官”概念还清晰地展示出它们之间是如何通过“血管”关系连接在一起的。与依赖云端API如GPT-4的方案不同本项目核心是“完全本地化”和“经济性”使用 Ollama 部署 Mistral 7B 这类开源模型使得数据处理过程零成本、隐私安全。它特别适合处理内部文档、敏感资料或者单纯想体验大模型能力而不想付费的研究者、开发者。接下来我将详细拆解这个项目的设计思路、每一步的具体操作、我踩过的坑以及如何让它更好地为你工作。2. 核心思路与架构设计解析2.1 为什么是“概念”而非“实体”传统的信息提取如NER命名实体识别专注于识别文本中的具体实体如人名“马斯克”、地点“上海”、组织“OpenAI”。这个项目的第一个关键设计抉择是提取“概念”而非“实体”。这是一个至关重要的认知提升。概念 vs. 实体实体是具体的、客观存在的对象。而概念可以是更抽象、更复合的语义单元。例如在句子“电动汽车的快速发展得益于电池能量密度的提升和充电网络的完善”中实体提取可能得到“电动汽车”、“电池”、“充电网络”。关系较难直接定义。概念提取则可能得到“电动汽车的快速发展”、“电池能量密度提升”、“充电网络完善”。这些概念本身就蕴含了状态、事件或属性它们之间的因果关系“得益于”更容易被模型识别和建立。我的实操心得在初期测试中使用实体构建的图谱往往节点稀疏关系多为简单的“位于”、“属于”图谱显得干瘪。而提取概念后图谱立刻变得“有血有肉”节点之间能形成“导致”、“促进”、“对比”、“需要”等丰富的语义关系对于理解文本的深层逻辑和论证结构帮助巨大。这相当于从“识别词汇”升级到了“理解意群”。2.2 核心流程从文本块到关系网络项目的核心流程可以概括为下图所示的几个步骤我将其拆解并补充了每个环节的设计考量原始文本 - 文本分块 - LLM提取概念与关系 - 基于共现补充关系 - 关系融合与权重计算 - 构建图谱 - 可视化文本分块这是所有文档处理的第一步。这里没有采用简单的固定长度分割而是需要考虑语义的完整性。一个段落或一个章节的结束通常是自然的分割点。分块的大小需要权衡块太大LLM提取的关系可能过于宏观和混杂块太小上下文信息不足可能无法识别有效关系。通常我会将块大小设置在200-500个token之间并尽量保证块在句子的完整边界处切断。LLM提取核心步骤对每一个文本块我们向本地运行的Mistral 7B模型发送一个精心设计的提示词Prompt要求它完成一项结构化输出任务。这个Prompt是关键中的关键。# 一个简化版的Prompt示例 system_prompt 你是一个知识提取专家。请从以下文本片段中提取核心概念以及概念之间的语义关系。 请严格按照JSON格式输出包含两个字段“concepts”和“relations”。 “concepts”是一个字符串列表。 “relations”是一个列表其中每个元素是一个字典包含“source”源概念、“target”目标概念和“relation”关系描述如“导致”、“包含”、“对比”等。 只输出JSON不要有任何额外解释。 user_prompt f文本片段{text_chunk}LLM会根据这个指令分析文本块输出结构化的JSON数据。这一步的质量直接决定了图谱的“原材料”好坏。关系融合与权重计算这是项目的巧妙之处。它识别了两种关系来源显式关系W1由LLM直接提取出的语义关系如“A导致B”。隐式关系W2在同一文本块中共同出现的概念被认为通过上下文存在某种关联。这种共现关系为图谱增加了密度和冗余路径。 如果同一对概念如“碳中和”和“可再生能源”在多个块中被LLM提取出关系可能是“推动”、“依赖”或者多次共现那么它们之间的连接权重就会累加。最终一对概念之间只保留一条边其权重是W1和W2的综合关系描述则是所有提取到关系的汇总。这种设计极大地增强了关系的鲁棒性避免了因单次LLM提取的偶然误差而丢失重要连接。图谱构建与增强分析使用NetworkX库将节点概念和带权重的边关系构建成图数据结构。之后可以运行图算法进行深入分析节点度中心性连接数多的节点往往是文本的核心主题。社区发现通过算法将图中联系紧密的节点聚类这些社区可能对应文本中的不同子主题或叙事线索。 这些分析结果可以反馈到可视化中例如用节点大小表示重要性用颜色区分社区。2.3 技术选型背后的逻辑模型Mistral 7B OpenOrca选择它而非更大的模型如Llama2 13B/70B首要考虑是“本地部署的可行性”。7B参数模型在消费级GPU甚至只有CPU上可以运行内存占用相对可控。OpenOrca版本经过了大量的指令微调在遵循复杂提示词和生成结构化输出JSON方面表现优异这正是本项目最需要的核心能力。部署工具Ollama它极大地简化了本地大模型的下载、管理和服务化过程。一条命令ollama run mistral:7b-openorca就完成了从拉取模型到启动API服务的所有步骤。它提供的API接口通常为http://localhost:11434与OpenAI API格式兼容使得我们可以用requests库轻松调用避免了复杂的模型加载和环境配置。图谱处理NetworkX PandasNetworkX是Python图论分析的事实标准API成熟社区算法丰富。Pandas则用于处理提取出的结构化数据概念列表、关系列表进行清洗、去重、分组和权重计算非常顺手。这是一个轻量级、快速迭代的方案。可视化PyvisPyvis能生成交互式的、基于Web的图。这意味着你可以缩放、拖拽节点、查看边的标签体验远胜于静态的matplotlib绘图。生成的HTML文件可以单独打开方便分享和展示。注意事项这个技术栈是“原型友好型”的。如果知识图谱规模变得非常大数万节点NetworkX在内存和计算效率上可能会遇到瓶颈届时需要考虑Neo4j、NebulaGraph等专业的图数据库。但对于大多数文档分析场景当前栈完全够用。3. 环境搭建与详细实操步骤3.1 基础环境准备假设你已经在电脑上安装了Python建议3.9和pip。首先为项目创建一个独立的虚拟环境这是一个好习惯能避免包依赖冲突。# 创建并激活虚拟环境以conda为例venv同理 conda create -n kg_demo python3.10 conda activate kg_demo3.2 安装Ollama并拉取模型安装Ollama访问 Ollama官网 根据你的操作系统Windows/macOS/Linux下载并安装。安装过程非常简单几乎是一键完成。拉取并运行模型打开终端或命令行执行以下命令。这会下载约4GB的模型文件。ollama run mistral:7b-openorca首次运行会先下载模型下载完成后会自动进入一个交互式聊天界面。你可以按CtrlD退出聊天但Ollama服务会在后台继续运行监听本地的11434端口等待我们的程序调用。踩坑记录确保你的网络环境能顺畅访问GitHub等资源因为模型是从Ollama的服务器拉取的。如果下载慢可以考虑配置镜像源。另外运行模型会占用较多内存约8-10GB请确保你的电脑有足够可用内存。3.3 安装Python依赖库在项目目录下创建一个requirements.txt文件内容如下requests2.28.0 # 用于调用Ollama API pandas1.5.0 # 数据处理与分析 networkx3.0 # 图构建与分析 pyvis0.3.0 # 交互式图谱可视化 pypdf23.0.0 # 用于读取PDF文档如果处理PDF tqdm4.65.0 # 显示进度条处理长文档时很实用然后在终端中安装pip install -r requirements.txt3.4 核心代码实现与解析我们围绕核心的extract_graph.ipynb笔记本将其关键步骤拆解为可执行的Python脚本模块。以下是核心函数的实现与注释。3.4.1 文本加载与分块import PyPDF2 from typing import List import re def load_and_chunk_pdf(pdf_path: str, chunk_size: int 300, overlap: int 50) - List[str]: 加载PDF文件并将其分割成有重叠的文本块。 参数: pdf_path: PDF文件路径。 chunk_size: 每个块的大致token数中文字可粗略按字计数。 overlap: 块之间的重叠token数防止在句子中间切断重要上下文。 返回: 文本块列表。 text with open(pdf_path, rb) as file: reader PyPDF2.PdfReader(file) for page in reader.pages: text page.extract_text() \n # 简单的按句号、问号、感叹号分割更复杂可用nltk或spacy sentences re.split(r(?[。]), text) chunks [] current_chunk [] current_length 0 for sent in sentences: sent_len len(sent) if current_length sent_len chunk_size and current_chunk: # 保存当前块 chunks.append(.join(current_chunk)) # 保留重叠部分构建新块 overlap_sents current_chunk[-int(overlap/20):] if len(current_chunk) 1 else current_chunk # 简单估算 current_chunk overlap_sents [sent] current_length sum(len(s) for s in current_chunk) else: current_chunk.append(sent) current_length sent_len if current_chunk: chunks.append(.join(current_chunk)) return chunks3.4.2 调用本地LLM提取概念与关系这是整个流程的引擎。我们需要设计一个稳定的Prompt并处理API调用。import requests import json import time OLLAMA_API_URL http://localhost:11434/api/generate def extract_concepts_and_relations(text_chunk: str, model_name: str mistral:7b-openorca) - dict: 调用本地Ollama服务的Mistral模型从文本块中提取概念和关系。 参数: text_chunk: 输入文本块。 model_name: Ollama中已拉取的模型名称。 返回: 包含concepts和relations的字典若失败返回None。 prompt f 你是一个知识图谱构建助手。请仔细分析以下文本提取其中出现的核心概念Concept以及概念之间的语义关系Relation。 **核心概念**指的是文本中讨论的主要观点、主题、事件、属性或复合语义单元。它们通常是名词性短语。 **语义关系**描述概念之间如何关联例如导致、促进、包含、属于、对比、需要、影响、是的一部分、依赖于、等等。 **输出要求** 1. 请以纯JSON格式输出且只输出JSON不要有任何额外的解释、标记或文字。 2. JSON必须包含且仅包含两个字段 - concepts: 一个字符串列表列出所有提取出的核心概念。 - relations: 一个字典列表每个字典代表一个关系包含三个键 * source: 源概念来自concepts列表 * target: 目标概念来自concepts列表 * relation: 关系描述字符串 文本内容{text_chunk} payload { model: model_name, prompt: prompt, stream: False, options: { temperature: 0.1, # 低温度保证输出稳定性避免天马行空 num_predict: 1024 # 最大输出token数 } } try: response requests.post(OLLAMA_API_URL, jsonpayload, timeout180) # 设置较长超时 response.raise_for_status() result response.json() # Ollama的响应在response字段中 response_text result.get(response, ).strip() # 尝试从响应中解析JSON。模型有时会在JSON外加json 标记。 if response_text.startswith(json): response_text response_text[7:-3].strip() elif response_text.startswith(): response_text response_text[3:-3].strip() extracted_data json.loads(response_text) # 简单验证结构 if concepts in extracted_data and relations in extracted_data: return extracted_data else: print(f响应结构异常: {extracted_data.keys()}) return {concepts: [], relations: []} except json.JSONDecodeError as e: print(fJSON解析失败响应文本: {response_text[:200]}...) return {concepts: [], relations: []} except requests.exceptions.RequestException as e: print(fAPI调用失败: {e}) time.sleep(5) # 失败后等待片刻 return None except Exception as e: print(f未知错误: {e}) return {concepts: [], relations: []}关键技巧temperature参数设置为较低值如0.1是为了让模型输出更确定、更遵循指令减少随机性。这对于生成结构化的JSON至关重要。如果发现模型经常不按格式输出可以进一步优化Prompt在最后加上“请确保输出是有效的JSON可以直接被json.loads()解析”。3.4.3 构建与可视化知识图谱处理完所有文本块后我们得到了大量的概念和关系对。接下来是融合、计算权重并构建图谱。import pandas as pd import networkx as nx from collections import defaultdict from pyvis.network import Network def build_knowledge_graph(extraction_results: List[dict]) - nx.Graph: 将LLM提取的结果构建成带权重的NetworkX图。 参数: extraction_results: 列表每个元素是extract_concepts_and_relations的返回结果。 返回: networkx.Graph 对象。 # 用于存储边和关系的字典。键(source, target) 值{weight: X, relations: set()} edge_dict defaultdict(lambda: {weight: 0, relations: set()}) all_concepts set() for idx, result in enumerate(extraction_results): if not result: continue concepts result.get(concepts, []) relations result.get(relations, []) # 1. 收集所有概念 all_concepts.update(concepts) # 2. 处理LLM提取的显式关系 (W1) for rel in relations: src rel.get(source, ).strip() tgt rel.get(target, ).strip() rel_desc rel.get(relation, ).strip() if src and tgt and src in concepts and tgt in concepts: # 确保边的方向一致这里按字母排序统一避免重复 edge_key tuple(sorted([src, tgt])) edge_dict[edge_key][weight] 1.0 # W1 权重加1 if rel_desc: edge_dict[edge_key][relations].add(rel_desc) # 3. 处理同一块内概念的共现关系 (W2) # 假设同一块内所有概念两两之间都存在弱关联 for i in range(len(concepts)): for j in range(i1, len(concepts)): src, tgt concepts[i], concepts[j] edge_key tuple(sorted([src, tgt])) edge_dict[edge_key][weight] 0.3 # W2 权重通常比W1小 # 共现关系可以不加具体描述或加一个通用描述 edge_dict[edge_key][relations].add(上下文共现) # 创建NetworkX图 G nx.Graph() # 添加节点 for concept in all_concepts: G.add_node(concept) # 添加边 for (src, tgt), data in edge_dict.items(): if data[weight] 0: # 过滤掉权重为0的边理论上不会有 # 将关系集合连接成一个字符串作为边标签 relation_label ; .join(sorted(list(data[relations]))) G.add_edge(src, tgt, weightdata[weight], labelrelation_label) return G def visualize_graph(G: nx.Graph, output_html: str knowledge_graph.html): 使用Pyvis将NetworkX图可视化为交互式HTML。 参数: G: 构建好的知识图谱。 output_html: 输出的HTML文件名。 # 计算节点度中心性用于决定节点大小 degrees dict(G.degree()) max_degree max(degrees.values()) if degrees else 1 # 创建Pyvis网络对象 net Network(height750px, width100%, bgcolor#222222, font_colorwhite) # 将NetworkX图转换为Pyvis可用的数据 net.from_nx(G) # 配置节点和边的可视化属性 for node in net.nodes: node_id node[id] # 节点大小与度中心性成正比 node_size 10 (degrees.get(node_id, 0) / max_degree) * 30 node[size] node_size node[color] #97c2fc # 统一颜色也可根据社区分配颜色 node[font] {size: 14, face: Arial} for edge in net.edges: # 边宽度与权重成正比 edge_width 0.5 edge[weight] * 2 edge[width] edge_width edge[color] #cccccc if label in edge: edge[title] edge[label] # 鼠标悬停显示关系标签 # 启用物理布局让图更美观 net.set_options( var options { physics: { forceAtlas2Based: { gravitationalConstant: -50, centralGravity: 0.01, springLength: 100, springConstant: 0.08 }, maxVelocity: 50, solver: forceAtlas2Based, timestep: 0.35, stabilization: { iterations: 150 } } } ) net.show(output_html) print(f知识图谱已生成并保存为: {output_html}) print(f总节点数: {G.number_of_nodes()}, 总边数: {G.number_of_edges()})3.4.4 主流程串联最后我们将所有步骤串联起来并加入进度条和简单的错误处理。from tqdm import tqdm def main(pdf_path: str): 主函数串联整个流程。 print(步骤1: 加载并分块PDF文档...) text_chunks load_and_chunk_pdf(pdf_path, chunk_size350, overlap50) print(f共得到 {len(text_chunks)} 个文本块。) print(步骤2: 使用LLM逐块提取概念和关系此步骤较慢...) all_results [] failed_chunks [] for i, chunk in enumerate(tqdm(text_chunks, desc处理中)): # 可选跳过太短的块 if len(chunk.strip()) 20: all_results.append({concepts: [], relations: []}) continue result extract_concepts_and_relations(chunk) if result is None: # API调用失败记录并跳过 failed_chunks.append(i) all_results.append({concepts: [], relations: []}) print(f\n警告第 {i} 块处理失败已跳过。) else: all_results.append(result) # 避免请求过快可添加短暂延迟 # time.sleep(0.5) print(f提取完成。失败块数: {len(failed_chunks)}) print(步骤3: 构建知识图谱...) G build_knowledge_graph(all_results) print(步骤4: 可视化...) visualize_graph(G, my_knowledge_graph.html) # 可选保存图数据供后续分析 # nx.write_gexf(G, knowledge_graph.gexf) print(流程结束。请用浏览器打开 my_knowledge_graph.html 查看交互式图谱。) if __name__ __main__: # 替换为你的PDF文件路径 pdf_file ./your_document.pdf main(pdf_file)4. 常见问题、优化策略与避坑指南在实际操作中你几乎一定会遇到下面这些问题。这里是我踩过坑后总结的解决方案。4.1 LLM提取质量不稳定问题模型有时会输出非JSON格式或提取的概念过于琐碎/宽泛关系不准确。排查与解决优化Prompt这是最有效的手段。在Prompt中给出更清晰的例子Few-shot Learning。例如在指令后附上一小段示例文本和对应的理想JSON输出。明确告诉模型“不要输出任何解释性文字”。后处理清洗对提取出的概念进行后处理比如过滤掉过短如少于2个字符或明显是停用词如“这个”、“一种”的# 1. 两数之和题目给定一个整数数组 nums 和一个整数目标值 target请你在该数组中找出 和为目标值 target 的那 两个 整数并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是数组中同一个元素在答案里不能重复出现。你可以按任意顺序返回答案。示例示例 1输入nums [2,7,11,15], target 9 输出[0,1] 解释因为 nums[0] nums[1] 9 返回 [0, 1] 。示例 2输入nums [3,2,4], target 6 输出[1,2]示例 3输入nums [3,3], target 6 输出[0,1]提示2 nums.length 104-109 nums[i] 109-109 target 109只会存在一个有效答案进阶你可以想出一个时间复杂度小于 O(n2) 的算法吗解题思路最简单的思路是暴力枚举时间复杂度为O(n^2)空间复杂度为O(1)。进阶的思路是用哈希表遍历数组对于每个元素在哈希表中查找是否存在target - nums[i]如果存在则返回两个下标如果不存在则将当前元素加入哈希表。时间复杂度为O(n)空间复杂度为O(n)。性能执行用时36 ms, 在所有 Python3 提交中击败了92.71%的用户内存消耗16.5 MB, 在所有 Python3 提交中击败了5.18%的用户声明来源力扣LeetCode 链接https://leetcode-cn.com/problems/two-sum 著作权归领扣网络所有。商业转载请联系官方授权非商业转载请注明出处。