Initial: integrated 2025 LawGraph (graphrag_pipeline) + 2026 kg_project

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 10:13:39 +08:00
commit 6c1a69af0d
83 changed files with 14295 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual Environment
.venv/
venv/
ENV/
env/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Environment variables
.env
# Output files
output/
*.json
*.csv
*.xlsx
# Logs
*.log
logs/
# OS
.DS_Store
Thumbs.db
# Project specific
data/
models/
*.db
*.sqlite
+38
View File
@@ -0,0 +1,38 @@
# 更新日志
## v0.1.0 (2025-01-XX)
### 新增功能
- ✅ 项目基础架构(uv项目配置、目录结构)
- ✅ 文本预处理模块(HanLP集成、文档解析)
- ✅ 本体模型定义(9种实体类型、8种关系类型)
- ✅ Prompt模板系统(五个模块、NER/RE构建器)
- ✅ LLM客户端封装(多提供商支持)
- ✅ 知识抽取模块:
- NER提取器(两阶段对话)
- RE提取器(多模型并行、二次对话验证)
- 三元组评估器(成对排序、多维度评分)
- 实体关系分类器(RAG-based + Prompt-based
- ✅ 知识图谱构建(索引器、图谱管理、社区检测、摘要生成)
- ✅ 分析和推理模块(法理结构提取、逻辑推理、演化分析框架)
- ✅ 查询系统(全局搜索、局部搜索、DRIFT搜索)
- ✅ 执行脚本(extract.py, build_kg.py, query_kg.py, complete_pipeline.py
### 技术特性
- 支持多个LLM提供商(OpenAI, Anthropic, Qwen, Doubao, GLM
- 参考GraphRAG实现社区检测和摘要生成
- 实现论文中描述的五个模块Prompt模板
- 支持两阶段对话验证机制
- LLM-as-a-Judge评估方法
### 已知限制
- 大规模数据处理需要进一步优化
- 向量数据库使用简化实现,生产环境建议使用专业向量数据库
- 社区检测在igraph未安装时使用替代方法
@@ -0,0 +1,83 @@
# 配置完成总结
## ✅ 已完成的配置
### 1. 硅基流动API配置
- ✅ API密钥已配置:`sk-pvvtosiglncktlucwarxilvsypqcttqizgpcfdvodgcuaezn`
- ✅ API端点:`https://api.siliconflow.cn/v1`
- ✅ 环境变量已设置(`.env`文件)
### 2. 模型配置
项目已配置为使用以下两个硅基流动模型:
#### NER(命名实体识别)
- **模型**: `deepseek-ai/DeepSeek-R1-Distill-Qwen-7B`
- **配置文件**: `src/extraction/ner.py`
- **状态**: ✅ 已测试,可用
#### RE(关系抽取)- 多模型并行
- **模型1**: `deepseek-ai/DeepSeek-R1-Distill-Qwen-7B`
- **模型2**: `Qwen/Qwen2.5-7B-Instruct`
- **配置文件**: `src/extraction/re.py`
- **状态**: ✅ 已测试,两个模型都可用
#### 社区摘要生成
- **模型**: `deepseek-ai/DeepSeek-R1-Distill-Qwen-7B`
- **配置文件**: `src/kg_builder/indexer.py`
- **状态**: ✅ 已配置
### 3. 环境修复
- ✅ 虚拟环境已重新创建
- ✅ 所有依赖包已安装(92个包)
- ✅ 修复了conda环境路径干扰问题
## 📊 快速测试结果
测试了2个TextUnit的完整流程:
### 成功指标
- ✅ 文档解析:成功
- ✅ 实体识别(NER):成功(使用deepseek-ai/DeepSeek-R1-Distill-Qwen-7B
- ✅ 关系抽取(RE):成功(两个模型并行运行)
- ✅ 知识图谱构建:成功
- 节点数:67个
- 边数:63条
- 三元组数:63个
- ✅ 社区检测:成功(11个社区)
- ⚠️ 社区摘要生成:部分成功(需要更长时间,但已修复配置)
## 🚀 运行完整流程
完整流程正在后台运行,处理3个文档:
```bash
cd E:\Project\2025_LawGraph\dofile
.venv\Scripts\python.exe scripts\complete_pipeline.py --data-dir ../data/1法律 --output-dir ./output --max-docs 3
```
预计运行时间:由于需要处理62个TextUnit,每个都需要调用LLM API,可能需要10-30分钟。
## 📁 输出文件
流程完成后,将生成:
- `output/knowledge_graph.json`: 知识图谱文件
- `output/knowledge_graph_stats.json`: 统计信息
## 🔍 验证模型
运行以下命令验证模型配置:
```bash
.venv\Scripts\python.exe tests\test_my_models.py
```
## 📝 下一步
1. 等待完整流程完成
2. 查看生成的知识图谱文件
3. 可以尝试查询功能:
```bash
.venv\Scripts\python.exe scripts/query_kg.py --kg output/knowledge_graph.json --query "乡村振兴" --mode global
```
配置已完成,项目已就绪!🎉
+73
View File
@@ -0,0 +1,73 @@
# 环境变量配置说明
## 创建 .env 文件
`dofile` 目录下创建 `.env` 文件,添加以下配置:
```bash
# LLM API Keys
OPENAI_API_KEY=your_openai_api_key_here
ANTHROPIC_API_KEY=your_anthropic_api_key_here
DASHSCOPE_API_KEY=your_dashscope_api_key_here
VOLCENGINE_ACCESS_KEY=your_volcengine_access_key_here
VOLCENGINE_SECRET_KEY=your_volcengine_secret_key_here
ZHIPUAI_API_KEY=your_zhipuai_api_key_here
# SiliconFlow API配置(硅基流动)
SILICONFLOW_API_KEY=sk-pvvtosiglncktlucwarxilvsypqcttqizgpcfdvodgcuaezn
SILICONFLOW_API_BASE=https://api.siliconflow.cn/v1
# 路径配置
DATA_DIR=../data
OUTPUT_DIR=./output
# 日志配置
LOG_LEVEL=INFO
# HanLP配置(可选)
HANLP_MODEL_PATH=
# LLM配置
TEMPERATURE=0.4
FREQUENCY_PENALTY=0.6
PRESENCE_PENALTY=0.6
# 文本处理配置
MAX_TEXTUNIT_LENGTH=500
```
## 硅基流动配置
硅基流动的API已经配置完成,您可以使用以下方式调用:
```python
from src.utils.llm_client import LLMClient, LLMProvider
from src.utils.config import load_config
config = load_config()
client = LLMClient(
provider=LLMProvider.SILICONFLOW,
model="你的模型名称", # 例如:Qwen/Qwen2.5-72B-Instruct
config=config
)
messages = [
{"role": "user", "content": "你好"}
]
response = client.chat(messages)
print(response)
```
## 支持的模型
硅基流动支持多种模型,常用的包括:
- `Qwen/Qwen2.5-72B-Instruct`
- `meta-llama/Llama-3.1-70B-Instruct`
- `01-ai/Yi-1.5-34B-Chat`
- 等等...
具体可用模型请查看硅基流动官网:https://siliconflow.cn/
+43
View File
@@ -0,0 +1,43 @@
# 模型配置说明
## 当前使用的模型
项目已配置为使用以下两个硅基流动模型:
### 1. NER(命名实体识别)
- **模型**: `deepseek-ai/DeepSeek-R1-Distill-Qwen-7B`
- **用途**: 从法规文本中提取实体(目标要素、原则要素、空间要素等)
- **特点**: DeepSeek R1系列,具有推理能力
### 2. RE(关系抽取)
- **模型1**: `deepseek-ai/DeepSeek-R1-Distill-Qwen-7B`
- **模型2**: `Qwen/Qwen2.5-7B-Instruct`
- **用途**: 从文本中提取实体之间的关系
- **特点**: 多模型并行,提高准确性和覆盖率
### 3. 社区摘要生成
- **模型**: `deepseek-ai/DeepSeek-R1-Distill-Qwen-7B`
- **用途**: 为知识图谱的社区生成摘要
## 配置位置
模型配置在以下文件中:
- `src/extraction/ner.py`: NER模型配置
- `src/extraction/re.py`: RE模型配置(多模型)
- `src/kg_builder/indexer.py`: 社区摘要生成模型配置
## 如何修改模型
如果要修改使用的模型,请编辑上述文件中的模型名称字符串。
## 测试模型可用性
运行以下命令测试模型:
```bash
uv run python tests/test_model.py
# 测试指定的两个模型
uv run python tests/test_my_models.py
```
+147
View File
@@ -0,0 +1,147 @@
# 项目实现状态
## 已完成工作 ✅
### 1. 项目基础架构
- ✅ 使用uv进行项目管理和依赖管理
- ✅ 完整的项目目录结构
- ✅ 配置文件(pyproject.toml, .env.example
- ✅ README和项目文档
### 2. 文本预处理模块 (`src/preprocessing/`)
-**TextProcessor**: HanLP集成,支持分词、依存句法、句法成分分析
-**DocumentParser**: Word文档解析(.docx),支持章节结构识别
- ✅ TextUnit切分功能(参考GraphRAG
### 3. 本体模型定义 (`src/ontology/`)
-**实体类型定义**: 9种实体类型(对应论文Table 1)
-**关系类型定义**: 8种关系类型(对应论文Table 2)
-**三维本体结构**: 法规层级—要素类型—关联关系
-**四要素架构**: 目标—原则—要素—管控
-**OntologySchema**: 本体模式管理和验证
### 4. Prompt模板系统 (`src/prompts/`)
-**PromptTemplate**: 统一模板框架,实现五个模块:
- 任务描述(系统消息+用户消息)
- 候选目标
- 任务示例(少样本学习)
- 任务强调
- 二次对话
-**NERPromptBuilder**: NER任务Prompt构建
-**REPromptBuilder**: RE任务Prompt构建
### 5. LLM客户端封装 (`src/utils/llm_client.py`)
- ✅ 支持多个LLM提供商:
- OpenAI (GPT-4o)
- Anthropic (Claude)
- 阿里云Qwen
- 字节跳动Doubao
- 智谱GLM
- ✅ 统一API接口
- ✅ 异步/同步支持
- ✅ 重试机制(tenacity
### 6. 知识抽取模块 (`src/extraction/`)
-**NERExtractor**: 使用Qwen-Max进行实体识别,支持两阶段对话验证
-**REExtractor**: 多模型并行关系抽取框架
-**TripletEvaluator**: LLM-as-a-Judge评估框架
-**EntityRelationClassifier**: RAG-based分类框架
### 7. 知识图谱构建 (`src/kg_builder/`)
-**GraphIndexer**: 索引器框架(参考GraphRAG
- 文本切分
- 实体提取
- 图构建
- 社区检测(待完善)
- 摘要生成(待完善)
-**KnowledgeGraph**: 图谱结构管理(NetworkX
### 8. 分析和推理模块 (`src/analysis/`)
-**LegalStructureExtractor**: 法理结构提取(四要素识别、跨法规引用)
-**LegalReasoner**: 逻辑推理(路径查询、中心性分析)
-**EvolutionAnalyzer**: 演化规律分析框架
### 9. 查询系统 (`src/query/`)
-**GlobalSearcher**: 全局搜索框架(参考GraphRAG)
-**LocalSearcher**: 局部搜索框架
-**DriftSearcher**: DRIFT搜索框架
### 10. 工具脚本
-`scripts/extract.py`: 知识抽取示例脚本
## 待完善功能 ⚠️
### 高优先级
1. **三元组评估算法实现** (`src/extraction/evaluator.py`)
- 成对排序逻辑
- 五维度评分(语义准确性、一致性、事实性、准确性、可理解性)
- 评分聚合和筛选
2. **RAG-based分类实现** (`src/extraction/classifier.py`)
- 向量嵌入(使用Text-embedding-3-large
- 向量数据库(相似度检索)
- 分类逻辑完善
3. **知识图谱构建完整实现** (`src/kg_builder/indexer.py`)
- 完整的三元组提取流程
- 社区检测(Leiden算法,需要igraph
- 社区摘要生成(使用LLM
4. **RE提取器完善** (`src/extraction/re.py`)
- 句法信息集成
- 上下文信息利用
- 省略成分补全逻辑
### 中优先级
5. **查询系统实现** (`src/query/`)
- 全局搜索:利用社区摘要
- 局部搜索:邻居扩展算法
- DRIFT搜索:社区上下文整合
6. **演化规律分析** (`src/analysis/evolution.py`)
- 时间序列分析
- 关系演化追踪
- 网络演化分析
### 低优先级
7. **共指消解完善** (`src/preprocessing/text_processor.py`)
8. **性能优化和缓存机制**
9. **可视化工具**
10. **单元测试和集成测试**
## 使用说明
### 环境设置
```bash
cd dofile
uv sync
source .venv/bin/activate # 或 .venv\Scripts\activate (Windows)
```
### 配置API密钥
复制`.env.example``.env`,填入相应的API密钥。
### 运行示例
```bash
# 实体识别
uv run python scripts/extract.py --input ../data/1法律/3-中华人民共和国城乡规划法.docx --output ./output/entities.json
```
## 参考资源
- GraphRAG文档: https://msdocs.cn/graphrag/
- 论文: `officefile/paper.tex`
- 数据目录: `../data/`
## 技术债务
1. 错误处理和日志记录需要更完善
2. 需要添加配置文件验证
3. 异步处理可以进一步优化
4. 需要添加单元测试
5. 文档需要补充更多示例
+161
View File
@@ -0,0 +1,161 @@
# 项目完善总结
## 已完成的核心功能
### ✅ 1. 三元组评估器完整实现
- **成对排序**: 比较不同模型生成的三元组组,确定最佳结果
- **多维度评分**: 五维度评估(语义准确性、一致性、事实性、准确性、可理解性)
- **Peer Examination**: 避免自我增强偏差,支持多评估者
- **低质量筛选**: 基于评分阈值筛选三元组
### ✅ 2. RE提取器完善
- **二次对话验证**: 完整实现两阶段对话优化
- **实体格式化**: 改进实体列表的格式化处理
- **句法信息集成**: 更好地利用依存句法和句法成分信息
- **错误处理**: 完善的异常处理和结果解析
### ✅ 3. 知识图谱索引器完整实现
- **完整索引流程**: 从TextUnit到知识图谱的完整流程
- **图构建**: 实现三元组到图结构的转换
- **社区检测**: Leiden算法实现(igraph)或连通分量替代
- **社区摘要生成**: 使用LLM生成社区摘要
- **元数据管理**: 存储图谱构建的元信息
### ✅ 4. RAG-based分类器实现
- **向量嵌入**: 使用OpenAI text-embedding-3-large
- **相似度计算**: 余弦相似度计算
- **知识库构建**: 支持从已标注样本构建知识库
- **Prompt-based分类**: 备选分类方法
### ✅ 5. 查询系统完整实现
- **全局搜索**:
- 社区相关性评估
- LLM生成综合答案
- 支持关键词匹配备选
- **局部搜索**:
- 实体邻居扩展
- 深度控制
- 中心性分析
- 模糊匹配
- **DRIFT搜索**:
- 结合局部和全局信息
- 社区上下文整合
- 综合答案生成
### ✅ 6. 图谱管理功能
- **保存/加载**: JSON和GraphML格式支持
- **元数据保存**: 保存社区信息和统计信息
### ✅ 7. 执行脚本完善
- `extract.py`: 实体识别脚本
- `build_kg.py`: 知识图谱构建脚本
- `query_kg.py`: 图谱查询脚本(支持三种模式)
- `complete_pipeline.py`: 完整流程脚本
### ✅ 8. 文档和测试
- 完整的README
- 使用示例文档
- 架构文档
- 基础测试
- 项目状态文档
## 项目文件结构
```
dofile/
├── pyproject.toml # uv项目配置 ✅
├── README.md # 项目文档 ✅
├── PROJECT_STATUS.md # 项目状态 ✅
├── CHANGELOG.md # 更新日志 ✅
├── USAGE_EXAMPLES.md # 使用示例 ✅
├── .env.example # 环境变量模板 ✅
├── .gitignore # Git忽略文件 ✅
├── src/ # 源代码 ✅
│ ├── preprocessing/ # 文本预处理 ✅
│ ├── ontology/ # 本体模型 ✅
│ ├── prompts/ # Prompt模板 ✅
│ ├── extraction/ # 知识抽取 ✅
│ ├── kg_builder/ # 图谱构建 ✅
│ ├── analysis/ # 分析推理 ✅
│ ├── query/ # 查询系统 ✅
│ └── utils/ # 工具函数 ✅
├── scripts/ # 执行脚本 ✅
│ ├── extract.py
│ ├── build_kg.py
│ ├── query_kg.py
│ └── complete_pipeline.py
├── config/ # 配置文件 ✅
│ ├── ontology.yaml
│ └── prompts/
├── docs/ # 文档 ✅
│ └── ARCHITECTURE.md
└── tests/ # 测试 ✅
└── test_basic.py
```
## 核心特性
1. **模块化设计**: 清晰的模块划分,易于扩展和维护
2. **多LLM支持**: 支持5个主流LLM提供商
3. **GraphRAG参考**: 参考GraphRAG实现社区检测和查询
4. **论文方法实现**: 完整实现论文中描述的方法论
5. **错误处理**: 完善的异常处理和日志记录
6. **可配置性**: 通过配置文件和环境变量灵活配置
## 使用方法
### 快速开始
```bash
cd dofile
uv sync
cp .env.example .env
# 编辑.env填入API密钥
# 完整流程
uv run python scripts/complete_pipeline.py --data-dir ../data/1法律 --output-dir ./output --max-docs 5
```
### 分步执行
```bash
# 1. 实体识别
uv run python scripts/extract.py --input ../data/1法律/3-中华人民共和国城乡规划法.docx
# 2. 构建图谱
uv run python scripts/build_kg.py --data-dir ../data/1法律 --output ./output/kg.json
# 3. 查询图谱
uv run python scripts/query_kg.py --kg ./output/kg.json --query "城乡规划" --mode global
```
## 技术亮点
1. **两阶段对话验证**: 提高知识抽取准确性
2. **多模型并行**: 利用不同模型的优势
3. **LLM-as-a-Judge**: 自动化质量评估
4. **社区检测和摘要**: 参考GraphRAG的层次结构
5. **RAG-based分类**: 利用向量检索提高分类准确率
6. **DRIFT搜索**: 结合局部和全局信息的智能搜索
## 下一步建议
1. **性能优化**:
- 批量处理优化
- 缓存机制
- 异步处理改进
2. **功能增强**:
- 可视化工具
- 更完善的演化分析
- 增量更新支持
3. **生产化**:
- 单元测试完善
- 集成测试
- 性能监控
- 错误恢复机制
项目已基本完成论文方法论的实现,可以直接用于法规知识图谱的构建和分析。
+220
View File
@@ -0,0 +1,220 @@
# 国土空间规划法规知识图谱构建与分析系统
基于大语言模型的国土空间规划法规知识图谱构建与原理提取研究项目。
## 项目简介
本项目实现了论文《基于大模型的国土空间规划法规知识图谱构建与原理提取研究》中提出的方法论,包括:
- 法规文本预处理(HanLP
- 基于大模型的知识抽取(NER、RE、评估、分类)
- 知识图谱构建(基于GraphRAG思路)
- 法理结构提取
- 法规逻辑推理与演化规律识别
## 技术栈
- Python 3.12+
- uv: 虚拟环境管理和包管理
- HanLP: 中文NLP处理
- LLM APIs: OpenAI, Anthropic, Qwen, Doubao, GLM
- NetworkX/iGraph: 图处理
- PyYAML: 配置文件
## 快速开始
### 1. 环境设置
使用uv创建虚拟环境并安装依赖:
```bash
# 进入项目目录
cd dofile
# 初始化uv项目并安装依赖
uv sync
# 激活虚拟环境
source .venv/bin/activate # Linux/Mac
# 或
.venv\Scripts\activate # Windows
# 如果uv未安装,先安装uv
# Windows: powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
# Linux/Mac: curl -LsSf https://astral.sh/uv/install.sh | sh
```
### 2. 配置环境变量
复制 `.env.example``.env` 并填入API密钥:
```bash
cp .env.example .env
```
编辑 `.env` 文件,填入所需的API密钥:
- OPENAI_API_KEY
- ANTHROPIC_API_KEY
- DASHSCOPE_API_KEY (Qwen)
- VOLCENGINE_ACCESS_KEY / VOLCENGINE_SECRET_KEY (Doubao)
- ZHIPUAI_API_KEY (GLM)
- SILICONFLOW_API_KEY (硅基流动,已在示例中配置)
- SILICONFLOW_API_BASE (可选,默认为 https://api.siliconflow.cn/v1)
### 3. 运行项目
```bash
# 方式1: 完整流程(推荐)
# 从文档解析到知识图谱构建
uv run python scripts/complete_pipeline.py --data-dir ../data/1法律 --output-dir ./output --max-docs 5
# 方式2: 分步骤执行
# 步骤1: 实体识别
uv run python scripts/extract.py --input ../data/1法律/3-中华人民共和国城乡规划法.docx --output ./output/entities.json
# 步骤2: 构建知识图谱
uv run python scripts/build_kg.py --data-dir ../data/1法律 --output ./output/kg.json --max-docs 5
# 步骤3: 查询知识图谱
uv run python scripts/query_kg.py --kg ./output/kg.json --query "城乡规划" --mode global
uv run python scripts/query_kg.py --kg ./output/kg.json --query "城市更新" --mode local
uv run python scripts/query_kg.py --kg ./output/kg.json --query "土地管理" --mode drift
```
### 4. 运行测试
```bash
# 基础功能测试
uv run python tests/test_basic.py
# 测试硅基流动API配置
uv run python tests/test_siliconflow.py
# 测试文档解析
uv run python tests/test_documents.py
# 测试模型可用性
uv run python tests/test_model.py
# 测试指定模型
uv run python tests/test_my_models.py
# 快速测试(小规模知识图谱构建)
uv run python tests/test_quick.py
```
### 5. 使用硅基流动API
硅基流动已经配置完成,您可以通过以下方式使用:
```python
from src.utils.llm_client import LLMClient, LLMProvider
from src.utils.config import load_config
config = load_config()
client = LLMClient(
provider=LLMProvider.SILICONFLOW,
model="Qwen/Qwen2.5-72B-Instruct", # 请根据实际可用模型修改
config=config
)
messages = [{"role": "user", "content": "你好"}]
response = client.chat(messages)
```
更多使用示例请参考:
- `examples/use_siliconflow.py` - 硅基流动使用示例
- `ENV_SETUP.md` - 环境变量配置说明
## 项目结构
```
dofile/
├── src/ # 源代码
│ ├── preprocessing/ # 文本预处理
│ ├── ontology/ # 本体模型定义
│ ├── prompts/ # Prompt模板
│ ├── extraction/ # 知识抽取
│ ├── kg_builder/ # 知识图谱构建
│ ├── analysis/ # 分析和推理
│ ├── query/ # 查询系统
│ └── utils/ # 工具函数
├── scripts/ # 执行脚本
├── config/ # 配置文件
└── tests/ # 测试文件
```
## 项目状态
### 已完成模块
**项目基础结构**
- uv项目配置(pyproject.toml
- 项目文档(README.md
- 目录结构
**文本预处理模块**
- HanLP文本处理器(分词、依存句法、句法成分分析)
- 法规文档解析器(.docx文件解析)
**本体模型定义**
- 实体类型定义(9种类型)
- 关系类型定义(8种类型)
- 三维本体结构(法规层级—要素类型—关联关系)
- 四要素架构(目标—原则—要素—管控)
**Prompt模板系统**
- 统一Prompt模板框架(五个模块)
- NER Prompt构建器
- RE Prompt构建器
**LLM客户端封装**
- 多提供商支持(OpenAI, Anthropic, Qwen, Doubao, GLM
- 统一API接口
- 重试机制
**知识抽取模块框架**
- NER提取器(两阶段对话)
- RE提取器(多模型并行)
- 三元组评估器(LLM-as-a-Judge
- 实体关系分类器(RAG-based
**知识图谱构建框架**
- 图谱索引器(参考GraphRAG
- 图谱结构管理(NetworkX
**分析和推理模块框架**
- 法理结构提取器
- 逻辑推理器
- 演化规律分析器
**查询系统框架**
- 全局搜索(参考GraphRAG
- 局部搜索
- DRIFT搜索
### 待完善模块
⚠️ **部分优化空间**
- 三元组评估的性能优化(批量评估)
- RAG-based分类的向量数据库优化(使用专业向量数据库)
- 大规模图谱的社区检测优化
- 演化规律分析的时间序列处理完善
- 可视化工具开发
- 性能监控和日志分析
### 开发指南
各模块的详细文档:
- `src/preprocessing/`: 文本预处理
- `src/ontology/`: 本体模型定义
- `src/prompts/`: Prompt模板系统
- `src/extraction/`: 知识抽取
- `src/kg_builder/`: 知识图谱构建
- `src/analysis/`: 分析和推理
- `src/query/`: 查询系统
## 许可证
[待定]
+111
View File
@@ -0,0 +1,111 @@
# 测试结果报告
## 测试目标
测试运行项目,处理3个法规文档文件,构建知识图谱。
## 测试环境
- 操作系统: Windows
- Python版本: 3.12.7 (通过uv管理)
- 虚拟环境: `.venv`
- 测试时间: 2025-01-XX
## 已完成的工作
### ✅ 1. 项目依赖安装
- 使用 `uv sync` 成功安装了所有依赖包
- 包括:hanlp, openai, networkx, python-docx 等92个包
### ✅ 2. 硅基流动API配置
- 成功添加硅基流动API支持
- 配置了API密钥和端点
- 更新了相关文档
### ✅ 3. 代码修复
- 修复了 `pyproject.toml` 中的包配置问题
- 修复了 `TextProcessor` 使其能在HanLP不可用时降级运行
- 所有代码通过语法检查
## 遇到的问题
### ⚠️ 问题1: lxml DLL加载失败
**错误信息**:
```
ImportError: DLL load failed while importing etree: 找不到指定的模块。
```
**原因分析**:
- 虚拟环境的Python在导入lxml时,误用了系统conda环境的lxml包
- conda环境的lxml可能缺少必要的DLL文件或版本不兼容
**解决方案**:
1. **方案A(推荐)**: 重新创建干净的虚拟环境
```bash
cd E:\Project\2025_LawGraph\dofile
rm -rf .venv # 或手动删除.venv目录
uv sync
```
2. **方案B**: 在虚拟环境中强制重新安装lxml
```bash
.venv\Scripts\python.exe -m pip install --force-reinstall --no-deps lxml
```
3. **方案C**: 使用conda环境(如果系统主要使用conda)
```bash
conda install lxml
```
## 测试脚本说明
已创建以下测试脚本:
1. **`scripts/test_documents.py`** - 测试文档解析(不调用LLM)
- 解析3个文档
- 切分为TextUnit
- 保存结果到JSON
2. **`scripts/test_siliconflow.py`** - 测试硅基流动API配置
3. **`scripts/complete_pipeline.py`** - 完整流程(包含LLM调用)
## 建议的测试步骤
### 步骤1: 修复环境问题
```bash
# 重新创建虚拟环境
cd E:\Project\2025_LawGraph\dofile
rm -rf .venv
uv sync
```
### 步骤2: 测试文档解析(不调用LLM)
```bash
uv run python scripts/test_documents.py
```
### 步骤3: 测试完整流程(调用LLM,需要API密钥)
```bash
# 确保.env文件已配置API密钥
uv run python scripts/complete_pipeline.py --data-dir ../data/1法律 --output-dir ./output --max-docs 3
```
## 项目状态
**已完成**:
- 项目结构搭建
- 所有核心模块实现
- 硅基流动API集成
- 文档完善
⚠️ **待解决**:
- 虚拟环境配置问题(lxml DLL)
- 完整的端到端测试
## 下一步
1. 修复虚拟环境问题后,可以成功运行测试
2. 所有功能模块已实现,只需要解决环境配置即可正常使用
+114
View File
@@ -0,0 +1,114 @@
# 使用示例
## 示例1: 从单个文档提取实体
```python
from src.preprocessing.document_parser import DocumentParser
from src.extraction.ner import NERExtractor
from src.utils.config import load_config
# 加载配置
config = load_config()
# 解析文档
parser = DocumentParser()
doc = parser.parse_docx("../data/1法律/3-中华人民共和国城乡规划法.docx")
# 提取实体
ner_extractor = NERExtractor(config=config)
entities = ner_extractor.extract(doc["text"])
print(f"识别到 {sum(len(v) for v in entities.values())} 个实体")
```
## 示例2: 构建知识图谱
```python
from src.preprocessing.document_parser import DocumentParser
from src.kg_builder.indexer import GraphIndexer
from src.utils.config import load_config
# 加载配置
config = load_config()
# 解析文档
parser = DocumentParser()
docs = parser.parse_directory("../data/1法律")
# 切分为TextUnit
all_textunits = []
for doc in docs[:3]: # 只处理前3个文档
textunits = parser.split_into_textunits(doc, max_length=500)
all_textunits.extend(textunits)
# 构建知识图谱
indexer = GraphIndexer(config=config)
kg = indexer.index(all_textunits)
print(f"图谱包含 {kg.number_of_nodes()} 个节点,{kg.number_of_edges()} 条边")
```
## 示例3: 查询知识图谱
```python
from src.kg_builder.graph import KnowledgeGraph
from src.query.global_search import GlobalSearcher
from src.query.local_search import LocalSearcher
# 加载图谱
kg = KnowledgeGraph()
kg.load("output/kg.json")
# 全局搜索
global_searcher = GlobalSearcher(kg.graph, communities={})
result = global_searcher.search("城市更新的法规要求")
print(result["answer"])
# 局部搜索
local_searcher = LocalSearcher(kg.graph)
result = local_searcher.search("城乡规划", depth=2)
print(f"找到 {result['subgraph']['num_nodes']} 个相关节点")
```
## 示例4: 法理结构提取
```python
from src.analysis.legal_structure import LegalStructureExtractor
extractor = LegalStructureExtractor()
# 提取四要素
text = "为了促进城市可持续发展,应当坚持生态优先、绿色发展原则..."
elements = extractor.extract_four_elements(text)
print(elements)
# 提取跨法规引用
references = extractor.extract_cross_references(text)
print(references)
```
## 示例5: 三元组评估
```python
from src.extraction.evaluator import TripletEvaluator
evaluator = TripletEvaluator()
# 评估多个模型的结果
peer_groups = {
"model_1": [{"head": "A", "relation": "管控", "tail": "B"}],
"model_2": [{"head": "A", "relation": "涉及", "tail": "B"}],
}
result = evaluator.evaluate_peer_groups(
peer_groups,
source_text="原始文本..."
)
print(f"最佳模型: {result['best_group']}")
print(f"评分: {result['scores']}")
```
@@ -0,0 +1,108 @@
# 法规知识本体定义文件
# 对应论文中的三维本体结构:法规层级—要素类型—关联关系
# 法规层级
legal_levels:
- 法律
- 行政法规
- 部门规章
- 党中央国务院文件
- 国家主管部门文件
- 技术标准
# 实体类型(对应论文Table 1
entity_types:
法规条文:
definition: "法规文档中的具体条文,包括章、节、条、款、项等不同层级"
category: "structural"
目标要素:
definition: "法规中明确表达的战略目标、发展愿景、总体目标等宏观性表述"
category: "four_elements"
element_type: "目标"
原则要素:
definition: "法规遵循的基本准则、指导方针、基本原则等"
category: "four_elements"
element_type: "原则"
空间要素:
definition: "法规涉及的地理空间实体,包括行政区划、功能区、保护区域等"
category: "four_elements"
element_type: "要素"
功能要素:
definition: "法规涉及的功能性要素,如土地利用类型、产业类型、基础设施等"
category: "four_elements"
element_type: "要素"
环境要素:
definition: "法规涉及的环境保护相关要素,如生态保护区、环境质量标准等"
category: "four_elements"
element_type: "要素"
管控要素:
definition: "法规对各类要素的管控要求,包括禁止、限制、引导等管控措施"
category: "four_elements"
element_type: "管控"
行政区域:
definition: "法规适用的行政区划,包括省、市、县、乡等不同层级"
category: "spatial"
时间要素:
definition: "法规涉及的时间概念,如规划期限、实施阶段、有效期等"
category: "temporal"
# 关系类型(对应论文Table 2
relation_types:
引用:
definition: "一条法规条文明确引用另一条法规条文"
category: "legal"
层级:
definition: "上下级法规之间的隶属关系或法规条文内部的层级关系"
category: "structural"
包含:
definition: "一个实体在逻辑上包含另一个实体"
category: "logical"
管控:
definition: "管控要素对目标要素、原则要素或其他要素的约束关系"
category: "four_elements"
遵循:
definition: "要素遵循某种原则或目标的关系"
category: "four_elements"
涉及:
definition: "法规条文涉及某个空间、功能或环境要素"
category: "semantic"
适用:
definition: "法规适用于某个行政区域"
category: "spatial"
演化:
definition: "法规修订前后的变化关系或时间序列上的演化关系"
category: "temporal"
# 四要素架构
four_elements:
目标:
entities:
- 目标要素
原则:
entities:
- 原则要素
要素:
entities:
- 空间要素
- 功能要素
- 环境要素
管控:
entities:
- 管控要素
# 层级关系
hierarchical_relations:
- from: 目标
to: 原则
relation: 遵循
- from: 原则
to: 要素
relation: 遵循
- from: 要素
to: 管控
relation: 管控
@@ -0,0 +1,199 @@
# 系统架构文档
## 整体架构
本系统采用模块化设计,参考GraphRAG的实现思路,实现法规知识图谱的构建、分析和查询。
## 核心模块说明
### 1. 文本预处理模块 (`src/preprocessing/`)
**职责**:处理法规文档,为知识抽取做准备
- `TextProcessor`: 使用HanLP进行中文文本处理
- 分词(粗粒度/细粒度)
- 依存句法分析
- 句法成分分析
- 词性标注
- 省略检测
- 共指消解候选提取
- `DocumentParser`: 解析Word文档
- .docx文件解析
- 章节结构识别
- TextUnit切分(参考GraphRAG
### 2. 本体模型模块 (`src/ontology/`)
**职责**:定义知识图谱的语义结构
- `EntityType`: 9种实体类型枚举
- `RelationType`: 8种关系类型枚举
- `OntologySchema`: 三维本体结构管理
- 法规层级维度
- 要素类型维度
- 关联关系维度
- 四要素架构(目标—原则—要素—管控)
### 3. Prompt模板模块 (`src/prompts/`)
**职责**:构建统一的Prompt模板系统
- `PromptTemplate`: 统一模板框架
- 任务描述模块
- 候选目标模块
- 任务示例模块(少样本学习)
- 任务强调模块
- 二次对话模块
- `NERPromptBuilder`: NER任务Prompt构建
- `REPromptBuilder`: RE任务Prompt构建
### 4. 知识抽取模块 (`src/extraction/`)
**职责**:从文本中提取结构化知识
- `NERExtractor`: 命名实体识别
- 使用Qwen-Max模型
- 两阶段对话验证
- JSON格式输出
- `REExtractor`: 关系抽取
- 多模型并行(GPT-4o, Doubao-pro, GLM-4
- 二次对话验证
- 句法信息集成
- 上下文利用
- `TripletEvaluator`: 三元组评估
- LLM-as-a-Judge方法
- 成对排序
- 五维度评分(语义准确性、一致性、事实性、准确性、可理解性)
- 低质量三元组筛选
- `EntityRelationClassifier`: 实体关系分类
- RAG-based分类(使用向量检索)
- Prompt-based分类
- 粗粒度和细粒度分类
### 5. 知识图谱构建模块 (`src/kg_builder/`)
**职责**:构建和管理知识图谱(参考GraphRAG)
- `GraphIndexer`: 图谱索引器
- 文本切分为TextUnit
- 实体和关系提取
- 图构建
- 社区检测(Leiden算法)
- 社区摘要生成
- `KnowledgeGraph`: 图谱结构管理
- 基于NetworkX
- 三元组添加
- 路径查询
- 子图提取
- 保存/加载
### 6. 分析和推理模块 (`src/analysis/`)
**职责**:分析法规体系和识别规律
- `LegalStructureExtractor`: 法理结构提取
- 四要素提取(目标、原则、要素、管控)
- 跨法规引用关系提取
- 例外条款识别
- `LegalReasoner`: 逻辑推理
- 路径查询(多跳关系)
- 中心性分析
- 子图分析
- `EvolutionAnalyzer`: 演化规律识别
- 时间维度分析
- 关系演化追踪
- 网络演化分析
### 7. 查询系统模块 (`src/query/`)
**职责**:知识图谱查询和问答(参考GraphRAG)
- `GlobalSearcher`: 全局搜索
- 利用社区摘要
- 相关性评估
- 综合答案生成
- `LocalSearcher`: 局部搜索
- 实体邻居扩展
- 深度控制
- 子图提取
- `DriftSearcher`: DRIFT搜索
- 结合局部和全局信息
- 社区上下文整合
- 综合答案生成
### 8. 工具模块 (`src/utils/`)
**职责**:提供通用工具函数
- `Config`: 配置管理
- 环境变量加载
- API密钥管理
- 参数配置
- `LLMClient`: LLM客户端封装
- 多提供商支持
- 统一API接口
- 异步/同步支持
- 重试机制
- `file_utils`: 文件操作工具
## 数据流
```
法规文档(.docx)
DocumentParser (解析)
TextUnit列表
TextProcessor (预处理) + NERExtractor (实体识别)
实体列表
REExtractor (关系抽取) + TextProcessor (句法信息)
三元组列表
TripletEvaluator (评估筛选)
高质量三元组
GraphIndexer (图构建)
KnowledgeGraph
社区检测 + 摘要生成
完整知识图谱 + 社区信息
Query系统 (查询和问答)
```
## 技术栈
- **语言**: Python 3.12+
- **包管理**: uv
- **NLP**: HanLP
- **图处理**: NetworkX, python-igraph
- **LLM**: OpenAI, Anthropic, Qwen, Doubao, GLM
- **数据处理**: pandas, numpy
## 参考实现
- GraphRAG: https://msdocs.cn/graphrag/
- 论文方法: `officefile/paper.tex`
@@ -0,0 +1,109 @@
"""
使用硅基流动API的示例
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from src.utils.llm_client import LLMClient, LLMProvider
from src.utils.config import load_config
from src.extraction.ner import NERExtractor
def example_basic_usage():
"""基础使用示例"""
print("示例1: 基础API调用")
print("-" * 60)
config = load_config()
client = LLMClient(
provider=LLMProvider.SILICONFLOW,
model="Qwen/Qwen2.5-72B-Instruct", # 请根据实际情况修改
config=config
)
messages = [
{"role": "system", "content": "你是一个法规文本分析专家。"},
{"role": "user", "content": "请分析以下文本中的实体:城市更新需要遵循国土空间规划的要求。"}
]
response = client.chat(messages)
print(f"响应: {response}\n")
def example_ner_with_siliconflow():
"""使用硅基流动进行实体识别"""
print("示例2: 使用硅基流动进行实体识别")
print("-" * 60)
config = load_config()
# 注意:NERExtractor默认使用Qwen,我们可以修改为使用硅基流动
# 方法1: 直接创建NER提取器并替换LLM客户端
ner_extractor = NERExtractor(model="Qwen/Qwen2.5-72B-Instruct", config=config)
# 替换为硅基流动客户端
ner_extractor.llm_client = LLMClient(
provider=LLMProvider.SILICONFLOW,
model="Qwen/Qwen2.5-72B-Instruct", # 请根据实际情况修改
config=config
)
text = "城市更新应当遵循国土空间规划,坚持生态优先、绿色发展原则。"
entities = ner_extractor.extract(text)
print(f"输入文本: {text}")
print(f"识别到的实体: {entities}\n")
def example_multi_provider_comparison():
"""多提供商对比示例"""
print("示例3: 对比不同LLM提供商的响应")
print("-" * 60)
config = load_config()
messages = [
{"role": "user", "content": "什么是知识图谱?用一句话回答。"}
]
providers = [
(LLMProvider.SILICONFLOW, "Qwen/Qwen2.5-72B-Instruct"),
# 可以添加其他提供商进行对比
]
for provider, model in providers:
try:
client = LLMClient(provider=provider, model=model, config=config)
response = client.chat(messages)
print(f"{provider.value}: {response[:100]}...")
except Exception as e:
print(f"{provider.value}: 调用失败 - {e}")
if __name__ == "__main__":
print("=" * 60)
print("硅基流动API使用示例")
print("=" * 60)
print()
# 运行示例
try:
example_basic_usage()
except Exception as e:
print(f"示例1执行失败: {e}\n")
try:
example_ner_with_siliconflow()
except Exception as e:
print(f"示例2执行失败: {e}\n")
try:
example_multi_provider_comparison()
except Exception as e:
print(f"示例3执行失败: {e}\n")
+63
View File
@@ -0,0 +1,63 @@
[project]
name = "law-graph-kg"
version = "0.1.0"
description = "国土空间规划法规知识图谱构建与分析系统"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"hanlp>=2.1.0",
"openai>=1.0.0",
"anthropic>=0.18.0",
"dashscope>=1.17.0", # 阿里云Qwen API
"volcengine-python-sdk>=1.0.0", # 字节跳动Doubao API
"zhipuai>=2.0.0", # 智谱GLM API
"networkx>=3.2.0",
"python-igraph>=0.11.0",
"pyyaml>=6.0",
"pandas>=2.0.0",
"python-dotenv>=1.0.0",
"numpy>=1.24.0",
"tqdm>=4.66.0",
"aiohttp>=3.9.0",
"tenacity>=8.2.0", # 重试机制
"python-docx>=1.1.0", # Word文档解析
]
[project.optional-dependencies]
dev = [
"pytest>=7.4.0",
"pytest-asyncio>=0.21.0",
"black>=23.0.0",
"ruff>=0.1.0",
"mypy>=1.5.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src"]
[tool.uv]
dev-dependencies = [
"pytest>=7.4.0",
"pytest-asyncio>=0.21.0",
"black>=23.0.0",
"ruff>=0.1.0",
"mypy>=1.5.0",
]
[tool.black]
line-length = 100
target-version = ['py312']
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.mypy]
python_version = "3.12"
warn_return_any = true
warn_unused_configs = true
@@ -0,0 +1,126 @@
#!/usr/bin/env python
"""
知识图谱构建脚本
示例用法:python scripts/build_kg.py --data-dir ../data/1法律 --output ./output/kg.json
"""
import argparse
import json
import logging
import sys
from pathlib import Path
# 添加src到路径
sys.path.insert(0, str(Path(__file__).parent.parent))
from src.preprocessing.document_parser import DocumentParser
from src.kg_builder.indexer import GraphIndexer
from src.kg_builder.graph import KnowledgeGraph
from src.utils.config import Config, load_config
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def main():
parser = argparse.ArgumentParser(description="构建法规知识图谱")
parser.add_argument(
"--data-dir",
type=str,
required=True,
help="法规数据目录路径"
)
parser.add_argument(
"--output",
type=str,
default="output/kg.json",
help="输出文件路径"
)
parser.add_argument(
"--max-docs",
type=int,
default=None,
help="最大处理文档数(用于测试)"
)
parser.add_argument(
"--no-verification",
action="store_true",
help="不使用二次对话验证(加快速度)"
)
args = parser.parse_args()
# 加载配置
config = load_config()
# 确保输出目录存在
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
# 解析文档
logger.info(f"解析目录: {args.data_dir}")
parser_obj = DocumentParser()
docs = parser_obj.parse_directory(args.data_dir)
if args.max_docs:
docs = docs[:args.max_docs]
logger.info(f"找到 {len(docs)} 个文档")
# 切分为TextUnit
logger.info("切分文档为TextUnit...")
all_textunits = []
for doc in docs:
textunits = parser_obj.split_into_textunits(
doc,
max_length=config.MAX_TEXTUNIT_LENGTH
)
for tu in textunits:
tu["doc_id"] = doc.get("file_path", "")
tu["id"] = f"{doc.get('file_path', '')}_{tu.get('paragraph_index', 0)}"
all_textunits.extend(textunits)
logger.info(f"共生成 {len(all_textunits)} 个TextUnit")
# 构建知识图谱
logger.info("开始构建知识图谱...")
indexer = GraphIndexer(config=config)
kg_graph = indexer.index(
all_textunits,
use_verification=not args.no_verification
)
# 保存图谱
kg_obj = KnowledgeGraph()
kg_obj.graph = kg_graph
kg_obj.save(str(output_path), format="json")
# 保存统计信息
stats = {
"num_docs": len(docs),
"num_textunits": len(all_textunits),
"num_nodes": kg_graph.number_of_nodes(),
"num_edges": kg_graph.number_of_edges(),
"metadata": kg_graph.graph.get("metadata", {})
}
stats_path = output_path.parent / f"{output_path.stem}_stats.json"
with open(stats_path, "w", encoding="utf-8") as f:
json.dump(stats, f, ensure_ascii=False, indent=2)
logger.info(f"知识图谱构建完成!")
logger.info(f" - 节点数: {stats['num_nodes']}")
logger.info(f" - 边数: {stats['num_edges']}")
logger.info(f" - 结果已保存到: {output_path}")
logger.info(f" - 统计信息已保存到: {stats_path}")
if __name__ == "__main__":
main()
@@ -0,0 +1,137 @@
#!/usr/bin/env python
"""
完整流程脚本:从文档到知识图谱构建和查询
示例用法:python scripts/complete_pipeline.py --data-dir ../data/1法律 --output-dir ./output
"""
import argparse
import logging
import sys
from pathlib import Path
# 修复:移除conda环境的路径,确保使用虚拟环境的包
sys.path = [p for p in sys.path if 'anaconda3' not in p.lower() and 'conda' not in p.lower()]
# 添加src到路径
sys.path.insert(0, str(Path(__file__).parent.parent))
from src.preprocessing.document_parser import DocumentParser
from src.kg_builder.indexer import GraphIndexer
from src.kg_builder.graph import KnowledgeGraph
from src.utils.config import Config, load_config
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def main():
parser = argparse.ArgumentParser(description="完整的知识图谱构建流程")
parser.add_argument(
"--data-dir",
type=str,
required=True,
help="法规数据目录路径"
)
parser.add_argument(
"--output-dir",
type=str,
default="output",
help="输出目录"
)
parser.add_argument(
"--max-docs",
type=int,
default=None,
help="最大处理文档数(用于测试)"
)
parser.add_argument(
"--skip-index",
action="store_true",
help="跳过索引构建(使用已有索引)"
)
args = parser.parse_args()
# 加载配置
config = load_config()
# 创建输出目录
output_dir = Path(args.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
kg_file = output_dir / "knowledge_graph.json"
# 步骤1: 构建知识图谱索引
if not args.skip_index:
logger.info("=" * 60)
logger.info("步骤1: 解析文档并构建知识图谱")
logger.info("=" * 60)
# 解析文档
logger.info(f"解析目录: {args.data_dir}")
parser_obj = DocumentParser()
docs = parser_obj.parse_directory(args.data_dir)
if args.max_docs:
docs = docs[:args.max_docs]
logger.info(f"找到 {len(docs)} 个文档")
# 切分为TextUnit
logger.info("切分文档为TextUnit...")
all_textunits = []
for doc in docs:
textunits = parser_obj.split_into_textunits(
doc,
max_length=config.MAX_TEXTUNIT_LENGTH
)
for tu in textunits:
tu["doc_id"] = doc.get("file_path", "")
tu["id"] = f"{doc.get('file_path', '')}_{tu.get('paragraph_index', 0)}"
all_textunits.extend(textunits)
logger.info(f"共生成 {len(all_textunits)} 个TextUnit")
# 构建知识图谱
logger.info("开始构建知识图谱...")
indexer = GraphIndexer(config=config)
kg_graph = indexer.index(all_textunits, use_verification=True)
# 保存图谱
kg_obj = KnowledgeGraph()
kg_obj.graph = kg_graph
kg_obj.save(str(kg_file), format="json")
logger.info(f"知识图谱已保存到: {kg_file}")
else:
logger.info(f"跳过索引构建,从文件加载: {kg_file}")
kg_obj = KnowledgeGraph()
kg_obj.load(str(kg_file), format="json")
# 步骤2: 展示图谱统计信息
logger.info("=" * 60)
logger.info("步骤2: 知识图谱统计信息")
logger.info("=" * 60)
if kg_obj.graph:
metadata = kg_obj.graph.graph.get("metadata", {})
logger.info(f"节点数: {kg_obj.graph.number_of_nodes()}")
logger.info(f"边数: {kg_obj.graph.number_of_edges()}")
logger.info(f"三元组数: {metadata.get('num_triplets', 0)}")
logger.info(f"社区数: {metadata.get('num_communities', 0)}")
# 展示一些社区摘要
summaries = metadata.get("summaries", {})
if summaries:
logger.info("\n前5个社区摘要:")
for i, (comm_id, comm_data) in enumerate(list(summaries.items())[:5], 1):
logger.info(f"{i}. {comm_id}: {comm_data.get('summary', '无摘要')[:100]}...")
logger.info("\n知识图谱构建流程完成!")
if __name__ == "__main__":
main()
@@ -0,0 +1,96 @@
#!/usr/bin/env python
"""
知识抽取脚本
示例用法:python scripts/extract.py --input data/sample.txt --output output/entities.json
"""
import argparse
import json
import logging
import sys
from pathlib import Path
# 添加src到路径
sys.path.insert(0, str(Path(__file__).parent.parent))
from src.preprocessing.document_parser import DocumentParser
from src.extraction.ner import NERExtractor
from src.utils.config import Config, load_config
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def main():
parser = argparse.ArgumentParser(description="法规知识抽取工具")
parser.add_argument(
"--input",
type=str,
required=True,
help="输入文件路径(.docx或.txt"
)
parser.add_argument(
"--output",
type=str,
default="output/entities.json",
help="输出文件路径"
)
parser.add_argument(
"--use-verification",
action="store_true",
default=True,
help="使用二次对话验证"
)
args = parser.parse_args()
# 加载配置
config = load_config()
# 确保输出目录存在
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
# 解析文档
input_path = Path(args.input)
if input_path.suffix == ".docx":
parser = DocumentParser()
doc_data = parser.parse_docx(str(input_path))
text = doc_data["text"]
else:
# 假设是纯文本文件
with open(input_path, "r", encoding="utf-8") as f:
text = f.read()
# 执行NER
logger.info("开始实体识别...")
ner_extractor = NERExtractor(config=config)
entities = ner_extractor.extract(text, use_verification=args.use_verification)
# 保存结果
result = {
"input_file": str(input_path),
"entities": entities,
"statistics": {
entity_type: len(entity_list)
for entity_type, entity_list in entities.items()
}
}
with open(output_path, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
logger.info(f"实体识别完成,结果已保存到: {output_path}")
logger.info(f"统计信息: {result['statistics']}")
if __name__ == "__main__":
main()
@@ -0,0 +1,100 @@
#!/usr/bin/env python
"""
知识图谱查询脚本
示例用法:python scripts/query_kg.py --kg output/kg.json --query "城市更新"
"""
import argparse
import json
import logging
import sys
from pathlib import Path
# 添加src到路径
sys.path.insert(0, str(Path(__file__).parent.parent))
from src.kg_builder.graph import KnowledgeGraph
from src.query.global_search import GlobalSearcher
from src.query.local_search import LocalSearcher
from src.utils.config import load_config
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def main():
parser = argparse.ArgumentParser(description="查询知识图谱")
parser.add_argument(
"--kg",
type=str,
required=True,
help="知识图谱文件路径(JSON格式)"
)
parser.add_argument(
"--query",
type=str,
required=True,
help="查询文本"
)
parser.add_argument(
"--mode",
type=str,
choices=["global", "local", "drift"],
default="global",
help="查询模式"
)
parser.add_argument(
"--output",
type=str,
default=None,
help="输出结果文件路径"
)
args = parser.parse_args()
# 加载知识图谱
logger.info(f"加载知识图谱: {args.kg}")
kg = KnowledgeGraph()
kg.load(str(args.kg), format="json")
# 获取社区信息(从元数据)
communities = {}
if hasattr(kg, 'graph') and kg.graph:
metadata = kg.graph.graph.get("metadata", {})
communities = metadata.get("summaries", {})
# 执行查询
logger.info(f"执行{args.mode}查询: {args.query}")
kg_graph = kg.graph if hasattr(kg, 'graph') and kg.graph else None
if args.mode == "global":
searcher = GlobalSearcher(kg_graph, communities)
result = searcher.search(args.query)
elif args.mode == "local":
searcher = LocalSearcher(kg_graph)
# 尝试从查询中提取实体
entity = args.query # 简化处理,可以将查询作为实体
result = searcher.search(entity, depth=2)
else:
# DRIFT search
from src.query.drift_search import DriftSearcher
searcher = DriftSearcher(kg_graph, communities)
result = searcher.search(args.query)
# 输出结果
print("\n查询结果:")
print(json.dumps(result, ensure_ascii=False, indent=2))
if args.output:
with open(args.output, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
logger.info(f"结果已保存到: {args.output}")
if __name__ == "__main__":
main()
+10
View File
@@ -0,0 +1,10 @@
"""
国土空间规划法规知识图谱构建与分析系统
"""
__version__ = "0.1.0"
@@ -0,0 +1,12 @@
"""分析和推理模块"""
from .legal_structure import LegalStructureExtractor
from .reasoning import LegalReasoner
from .evolution import EvolutionAnalyzer
__all__ = ["LegalStructureExtractor", "LegalReasoner", "EvolutionAnalyzer"]
@@ -0,0 +1,54 @@
"""
演化规律识别模块
"""
import logging
from typing import List, Dict, Any
from datetime import datetime
logger = logging.getLogger(__name__)
class EvolutionAnalyzer:
"""演化规律分析器"""
def analyze_temporal_changes(
self,
regulations: List[Dict[str, Any]],
) -> Dict[str, Any]:
"""
时间维度分析:识别不同时期法规内容的变化
Args:
regulations: 法规列表,每个包含时间和内容
Returns:
演化分析结果
"""
# TODO: 实现时间序列分析
logger.info("执行时间维度演化分析...")
return {}
def track_relation_evolution(
self,
entity: str,
time_points: List[datetime],
) -> Dict[str, Any]:
"""
关系演化追踪
Args:
entity: 实体名称
time_points: 时间点列表
Returns:
演化模式
"""
# TODO: 实现关系演化追踪
logger.info(f"追踪实体 {entity} 的关系演化...")
return {}
@@ -0,0 +1,98 @@
"""
法理结构提取模块
"""
import logging
from typing import List, Dict, Any
import re
logger = logging.getLogger(__name__)
class LegalStructureExtractor:
"""法理结构提取器"""
def extract_four_elements(self, text: str) -> Dict[str, List[str]]:
"""
提取四要素:目标、原则、要素、管控
Args:
text: 法规文本
Returns:
四要素字典
"""
result = {
"目标": [],
"原则": [],
"要素": [],
"管控": []
}
# 目标要素识别("为了...""应当..."等)
target_patterns = [
r"为了[^,。]+",
r"应当[^,。]+",
r"旨在[^,。]+",
]
for pattern in target_patterns:
matches = re.findall(pattern, text)
result["目标"].extend(matches)
# 原则要素识别("遵循...""坚持..."等)
principle_patterns = [
r"遵循[^,。]+",
r"坚持[^,。]+",
r"按照[^,。]+",
]
for pattern in principle_patterns:
matches = re.findall(pattern, text)
result["原则"].extend(matches)
# 管控要素识别("禁止...""限制...""引导..."等)
control_patterns = [
r"禁止[^,。]+",
r"限制[^,。]+",
r"引导[^,。]+",
r"不得[^,。]+",
]
for pattern in control_patterns:
matches = re.findall(pattern, text)
result["管控"].extend(matches)
return result
def extract_cross_references(self, text: str) -> List[Dict[str, str]]:
"""
提取跨法规引用关系
Args:
text: 法规文本
Returns:
引用关系列表
"""
references = []
# 引用模式:"依据...""按照..."等
patterns = [
(r"依据《([^》]+)》", "依据"),
(r"按照《([^》]+)》", "按照"),
(r"参照《([^》]+)》", "参照"),
]
for pattern, relation_type in patterns:
matches = re.finditer(pattern, text)
for match in matches:
references.append({
"cited_document": match.group(1),
"relation": relation_type,
"position": match.start()
})
return references
@@ -0,0 +1,59 @@
"""
逻辑推理模块
"""
import logging
from typing import List, Dict, Any
import networkx as nx
from ..kg_builder.graph import KnowledgeGraph
logger = logging.getLogger(__name__)
class LegalReasoner:
"""法规逻辑推理器"""
def __init__(self, kg: KnowledgeGraph):
"""
初始化推理器
Args:
kg: 知识图谱
"""
self.kg = kg
def path_query(self, source: str, target: str, max_hops: int = 3) -> List:
"""
路径查询:发现实体间的多跳关系
Args:
source: 源实体
target: 目标实体
max_hops: 最大跳数
Returns:
路径列表
"""
return self.kg.query_path(source, target, max_hops)
def centrality_analysis(self) -> Dict[str, Any]:
"""
中心性分析:识别关键节点
Returns:
中心性分析结果
"""
graph = self.kg.graph
degree_centrality = nx.degree_centrality(graph)
betweenness_centrality = nx.betweenness_centrality(graph)
return {
"degree": degree_centrality,
"betweenness": betweenness_centrality
}
@@ -0,0 +1,18 @@
"""知识抽取模块"""
from .ner import NERExtractor
from .re import REExtractor
from .evaluator import TripletEvaluator
from .classifier import EntityRelationClassifier
__all__ = [
"NERExtractor",
"REExtractor",
"TripletEvaluator",
"EntityRelationClassifier",
]
@@ -0,0 +1,325 @@
"""
实体关系分类模块
使用RAG-based方法进行分类
"""
import logging
import numpy as np
from typing import List, Dict, Any, Optional
from collections import defaultdict
from ..utils.config import Config
from ..utils.llm_client import LLMClient, LLMProvider
from ..ontology.entities import get_four_element_types
logger = logging.getLogger(__name__)
class EntityRelationClassifier:
"""实体关系分类器"""
def __init__(self, config: Optional[Config] = None):
"""
初始化分类器
Args:
config: 配置对象
"""
self.config = config or Config()
self.embedding_model = None
self.knowledge_base = {} # 向量数据库(简化版)
self.four_elements = get_four_element_types()
def _get_embedding(self, text: str) -> Optional[np.ndarray]:
"""
获取文本嵌入向量
Args:
text: 输入文本
Returns:
嵌入向量
"""
try:
# 使用OpenAI的embedding模型
import openai
client = openai.OpenAI(api_key=self.config.OPENAI_API_KEY)
response = client.embeddings.create(
model="text-embedding-3-large",
input=text
)
return np.array(response.data[0].embedding)
except Exception as e:
logger.warning(f"获取嵌入向量失败: {e}")
return None
def _cosine_similarity(self, vec_a: np.ndarray, vec_b: np.ndarray) -> float:
"""计算余弦相似度"""
dot_product = np.dot(vec_a, vec_b)
norm_a = np.linalg.norm(vec_a)
norm_b = np.linalg.norm(vec_b)
if norm_a == 0 or norm_b == 0:
return 0.0
return dot_product / (norm_a * norm_b)
def build_knowledge_base(
self,
labeled_entities: List[Dict[str, Any]],
):
"""
构建RAG知识库(使用已标注的实体样本)
Args:
labeled_entities: 已标注实体列表,格式:[{"text": "实体", "label": "标签", ...}, ...]
"""
logger.info(f"构建知识库,包含 {len(labeled_entities)} 个样本...")
self.knowledge_base = defaultdict(list)
for entity in labeled_entities:
text = entity.get("text", "")
label = entity.get("label", "")
if not text or not label:
continue
# 获取嵌入向量
embedding = self._get_embedding(text)
if embedding is not None:
self.knowledge_base[label].append({
"text": text,
"embedding": embedding,
"metadata": entity
})
logger.info(f"知识库构建完成,包含 {len(self.knowledge_base)} 个类别")
def classify_entities_rag(
self,
entities: List[str],
top_k: int = 5,
) -> List[Dict[str, str]]:
"""
使用RAG方法对实体进行粗粒度分类
Args:
entities: 实体文本列表
top_k: 检索的top-k相似样本数
Returns:
分类结果列表,格式:[{"entity": "...", "label": "...", "confidence": ...}, ...]
"""
if not self.knowledge_base:
logger.warning("知识库为空,无法进行RAG分类")
return [{"entity": e, "label": "未知", "confidence": 0.0} for e in entities]
results = []
for entity_text in entities:
# 获取实体嵌入
entity_embedding = self._get_embedding(entity_text)
if entity_embedding is None:
results.append({
"entity": entity_text,
"label": "未知",
"confidence": 0.0
})
continue
# 在所有类别中查找最相似的样本
best_match = None
best_similarity = -1.0
for label, samples in self.knowledge_base.items():
for sample in samples:
similarity = self._cosine_similarity(
entity_embedding,
sample["embedding"]
)
if similarity > best_similarity:
best_similarity = similarity
best_match = label
results.append({
"entity": entity_text,
"label": best_match or "未知",
"confidence": best_similarity
})
return results
def classify_entities_prompt(
self,
entities: List[str],
categories: List[str] = None,
) -> List[Dict[str, str]]:
"""
使用Prompt方法对实体进行分类
Args:
entities: 实体文本列表
categories: 分类类别列表,默认使用四要素类型
Returns:
分类结果列表
"""
if categories is None:
categories = list(self.four_elements.keys())
try:
llm_client = LLMClient(
provider=LLMProvider.QWEN,
model="qwen-max",
config=self.config
)
except Exception as e:
logger.error(f"LLM客户端初始化失败: {e}")
return []
results = []
for entity_text in entities:
prompt = f"""请将以下实体分类到以下类别之一:
类别:
{chr(10).join([f'- {cat}' for cat in categories])}
实体:{entity_text}
请只回答类别名称:"""
messages = [
{"role": "system", "content": "你是一个城市规划和法律文本分类专家。"},
{"role": "user", "content": prompt}
]
try:
label = llm_client.chat(messages).strip()
# 验证标签是否在有效类别中
if label not in categories:
label = "未知"
results.append({
"entity": entity_text,
"label": label,
"confidence": 1.0
})
except Exception as e:
logger.warning(f"实体分类失败 {entity_text}: {e}")
results.append({
"entity": entity_text,
"label": "未知",
"confidence": 0.0
})
return results
def classify_entities(
self,
entities: List[str],
method: str = "rag",
) -> List[Dict[str, str]]:
"""
对实体进行粗粒度分类(目标、原则、要素、管控)
Args:
entities: 实体文本列表
method: 分类方法("rag""prompt"
Returns:
分类结果列表
"""
logger.info(f"使用 {method} 方法对 {len(entities)} 个实体进行分类...")
if method == "rag":
if not self.knowledge_base:
logger.warning("RAG知识库为空,切换到prompt方法")
method = "prompt"
else:
return self.classify_entities_rag(entities)
if method == "prompt":
return self.classify_entities_prompt(entities)
return []
def classify_relations(
self,
relations: List[str],
) -> List[Dict[str, str]]:
"""
对关系进行分类
Args:
relations: 关系文本列表
Returns:
分类结果列表
"""
logger.info(f"{len(relations)} 个关系进行分类...")
# 关系分类类别(对应论文Table 5)
relation_categories = {
"引用": "一条法规条文明确引用另一条法规条文",
"层级": "上下级法规之间的隶属关系",
"包含": "一个实体在逻辑上包含另一个实体",
"管控": "管控要素对目标要素、原则要素或其他要素的约束关系",
"遵循": "要素遵循某种原则或目标的关系",
"涉及": "法规条文涉及某个空间、功能或环境要素",
"适用": "法规适用于某个行政区域",
"演化": "法规修订前后的变化关系",
}
try:
llm_client = LLMClient(
provider=LLMProvider.QWEN,
model="qwen-max",
config=self.config
)
except Exception as e:
logger.error(f"LLM客户端初始化失败: {e}")
return []
results = []
for relation_text in relations:
categories_desc = "\n".join([
f"- {cat}: {desc}" for cat, desc in relation_categories.items()
])
prompt = f"""请将以下关系分类到以下类别之一:
类别定义:
{categories_desc}
关系:{relation_text}
请只回答类别名称:"""
messages = [
{"role": "system", "content": "你是一个法规关系分类专家。"},
{"role": "user", "content": prompt}
]
try:
label = llm_client.chat(messages).strip()
# 验证标签
if label not in relation_categories:
label = "未知"
results.append({
"relation": relation_text,
"label": label,
"confidence": 1.0
})
except Exception as e:
logger.warning(f"关系分类失败 {relation_text}: {e}")
results.append({
"relation": relation_text,
"label": "未知",
"confidence": 0.0
})
return results
@@ -0,0 +1,322 @@
"""
三元组评估模块
使用LLM-as-a-Judge方法评估三元组质量
实现成对排序和多维度评分(五维度:语义准确性、一致性、事实性、准确性、可理解性)
"""
import json
import logging
import random
from typing import List, Dict, Any, Optional, Tuple
from ..utils.llm_client import LLMClient, LLMProvider
from ..utils.config import Config
logger = logging.getLogger(__name__)
# 评估维度定义(对应论文Table 4)
EVALUATION_DIMENSIONS = {
"SEM": "语义准确性(Semantically): 三元组是否准确表示或涵盖了源文本的语义?",
"CON": "一致性(Consistency): 三元组提供的信息是否与源文本的逻辑一致?",
"FAC": "事实性(Factuality): 三元组是否保留了源文本的事实陈述?",
"ACC": "准确性(Accuracy): 三元组中是否有不准确、遗漏或错误的信息?",
"UND": "可理解性(Understandability): 三元组是否对非专业人员来说可理解和可解释?",
}
class TripletEvaluator:
"""三元组评估器(LLM-as-a-Judge"""
def __init__(
self,
judge_models: Optional[List[Tuple[str, str]]] = None,
config: Optional[Config] = None,
):
"""
初始化三元组评估器
Args:
judge_models: 评估模型列表,格式[(provider, model), ...]
config: 配置对象
"""
self.config = config or Config()
# 默认使用GPT-4o作为评估者
if judge_models is None:
judge_models = [
("openai", "gpt-4o"),
]
self.judge_clients = []
for provider_str, model in judge_models:
provider = LLMProvider(provider_str)
self.judge_clients.append(LLMClient(provider, model, self.config))
def pairwise_ranking(
self,
triplet_group_a: List[Dict[str, str]],
triplet_group_b: List[Dict[str, str]],
source_text: str,
judge_index: int = 0,
) -> str:
"""
成对排序:比较两个三元组组,确定哪个更优
Args:
triplet_group_a: 三元组组A
triplet_group_b: 三元组组B
source_text: 源文本
judge_index: 使用的评估者索引
Returns:
"A""B",表示哪个组更优
"""
judge = self.judge_clients[judge_index]
# 构建评估提示
prompt = f"""请比较两个三元组组,确定哪个更准确地反映了源文本的语义。
源文本:
{source_text}
三元组组A
{json.dumps(triplet_group_a, ensure_ascii=False, indent=2)}
三元组组B
{json.dumps(triplet_group_b, ensure_ascii=False, indent=2)}
请从以下维度进行比较:
1. 语义准确性:是否准确表示源文本的语义
2. 完整性:是否涵盖了源文本中的关键信息
3. 正确性:三元组之间的关系是否正确
请只回答"A""B",表示哪个组更优。如果质量相当,请回答"TIE"(平局)。"""
messages = [
{"role": "system", "content": "你是一个知识图谱质量评估专家,擅长评估三元组的质量。"},
{"role": "user", "content": prompt}
]
try:
result = judge.chat(messages)
result = result.strip().upper()
if result in ["A", "B", "TIE"]:
return result
else:
# 尝试从结果中提取
if "A" in result:
return "A"
elif "B" in result:
return "B"
else:
return "TIE"
except Exception as e:
logger.error(f"成对排序失败: {e}")
return "TIE"
def multi_dimensional_scoring(
self,
triplets: List[Dict[str, str]],
source_text: str,
judge_index: int = 0,
) -> Dict[str, List[int]]:
"""
多维度评分:从五个维度对每个三元组进行评分(1-5分)
Args:
triplets: 三元组列表
source_text: 源文本
judge_index: 使用的评估者索引
Returns:
评分字典,格式:{维度: [分数列表]}
"""
judge = self.judge_clients[judge_index]
# 为每个三元组构建评估
scores = {dim: [] for dim in EVALUATION_DIMENSIONS.keys()}
for triplet in triplets:
triplet_str = f"<{triplet.get('head', '')}, {triplet.get('relation', '')}, {triplet.get('tail', '')}>"
prompt = f"""请从以下五个维度评估这个三元组的质量,每个维度给出1-5分的评分(5分最高,1分最低)。
源文本:
{source_text}
三元组:
{triplet_str}
评估维度:
{chr(10).join([f'{dim}: {desc}' for dim, desc in EVALUATION_DIMENSIONS.items()])}
请以JSON格式输出评分:
{{
"SEM": <分数1-5>,
"CON": <分数1-5>,
"FAC": <分数1-5>,
"ACC": <分数1-5>,
"UND": <分数1-5>
}}"""
messages = [
{"role": "system", "content": "你是一个知识图谱质量评估专家。"},
{"role": "user", "content": prompt}
]
try:
result = judge.chat(messages)
# 尝试解析JSON
try:
triplet_scores = json.loads(result)
for dim in EVALUATION_DIMENSIONS.keys():
score = triplet_scores.get(dim, 3) # 默认3分
# 确保分数在1-5范围内
score = max(1, min(5, int(score)))
scores[dim].append(score)
except json.JSONDecodeError:
# 如果解析失败,使用默认分数
logger.warning(f"无法解析评分结果,使用默认分数: {result}")
for dim in EVALUATION_DIMENSIONS.keys():
scores[dim].append(3)
except Exception as e:
logger.error(f"评分失败: {e}")
for dim in EVALUATION_DIMENSIONS.keys():
scores[dim].append(3)
return scores
def evaluate_peer_groups(
self,
peer_groups: Dict[str, List[Dict[str, str]]],
source_text: str,
) -> Dict[str, Any]:
"""
评估多个模型生成的三元组组(Peer Examination
Args:
peer_groups: 多个模型的结果,格式:{"model_a": [triplets], "model_b": [triplets], ...}
source_text: 源文本
Returns:
评估结果,包含排序和评分
"""
model_names = list(peer_groups.keys())
# 随机打乱顺序以降低位置偏差
shuffled_pairs = []
for i, model_a in enumerate(model_names):
for model_b in model_names[i+1:]:
shuffled_pairs.append((model_a, model_b))
random.shuffle(shuffled_pairs)
# 成对排序
rankings = {}
for model_a, model_b in shuffled_pairs:
# 每个评估者只评估其他模型的结果,避免自我增强偏差
for judge_idx in range(len(self.judge_clients)):
result = self.pairwise_ranking(
peer_groups[model_a],
peer_groups[model_b],
source_text,
judge_idx
)
key = f"{model_a}_vs_{model_b}"
if key not in rankings:
rankings[key] = []
rankings[key].append(result)
# 确定最佳组
best_group = self._determine_best_group(rankings, model_names)
# 对最佳组进行多维度评分
if best_group:
scores = self.multi_dimensional_scoring(
peer_groups[best_group],
source_text
)
else:
scores = {}
return {
"rankings": rankings,
"best_group": best_group,
"scores": scores,
"selected_triplets": peer_groups.get(best_group, [])
}
def _determine_best_group(
self,
rankings: Dict[str, List[str]],
model_names: List[str],
) -> Optional[str]:
"""
根据排序结果确定最佳组
Args:
rankings: 排序结果
model_names: 模型名称列表
Returns:
最佳模型名称
"""
wins = {model: 0 for model in model_names}
for pair_key, results in rankings.items():
# 统计每个结果
counts = {"A": 0, "B": 0, "TIE": 0}
for result in results:
if result in counts:
counts[result] += 1
# 解析模型名称
parts = pair_key.split("_vs_")
if len(parts) == 2:
model_a, model_b = parts
if counts["A"] > counts["B"]:
wins[model_a] += 1
elif counts["B"] > counts["A"]:
wins[model_b] += 1
# 返回获胜次数最多的模型
if wins:
best_model = max(wins.items(), key=lambda x: x[1])
return best_model[0] if best_model[1] > 0 else model_names[0]
return model_names[0] if model_names else None
def filter_low_quality_triplets(
self,
triplets: List[Dict[str, str]],
scores: Dict[str, List[int]],
min_score: int = 2,
) -> List[Dict[str, str]]:
"""
根据评分筛选低质量三元组
Args:
triplets: 三元组列表
scores: 评分字典
min_score: 最低分数阈值
Returns:
筛选后的三元组列表
"""
filtered = []
for i, triplet in enumerate(triplets):
# 计算平均分
avg_score = 0
count = 0
for dim_scores in scores.values():
if i < len(dim_scores):
avg_score += dim_scores[i]
count += 1
if count > 0:
avg_score /= count
# 只保留平均分 >= min_score的三元组
if avg_score >= min_score:
filtered.append(triplet)
return filtered
@@ -0,0 +1,178 @@
"""
命名实体识别模块
使用Qwen-Max进行法规实体识别(两阶段对话)
"""
import json
import logging
from typing import List, Dict, Any, Optional
from ..prompts.ner_prompts import NERPromptBuilder
from ..utils.llm_client import LLMClient, LLMProvider
from ..utils.config import Config
logger = logging.getLogger(__name__)
class NERExtractor:
"""命名实体识别器"""
def __init__(
self,
model: Optional[str] = None,
config: Optional[Config] = None,
):
"""
初始化NER提取器
Args:
model: 模型名称(如果为None,会根据配置自动选择)
config: 配置对象
"""
self.config = config or Config()
# 优先使用硅基流动,如果API密钥未配置则尝试Qwen
if self.config.SILICONFLOW_API_KEY:
provider = LLMProvider.SILICONFLOW
if model is None or model == "qwen-max": # 如果使用默认值,改为硅基流动模型
# 使用指定的模型
model = "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B"
elif self.config.DASHSCOPE_API_KEY:
provider = LLMProvider.QWEN
if model is None:
model = "qwen-max"
else:
raise ValueError("请配置SILICONFLOW_API_KEY或DASHSCOPE_API_KEY")
self.llm_client = LLMClient(
provider=provider,
model=model,
config=self.config
)
self.prompt_builder = NERPromptBuilder()
def extract(
self,
text: str,
use_verification: bool = True,
) -> Dict[str, List[str]]:
"""
从文本中提取实体(两阶段对话)
Args:
text: 输入文本
use_verification: 是否使用二次对话验证
Returns:
实体字典,格式:{实体类型: [实体列表]}
"""
# 获取Prompt模板
template = self.prompt_builder.build_template()
# 构建第一阶段对话
system_msg = template.task_description["system"]
user_msg = template.task_description["user"]
# 构建完整的第一阶段消息
full_user_msg = user_msg
# 添加候选目标
if template.candidate_targets:
full_user_msg += "\n\n" + template.build_candidate_targets_section()
# 添加任务示例
if template.task_examples:
full_user_msg += "\n\n" + template.build_task_examples_section()
# 添加任务强调
if template.task_emphasis:
full_user_msg += "\n\n" + template.build_task_emphasis_section()
# 添加输入数据
full_user_msg += f"\n\n输入数据:\n{text}"
messages = [
{"role": "system", "content": system_msg},
{"role": "user", "content": full_user_msg}
]
# 第一阶段:初始提取
logger.info("开始第一阶段实体识别...")
first_result = self.llm_client.chat(messages)
# 解析第一阶段结果
try:
first_entities = json.loads(first_result)
except json.JSONDecodeError:
logger.warning("第一阶段结果不是有效JSON,尝试修复...")
first_entities = self._try_parse_json(first_result)
# 第二阶段:验证和优化
if use_verification:
logger.info("开始第二阶段实体验证...")
verification_prompt = self.prompt_builder.build_verification_prompt()
# 移除assistant消息,将第一阶段结果整合到user消息中(符合硅基流动API要求)
second_messages = [
{"role": "system", "content": "你需要验证和优化之前的结果。请仔细检查结果的正确性和完整性。"},
{"role": "user", "content": f"{verification_prompt}\n\n原始输入:\n{text}\n\n第一阶段结果:\n{first_result}\n\n请验证并优化这个结果,确保输出为有效的JSON格式。"}
]
second_result = self.llm_client.chat(second_messages)
try:
final_entities = json.loads(second_result)
except json.JSONDecodeError:
logger.warning("第二阶段结果不是有效JSON,使用第一阶段结果")
final_entities = first_entities
else:
final_entities = first_entities
return final_entities
def _try_parse_json(self, text: str) -> Dict[str, List[str]]:
"""尝试从文本中提取JSON(支持markdown代码块格式)"""
import re
# 方法1: 尝试从markdown代码块中提取(```json ... ```
json_block_match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', text, re.DOTALL)
if json_block_match:
try:
json_str = json_block_match.group(1).strip()
return json.loads(json_str)
except json.JSONDecodeError:
pass
# 方法2: 尝试找到完整的JSON对象(处理嵌套花括号,从后往前找最后一个完整的JSON)
# 从后往前找到最后一个完整的JSON对象
brace_count = 0
end_idx = -1
for i in range(len(text) - 1, -1, -1):
char = text[i]
if char == '}':
if end_idx == -1:
end_idx = i
brace_count += 1
elif char == '{':
brace_count -= 1
if brace_count == 0 and end_idx != -1:
try:
json_str = text[i:end_idx+1]
return json.loads(json_str)
except json.JSONDecodeError:
end_idx = -1
continue
# 方法3: 简单的正则匹配(作为fallback)
json_match = re.search(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', text, re.DOTALL)
if json_match:
try:
return json.loads(json_match.group())
except json.JSONDecodeError:
pass
# 如果都失败,返回空字典
logger.error(f"无法解析JSON结果。原始输出前200字符: {text[:200]}")
return {}
@@ -0,0 +1,236 @@
"""
关系抽取模块
使用多个LLMGPT-4o, Doubao-pro, GLM-4)并行进行关系抽取
"""
import json
import logging
from typing import List, Dict, Any, Optional
from ..prompts.re_prompts import REPromptBuilder
from ..utils.llm_client import LLMClient, LLMProvider
from ..utils.config import Config
logger = logging.getLogger(__name__)
class REExtractor:
"""关系抽取器"""
def __init__(
self,
models: Optional[List[str]] = None,
config: Optional[Config] = None,
):
"""
初始化RE提取器
Args:
models: 使用的模型列表,默认使用多个模型并行
config: 配置对象
"""
self.config = config or Config()
# 默认使用多个模型(优先使用硅基流动)
if models is None:
models = []
# 优先使用硅基流动(使用两个模型)
if self.config.SILICONFLOW_API_KEY:
models.append(("siliconflow", "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B"))
models.append(("siliconflow", "Qwen/Qwen2.5-7B-Instruct"))
# 如果有其他API密钥,也添加
if self.config.OPENAI_API_KEY:
models.append(("openai", "gpt-4o"))
if self.config.VOLCENGINE_ACCESS_KEY and self.config.VOLCENGINE_SECRET_KEY:
models.append(("doubao", "doubao-pro-32k"))
if self.config.ZHIPUAI_API_KEY:
models.append(("glm", "glm-4-airx"))
# 如果都没有配置,至少使用硅基流动(即使没有密钥也会报错)
if not models:
models = [
("siliconflow", "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B"),
("siliconflow", "Qwen/Qwen2.5-7B-Instruct")
]
self.clients = []
for provider_str, model in models:
try:
provider = LLMProvider(provider_str)
self.clients.append(LLMClient(provider, model, self.config))
except Exception as e:
logger.warning(f"初始化{provider_str}客户端失败: {e},跳过该模型")
self.prompt_builder = REPromptBuilder()
def extract(
self,
sentence: str,
entities: List[Dict[str, str]],
syntax_info: Dict = None,
context: str = None,
use_verification: bool = True,
) -> List[Dict[str, Any]]:
"""
从句子中提取关系(多模型并行,支持二次对话验证)
Args:
sentence: 输入句子
entities: 已识别的实体列表,格式:[{"text": "实体文本", "type": "实体类型"}, ...]
syntax_info: 句法分析结果
context: 上下文段落
use_verification: 是否使用二次对话验证
Returns:
结果列表,每个包含模型名称和三元组列表
"""
# 格式化实体列表(用于Prompt
formatted_entities = self._format_entities_for_prompt(entities)
# 格式化句法信息
formatted_syntax = self._format_syntax_info(syntax_info) if syntax_info else "无句法信息"
# 构建Prompt模板
template = self.prompt_builder.build_template(
entities=entities,
syntax_info=formatted_syntax,
context=context or ""
)
# 构建第一阶段消息
system_msg = template.task_description["system"]
user_msg = template.task_description["user"]
# 构建完整的用户消息
input_data = f"""已识别实体:
{formatted_entities}
句子文本:
{sentence}
句法分析结果:
{formatted_syntax}
上下文段落:
{context or "无上下文"}"""
full_user_msg = user_msg
if template.candidate_targets:
full_user_msg += "\n\n" + template.build_candidate_targets_section()
if template.task_examples:
full_user_msg += "\n\n" + template.build_task_examples_section()
if template.task_emphasis:
full_user_msg += "\n\n" + template.build_task_emphasis_section()
full_user_msg += f"\n\n输入数据:\n{input_data}"
messages = [
{"role": "system", "content": system_msg},
{"role": "user", "content": full_user_msg}
]
# 并行调用多个模型
results = []
for i, client in enumerate(self.clients):
try:
model_name = f"model_{i+1}"
logger.info(f"使用模型 {model_name} 进行关系抽取...")
# 第一阶段:初始提取
first_result = client.chat(messages)
# 解析第一阶段结果
first_triplets = self._parse_triplets(first_result)
# 第二阶段:验证和优化
if use_verification and first_triplets:
verification_prompt = self.prompt_builder.build_verification_prompt()
# 移除assistant消息,将第一阶段结果整合到user消息中(符合硅基流动API要求)
second_messages = [
{"role": "system", "content": "你需要验证和优化之前提取的三元组结果。请特别关注语义逻辑的正确性、遗漏关系的补全,以及利用上下文补全省略的成分。"},
{"role": "user", "content": f"{verification_prompt}\n\n原始输入:\n{input_data}\n\n第一阶段结果:\n{first_result}\n\n请验证并优化这个结果,确保输出为有效的JSON格式。"}
]
second_result = client.chat(second_messages)
final_triplets = self._parse_triplets(second_result)
else:
final_triplets = first_triplets
results.append({
"model": model_name,
"triplets": final_triplets,
"first_triplets": first_triplets,
})
except Exception as e:
logger.error(f"模型 {i+1} 调用失败: {e}")
results.append({
"model": f"model_{i+1}",
"triplets": [],
"error": str(e)
})
return results
def _format_entities_for_prompt(self, entities: List[Dict[str, str]]) -> str:
"""格式化实体列表用于Prompt"""
if not entities:
return "无识别实体"
lines = []
for entity in entities:
entity_type = entity.get("type", "未知类型")
entity_text = entity.get("text", "")
lines.append(f"- {entity_type}: {entity_text}")
return "\n".join(lines)
def _format_syntax_info(self, syntax_info: Dict) -> str:
"""格式化句法信息"""
if not syntax_info:
return "无句法信息"
formatted = []
if "dependency" in syntax_info:
deps = syntax_info["dependency"]
formatted.append("依存关系:")
for dep in deps[:5]: # 只显示前5个
formatted.append(f" {dep.get('word', '')} <-{dep.get('deprel', '')}- {dep.get('head', '')}")
return "\n".join(formatted)
def _parse_triplets(self, result: str) -> List[Dict[str, str]]:
"""解析三元组结果"""
# 尝试解析JSON
try:
triplets = json.loads(result)
if isinstance(triplets, list):
# 验证三元组格式
valid_triplets = []
for t in triplets:
if isinstance(t, dict) and "head" in t and "relation" in t and "tail" in t:
valid_triplets.append({
"head": str(t.get("head", "")),
"relation": str(t.get("relation", "")),
"tail": str(t.get("tail", ""))
})
return valid_triplets
elif isinstance(triplets, dict):
# 可能是嵌套结构,尝试提取
return []
return []
except json.JSONDecodeError:
# 尝试从文本中提取JSON
import re
json_match = re.search(r'\[.*\]', result, re.DOTALL)
if json_match:
try:
triplets = json.loads(json_match.group())
if isinstance(triplets, list):
return triplets
except:
pass
logger.warning(f"无法解析三元组结果: {result[:100]}")
return []
@@ -0,0 +1,11 @@
"""知识图谱构建模块(参考GraphRAG)"""
from .indexer import GraphIndexer
from .graph import KnowledgeGraph
__all__ = ["GraphIndexer", "KnowledgeGraph"]
@@ -0,0 +1,129 @@
"""
知识图谱结构管理
"""
import networkx as nx
import logging
from typing import List, Dict, Any, Optional
import json
logger = logging.getLogger(__name__)
class KnowledgeGraph:
"""知识图谱类"""
def __init__(self):
"""初始化知识图谱"""
self.graph = nx.DiGraph()
def add_triplet(
self,
head: str,
relation: str,
tail: str,
metadata: Optional[Dict] = None,
):
"""
添加三元组到图谱
Args:
head: 头实体
relation: 关系
tail: 尾实体
metadata: 元数据
"""
if not self.graph.has_node(head):
self.graph.add_node(head, type="entity")
if not self.graph.has_node(tail):
self.graph.add_node(tail, type="entity")
self.graph.add_edge(head, tail, relation=relation, **(metadata or {}))
def query_path(self, source: str, target: str, max_hops: int = 3) -> List:
"""查询路径"""
try:
paths = list(nx.all_simple_paths(
self.graph, source, target, cutoff=max_hops
))
return paths
except nx.NodeNotFound:
return []
def get_subgraph(self, nodes: List[str]) -> nx.DiGraph:
"""获取子图"""
return self.graph.subgraph(nodes)
def save(self, filepath: str, format: str = "json"):
"""
保存图谱
Args:
filepath: 文件路径
format: 保存格式("json""graphml"
"""
if format == "json":
data = {
"nodes": [
{"id": str(node), **{k: v for k, v in attrs.items()}}
for node, attrs in self.graph.nodes(data=True)
],
"edges": [
{
"source": str(u),
"target": str(v),
**{k: v for k, v in attrs.items()}
}
for u, v, attrs in self.graph.edges(data=True)
],
"metadata": self.graph.graph.get("metadata", {})
}
with open(filepath, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
elif format == "graphml":
try:
nx.write_graphml(self.graph, filepath)
except Exception as e:
logger.error(f"保存GraphML失败: {e}")
def load(self, filepath: str, format: str = "json"):
"""
从文件加载图谱
Args:
filepath: 文件路径
format: 文件格式("json""graphml"
"""
if format == "json":
with open(filepath, "r", encoding="utf-8") as f:
data = json.load(f)
self.graph.clear()
# 加载节点
for node_data in data.get("nodes", []):
node_id = node_data.pop("id", "")
if node_id:
self.graph.add_node(node_id, **node_data)
# 加载边
for edge_data in data.get("edges", []):
source = edge_data.pop("source", "")
target = edge_data.pop("target", "")
if source and target:
self.graph.add_edge(source, target, **edge_data)
# 加载元数据
if "metadata" in data:
self.graph.graph["metadata"] = data["metadata"]
logger.info(f"图谱加载完成:{self.graph.number_of_nodes()} 个节点,{self.graph.number_of_edges()} 条边")
elif format == "graphml":
try:
self.graph = nx.read_graphml(filepath)
except Exception as e:
logger.error(f"加载GraphML失败: {e}")
raise
@@ -0,0 +1,361 @@
"""
知识图谱索引器(参考GraphRAG
实现索引阶段:文本切分、实体提取、图构建、社区分析、摘要生成
"""
import logging
from typing import List, Dict, Any, Optional
import networkx as nx
from ..extraction.ner import NERExtractor
from ..extraction.re import REExtractor
from ..utils.config import Config
logger = logging.getLogger(__name__)
class GraphIndexer:
"""
知识图谱索引器
参考GraphRAG的实现思路
"""
def __init__(self, config: Optional[Config] = None):
"""
初始化索引器
Args:
config: 配置对象
"""
self.config = config or Config()
self.ner_extractor = NERExtractor(config=config)
self.re_extractor = REExtractor(config=config)
self.graph = nx.DiGraph()
def index(
self,
textunits: List[Dict[str, Any]],
use_verification: bool = True,
) -> nx.DiGraph:
"""
构建知识图谱索引(参考GraphRAG流程)
Args:
textunits: TextUnit列表
use_verification: 是否使用二次对话验证
Returns:
构建的知识图谱
"""
logger.info(f"开始索引 {len(textunits)} 个TextUnit...")
# 步骤1: 提取实体和关系(从每个TextUnit)
all_triplets = []
all_entities = {}
from ..preprocessing.text_processor import TextProcessor
text_processor = TextProcessor(self.config.HANLP_MODEL_PATH)
for i, textunit in enumerate(textunits):
text = textunit.get("text", "")
textunit_id = textunit.get("id", f"textunit_{i}")
if not text.strip():
continue
logger.info(f"处理TextUnit {i+1}/{len(textunits)}: {textunit_id}")
# 1.1 NER:提取实体
entities_result = self.ner_extractor.extract(text, use_verification=use_verification)
# 转换实体格式为RE需要的格式
entity_list = []
for entity_type, entity_texts in entities_result.items():
for entity_text in entity_texts:
if entity_text: # 过滤空字符串
entity_list.append({
"text": entity_text,
"type": entity_type
})
all_entities[textunit_id] = entity_list
# 1.2 文本预处理:获取句法信息
sentence_analysis = text_processor.process_sentence(text)
syntax_info = {
"dependency": sentence_analysis.get("dependency", []),
"constituency": sentence_analysis.get("constituency", {})
}
# 1.3 RE:提取关系(多模型并行)
if entity_list:
re_results = self.re_extractor.extract(
sentence=text,
entities=entity_list,
syntax_info=syntax_info,
context=textunit.get("context", ""),
use_verification=use_verification
)
# 收集所有模型的三元组
for re_result in re_results:
triplets = re_result.get("triplets", [])
for triplet in triplets:
triplet["textunit_id"] = textunit_id
triplet["source"] = text
all_triplets.append(triplet)
logger.info(f"提取完成:{len(all_triplets)} 个三元组,{sum(len(ents) for ents in all_entities.values())} 个实体")
# 步骤2: 三元组评估和筛选
evaluated_triplets = all_triplets
# 注意:完整的三元组评估需要大量LLM调用,可能会很慢
# 这里可以根据需要启用评估,或者使用简化的筛选策略
if False and all_triplets: # 默认关闭,避免大量API调用
from ..extraction.evaluator import TripletEvaluator
evaluator = TripletEvaluator(config=self.config)
# 按TextUnit分组进行评估
textunit_groups = {}
for triplet in all_triplets:
textunit_id = triplet.get("textunit_id")
source = triplet.get("source", "")
if textunit_id not in textunit_groups:
textunit_groups[textunit_id] = {
"triplets": [],
"source": source
}
textunit_groups[textunit_id]["triplets"].append(triplet)
# 对每个TextUnit的三元组进行评估(可选)
evaluated_triplets = []
for textunit_id, group_data in list(textunit_groups.items())[:5]: # 限制评估数量
triplets = group_data["triplets"]
source = group_data["source"]
evaluated_triplets.extend(triplets)
# 简单去重
seen = set()
unique_triplets = []
for triplet in evaluated_triplets:
key = (triplet.get("head", ""), triplet.get("relation", ""), triplet.get("tail", ""))
if key and key not in seen:
seen.add(key)
unique_triplets.append(triplet)
evaluated_triplets = unique_triplets
# 步骤3: 构建图
self._build_graph(evaluated_triplets if all_triplets else [])
# 步骤4: 社区层次结构(使用Leiden算法)
communities = self._detect_communities()
# 步骤5: 生成社区摘要
summaries = self._generate_community_summaries(communities)
# 存储元数据
self.graph.graph["metadata"] = {
"num_textunits": len(textunits),
"num_triplets": len(all_triplets),
"num_entities": sum(len(ents) for ents in all_entities.values()),
"num_communities": len(communities),
"communities": communities,
"summaries": summaries
}
return self.graph
def _build_graph(self, triplets: List[Dict[str, str]]):
"""
构建图结构
Args:
triplets: 三元组列表
"""
logger.info(f"构建图结构,包含 {len(triplets)} 个三元组...")
self.graph.clear()
for triplet in triplets:
head = triplet.get("head", "")
tail = triplet.get("tail", "")
relation = triplet.get("relation", "")
if not head or not tail or not relation:
continue
# 添加节点(如果不存在)
if not self.graph.has_node(head):
self.graph.add_node(head, type="entity")
if not self.graph.has_node(tail):
self.graph.add_node(tail, type="entity")
# 添加边
if self.graph.has_edge(head, tail):
# 如果边已存在,更新关系列表
edge_data = self.graph[head][tail]
if "relations" not in edge_data:
edge_data["relations"] = [edge_data.get("relation", "")]
if relation not in edge_data["relations"]:
edge_data["relations"].append(relation)
else:
# 添加新边
self.graph.add_edge(
head,
tail,
relation=relation,
textunit_id=triplet.get("textunit_id", ""),
source=triplet.get("source", "")
)
logger.info(f"图构建完成:{self.graph.number_of_nodes()} 个节点,{self.graph.number_of_edges()} 条边")
def _detect_communities(self):
"""
检测社区(使用Leiden算法)
Returns:
社区列表,每个社区包含节点列表
"""
if self.graph.number_of_nodes() == 0:
return []
try:
import igraph as ig
logger.info("使用Leiden算法检测社区...")
# 将NetworkX图转换为igraph图
# 创建节点映射
node_list = list(self.graph.nodes())
node_to_idx = {node: i for i, node in enumerate(node_list)}
# 创建边列表(使用节点索引)
edges = [(node_to_idx[u], node_to_idx[v]) for u, v in self.graph.edges()]
# 创建无向图
g_ig = ig.Graph(edges, directed=False)
# 设置节点名称
g_ig.vs["name"] = node_list
# 运行Leiden算法
communities_result = g_ig.community_leiden(
objective_function="modularity",
resolution_parameter=1.0
)
# 转换为节点列表
communities = []
for community in communities_result:
nodes = [g_ig.vs[i]["name"] for i in community]
communities.append(nodes)
logger.info(f"检测到 {len(communities)} 个社区")
return communities
except ImportError:
logger.warning("igraph未安装,无法使用Leiden算法,使用简单连通分量代替")
# 使用NetworkX的连通分量作为替代
if not self.graph.is_directed():
components = list(nx.connected_components(self.graph.to_undirected()))
else:
# 对于有向图,转换为无向图
undirected = self.graph.to_undirected()
components = list(nx.connected_components(undirected))
return [list(comp) for comp in components]
except Exception as e:
logger.error(f"社区检测失败: {e}")
return []
def _generate_community_summaries(self, communities: List):
"""
生成社区摘要(使用LLM)
Args:
communities: 社区列表
Returns:
社区摘要字典
"""
if not communities:
return {}
logger.info(f"{len(communities)} 个社区生成摘要...")
summaries = {}
# 使用第一个可用的LLM客户端生成摘要
try:
from ..utils.llm_client import LLMClient, LLMProvider
# 优先使用硅基流动
if self.config.SILICONFLOW_API_KEY:
summarizer = LLMClient(
provider=LLMProvider.SILICONFLOW,
model="deepseek-ai/DeepSeek-R1-Distill-Qwen-7B",
config=self.config
)
elif self.config.DASHSCOPE_API_KEY:
summarizer = LLMClient(
provider=LLMProvider.QWEN,
model="qwen-max",
config=self.config
)
else:
raise ValueError("请配置SILICONFLOW_API_KEY或DASHSCOPE_API_KEY")
for i, community_nodes in enumerate(communities[:20]): # 限制前20个社区
# 获取社区中的节点和关系信息
subgraph = self.graph.subgraph(community_nodes)
# 构建摘要提示
nodes_info = "\n".join([f"- {node}" for node in list(community_nodes)[:10]])
edges_info = "\n".join([
f"- {u} --{self.graph[u][v].get('relation', '')}--> {v}"
for u, v in list(subgraph.edges())[:10]
])
prompt = f"""请为以下知识图谱社区生成一个简洁的摘要(1-2句话),概括该社区的主要主题和内容。
社区节点:
{nodes_info}
社区关系:
{edges_info}
摘要:"""
messages = [
{"role": "system", "content": "你是一个知识图谱分析专家,擅长生成简洁准确的社区摘要。"},
{"role": "user", "content": prompt}
]
try:
summary = summarizer.chat(messages)
summaries[f"community_{i+1}"] = {
"nodes": list(community_nodes),
"summary": summary.strip(),
"size": len(community_nodes)
}
except Exception as e:
logger.warning(f"社区 {i+1} 摘要生成失败: {e}")
summaries[f"community_{i+1}"] = {
"nodes": list(community_nodes),
"summary": "摘要生成失败",
"size": len(community_nodes)
}
except Exception as e:
logger.warning(f"摘要生成器初始化失败: {e}")
# 生成简单摘要
for i, community_nodes in enumerate(communities):
summaries[f"community_{i+1}"] = {
"nodes": list(community_nodes),
"summary": f"包含 {len(community_nodes)} 个实体的社区",
"size": len(community_nodes)
}
return summaries
@@ -0,0 +1,18 @@
"""本体模型定义模块"""
from .entities import EntityType, get_entity_definitions
from .relations import RelationType, get_relation_definitions
from .schema import OntologySchema
__all__ = [
"EntityType",
"get_entity_definitions",
"RelationType",
"get_relation_definitions",
"OntologySchema",
]
@@ -0,0 +1,82 @@
"""
实体类型定义
对应论文中的9种实体类型
"""
from enum import Enum
from typing import Dict
class EntityType(str, Enum):
"""实体类型枚举"""
# 法规条文
LEGAL_PROVISION = "法规条文"
# 目标要素
TARGET_ELEMENT = "目标要素"
# 原则要素
PRINCIPLE_ELEMENT = "原则要素"
# 空间要素
SPATIAL_ELEMENT = "空间要素"
# 功能要素
FUNCTIONAL_ELEMENT = "功能要素"
# 环境要素
ENVIRONMENTAL_ELEMENT = "环境要素"
# 管控要素
CONTROL_ELEMENT = "管控要素"
# 行政区域
ADMINISTRATIVE_REGION = "行政区域"
# 时间要素
TEMPORAL_ELEMENT = "时间要素"
def get_entity_definitions() -> Dict[str, str]:
"""
获取实体类型定义(对应论文Table 1)
Returns:
实体类型名称到定义的映射
"""
return {
EntityType.LEGAL_PROVISION.value: "法规文档中的具体条文,包括章、节、条、款、项等不同层级",
EntityType.TARGET_ELEMENT.value: "法规中明确表达的战略目标、发展愿景、总体目标等宏观性表述",
EntityType.PRINCIPLE_ELEMENT.value: "法规遵循的基本准则、指导方针、基本原则等",
EntityType.SPATIAL_ELEMENT.value: "法规涉及的地理空间实体,包括行政区划、功能区、保护区域等",
EntityType.FUNCTIONAL_ELEMENT.value: "法规涉及的功能性要素,如土地利用类型、产业类型、基础设施等",
EntityType.ENVIRONMENTAL_ELEMENT.value: "法规涉及的环境保护相关要素,如生态保护区、环境质量标准等",
EntityType.CONTROL_ELEMENT.value: "法规对各类要素的管控要求,包括禁止、限制、引导等管控措施",
EntityType.ADMINISTRATIVE_REGION.value: "法规适用的行政区划,包括省、市、县、乡等不同层级",
EntityType.TEMPORAL_ELEMENT.value: "法规涉及的时间概念,如规划期限、实施阶段、有效期等",
}
def get_four_element_types() -> Dict[str, str]:
"""
获取四要素类型(目标、原则、要素、管控)
Returns:
四要素类型定义
"""
return {
"目标": EntityType.TARGET_ELEMENT.value,
"原则": EntityType.PRINCIPLE_ELEMENT.value,
"要素": [
EntityType.SPATIAL_ELEMENT.value,
EntityType.FUNCTIONAL_ELEMENT.value,
EntityType.ENVIRONMENTAL_ELEMENT.value,
],
"管控": EntityType.CONTROL_ELEMENT.value,
}
@@ -0,0 +1,73 @@
"""
关系类型定义
对应论文中的8种关系类型
"""
from enum import Enum
from typing import Dict
class RelationType(str, Enum):
"""关系类型枚举"""
# 引用关系
CITES = "引用"
# 层级关系
HIERARCHY = "层级"
# 包含关系
CONTAINS = "包含"
# 管控关系
CONTROLS = "管控"
# 遵循关系
FOLLOWS = "遵循"
# 涉及关系
INVOLVES = "涉及"
# 适用关系
APPLIES_TO = "适用"
# 演化关系
EVOLVES = "演化"
def get_relation_definitions() -> Dict[str, str]:
"""
获取关系类型定义(对应论文Table 2)
Returns:
关系类型名称到定义的映射
"""
return {
RelationType.CITES.value: "一条法规条文明确引用另一条法规条文",
RelationType.HIERARCHY.value: "上下级法规之间的隶属关系或法规条文内部的层级关系",
RelationType.CONTAINS.value: "一个实体在逻辑上包含另一个实体",
RelationType.CONTROLS.value: "管控要素对目标要素、原则要素或其他要素的约束关系",
RelationType.FOLLOWS.value: "要素遵循某种原则或目标的关系",
RelationType.INVOLVES.value: "法规条文涉及某个空间、功能或环境要素",
RelationType.APPLIES_TO.value: "法规适用于某个行政区域",
RelationType.EVOLVES.value: "法规修订前后的变化关系或时间序列上的演化关系",
}
def get_hierarchical_relations() -> Dict[str, str]:
"""
获取层级化关系(目标—原则—要素—管控)
Returns:
层级关系定义
"""
return {
"目标→原则": RelationType.FOLLOWS.value,
"原则→要素": RelationType.FOLLOWS.value,
"要素→管控": RelationType.CONTROLS.value,
}
@@ -0,0 +1,126 @@
"""
本体模式定义
实现三维本体结构:法规层级—要素类型—关联关系
"""
from typing import Dict, List, Optional
from .entities import EntityType, get_entity_definitions, get_four_element_types
from .relations import RelationType, get_relation_definitions, get_hierarchical_relations
class OntologySchema:
"""
法规知识本体模式
三维结构:法规层级—要素类型—关联关系
"""
# 法规层级
LEGAL_LEVELS = [
"法律",
"行政法规",
"部门规章",
"党中央国务院文件",
"国家主管部门文件",
"技术标准",
]
def __init__(self):
"""初始化本体模式"""
self.entity_types = get_entity_definitions()
self.relation_types = get_relation_definitions()
self.four_elements = get_four_element_types()
self.hierarchical_relations = get_hierarchical_relations()
def get_entity_type(self, entity_name: str) -> Optional[str]:
"""
根据实体名称获取实体类型
Args:
entity_name: 实体名称
Returns:
实体类型,如果未找到返回None
"""
for entity_type, definition in self.entity_types.items():
if entity_name in definition:
return entity_type
return None
def get_relation_type(self, relation_name: str) -> Optional[str]:
"""
根据关系名称获取关系类型
Args:
relation_name: 关系名称
Returns:
关系类型,如果未找到返回None
"""
return self.relation_types.get(relation_name)
def is_valid_entity_relation_pair(
self,
head_entity_type: str,
relation_type: str,
tail_entity_type: str
) -> bool:
"""
检查实体-关系-实体三元组是否有效
Args:
head_entity_type: 头实体类型
relation_type: 关系类型
tail_entity_type: 尾实体类型
Returns:
是否为有效的三元组
"""
# 检查层级关系:目标→原则→要素→管控
if relation_type == RelationType.FOLLOWS.value:
if (head_entity_type == EntityType.TARGET_ELEMENT.value and
tail_entity_type == EntityType.PRINCIPLE_ELEMENT.value):
return True
if (head_entity_type == EntityType.PRINCIPLE_ELEMENT.value and
tail_entity_type in [
EntityType.SPATIAL_ELEMENT.value,
EntityType.FUNCTIONAL_ELEMENT.value,
EntityType.ENVIRONMENTAL_ELEMENT.value,
]):
return True
if relation_type == RelationType.CONTROLS.value:
if (head_entity_type == EntityType.CONTROL_ELEMENT.value and
tail_entity_type in [
EntityType.TARGET_ELEMENT.value,
EntityType.PRINCIPLE_ELEMENT.value,
EntityType.SPATIAL_ELEMENT.value,
EntityType.FUNCTIONAL_ELEMENT.value,
EntityType.ENVIRONMENTAL_ELEMENT.value,
]):
return True
# 其他关系类型的基本检查
return (head_entity_type in self.entity_types and
tail_entity_type in self.entity_types and
relation_type in self.relation_types)
def get_schema_summary(self) -> Dict:
"""
获取本体模式摘要
Returns:
包含实体类型数、关系类型数、法规层级数等的字典
"""
return {
"legal_levels": len(self.LEGAL_LEVELS),
"entity_types": len(self.entity_types),
"relation_types": len(self.relation_types),
"four_elements": list(self.four_elements.keys()),
"entity_definitions": self.entity_types,
"relation_definitions": self.relation_types,
}
@@ -0,0 +1,11 @@
"""文本预处理模块"""
from .text_processor import TextProcessor
from .document_parser import DocumentParser
__all__ = ["TextProcessor", "DocumentParser"]
@@ -0,0 +1,242 @@
"""
法规文档解析模块
解析Word文档(.docx)格式的法规文件,提取文本并进行切分
"""
import os
import logging
from pathlib import Path
from typing import List, Dict, Optional
from docx import Document
from docx.shared import Inches
logger = logging.getLogger(__name__)
class DocumentParser:
"""法规文档解析器"""
def __init__(self):
"""初始化文档解析器"""
pass
def parse_docx(self, file_path: str) -> Dict:
"""
解析.docx文档
Args:
file_path: 文档路径
Returns:
包含文档信息的字典:
- title: 文档标题
- text: 完整文本
- paragraphs: 段落列表
- structure: 文档结构(章节信息)
"""
try:
doc = Document(file_path)
# 提取标题(通常在第一段)
title = ""
if doc.paragraphs:
title = doc.paragraphs[0].text.strip()
# 提取所有段落
paragraphs = []
structure = []
current_chapter = None
current_section = None
for para in doc.paragraphs:
text = para.text.strip()
if not text:
continue
# 识别章节结构
if self._is_chapter_heading(text):
current_chapter = text
current_section = None
structure.append({
'type': 'chapter',
'title': text,
'level': self._get_heading_level(para)
})
elif self._is_section_heading(text):
current_section = text
structure.append({
'type': 'section',
'title': text,
'chapter': current_chapter,
'level': self._get_heading_level(para)
})
paragraphs.append({
'text': text,
'chapter': current_chapter,
'section': current_section,
'style': para.style.name if para.style else None
})
# 合并所有文本
full_text = '\n'.join([p['text'] for p in paragraphs])
return {
'file_path': file_path,
'title': title,
'text': full_text,
'paragraphs': paragraphs,
'structure': structure
}
except Exception as e:
logger.error(f"解析文档失败 {file_path}: {e}")
raise
def _is_chapter_heading(self, text: str) -> bool:
"""判断是否为章节标题"""
# 匹配模式:第一章、第一章、第一编等
patterns = [
r'^第[一二三四五六七八九十]+章',
r'^第[一二三四五六七八九十]+编',
r'^第\d+章',
r'^第\d+编'
]
import re
for pattern in patterns:
if re.match(pattern, text):
return True
return False
def _is_section_heading(self, text: str) -> bool:
"""判断是否为节标题"""
patterns = [
r'^第[一二三四五六七八九十]+节',
r'^第\d+节'
]
import re
for pattern in patterns:
if re.match(pattern, text):
return True
return False
def _get_heading_level(self, para) -> int:
"""获取标题层级"""
style_name = para.style.name if para.style else ""
if 'Heading 1' in style_name or '标题 1' in style_name:
return 1
elif 'Heading 2' in style_name or '标题 2' in style_name:
return 2
elif 'Heading 3' in style_name or '标题 3' in style_name:
return 3
else:
return 0
def split_into_textunits(
self,
doc_data: Dict,
max_length: int = 500
) -> List[Dict]:
"""
将文档切分为TextUnit(参考GraphRAG
Args:
doc_data: 解析后的文档数据
max_length: 每个TextUnit的最大字符数
Returns:
TextUnit列表,每个包含:
- text: 文本内容
- chapter: 所属章节
- section: 所属节
- paragraph_index: 段落索引
- start_char: 起始字符位置
- end_char: 结束字符位置
"""
textunits = []
current_text = ""
current_unit = {
'chapter': None,
'section': None,
'paragraph_index': None,
'start_char': 0
}
char_offset = 0
for i, para in enumerate(doc_data['paragraphs']):
para_text = para['text']
# 如果当前累积文本加上新段落超过限制,保存当前TextUnit
if len(current_text) + len(para_text) > max_length and current_text:
current_unit.update({
'text': current_text.strip(),
'end_char': char_offset,
'file_path': doc_data['file_path']
})
textunits.append(current_unit.copy())
# 开始新的TextUnit
current_text = para_text
current_unit = {
'chapter': para.get('chapter'),
'section': para.get('section'),
'paragraph_index': i,
'start_char': char_offset
}
else:
# 累积文本
if current_text:
current_text += "\n" + para_text
else:
current_text = para_text
current_unit.update({
'chapter': para.get('chapter'),
'section': para.get('section'),
'paragraph_index': i,
'start_char': char_offset
})
char_offset += len(para_text) + 1 # +1 for newline
# 保存最后一个TextUnit
if current_text:
current_unit.update({
'text': current_text.strip(),
'end_char': char_offset,
'file_path': doc_data['file_path']
})
textunits.append(current_unit)
return textunits
def parse_directory(
self,
directory: str,
file_pattern: str = "*.docx"
) -> List[Dict]:
"""
解析目录中的所有文档
Args:
directory: 目录路径
file_pattern: 文件匹配模式
Returns:
所有文档的解析结果列表
"""
docs = []
dir_path = Path(directory)
for file_path in dir_path.rglob(file_pattern):
try:
doc_data = self.parse_docx(str(file_path))
docs.append(doc_data)
logger.info(f"成功解析文档: {file_path}")
except Exception as e:
logger.warning(f"跳过文档 {file_path}: {e}")
return docs
@@ -0,0 +1,271 @@
"""
HanLP文本处理模块
处理中文法规文本的分词、依存句法分析和句法成分分析
"""
from typing import List, Dict, Optional, Tuple
import logging
logger = logging.getLogger(__name__)
# 延迟导入hanlp,避免在导入时失败
try:
import hanlp
HANLP_AVAILABLE = True
except ImportError:
HANLP_AVAILABLE = False
logger.warning("HanLP未安装,将使用简化处理")
class TextProcessor:
"""文本处理器,使用HanLP进行中文文本处理"""
def __init__(self, model_path: Optional[str] = None):
"""
初始化文本处理器
Args:
model_path: HanLP模型路径,如果为None则使用默认模型
"""
self.hanlp = None
if not HANLP_AVAILABLE:
logger.warning("HanLP未安装,将使用简化处理。安装命令: pip install 'hanlp[full]' -U")
return
try:
if model_path:
logger.info(f"加载HanLP模型: {model_path}")
self.hanlp = hanlp.load(model_path)
logger.info("HanLP模型加载成功")
else:
# 尝试加载HanLP默认模型(较小的中文模型)
logger.info("尝试加载HanLP默认模型...")
try:
# 方法1: 尝试使用预定义的小型中文模型
import hanlp.pretrained
model_name = hanlp.pretrained.mtl.CLOSE_TOK_POS_NER_SRL_DEP_SDP_CON_ELECTRA_SMALL_ZH
logger.info(f"使用模型: {model_name}")
self.hanlp = hanlp.load(model_name)
logger.info("HanLP默认模型加载成功")
except (AttributeError, ImportError) as e1:
logger.debug(f"方法1失败: {e1},尝试方法2...")
try:
# 方法2: 使用pipeline方式
self.hanlp = hanlp.pipeline('tok/coarse')
logger.info("HanLP pipeline加载成功")
except Exception as e2:
logger.debug(f"方法2失败: {e2},尝试方法3...")
# 方法3: 尝试最简单的加载方式
self.hanlp = hanlp.load()
logger.info("HanLP默认加载成功")
except Exception as e:
logger.warning(f"HanLP模型加载失败: {e},将使用简化处理(仅文档解析和基本分词)")
logger.info("提示: 首次加载会自动下载模型,可能需要一些时间")
logger.info("如需使用完整功能,请安装完整版: pip install 'hanlp[full]' -U")
self.hanlp = None
def tokenize(self, text: str, coarse: bool = True) -> List[str]:
"""
文本分词
Args:
text: 输入文本
coarse: 是否使用粗粒度分词(默认True,保持术语完整性)
Returns:
分词结果列表
"""
if self.hanlp is None:
# 简化处理:按标点符号和空格简单切分
import re
tokens = re.split(r'[,。;:!?、\s]+', text)
return [t.strip() for t in tokens if t.strip()]
if coarse:
# 使用粗粒度分词器,保持术语完整性(如"开敞空间")
result = self.hanlp(text, tasks='tok/coarse')
else:
result = self.hanlp(text, tasks='tok/fine')
return result.get('tok', [])
def dependency_parse(self, text: str) -> List[Dict]:
"""
依存句法分析
Args:
text: 输入文本
Returns:
依存关系列表,每个元素包含:head(依存头)、deprel(依存关系类型)
"""
if self.hanlp is None:
# 返回空列表,简化处理
return []
# 分别获取分词和依存句法,因为HanLP MTL模型不支持同时指定多个任务
# 先获取分词结果
tok_result = self.hanlp(text, tasks='tok/coarse')
tokens = tok_result.get('tok/coarse', [])
# 再获取依存句法分析(会自动使用已分词的token)
dep_result = self.hanlp(text, tasks='dep')
dependencies = dep_result.get('dep', [])
parsed = []
for i, dep in enumerate(dependencies):
# HanLP返回的依赖关系格式:(head_index, deprel)
word = tokens[i] if i < len(tokens) else ''
if isinstance(dep, tuple):
# tuple格式:(head_index, deprel)
head_idx = dep[0] if len(dep) > 0 else -1
deprel = dep[1] if len(dep) > 1 else ''
head_word = tokens[head_idx] if 0 <= head_idx < len(tokens) else '' if head_idx >= 0 else 'ROOT'
parsed.append({
'word': word,
'head': head_idx,
'head_word': head_word,
'deprel': deprel
})
elif hasattr(dep, 'form'):
# 如果是对象格式,有form属性
parsed.append({
'word': dep.form,
'head': dep.head,
'head_word': tokens[dep.head] if 0 <= dep.head < len(tokens) else '' if dep.head >= 0 else 'ROOT',
'deprel': dep.deprel
})
elif isinstance(dep, dict):
# 如果是字典格式
head_idx = dep.get('head', -1)
parsed.append({
'word': dep.get('form', dep.get('word', word)),
'head': head_idx,
'head_word': tokens[head_idx] if 0 <= head_idx < len(tokens) else '' if head_idx >= 0 else 'ROOT',
'deprel': dep.get('deprel', '')
})
else:
# 其他格式
logger.warning(f"未知的依存关系格式: {type(dep)}, {dep}")
parsed.append({
'word': word,
'head': -1,
'head_word': 'ROOT',
'deprel': ''
})
return parsed
def constituency_parse(self, text: str) -> Dict:
"""
句法成分分析
Args:
text: 输入文本
Returns:
句法成分树(嵌套字典结构)
"""
if self.hanlp is None:
# 返回空字典,简化处理
return {}
result = self.hanlp(text, tasks='con')
return result.get('con', {})
def pos_tag(self, text: str) -> List[Tuple[str, str]]:
"""
词性标注
Args:
text: 输入文本
Returns:
(词, 词性)元组列表
"""
if self.hanlp is None:
# 简化处理:返回分词结果和默认词性
tokens = self.tokenize(text)
return [(token, 'UNKNOWN') for token in tokens]
result = self.hanlp(text, tasks='pos')
tokens = result.get('tok', [])
pos_tags = result.get('pos', [])
return list(zip(tokens, pos_tags))
def process_sentence(
self,
sentence: str,
include_context: bool = True
) -> Dict:
"""
处理单个句子,返回所有分析结果
Args:
sentence: 输入句子
include_context: 是否包含上下文信息
Returns:
包含分词、依存、句法成分等信息的字典
"""
result = {
'text': sentence,
'tokens': self.tokenize(sentence, coarse=True),
'dependency': self.dependency_parse(sentence),
'constituency': self.constituency_parse(sentence),
'pos_tags': self.pos_tag(sentence)
}
return result
def has_omitted_subject(self, dependencies: List[Dict]) -> bool:
"""
检查句子是否省略了主语
Args:
dependencies: 依存关系列表
Returns:
是否存在省略的主语
"""
# 检查是否有主语相关的依存关系(nsubj等)
subject_deprels = ['nsubj', 'nsubjpass', 'csubj', 'csubjpass']
has_subject = any(
dep['deprel'] in subject_deprels
for dep in dependencies
)
return not has_subject
def extract_coreference_candidates(
self,
text: str,
pronouns: List[str] = None
) -> List[Dict]:
"""
提取共指消解候选
Args:
text: 输入文本
pronouns: 代词列表,默认为["", "", "", ""]
Returns:
共指候选列表
"""
if pronouns is None:
pronouns = ["", "", "", ""]
candidates = []
tokens = self.tokenize(text)
pos_tags = self.pos_tag(text)
for i, (token, pos) in enumerate(zip(tokens, pos_tags)):
if token in pronouns or pos[1] == 'PN': # PN表示代词
candidates.append({
'pronoun': token,
'position': i,
'pos': pos[1]
})
return candidates
@@ -0,0 +1,17 @@
"""Prompt模板模块"""
from .template import PromptTemplate, PromptModule
from .ner_prompts import NERPromptBuilder
from .re_prompts import REPromptBuilder
__all__ = [
"PromptTemplate",
"PromptModule",
"NERPromptBuilder",
"REPromptBuilder",
]
@@ -0,0 +1,95 @@
"""
NER (命名实体识别) Prompt构建器
"""
from typing import List, Dict, Any
from .template import PromptTemplate
from ..ontology.entities import EntityType, get_entity_definitions
class NERPromptBuilder:
"""NER Prompt构建器"""
def __init__(self):
"""初始化NER Prompt构建器"""
self.entity_definitions = get_entity_definitions()
def build_template(self) -> PromptTemplate:
"""
构建NER Prompt模板
Returns:
PromptTemplate对象
"""
# 任务描述
system_message = "你是一个经验丰富的国土空间规划法规分析专家,擅长从法规文本中识别各类知识实体。"
user_message = """请从给定的法规文本中识别并分类所有实体。
任务要求:
1. 仔细阅读输入的法规文本
2. 识别所有属于预定义实体类型的实体
3. 对每个实体进行正确分类
4. 确保识别结果的完整性和准确性
输出格式要求:请以JSON格式输出,格式如下:
{
"法规条文": ["实体1", "实体2", ...],
"目标要素": ["实体1", "实体2", ...],
"原则要素": ["实体1", "实体2", ...],
...
}
每个实体类型对应一个列表,包含该类型的所有实体。如果某个类型没有实体,请使用空列表[]。"""
# 候选目标
candidate_targets = self.entity_definitions
# 任务示例
task_examples = [
{
"input": "优先保障先进制造业、战略性新兴产业、都市产业等发展空间,促进价值创新园区建设。",
"output": '{"法规条文": [], "目标要素": [], "原则要素": [], "空间要素": [], "功能要素": ["先进制造业", "战略性新兴产业", "都市产业", "价值创新园区"], "环境要素": [], "管控要素": ["优先保障", "促进"], "行政区域": [], "时间要素": []}'
},
{
"input": "广州市应当坚持生态优先、绿色发展,建设宜居花城、活力全球城市。",
"output": '{"法规条文": [], "目标要素": ["宜居花城", "活力全球城市"], "原则要素": ["生态优先", "绿色发展"], "空间要素": [], "功能要素": [], "环境要素": [], "管控要素": ["坚持", "建设"], "行政区域": ["广州市"], "时间要素": []}'
}
]
# 任务强调
task_emphasis = """严格按要求:
1. **仅输出JSON格式结果**,不要添加任何解释性文字、markdown代码块标记或其他内容
2. 直接输出JSON对象,不要使用```json```等markdown标记
3. 不要添加"好的""让我分析"等开场白或解释
4. 确保所有实体都被正确识别和分类
5. 保持实体文本的完整性,不要截断
重要:请直接输出JSON,格式如下(不要添加任何其他文字):
{"法规条文": [], "目标要素": [], "原则要素": [], ...}"""
return PromptTemplate(
task_description={
"system": system_message,
"user": user_message
},
candidate_targets=candidate_targets,
task_examples=task_examples,
task_emphasis=task_emphasis,
)
def build_verification_prompt(self) -> str:
"""
构建验证提示(用于二次对话)
Returns:
验证提示文本
"""
return """你之前提取的地理规划知识实体可能不完整或不准确。请结合之前的提取内容和经验,重新提取并验证结果。
请特别关注:
1. 是否有遗漏的实体
2. 实体的分类是否正确
3. 实体文本是否完整"""
@@ -0,0 +1,129 @@
"""
RE (关系抽取) Prompt构建器
"""
from typing import List, Dict, Any
from .template import PromptTemplate
from ..ontology.relations import RelationType, get_relation_definitions
class REPromptBuilder:
"""RE Prompt构建器"""
def __init__(self):
"""初始化RE Prompt构建器"""
self.relation_definitions = get_relation_definitions()
def build_template(
self,
entities: List[Dict[str, str]],
syntax_info: Dict = None,
context: str = None
) -> PromptTemplate:
"""
构建RE Prompt模板
Args:
entities: 已识别的实体列表
syntax_info: 句法分析结果
context: 上下文段落信息
Returns:
PromptTemplate对象
"""
# 任务描述
system_message = "你是一个自然语言处理专家,擅长在城市规划领域处理关系抽取任务。"
user_message = """请从给定的法规文本中识别实体之间的关系,并构建知识三元组。
任务要求:
1. 基于已识别的实体列表,找出实体之间的语义关系
2. 对于省略主语的句子,需要利用上下文信息补全
3. 构建<head entity, relation, tail entity>形式的三元组
4. 确保三元组的语义逻辑正确
输出格式要求:请以JSON格式输出三元组列表,格式如下:
[
{"head": "头实体", "relation": "关系类型", "tail": "尾实体"},
...
]
如果句子中无法提取完整的三元组,缺失的部分请标记为null。"""
# 候选目标(关系类型)
candidate_targets = self.relation_definitions
# 构建输入数据说明
input_data_desc = f"""已识别实体:
{self._format_entities(entities)}
句子文本:
[待填充]
句法分析结果:
{syntax_info or "待填充"}
上下文段落:
{context or "待填充"}"""
# 任务示例
task_examples = [
{
"input": "实体:['广州市', '生态优先'],句子:'广州市应当坚持生态优先、绿色发展。'",
"output": '[{"head": "广州市", "relation": "遵循", "tail": "生态优先"}]'
}
]
# 任务强调
task_emphasis = """严格按要求:
1. 仅输出JSON格式的三元组数组,不要添加任何解释
2. 确保三元组中的实体必须是已识别实体列表中的实体
3. 关系类型必须是预定义关系类型之一
4. 对于省略成分,利用上下文补全"""
template = PromptTemplate(
task_description={
"system": system_message,
"user": user_message
},
candidate_targets=candidate_targets,
task_examples=task_examples,
task_emphasis=task_emphasis,
)
# 存储额外信息
template.input_data_description = input_data_desc
return template
def _format_entities(self, entities: List[Dict[str, str]]) -> str:
"""格式化实体列表"""
if not entities:
return ""
lines = []
for entity in entities:
entity_type = entity.get("type", "未知类型")
entity_text = entity.get("text", "")
lines.append(f"- {entity_type}: {entity_text}")
return "\n".join(lines)
def build_verification_prompt(self) -> str:
"""
构建验证提示(用于二次对话)
Returns:
验证提示文本
"""
return """请验证和优化之前提取的三元组结果。
请特别关注:
1. 三元组的语义逻辑是否正确
2. 是否遗漏了重要的关系
3. 对于省略的成分,是否利用上下文正确补全
4. 三元组是否准确反映了源文本的语义"""
@@ -0,0 +1,227 @@
"""
统一Prompt模板框架
实现五个核心模块:任务描述、候选目标、任务示例、任务强调、二次对话
"""
from typing import Dict, List, Optional, Any
from enum import Enum
class PromptModule(str, Enum):
"""Prompt模块类型"""
TASK_DESCRIPTION = "任务描述"
CANDIDATE_TARGETS = "候选目标"
TASK_EXAMPLES = "任务示例"
TASK_EMPHASIS = "任务强调"
SECOND_CONVERSATION = "二次对话"
class PromptTemplate:
"""
统一Prompt模板类
实现论文中描述的五个模块设计
"""
def __init__(
self,
task_description: Optional[str] = None,
candidate_targets: Optional[Dict[str, str]] = None,
task_examples: Optional[List[Dict[str, Any]]] = None,
task_emphasis: Optional[str] = None,
second_conversation: Optional[str] = None,
):
"""
初始化Prompt模板
Args:
task_description: 任务描述(系统消息和用户消息)
candidate_targets: 候选目标(实体类型或关系类型定义)
task_examples: 任务示例(少样本学习)
task_emphasis: 任务强调(输出格式要求)
second_conversation: 二次对话提示
"""
self.task_description = task_description or {}
self.candidate_targets = candidate_targets or {}
self.task_examples = task_examples or []
self.task_emphasis = task_emphasis or ""
self.second_conversation = second_conversation or ""
def build_system_message(self, role: str, capability: str, task: str) -> str:
"""
构建系统消息(任务描述模块的一部分)
Args:
role: 模型角色(如"经验丰富的国土空间规划法规分析专家"
capability: 模型能力
task: 任务描述
Returns:
系统消息字符串
"""
return f"""你是一个{role}{capability}。你的任务是{task}"""
def build_user_message(
self,
task_steps: List[str],
input_format: str,
output_format: str,
) -> str:
"""
构建用户消息(任务描述模块的一部分)
Args:
task_steps: 任务步骤列表
input_format: 输入格式说明
output_format: 输出格式说明
Returns:
用户消息字符串
"""
steps_str = "\n".join([f"{i+1}. {step}" for i, step in enumerate(task_steps)])
return f"""任务步骤:
{steps_str}
输入格式:
{input_format}
输出格式:
{output_format}"""
def build_candidate_targets_section(self) -> str:
"""
构建候选目标部分
Returns:
候选目标文本
"""
if not self.candidate_targets:
return ""
lines = ["候选目标及其定义:"]
for target, definition in self.candidate_targets.items():
lines.append(f"- {target}: {definition}")
return "\n".join(lines)
def build_task_examples_section(self) -> str:
"""
构建任务示例部分(少样本学习)
Returns:
任务示例文本
"""
if not self.task_examples:
return ""
examples_text = "任务示例:\n\n"
for i, example in enumerate(self.task_examples, 1):
examples_text += f"示例 {i}\n"
examples_text += f"输入:{example.get('input', '')}\n"
examples_text += f"输出:{example.get('output', '')}\n\n"
return examples_text
def build_task_emphasis_section(self) -> str:
"""
构建任务强调部分
Returns:
任务强调文本
"""
if not self.task_emphasis:
return ""
return f"""重要提示:
{self.task_emphasis}
请严格按照输出格式要求,仅输出结果,避免任何解释性内容。"""
def build_first_conversation(
self,
input_data: str,
system_message: str,
user_message: str,
) -> List[Dict[str, str]]:
"""
构建第一阶段对话消息
Args:
input_data: 输入数据
system_message: 系统消息
user_message: 用户消息
Returns:
对话消息列表
"""
# 构建完整的用户消息
full_user_message = user_message
# 添加候选目标
if self.candidate_targets:
full_user_message += "\n\n" + self.build_candidate_targets_section()
# 添加任务示例
if self.task_examples:
full_user_message += "\n\n" + self.build_task_examples_section()
# 添加任务强调
if self.task_emphasis:
full_user_message += "\n\n" + self.build_task_emphasis_section()
# 添加输入数据
full_user_message += f"\n\n输入数据:\n{input_data}"
return [
{"role": "system", "content": system_message},
{"role": "user", "content": full_user_message}
]
def build_second_conversation(
self,
original_input: str,
first_result: str,
verification_instruction: str,
) -> List[Dict[str, str]]:
"""
构建第二阶段对话消息(验证和优化)
Args:
original_input: 原始输入数据
first_result: 第一阶段的结果
verification_instruction: 验证指令
Returns:
对话消息列表
"""
system_message = """你需要验证和优化之前的结果。请仔细检查结果的正确性和完整性,根据上下文信息补全缺失的内容,并修正任何错误。"""
user_message = f"""{verification_instruction}
原始输入:
{original_input}
之前的结果:
{first_result}
请对结果进行验证、补全和优化。"""
return [
{"role": "system", "content": system_message},
{"role": "assistant", "content": first_result},
{"role": "user", "content": user_message}
]
def to_dict(self) -> Dict[str, Any]:
"""将模板转换为字典"""
return {
"task_description": self.task_description,
"candidate_targets": self.candidate_targets,
"task_examples": self.task_examples,
"task_emphasis": self.task_emphasis,
"second_conversation": self.second_conversation,
}
@@ -0,0 +1,12 @@
"""查询模块(参考GraphRAG"""
from .global_search import GlobalSearcher
from .local_search import LocalSearcher
from .drift_search import DriftSearcher
__all__ = ["GlobalSearcher", "LocalSearcher", "DriftSearcher"]
@@ -0,0 +1,187 @@
"""
DRIFT搜索(参考GraphRAG
结合社区信息的上下文搜索(Community-aware search
"""
import logging
from typing import List, Dict, Any, Optional, Set
import networkx as nx
from .local_search import LocalSearcher
from .global_search import GlobalSearcher
from ..utils.config import Config
logger = logging.getLogger(__name__)
class DriftSearcher:
"""DRIFT搜索器(结合社区信息的局部搜索)"""
def __init__(
self,
kg: Optional[nx.DiGraph] = None,
communities: Dict = None,
config: Optional[Config] = None,
):
"""
初始化DRIFT搜索器
Args:
kg: 知识图谱
communities: 社区信息(包含摘要)
config: 配置对象
"""
self.kg = kg
self.communities = communities or {}
self.config = config or Config()
# 使用局部搜索器作为基础
self.local_searcher = LocalSearcher(kg, config)
self.global_searcher = GlobalSearcher(kg, communities, config)
def search(self, query: str, entity: str = None, depth: int = 2) -> Dict[str, Any]:
"""
DRIFT搜索:结合社区上下文信息的局部搜索
Args:
query: 查询文本
entity: 实体名称(如果提供,则进行局部搜索)
depth: 搜索深度
Returns:
搜索结果,结合了局部搜索和社区信息
"""
logger.info(f"执行DRIFT搜索: query={query}, entity={entity}")
result = {
"query": query,
"entity": entity,
}
# 1. 如果有实体,先进行局部搜索
local_result = None
if entity:
local_result = self.local_searcher.search(entity, depth=depth)
result["local_search"] = local_result
# 2. 执行全局搜索获取相关社区
global_result = self.global_searcher.search(query, top_k=3)
result["global_context"] = global_result
# 3. 结合社区信息扩展局部搜索结果
if local_result and "subgraph" in local_result:
subgraph_nodes = set(local_result["subgraph"]["nodes"])
# 从相关社区中获取额外的上下文节点
context_nodes = set()
for comm in global_result.get("relevant_communities", []):
context_nodes.update(comm.get("nodes", []))
# 合并节点
expanded_nodes = subgraph_nodes | context_nodes
expanded_nodes = list(expanded_nodes)[:100] # 限制大小
# 构建扩展子图
if self.kg:
expanded_subgraph = self.kg.subgraph(expanded_nodes)
# 提取社区上下文关系
community_relations = []
for comm in global_result.get("relevant_communities", []):
comm_nodes = set(comm.get("nodes", []))
# 找到子图中的社区节点之间的连接
for u in comm_nodes:
if u in expanded_subgraph:
for v in expanded_subgraph.successors(u):
if v in comm_nodes:
edge_data = expanded_subgraph[u][v]
community_relations.append({
"source": u,
"target": v,
"relation": edge_data.get("relation", ""),
"community": comm.get("community_id", ""),
"summary": comm.get("summary", "")
})
result["expanded_subgraph"] = {
"nodes": expanded_nodes,
"num_nodes": len(expanded_nodes),
"num_edges": expanded_subgraph.number_of_edges(),
"community_relations": community_relations
}
# 4. 生成综合答案(结合局部和全局信息)
if self.global_searcher.llm_client:
answer = self._generate_combined_answer(
query,
local_result,
global_result
)
result["answer"] = answer
return result
def _generate_combined_answer(
self,
query: str,
local_result: Optional[Dict],
global_result: Dict,
) -> str:
"""
生成结合局部和全局信息的综合答案
Args:
query: 查询文本
local_result: 局部搜索结果
global_result: 全局搜索结果
Returns:
综合答案
"""
llm_client = self.global_searcher.llm_client
if not llm_client:
return "答案生成功能不可用"
# 构建上下文信息
context_parts = []
# 添加局部搜索信息
if local_result and "subgraph" in local_result:
subgraph = local_result["subgraph"]
context_parts.append(f"""局部实体关系网络:
- 实体节点数:{subgraph['num_nodes']}
- 关系边数:{subgraph['num_edges']}
- 主要关系:{', '.join([r['relation'] for r in subgraph.get('relations', [])[:5]])}""")
# 添加全局社区信息
communities_info = "\n\n".join([
f"社区 {i+1}{comm['summary']}"
for i, comm in enumerate(global_result.get("relevant_communities", [])[:3])
])
if communities_info:
context_parts.append(f"相关社区摘要:\n{communities_info}")
context = "\n\n".join(context_parts)
prompt = f"""基于以下知识图谱的局部和全局信息,回答用户的问题。
查询:{query}
知识图谱上下文:
{context}
请生成一个准确、完整的答案,充分利用局部实体关系和全局社区信息:"""
messages = [
{"role": "system", "content": "你是一个法规知识问答专家,擅长结合知识图谱的局部和全局信息回答复杂问题。"},
{"role": "user", "content": prompt}
]
try:
answer = llm_client.chat(messages)
return answer.strip()
except Exception as e:
logger.error(f"综合答案生成失败: {e}")
return "答案生成失败"
@@ -0,0 +1,194 @@
"""
全局搜索(参考GraphRAG
利用社区摘要进行整体推理
"""
import logging
from typing import List, Dict, Any, Optional
import networkx as nx
from ..utils.llm_client import LLMClient, LLMProvider
from ..utils.config import Config
logger = logging.getLogger(__name__)
class GlobalSearcher:
"""全局搜索器"""
def __init__(
self,
kg: Optional[nx.DiGraph] = None,
communities: Dict = None,
config: Optional[Config] = None,
):
"""
初始化全局搜索器
Args:
kg: 知识图谱
communities: 社区信息(包含摘要)
config: 配置对象
"""
self.kg = kg
self.communities = communities or {}
self.config = config or Config()
try:
self.llm_client = LLMClient(
provider=LLMProvider.QWEN,
model="qwen-max",
config=self.config
)
except Exception as e:
logger.warning(f"LLM客户端初始化失败: {e}")
self.llm_client = None
def search(self, query: str, top_k: int = 5) -> Dict[str, Any]:
"""
全局搜索:利用社区摘要进行整体推理
Args:
query: 查询文本
top_k: 返回top-k个最相关的社区
Returns:
搜索结果,包含相关社区及其摘要
"""
logger.info(f"执行全局搜索: {query}")
if not self.communities:
logger.warning("社区信息为空,无法执行全局搜索")
return {
"query": query,
"results": [],
"message": "社区信息不可用"
}
# 使用LLM评估查询与社区摘要的相关性
relevant_communities = []
if self.llm_client:
# 使用LLM进行相关性评估
for comm_id, comm_data in self.communities.items():
summary = comm_data.get("summary", "")
nodes = comm_data.get("nodes", [])
if not summary:
continue
# 评估相关性
relevance_score = self._evaluate_relevance(query, summary)
relevant_communities.append({
"community_id": comm_id,
"summary": summary,
"nodes": nodes,
"size": len(nodes),
"relevance_score": relevance_score
})
else:
# 简单的关键词匹配
query_lower = query.lower()
for comm_id, comm_data in self.communities.items():
summary = comm_data.get("summary", "")
if query_lower in summary.lower():
relevant_communities.append({
"community_id": comm_id,
"summary": summary,
"nodes": comm_data.get("nodes", []),
"size": len(comm_data.get("nodes", [])),
"relevance_score": 0.5
})
# 按相关性排序
relevant_communities.sort(key=lambda x: x.get("relevance_score", 0), reverse=True)
# 取top-k
top_communities = relevant_communities[:top_k]
# 使用LLM生成综合答案
answer = self._generate_answer(query, top_communities)
return {
"query": query,
"answer": answer,
"relevant_communities": top_communities,
"total_communities": len(self.communities)
}
def _evaluate_relevance(self, query: str, summary: str) -> float:
"""
评估查询与社区摘要的相关性
Args:
query: 查询文本
summary: 社区摘要
Returns:
相关性分数(0-1)
"""
if not self.llm_client:
return 0.5
prompt = f"""请评估以下查询与社区摘要的相关性,给出0-1之间的分数(1表示高度相关,0表示不相关)。
查询:{query}
社区摘要:{summary}
请只回答一个0-1之间的浮点数:"""
messages = [
{"role": "system", "content": "你是一个相关性评估专家。"},
{"role": "user", "content": prompt}
]
try:
result = self.llm_client.chat(messages).strip()
score = float(result)
return max(0.0, min(1.0, score))
except:
return 0.5
def _generate_answer(self, query: str, communities: List[Dict]) -> str:
"""
基于相关社区生成综合答案
Args:
query: 查询文本
communities: 相关社区列表
Returns:
生成的答案
"""
if not self.llm_client:
return "答案生成功能不可用(LLM未初始化)"
communities_info = "\n\n".join([
f"社区 {i+1}(包含{comm['size']}个实体):\n{comm['summary']}"
for i, comm in enumerate(communities)
])
prompt = f"""基于以下知识图谱社区摘要,回答用户的问题。
查询:{query}
相关社区摘要:
{communities_info}
请生成一个准确、完整的答案:"""
messages = [
{"role": "system", "content": "你是一个法规知识问答专家,擅长基于知识图谱信息回答法规相关问题。"},
{"role": "user", "content": prompt}
]
try:
answer = self.llm_client.chat(messages)
return answer.strip()
except Exception as e:
logger.error(f"答案生成失败: {e}")
return "答案生成失败"
@@ -0,0 +1,162 @@
"""
局部搜索(参考GraphRAG
扩展到邻居和相关概念
"""
import logging
from typing import List, Dict, Any, Optional, Set
import networkx as nx
from ..utils.config import Config
logger = logging.getLogger(__name__)
class LocalSearcher:
"""局部搜索器"""
def __init__(self, kg: Optional[nx.DiGraph] = None, config: Optional[Config] = None):
"""
初始化局部搜索器
Args:
kg: 知识图谱
config: 配置对象
"""
self.kg = kg
self.config = config or Config()
def search(self, entity: str, depth: int = 2, max_nodes: int = 50) -> Dict[str, Any]:
"""
局部搜索:从指定实体扩展到邻居和相关概念
Args:
entity: 实体名称
depth: 搜索深度(跳数)
max_nodes: 最大节点数
Returns:
搜索结果,包含子图和相关信息
"""
logger.info(f"执行局部搜索: {entity}, depth={depth}")
if not self.kg:
return {
"entity": entity,
"error": "知识图谱未加载"
}
# 查找实体节点(支持模糊匹配)
target_nodes = self._find_entity_nodes(entity)
if not target_nodes:
return {
"entity": entity,
"error": f"未找到实体: {entity}",
"suggestions": list(self.kg.nodes())[:10] # 提供一些建议
}
# 扩展搜索:收集所有相关的节点
explored_nodes: Set[str] = set()
nodes_to_explore = list(target_nodes)
for current_depth in range(depth + 1):
if not nodes_to_explore:
break
current_level_nodes = list(nodes_to_explore)
nodes_to_explore = []
for node in current_level_nodes:
if node in explored_nodes:
continue
explored_nodes.add(node)
# 添加到下一层探索的节点
if current_depth < depth:
# 前驱节点
predecessors = list(self.kg.predecessors(node))
# 后继节点
successors = list(self.kg.successors(node))
for neighbor in predecessors + successors:
if neighbor not in explored_nodes:
nodes_to_explore.append(neighbor)
if len(explored_nodes) >= max_nodes:
break
# 构建子图
subgraph_nodes = list(explored_nodes)
subgraph = self.kg.subgraph(subgraph_nodes)
# 提取关系信息
relations = []
for u, v, data in subgraph.edges(data=True):
relations.append({
"source": u,
"target": v,
"relation": data.get("relation", ""),
"metadata": {k: v for k, v in data.items() if k != "relation"}
})
# 计算中心性(在子图中)
centrality = {}
if subgraph.number_of_nodes() > 0:
try:
degree_centrality = nx.degree_centrality(subgraph)
centrality = {
node: {
"degree": degree_centrality.get(node, 0),
"neighbors": len(list(subgraph.neighbors(node)))
}
for node in target_nodes
}
except:
pass
return {
"entity": entity,
"target_nodes": list(target_nodes),
"subgraph": {
"nodes": subgraph_nodes,
"num_nodes": len(subgraph_nodes),
"num_edges": subgraph.number_of_edges(),
"relations": relations
},
"centrality": centrality,
"depth": depth
}
def _find_entity_nodes(self, entity: str) -> List[str]:
"""
在图中查找实体节点(支持模糊匹配)
Args:
entity: 实体名称
Returns:
匹配的节点列表
"""
if not self.kg:
return []
entity_lower = entity.lower()
exact_matches = []
partial_matches = []
for node in self.kg.nodes():
node_str = str(node).lower()
if node_str == entity_lower:
exact_matches.append(node)
elif entity_lower in node_str or node_str in entity_lower:
partial_matches.append(node)
# 优先返回精确匹配
if exact_matches:
return exact_matches
else:
return partial_matches[:5] # 最多返回5个部分匹配
@@ -0,0 +1,17 @@
"""工具函数模块"""
from .config import Config, load_config
from .llm_client import LLMClient, LLMProvider
from .file_utils import save_json, load_json, save_text
__all__ = [
"Config",
"load_config",
"LLMClient",
"LLMProvider",
"save_json",
"load_json",
"save_text",
]
@@ -0,0 +1,52 @@
"""
配置管理模块
"""
import os
from pathlib import Path
from typing import Optional
from dotenv import load_dotenv
# 加载.env文件
env_path = Path(__file__).parent.parent.parent / ".env"
if env_path.exists():
load_dotenv(env_path)
class Config:
"""配置类"""
# API Keys
OPENAI_API_KEY: Optional[str] = os.getenv("OPENAI_API_KEY")
ANTHROPIC_API_KEY: Optional[str] = os.getenv("ANTHROPIC_API_KEY")
DASHSCOPE_API_KEY: Optional[str] = os.getenv("DASHSCOPE_API_KEY")
VOLCENGINE_ACCESS_KEY: Optional[str] = os.getenv("VOLCENGINE_ACCESS_KEY")
VOLCENGINE_SECRET_KEY: Optional[str] = os.getenv("VOLCENGINE_SECRET_KEY")
ZHIPUAI_API_KEY: Optional[str] = os.getenv("ZHIPUAI_API_KEY")
SILICONFLOW_API_KEY: Optional[str] = os.getenv("SILICONFLOW_API_KEY")
SILICONFLOW_API_BASE: Optional[str] = os.getenv("SILICONFLOW_API_BASE", "https://api.siliconflow.cn/v1")
# 路径配置
DATA_DIR: str = os.getenv("DATA_DIR", "../data")
OUTPUT_DIR: str = os.getenv("OUTPUT_DIR", "./output")
# 日志配置
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
# HanLP配置
HANLP_MODEL_PATH: Optional[str] = os.getenv("HANLP_MODEL_PATH")
# LLM配置
TEMPERATURE: float = float(os.getenv("TEMPERATURE", "0.4"))
FREQUENCY_PENALTY: float = float(os.getenv("FREQUENCY_PENALTY", "0.6"))
PRESENCE_PENALTY: float = float(os.getenv("PRESENCE_PENALTY", "0.6"))
# 文本处理配置
MAX_TEXTUNIT_LENGTH: int = int(os.getenv("MAX_TEXTUNIT_LENGTH", "500"))
def load_config() -> Config:
"""加载配置"""
return Config()
@@ -0,0 +1,58 @@
"""
文件工具函数
"""
import json
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional
logger = logging.getLogger(__name__)
def save_json(data: Any, filepath: str, indent: int = 2, ensure_ascii: bool = False):
"""
保存数据为JSON文件
Args:
data: 要保存的数据
filepath: 文件路径
indent: JSON缩进
ensure_ascii: 是否确保ASCII编码
"""
path = Path(filepath)
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=ensure_ascii, indent=indent)
logger.info(f"数据已保存到: {filepath}")
def load_json(filepath: str) -> Any:
"""
从JSON文件加载数据
Args:
filepath: 文件路径
Returns:
加载的数据
"""
with open(filepath, "r", encoding="utf-8") as f:
return json.load(f)
def save_text(text: str, filepath: str):
"""保存文本到文件"""
path = Path(filepath)
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
f.write(text)
logger.info(f"文本已保存到: {filepath}")
@@ -0,0 +1,219 @@
"""
LLM客户端封装
支持多种LLM提供商:OpenAI, Anthropic, Qwen, Doubao, GLM, SiliconFlow
"""
import asyncio
import logging
from typing import List, Dict, Optional, Any
from enum import Enum
from tenacity import retry, stop_after_attempt, wait_exponential
from .config import Config
logger = logging.getLogger(__name__)
class LLMProvider(str, Enum):
"""LLM提供商枚举"""
OPENAI = "openai"
ANTHROPIC = "anthropic"
QWEN = "qwen"
DOUBAO = "doubao"
GLM = "glm"
SILICONFLOW = "siliconflow"
class LLMClient:
"""LLM客户端,统一封装不同提供商的API"""
def __init__(
self,
provider: LLMProvider,
model: str,
config: Optional[Config] = None,
):
"""
初始化LLM客户端
Args:
provider: LLM提供商
model: 模型名称
config: 配置对象
"""
self.provider = provider
self.model = model
self.config = config or Config()
# 初始化对应的客户端
self._init_client()
def _init_client(self):
"""初始化对应提供商的客户端"""
if self.provider == LLMProvider.OPENAI:
try:
import openai
self.client = openai.OpenAI(api_key=self.config.OPENAI_API_KEY)
except ImportError:
raise ImportError("请安装openai包: pip install openai")
elif self.provider == LLMProvider.ANTHROPIC:
try:
import anthropic
self.client = anthropic.Anthropic(api_key=self.config.ANTHROPIC_API_KEY)
except ImportError:
raise ImportError("请安装anthropic包: pip install anthropic")
elif self.provider == LLMProvider.QWEN:
try:
import dashscope
dashscope.api_key = self.config.DASHSCOPE_API_KEY
self.client = dashscope
except ImportError:
raise ImportError("请安装dashscope包: pip install dashscope")
elif self.provider == LLMProvider.DOUBAO:
try:
from volcengine.maas import MaasService, MaasException, ChatRole
self.client = MaasService(
"cn-beijing",
access_key=self.config.VOLCENGINE_ACCESS_KEY,
secret_key=self.config.VOLCENGINE_SECRET_KEY,
)
except ImportError:
raise ImportError("请安装volcengine-python-sdk包")
elif self.provider == LLMProvider.GLM:
try:
import zhipuai
zhipuai.api_key = self.config.ZHIPUAI_API_KEY
self.client = zhipuai
except ImportError:
raise ImportError("请安装zhipuai包: pip install zhipuai")
elif self.provider == LLMProvider.SILICONFLOW:
try:
import openai
# 硅基流动兼容OpenAI API格式
self.client = openai.OpenAI(
api_key=self.config.SILICONFLOW_API_KEY,
base_url=self.config.SILICONFLOW_API_BASE or "https://api.siliconflow.cn/v1"
)
except ImportError:
raise ImportError("请安装openai包: pip install openai")
else:
raise ValueError(f"不支持的LLM提供商: {self.provider}")
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=4, max=10)
)
async def chat_async(
self,
messages: List[Dict[str, str]],
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
) -> str:
"""
异步聊天接口
Args:
messages: 消息列表
temperature: 温度参数
max_tokens: 最大token数
Returns:
模型响应文本
"""
temperature = temperature or self.config.TEMPERATURE
try:
if self.provider == LLMProvider.OPENAI:
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
)
return response.choices[0].message.content
elif self.provider == LLMProvider.ANTHROPIC:
response = await self.client.messages.create(
model=self.model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens or 4096,
)
return response.content[0].text
elif self.provider == LLMProvider.QWEN:
response = self.client.Generation.call(
model=self.model,
messages=messages,
temperature=temperature,
)
return response.output.text
elif self.provider == LLMProvider.DOUBAO:
# 字节跳动API同步调用
response = self.client.chat(
model=self.model,
messages=messages,
parameters={
"temperature": temperature,
"max_tokens": max_tokens or 4096,
}
)
return response.get("choices", [{}])[0].get("message", {}).get("content", "")
elif self.provider == LLMProvider.GLM:
response = self.client.model_api.invoke(
model=self.model,
messages=messages,
temperature=temperature,
)
return response.get("data", {}).get("choices", [{}])[0].get("message", {}).get("content", "")
elif self.provider == LLMProvider.SILICONFLOW:
# 硅基流动兼容OpenAI API格式
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
)
return response.choices[0].message.content
except Exception as e:
logger.error(f"LLM调用失败 ({self.provider}): {e}")
raise
def chat(
self,
messages: List[Dict[str, str]],
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
) -> str:
"""
同步聊天接口(内部调用异步接口)
Args:
messages: 消息列表
temperature: 温度参数
max_tokens: 最大token数
Returns:
模型响应文本
"""
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop.run_until_complete(
self.chat_async(messages, temperature, max_tokens)
)
+75
View File
@@ -0,0 +1,75 @@
# 测试文件说明
本目录包含所有测试脚本。
## 测试文件列表
### 1. `test_basic.py`
基础功能测试,包括:
- 模块导入测试
- 本体模型测试
- Prompt模板测试
运行方式:
```bash
uv run python tests/test_basic.py
```
### 2. `test_siliconflow.py`
测试硅基流动API配置,验证API密钥和连接是否正常。
运行方式:
```bash
uv run python tests/test_siliconflow.py
```
### 3. `test_documents.py`
测试文档解析功能,包括:
- 文档解析(.docx文件)
- TextUnit切分
- 不调用LLM,仅测试解析逻辑
运行方式:
```bash
uv run python tests/test_documents.py
```
### 4. `test_model.py`
测试硅基流动平台上的多个模型,找出可用模型。
运行方式:
```bash
uv run python tests/test_model.py
```
### 5. `test_my_models.py`
测试项目配置使用的两个指定模型:
- `deepseek-ai/DeepSeek-R1-Distill-Qwen-7B`
- `Qwen/Qwen2.5-7B-Instruct`
运行方式:
```bash
uv run python tests/test_my_models.py
```
### 6. `test_quick.py`
快速测试完整流程,处理少量TextUnit(1个文档的前2个),验证:
- 文档解析
- 实体识别(NER
- 关系抽取(RE
- 知识图谱构建
- 社区检测
运行方式:
```bash
uv run python tests/test_quick.py
```
## 注意事项
- 测试脚本需要使用配置的API密钥(`.env`文件)
- 部分测试会调用LLM API,可能需要一些时间
- 确保已安装所有依赖包:`uv sync`
@@ -0,0 +1,72 @@
"""
基础功能测试
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
def test_imports():
"""测试模块导入"""
try:
from src.preprocessing.text_processor import TextProcessor
from src.preprocessing.document_parser import DocumentParser
from src.ontology.schema import OntologySchema
from src.prompts.ner_prompts import NERPromptBuilder
from src.extraction.ner import NERExtractor
from src.kg_builder.graph import KnowledgeGraph
print("✅ 所有模块导入成功")
return True
except ImportError as e:
print(f"❌ 模块导入失败: {e}")
return False
def test_ontology():
"""测试本体模型"""
try:
from src.ontology.schema import OntologySchema
schema = OntologySchema()
summary = schema.get_schema_summary()
assert summary["entity_types"] == 9
assert summary["relation_types"] == 8
print("✅ 本体模型测试通过")
return True
except Exception as e:
print(f"❌ 本体模型测试失败: {e}")
return False
def test_prompt_template():
"""测试Prompt模板"""
try:
from src.prompts.template import PromptTemplate
template = PromptTemplate(
task_description={"system": "test", "user": "test"},
candidate_targets={"entity": "definition"},
task_examples=[{"input": "test", "output": "test"}],
task_emphasis="test",
)
assert template.task_description is not None
print("✅ Prompt模板测试通过")
return True
except Exception as e:
print(f"❌ Prompt模板测试失败: {e}")
return False
if __name__ == "__main__":
print("开始运行基础测试...")
test_imports()
test_ontology()
test_prompt_template()
print("测试完成!")
@@ -0,0 +1,100 @@
#!/usr/bin/env python
"""
测试文档解析(仅测试3个文件,不调用LLM)
"""
import sys
import json
import logging
from pathlib import Path
# 修复:移除conda环境的路径,确保使用虚拟环境的包
sys.path = [p for p in sys.path if 'anaconda3' not in p.lower() and 'conda' not in p.lower()]
sys.path.insert(0, str(Path(__file__).parent.parent))
from src.preprocessing.document_parser import DocumentParser
from src.utils.config import load_config
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def main():
"""测试文档解析"""
logger.info("=" * 60)
logger.info("测试文档解析(3个文件)")
logger.info("=" * 60)
# 配置
config = load_config()
data_dir = Path("../data/1法律")
output_dir = Path("./output")
output_dir.mkdir(parents=True, exist_ok=True)
# 解析文档
logger.info(f"解析目录: {data_dir}")
parser = DocumentParser()
docs = parser.parse_directory(str(data_dir))
# 只处理前3个
docs = docs[:3]
logger.info(f"处理 {len(docs)} 个文档")
# 切分为TextUnit
logger.info("切分文档为TextUnit...")
all_textunits = []
for i, doc in enumerate(docs, 1):
logger.info(f"处理文档 {i}/{len(docs)}: {Path(doc.get('file_path', '')).name}")
textunits = parser.split_into_textunits(
doc,
max_length=config.MAX_TEXTUNIT_LENGTH
)
for tu in textunits:
tu["doc_id"] = doc.get("file_path", "")
tu["id"] = f"{doc.get('file_path', '')}_{tu.get('paragraph_index', 0)}"
all_textunits.extend(textunits)
logger.info(f" - 生成 {len(textunits)} 个TextUnit")
logger.info(f"共生成 {len(all_textunits)} 个TextUnit")
# 保存结果
output_file = output_dir / "test_textunits.json"
result = {
"num_docs": len(docs),
"num_textunits": len(all_textunits),
"docs": [
{
"file_path": doc.get("file_path", ""),
"title": doc.get("title", ""),
"text_length": len(doc.get("text", "")),
}
for doc in docs
],
"sample_textunits": [
{
"id": tu.get("id", ""),
"text": tu.get("text", "")[:200] + "..." if len(tu.get("text", "")) > 200 else tu.get("text", ""),
"paragraph_index": tu.get("paragraph_index", 0),
}
for tu in all_textunits[:10] # 只保存前10个作为示例
]
}
with open(output_file, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
logger.info("=" * 60)
logger.info("测试完成!")
logger.info(f"结果已保存到: {output_file}")
logger.info(f"统计信息:")
logger.info(f" - 文档数: {result['num_docs']}")
logger.info(f" - TextUnit数: {result['num_textunits']}")
logger.info("=" * 60)
if __name__ == "__main__":
main()
@@ -0,0 +1,73 @@
#!/usr/bin/env python
"""
测试硅基流动API并找到可用的模型
"""
import sys
import os
from pathlib import Path
sys.path = [p for p in sys.path if 'anaconda3' not in p.lower() and 'conda' not in p.lower()]
sys.path.insert(0, str(Path(__file__).parent.parent))
from src.utils.config import load_config
from src.utils.llm_client import LLMClient, LLMProvider
# 常见的硅基流动模型名称
TEST_MODELS = [
"deepseek-ai/DeepSeek-V2.5",
"deepseek-ai/DeepSeek-V2",
"Qwen/Qwen2.5-72B-Instruct",
"Qwen/Qwen2.5-72B-Chat",
"meta-llama/Llama-3.1-70B-Instruct",
"01-ai/Yi-1.5-34B-Chat",
"mistralai/Mistral-7B-Instruct-v0.2",
]
def test_model(provider, model_name):
"""测试单个模型"""
try:
config = load_config()
client = LLMClient(provider=provider, model=model_name, config=config)
messages = [{"role": "user", "content": "你好"}]
response = client.chat(messages)
print(f"✅ 模型 {model_name} 可用")
print(f" 响应: {response[:50]}...")
return True
except Exception as e:
print(f"❌ 模型 {model_name} 不可用: {str(e)[:100]}")
return False
if __name__ == "__main__":
print("=" * 60)
print("测试硅基流动模型")
print("=" * 60)
print()
config = load_config()
if not config.SILICONFLOW_API_KEY:
print("❌ SILICONFLOW_API_KEY 未配置")
sys.exit(1)
print(f"API密钥: {config.SILICONFLOW_API_KEY[:20]}...")
print(f"API Base: {config.SILICONFLOW_API_BASE}")
print()
# 测试所有模型
available_models = []
for model in TEST_MODELS:
if test_model(LLMProvider.SILICONFLOW, model):
available_models.append(model)
print()
print("=" * 60)
if available_models:
print(f"✅ 找到 {len(available_models)} 个可用模型:")
for model in available_models:
print(f" - {model}")
print(f"\n建议使用的模型: {available_models[0]}")
else:
print("❌ 没有找到可用的模型,请检查API密钥或模型名称")
print("=" * 60)
@@ -0,0 +1,61 @@
#!/usr/bin/env python
"""
测试用户指定的两个模型
"""
import sys
from pathlib import Path
sys.path = [p for p in sys.path if 'anaconda3' not in p.lower() and 'conda' not in p.lower()]
sys.path.insert(0, str(Path(__file__).parent.parent))
from src.utils.llm_client import LLMClient, LLMProvider
from src.utils.config import load_config
def test_model(provider, model_name):
"""测试单个模型"""
try:
config = load_config()
client = LLMClient(provider=provider, model=model_name, config=config)
messages = [{"role": "user", "content": "你好"}]
response = client.chat(messages)
print(f"✅ 模型 {model_name} 可用")
print(f" 响应: {response[:50]}...")
return True
except Exception as e:
print(f"❌ 模型 {model_name} 不可用: {str(e)[:100]}")
return False
if __name__ == "__main__":
print("=" * 60)
print("测试指定的两个模型")
print("=" * 60)
print()
config = load_config()
if not config.SILICONFLOW_API_KEY:
print("❌ SILICONFLOW_API_KEY 未配置")
sys.exit(1)
# 测试用户指定的两个模型
models_to_test = [
"deepseek-ai/DeepSeek-R1-Distill-Qwen-7B",
"Qwen/Qwen2.5-7B-Instruct",
]
available = []
for model in models_to_test:
if test_model(LLMProvider.SILICONFLOW, model):
available.append(model)
print()
print("=" * 60)
if len(available) == len(models_to_test):
print(f"✅ 所有模型都可用!")
elif available:
print(f"⚠️ 部分模型可用: {len(available)}/{len(models_to_test)}")
else:
print("❌ 没有可用模型")
print("=" * 60)
@@ -0,0 +1,57 @@
#!/usr/bin/env python
"""
快速测试:只处理1个文档的前2个TextUnit
"""
import sys
import logging
from pathlib import Path
sys.path = [p for p in sys.path if 'anaconda3' not in p.lower() and 'conda' not in p.lower()]
sys.path.insert(0, str(Path(__file__).parent.parent))
from src.preprocessing.document_parser import DocumentParser
from src.kg_builder.indexer import GraphIndexer
from src.utils.config import load_config
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def main():
logger.info("=" * 60)
logger.info("快速测试:处理1个文档的前2个TextUnit")
logger.info("=" * 60)
config = load_config()
# 解析1个文档
parser = DocumentParser()
doc_path = "../data/1法律/10-中华人民共和国乡村振兴促进法.docx"
doc = parser.parse_docx(doc_path)
# 切分为TextUnit,只取前2个
textunits = parser.split_into_textunits(doc, max_length=config.MAX_TEXTUNIT_LENGTH)[:2]
for tu in textunits:
tu["doc_id"] = doc.get("file_path", "")
tu["id"] = f"{doc.get('file_path', '')}_{tu.get('paragraph_index', 0)}"
logger.info(f"准备处理 {len(textunits)} 个TextUnit")
# 构建知识图谱(只处理这2个)
logger.info("开始构建知识图谱...")
indexer = GraphIndexer(config=config)
kg_graph = indexer.index(textunits, use_verification=True)
logger.info("=" * 60)
logger.info("快速测试完成!")
logger.info(f"节点数: {kg_graph.number_of_nodes()}")
logger.info(f"边数: {kg_graph.number_of_edges()}")
logger.info("=" * 60)
if __name__ == "__main__":
main()
@@ -0,0 +1,81 @@
#!/usr/bin/env python
"""
测试硅基流动API配置
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from src.utils.llm_client import LLMClient, LLMProvider
from src.utils.config import load_config
def test_siliconflow():
"""测试硅基流动API"""
print("=" * 60)
print("测试硅基流动API配置")
print("=" * 60)
# 加载配置
config = load_config()
# 检查API密钥
if not config.SILICONFLOW_API_KEY:
print("❌ 错误: SILICONFLOW_API_KEY 未配置")
print("请在 .env 文件中设置 SILICONFLOW_API_KEY")
return False
print(f"✅ API密钥已配置: {config.SILICONFLOW_API_KEY[:20]}...")
print(f"✅ API Base URL: {config.SILICONFLOW_API_BASE}")
# 创建客户端(使用一个常见的模型,用户需要根据实际可用模型调整)
print("\n尝试创建客户端...")
try:
# 注意:这里使用的模型名称需要根据硅基流动平台实际可用模型调整
# 常用的模型包括:Qwen/Qwen2.5-72B-Instruct, meta-llama/Llama-3.1-70B-Instruct 等
client = LLMClient(
provider=LLMProvider.SILICONFLOW,
model="Qwen/Qwen2.5-72B-Instruct", # 请根据实际情况修改模型名称
config=config
)
print("✅ 客户端创建成功")
except Exception as e:
print(f"❌ 客户端创建失败: {e}")
return False
# 测试简单对话
print("\n测试API调用...")
messages = [
{"role": "user", "content": "请用一句话介绍知识图谱。"}
]
try:
response = client.chat(messages)
print(f"✅ API调用成功")
print(f"\n响应内容:\n{response}")
return True
except Exception as e:
print(f"❌ API调用失败: {e}")
print("\n提示:")
print("1. 请检查API密钥是否正确")
print("2. 请检查网络连接")
print("3. 请确认模型名称是否在硅基流动平台可用")
print("4. 可以访问 https://siliconflow.cn/ 查看可用模型列表")
return False
if __name__ == "__main__":
success = test_siliconflow()
if success:
print("\n" + "=" * 60)
print("✅ 硅基流动API配置测试通过!")
print("=" * 60)
else:
print("\n" + "=" * 60)
print("❌ 硅基流动API配置测试失败,请检查配置")
print("=" * 60)
sys.exit(1)
+2379
View File
File diff suppressed because it is too large Load Diff