M0nkeyFl0wer/investigative-journalism-kg
GitHub: M0nkeyFl0wer/investigative-journalism-kg
面向调查记者的隐私优先知识图谱工具包,可在本地完成文档摄取、实体关系提取、图谱构建和结构缺口分析。
Stars: 0 | Forks: 0
# open-newsroom-graph
面向调查记者的隐私优先知识图谱工具包。摄取文档、提取实体和关系、构建可搜索图谱,并发现结构缺口以寻找线索。完全在笔记本电脑上运行。
**无需云端。无需账号。数据不会离开您的设备。**
## 这是做什么的
您有一堆文档——法庭文件、公司注册信息、泄露的电子邮件、公共记录。您需要找到关联,更重要的是,找到*缺失*的内容。
此工具包可以:
1. **摄取**您的文档(PDF、文本、markdown、HTML)
2. **提取**人物、组织、交易及其之间的关系
3. **构建**本地可搜索的知识图谱
4. **分析**图谱结构以发现缺口、矛盾和出人意料的关联
5. **简报**每天生成一份 markdown 摘要,总结图谱发现的内容
## 快速开始
### 前置条件
- **Python 3.10 或更高版本**(检查:`python3 --version`)
- **[Ollama](https://ollama.com)** 已安装并运行(在本地处理所有 AI)
- **基本熟悉命令行**(所有操作通过终端运行)
### 设置
```
# 克隆仓库
git clone https://github.com/M0nkeyFl0wer/open-newsroom-graph.git
cd open-newsroom-graph
# 运行设置(安装 Python 包 + 下载本地 AI 模型)
bash setup.sh
# 验证一切正常
python -m newsroom_graph.check
```
您应该看到:
```
open-newsroom-graph system check
========================================
LadybugDB: 0.15.3
PyArrow: 23.0.1
spaCy: 3.8.14
spaCy model: en_core_web_sm OK
NetworkX: 3.6.1
Ripser: not installed (optional, pip install ripser)
Ollama: OK (2 models)
Embedding model: nomic-embed-text OK
Ontology: Ontology(8 entity types, 14 edge types)
All checks passed.
```
如果任何内容显示 NOT INSTALLED 或 MISSING,检查会告诉您具体需要运行什么。
### 摄取您的第一批文档
```
# 将文档放入摄取文件夹
cp /path/to/your/documents/*.pdf ingest/
cp /path/to/your/documents/*.txt ingest/
# 运行摄取
python scripts/ingest_folder.py
```
输出类似于:
```
Found 3 documents to ingest.
[1/3] harbor-city-expose.txt
Extracted: 27 entities, 13 edges
Embedded: 2 chunks
[2/3] property-records.md
Extracted: 21 entities, 8 edges
Embedded: 2 chunks
[3/3] financial-disclosure.html
Extracted: 32 entities, 8 edges
Embedded: 2 chunks
Bulk loading 80 entities...
Loaded: 80
Computing entity embeddings...
Loading 29 edges...
Loaded: 29
==================================================
Ingestion complete in 99.3s.
Documents processed: 3
Total entities: 80
Total edges: 28
Total documents: 3
```
### 搜索图谱
```
# 关键词搜索 — 查找精确匹配
python scripts/search_cli.py -q "Acme Corp"
# 语义搜索 — 即使没有关键词匹配也能找到相关内容
python scripts/search_cli.py -q "payments to contractors" --mode semantic
# 混合搜索 — 结合关键词和语义,两全其美
python scripts/search_cli.py -q "financial fraud" --mode hybrid
# 查找两个实体之间的联系
python scripts/search_cli.py --path "Jane Smith" "Harbor Development LLC"
# 按实体类型筛选
python scripts/search_cli.py -q "Chen" --type person
```
**路径搜索**显示关系链:
```
Found 3 paths:
Path 1 (confidence: 0.36):
Chen --[EMPLOYED_BY]--> Brightpath Advisors --[FUNDED_BY]--> Meridian Holdings LLC
Path 2 (confidence: 0.22):
Chen --[EMPLOYED_BY]--> Brightpath Advisors --[FUNDED_BY]--> Harbor City
Redevelopment Authority --[OCCURRED_ON]--> Meridian Holdings LLC
```
每一跳都有一个置信度分数。路径置信度是所有跳的乘积——置信度越低意味着连接越不确定。
### 运行分析
```
python scripts/run_analysis.py
```
输出:
```
TOPOLOGY REPORT
============================================================
Entities: 80
Edges: 28
Connected components: 57
Largest component: 11 nodes
Communities (Louvain): 58
STRUCTURAL GAPS: 3
------------------------------------------------------------
[HIGH] Brightpath Advisors ↔ Harbor City Redevelopment Authority
7 entities ↔ 7 entities | cross-edges: 0
→ How do Brightpath Advisors and Harbor City Redevelopment
Authority relate? Your knowledge about these is not yet connected.
SURPRISING CONNECTIONS: 6
------------------------------------------------------------
Robert Chen (person)
Betweenness: 0.5111 | Degree: 4
→ Structurally important despite low frequency
```
**结构缺口**是调查线索——应该合理连接但您的数据中没有连接的实体群落。这就是缺失文档所在。
**出人意料的关联**是具有高介数中心性(它们桥接原本分离的网络)但低度数(它们没有出现在很多文档中)的实体。这些是安静的连接者。
### 每日简报
```
python scripts/daily_briefing.py
```
生成 `briefings/2026-04-04.md`——一份 markdown 摘要,包括:
- 最近 24 小时内添加的新实体
- 来源之间发现的矛盾
- 结构缺口(作为调查问题)
- 出人意料的连接者
- 需要关注的未链接实体
如果您配置了 Obsidian 保险库路径,简报会自动复制到那里。
### 本体论健康
```
python scripts/validate_ontology.py
```
显示您的本体论与现实的匹配程度:
```
ICR (type coverage): 0.75 — warning (some declared types have no data)
CI (class imbalance): 0.34 — warning (dominant: organization at 27/80)
IPR (edge coverage): 0.57 — warning
Type distribution:
organization 27 ( 33.8%) ████████████████
transaction 25 ( 31.2%) ███████████████
event 16 ( 20.0%) ██████████
person 9 ( 11.2%) █████
Unpopulated types: asset, claim
```
- **ICR**(实例化类比率):您声明的类型中有多少比例有实际数据。低于 0.8 意味着某些类型是死模式。
- **CI**(类不平衡):如果一个类型占主导地位(高于 0.5),您的提取可能正在错误分类实体。
- **IPR**(实例化属性比率):与 ICR 相同,但针对边类型。
## 七个阶段
### 1. 本体论——什么很重要
编辑 `ONTOLOGY.md` 为您的报道领域定义实体类型和关系类型。附带一个涵盖 8 种实体类型和 14 种边类型的一般调查新闻本体论。
系统在写入时**拒绝与本体论不匹配的实体**——不会积累垃圾。拒绝会被计数和报告,因此您知道何时需要扩展本体论。
每种类型都包含边界示例:
| 列 | 目的 |
|--------|---------|
| 典型 | 明确属于此类型 |
| 非典型 | 仍属于此类型的边缘情况 |
| 外部典型 | 看起来相似但*不属于*——显示它*属于*哪种类型 |
外部类型示例可以防止最常见的提取错误:所有内容都被放入一个包罗万象的类型中。
### 2. 嵌入——语义理解
文档被分块(1000 个字符,200 个字符重叠)并使用本地 AI 模型(Ollama + nomic-embed-text)转换为 768 维向量。这些支持语义搜索——按含义而非关键词查找文档。
嵌入直接存储在图数据库中作为 `FLOAT[768]` 列。不需要单独的向量数据库。一个数据库包含一切。
### 3. 提取——三阶段实体提取
每个文档都经过三个提取阶段:
**阶段 1——确定性**(即时、免费、始终运行):
- 日期、美元金额、电子邮件地址的正则表达式模式
- 从文档格式中结构化提取
- 置信度:0.85-0.90(高——这些是模式匹配)
**阶段 2——spaCy NER**(快速、本地、无需 GPU):
- 命名实体识别:人物、组织、地点
- 将 spaCy 标签映射到您的本体论类型(PERSON → person、ORG → organization 等)
- 置信度:0.70(好——NER 已成熟)
**阶段 3——LLM**(较慢、通过 Ollama 本地运行):
- 关系提取:谁与谁有关联,如何关联
- 类型细化:使用本体论上下文纠正阶段 2 的错误分类
- 生成带类型的边(EMPLOYED_BY、FUNDED_BY、CONTRACTED_WITH 等)
- 置信度:0.60(较低——LLM 提取需要人工审查)
- 受本体论约束:LLM 提示包含所有类型和边界示例
每个实体都记录其 `provenance`(哪个阶段提取的)和 `source_url`(来自哪个文档)。没有记录来源的内容不会进入图谱。
### 4. 质量控制——本体论验证
每个实体在进入图谱之前都会根据 `ONTOLOGY.md` 进行验证。如果提取管道产生一个类型为"weapon"的实体,但您的本体论不包含"weapon",它将被拒绝并且拒绝会被计数。
摄取后,您会看到:
```
Ontology rejections (types not in ONTOLOGY.md):
weapon: 3 rejections
vehicle: 2 rejections
Tip: Consider adding frequently rejected types to ONTOLOGY.md
```
这告诉您何时需要扩展本体论——数据正在告诉您它包含哪些类型。
### 5. 搜索和路径——追踪关联
三种搜索模式:
| 模式 | 工作方式 | 适用于 |
|------|-------------|----------|
| `keyword` | Cypher `CONTAINS` 匹配实体标签 | 按名称查找特定实体 |
| `semantic` | 查询嵌入与实体嵌入之间的余弦相似度 | 不知道名称时查找相关实体 |
| `hybrid` | 互惠秩融合(RRF)——按两个列表中的位置排名,无需权重调整 | 最佳通用搜索 |
**路径搜索**找到实体之间的类型化链。不只是"这些有关联",而是:
```
Jane Smith --[EMPLOYED_BY]--> Acme Corp --[CONTRACTED_WITH]--> Harbor Dev LLC
```
每条路径都有一个置信度分数(边置信度的乘积)。同一实体之间的多条路径通常意味着更强的关联。
### 6. 拓扑——发现缺失的内容
图分析运行确定性算法——没有 AI,只是对图结构进行数学运算:
| 算法 | 发现什么 | 为什么重要 |
|-----------|--------------|----------------|
| 连通分量 | 图中的独立集群 | 显示哪些调查是孤立的 |
| Louvain 社区 | 密集子群 | 自然的话题聚类 |
| 介数中心性 | 桥接社区的实体 | 安静的连接者——连接原本分离网络的人/组织 |
| 桥接检测 | 单点故障边 | 脆弱的连接,如果移除就会断裂 |
| 缺口检测 | 跨边较少的社区对 | **线索**——您的调查缺少什么 |
| 持续同调 | 图中的拓扑孔 | 更高阶的结构缺口(需要 Ripser) |
**缺口就是故事。** 两个大型社区之间没有跨边意味着您的文档覆盖了两个相关领域,但您还没有将它们连接起来。缺口问题告诉您下一步要找什么。
### 7. 每日简报——图谱发现了什么
仅从图结构生成的 markdown 文件。包含以下部分:
- **新实体**:过去 24 小时内添加的内容
- **矛盾**:来自不同来源的声明之间的 `CONTRADICTS` 边
- **结构缺口**:连接性低的社区对(作为调查问题)
- **出人意料的关联**:低度数实体上的高介数
- **未的实体**:超过 7 天且没有连接的实体(需要关注或删除)
可在 Obsidian、任何文本编辑器或终端中阅读。可选地自动复制到您的 Obsidian 保险库收件箱。
## 隐私
### 本地模式(默认)
一切都在您的机器上运行。没有网络连接。没有 API 密钥。没有账号。
| 组件 | 运行位置 |
|-----------|--------------|
| 文档文本 | 保留在磁盘上 |
| 实体提取 | 在您的 CPU/GPU 上运行 Ollama |
| 嵌入 | 在您的 CPU/GPU 上运行 Ollama |
| 知识图谱 | 磁盘上的 LadybugDB 目录 |
| 向量搜索 | LadybugDB 原生(同一数据库) |
| 分析 | Python (NetworkX) 在您的 CPU 上运行 |
| 每日简报 | 写入磁盘 |
**何时使用:** 敏感来源、泄露的文档、任何您不愿冒险传输的内容。
### 混合模式
嵌入保持在本地。实体提取可选择使用具有零数据保留(ZDR)的远程 LLM 处理非敏感文档。在复杂文档上获得更好的提取质量。
```
# 在 newsroom_graph/config.py 中
PRIVACY_MODE = "hybrid"
REMOTE_API_BASE = "https://api.anthropic.com/v1"
REMOTE_MODEL = "claude-haiku-4-5-20251001"
# 将 NEWSROOM_API_KEY 设置为环境变量 — 切勿硬编码
```
**何时使用:** 公共记录(非敏感)和机密材料(敏感)的混合。图谱、嵌入和分析始终保持在本地。
### 远程模式
一切通过远程 API。不建议用于敏感材料。
**何时使用:** 纯公共数据集的批量处理,在这种情况下速度和质量比机密性更重要。
请参阅 `docs/privacy-guide.md` 获取详细比较和提供商建议。
### 伦理:身份模糊性和来源保护
自动化提取为调查记者带来的两个风险:
**身份模糊性。** 管道会将"John Smith"、"J. Smith"和"John S. Smith"提取为三个单独的实体。它还可能将"BP US"和"British Petroleum"拆分为不同的组织。在基于图关联发布任何发现之前,**手动验证链接的实体实际上是同一个人或组织。** 自动图中的错误关联可能导致错误指控个人。`config.py` 中的去重阈值(`DEDUP_THRESHOLD = 0.92`)通过嵌入相似度捕获一些重复,但对于指代不同人的相似名称还不够。
**三角测量风险。** 结合多个数据集(公共记录 + 泄露的内部电子邮件 + 机密来源访谈)会创建一个图,其中实体的结构位置可能无意中泄露机密来源。如果您发布图的子集——即使已删除名称——来源周围独特的连接模式可能足以让对手识别谁泄露了信息。在分享任何图可视化或导出之前:
- 检查结构布局是否通过独特的关联位置泄露来源身份
- 考虑删除或泛化可追溯到机密来源的边
- 请记住,即使是聚合统计(社区成员资格、介数分数)也可以缩小候选人范围
**图谱是一种情报产品。** 以与您的来源列表相同的操作安全级别对待它。
## 配置
所有配置都在 `newsroom_graph/config.py` 中:
### 路径
```
GRAPH_DIR = Path("data/graph.lbug") # Where the graph database lives
INGEST_DIR = Path("ingest") # Where documents go for ingestion
BRIEFING_DIR = Path("briefings") # Where daily briefings are written
OBSIDIAN_VAULT = "" # Optional: Obsidian vault for briefing delivery
```
### 模型
```
EMBEDDING_MODEL = "nomic-embed-text" # Local embedding model (768 dimensions)
EMBEDDING_DIM = 768 # Must match model output dimension
LOCAL_EXTRACTION_MODEL = "llama3.2:3b" # Local LLM for entity/relationship extraction
```
默认提取模型(`llama3.2:3b`)小而快。要在速度代价下获得更好的提取质量,请尝试 `mistral`、`llama3:8b` 或 `gemma2`。
### 提取调优
```
MIN_CONFIDENCE = 0.5 # Minimum confidence to keep an entity (0.0-1.0)
MAX_ENTITIES_PER_DOC = 200 # Safety limit per document
DEDUP_THRESHOLD = 0.92 # Cosine similarity above this = likely duplicate
```
### 分析调优
```
AUTO_ANALYSIS = False # Run analysis after every ingestion
PRUNE_AGE_DAYS = 7 # Flag unlinked entities older than this
MIN_COMMUNITY_SIZE = 5 # Minimum community size for gap analysis
MAX_CROSS_EDGES_FOR_GAP = 3 # Below this = flagged as gap
TOP_BETWEENNESS = 10 # How many high-betweenness entities to report
```
### 每日简报部分
```
BRIEFING_SECTIONS = [
"new_entities", # Entities added in last 24h
"contradictions", # CONTRADICTS edges found
"structural_gaps", # Community pairs with low cross-connection
"surprising_connections", # High betweenness on low-frequency entities
"unlinked_entities", # Entities needing attention
]
```
删除部分名称以将其从简报中排除。
## 扩展本体论
编辑 `ONTOLOGY.md` 为您的报道领域添加实体类型和边类型。
**经验法则:** 只有当您看到 3 个以上不适合现有类型的实例时才添加类型。检查摄取后的拒绝日志——它会告诉您文档需要什么类型。
### 添加实体类型
在 `ONTOLOGY.md` 的实体类型表中添加一行:
```
| permit | A government permit, license, or approval | "Building permit #2024-087" | "Informal verbal approval" | "The permit office" → organization |
```
最后三列(典型、非典型、外部类型)提高提取准确性。外部类型列特别重要——它向 LLM 展示什么*不属于*此类型。
### 添加边类型
在边类型表中添加一行:
```
| ISSUED_BY | permit → organization | Government body that issued the permit | Regulatory authority, approval chain |
```
每种边类型都应该有明确的调查目的。如果您无法解释为什么一种关系对调查很重要,就不要添加它。
### 验证更改
```
python scripts/validate_ontology.py
```
这会检查:
- 所有类型在语法上有效
- 更新本体论后的图谱健康指标(ICR/CI/IPR)
- 哪些类型已填充,哪些为空
## 技术栈
全部开源。除 Ollama 外均可通过 pip 安装。
| 工具 | 版本 | 用途 | 为什么选择这个 |
|------|---------|---------|-------------|
| [LadybugDB](https://ladybugdb.com) | 0.15.3 | 图数据库 + 向量存储 | 嵌入式列式图数据库。Cypher 查询。原生的 `FLOAT[768]` 向量列,带有 `array_cosine_similarity`。无需服务器。一个目录 = 一个调查。KuzuDB 的延续。 |
| [PyArrow](https://arrow.apache.org) | 23.0+ | 批量数据加载 | LadybugDB 的 `COPY FROM` Parquet 比迭代插入快 25 倍。PyArrow 写入 Parquet 文件。 |
| [Pandas](https://pandas.pydata.org) | 3.0+ | 数据操作 | 用于 Parquet 导出前批量实体准备的数据框操作。 |
| [spaCy](https://spacy.io) | 3.8+ | NLP 提取(阶段 2) | 命名实体识别。`en_core_web_sm` 模型——小、快、足以处理人物/组织/地点。 |
| [NetworkX](https://networkx.org) | 3.6+ | 图分析 | Louvain 社区、介数中心性、桥接检测、连通分量。在提取的图上运行。 |
| [Ripser](https://ripser.scikit-tda.org) | 0.6+ | 持续同调(可选) | 发现拓扑孔——社区检测遗漏的更高阶结构缺口。 |
| [Ollama](https://ollama.com) | 0.3+ | 本地 AI 模型 | 在您的硬件上运行嵌入和提取模型。无 API 密钥。无云。 |
| [Obsidian]((b:Entity)
WHERE r.created_at <= $cutoff AND r.expired_at = 0
RETURN a.label, r.edge_type, b.label, r.created_at
ORDER BY r.created_at DESC LIMIT 20
""", parameters={"cutoff": cutoff})
```
要将关系标记为已取代(例如,证人翻供):
```
# 软过期旧声明,添加新声明
now = int(time.time())
graph.query("""
MATCH (a:Entity {label: $claim})-[r:RELATES_TO]->(b:Entity)
SET r.expired_at = $now
""", parameters={"claim": "No payments were made", "now": now})
```
旧关系保留在图中作为审计跟踪。当前查询过滤 `expired_at = 0`;历史查询取消过滤。
### 导出图谱以共享
```
# 图位于 data/graph.lbug — 复制它与同事分享
# (敏感调查仅使用加密传输)
cp -r data/graph.lbug /encrypted-usb/investigation-backup/
```
### 备份您的调查
```
# 整个调查状态位于 data/ 和 briefings/
tar czf investigation-backup-$(date +%Y%m%d).tar.gz data/ briefings/ ONTOLOGY.md
```
## 故障排除
### "Ollama: 未运行"
启动 Ollama:`ollama serve`(在后台运行)。然后拉取模型:
```
ollama pull nomic-embed-text
ollama pull llama3.2:3b
```
### "spaCy 模型: 缺失"
```
source .venv/bin/activate
python -m spacy download en_core_web_sm
```
### "LLM 提取失败: 未找到模型"
配置的模型未在 Ollama 中拉取。检查 `newsroom_graph/config.py` 中的 `LOCAL_EXTRACTION_MODEL` 并拉取它:
```
ollama pull llama3.2:3b
```
### 摄取很慢
大部分时间花在 LLM 提取(阶段 3)上。选项:
- 使用更小的模型:在 config.py 中设置 `LOCAL_EXTRACTION_MODEL = "llama3.2:1b"`
- 完全跳过阶段 3 进行批量导入,稍后重新运行
- 如果您有 GPU,Ollama 会自动使用它
### 某一种类型的实体太多
检查 `validate_ontology.py` 输出。如果 CI(类不平衡)高于 0.5,一种类型正在捕获一切。修复方法:
1. 为主导类型在 ONTOLOGY.md 中添加更好的外部类型示例
2. 添加主导类型正在吸收的新类型
### PDF 摄取不起作用
安装 `pdftotext`:
```
# Ubuntu/Debian
sudo apt install poppler-utils
# macOS
brew install poppler
```
## 文件参考
```
open-newsroom-graph/
├── ONTOLOGY.md # Entity and edge type definitions (you edit this)
├── README.md # This file
├── LICENSE # MIT
├── requirements.txt # Python dependencies
├── setup.sh # One-command setup script
├── newsroom_graph/
│ ├── __init__.py # Package init (version 0.1.0)
│ ├── config.py # All configuration (paths, models, thresholds)
│ ├── ontology.py # ONTOLOGY.md parser + write-time validator
│ ├── graph.py # LadybugDB wrapper (schema, CRUD, vector search, bulk load)
│ ├── embed.py # Ollama embedding wrapper (single + batch)
│ ├── extract.py # Three-phase extraction pipeline
│ ├── topology.py # NetworkX graph analysis
│ ├── briefing.py # Daily briefing markdown generator
│ ├── queries.py # All Cypher query patterns (centralized)
│ └── check.py # Dependency verification
├── scripts/
│ ├── ingest_folder.py # Main entry point: documents → graph
│ ├── search_cli.py # Search: keyword, semantic, hybrid, path
│ ├── run_analysis.py # Topology analysis with report output
│ ├── daily_briefing.py # Generate daily briefing markdown
│ └── validate_ontology.py # Ontology health check (ICR/CI/IPR)
├── docs/
│ └── privacy-guide.md # Detailed privacy mode comparison
├── data/ # Graph database (gitignored)
├── ingest/ # Drop documents here (gitignored)
└── briefings/ # Generated briefings (gitignored)
```
## 贡献
欢迎提交 issue 和 PR。保持简单——这是记者的工具,不是开发者的框架。
## 许可证
MIT
## 联系
由 [Ben West](https://benwest.blog) 构建。如果你想帮助你的新闻room 设置它,请通过 [benwest.bsky.social](https://bsky.app/profile/benwest.bsky.social) 联系。
## 这是做什么的
您有一堆文档——法庭文件、公司注册信息、泄露的电子邮件、公共记录。您需要找到关联,更重要的是,找到*缺失*的内容。
此工具包可以:
1. **摄取**您的文档(PDF、文本、markdown、HTML)
2. **提取**人物、组织、交易及其之间的关系
3. **构建**本地可搜索的知识图谱
4. **分析**图谱结构以发现缺口、矛盾和出人意料的关联
5. **简报**每天生成一份 markdown 摘要,总结图谱发现的内容
## 快速开始
### 前置条件
- **Python 3.10 或更高版本**(检查:`python3 --version`)
- **[Ollama](https://ollama.com)** 已安装并运行(在本地处理所有 AI)
- **基本熟悉命令行**(所有操作通过终端运行)
### 设置
```
# 克隆仓库
git clone https://github.com/M0nkeyFl0wer/open-newsroom-graph.git
cd open-newsroom-graph
# 运行设置(安装 Python 包 + 下载本地 AI 模型)
bash setup.sh
# 验证一切正常
python -m newsroom_graph.check
```
您应该看到:
```
open-newsroom-graph system check
========================================
LadybugDB: 0.15.3
PyArrow: 23.0.1
spaCy: 3.8.14
spaCy model: en_core_web_sm OK
NetworkX: 3.6.1
Ripser: not installed (optional, pip install ripser)
Ollama: OK (2 models)
Embedding model: nomic-embed-text OK
Ontology: Ontology(8 entity types, 14 edge types)
All checks passed.
```
如果任何内容显示 NOT INSTALLED 或 MISSING,检查会告诉您具体需要运行什么。
### 摄取您的第一批文档
```
# 将文档放入摄取文件夹
cp /path/to/your/documents/*.pdf ingest/
cp /path/to/your/documents/*.txt ingest/
# 运行摄取
python scripts/ingest_folder.py
```
输出类似于:
```
Found 3 documents to ingest.
[1/3] harbor-city-expose.txt
Extracted: 27 entities, 13 edges
Embedded: 2 chunks
[2/3] property-records.md
Extracted: 21 entities, 8 edges
Embedded: 2 chunks
[3/3] financial-disclosure.html
Extracted: 32 entities, 8 edges
Embedded: 2 chunks
Bulk loading 80 entities...
Loaded: 80
Computing entity embeddings...
Loading 29 edges...
Loaded: 29
==================================================
Ingestion complete in 99.3s.
Documents processed: 3
Total entities: 80
Total edges: 28
Total documents: 3
```
### 搜索图谱
```
# 关键词搜索 — 查找精确匹配
python scripts/search_cli.py -q "Acme Corp"
# 语义搜索 — 即使没有关键词匹配也能找到相关内容
python scripts/search_cli.py -q "payments to contractors" --mode semantic
# 混合搜索 — 结合关键词和语义,两全其美
python scripts/search_cli.py -q "financial fraud" --mode hybrid
# 查找两个实体之间的联系
python scripts/search_cli.py --path "Jane Smith" "Harbor Development LLC"
# 按实体类型筛选
python scripts/search_cli.py -q "Chen" --type person
```
**路径搜索**显示关系链:
```
Found 3 paths:
Path 1 (confidence: 0.36):
Chen --[EMPLOYED_BY]--> Brightpath Advisors --[FUNDED_BY]--> Meridian Holdings LLC
Path 2 (confidence: 0.22):
Chen --[EMPLOYED_BY]--> Brightpath Advisors --[FUNDED_BY]--> Harbor City
Redevelopment Authority --[OCCURRED_ON]--> Meridian Holdings LLC
```
每一跳都有一个置信度分数。路径置信度是所有跳的乘积——置信度越低意味着连接越不确定。
### 运行分析
```
python scripts/run_analysis.py
```
输出:
```
TOPOLOGY REPORT
============================================================
Entities: 80
Edges: 28
Connected components: 57
Largest component: 11 nodes
Communities (Louvain): 58
STRUCTURAL GAPS: 3
------------------------------------------------------------
[HIGH] Brightpath Advisors ↔ Harbor City Redevelopment Authority
7 entities ↔ 7 entities | cross-edges: 0
→ How do Brightpath Advisors and Harbor City Redevelopment
Authority relate? Your knowledge about these is not yet connected.
SURPRISING CONNECTIONS: 6
------------------------------------------------------------
Robert Chen (person)
Betweenness: 0.5111 | Degree: 4
→ Structurally important despite low frequency
```
**结构缺口**是调查线索——应该合理连接但您的数据中没有连接的实体群落。这就是缺失文档所在。
**出人意料的关联**是具有高介数中心性(它们桥接原本分离的网络)但低度数(它们没有出现在很多文档中)的实体。这些是安静的连接者。
### 每日简报
```
python scripts/daily_briefing.py
```
生成 `briefings/2026-04-04.md`——一份 markdown 摘要,包括:
- 最近 24 小时内添加的新实体
- 来源之间发现的矛盾
- 结构缺口(作为调查问题)
- 出人意料的连接者
- 需要关注的未链接实体
如果您配置了 Obsidian 保险库路径,简报会自动复制到那里。
### 本体论健康
```
python scripts/validate_ontology.py
```
显示您的本体论与现实的匹配程度:
```
ICR (type coverage): 0.75 — warning (some declared types have no data)
CI (class imbalance): 0.34 — warning (dominant: organization at 27/80)
IPR (edge coverage): 0.57 — warning
Type distribution:
organization 27 ( 33.8%) ████████████████
transaction 25 ( 31.2%) ███████████████
event 16 ( 20.0%) ██████████
person 9 ( 11.2%) █████
Unpopulated types: asset, claim
```
- **ICR**(实例化类比率):您声明的类型中有多少比例有实际数据。低于 0.8 意味着某些类型是死模式。
- **CI**(类不平衡):如果一个类型占主导地位(高于 0.5),您的提取可能正在错误分类实体。
- **IPR**(实例化属性比率):与 ICR 相同,但针对边类型。
## 七个阶段
### 1. 本体论——什么很重要
编辑 `ONTOLOGY.md` 为您的报道领域定义实体类型和关系类型。附带一个涵盖 8 种实体类型和 14 种边类型的一般调查新闻本体论。
系统在写入时**拒绝与本体论不匹配的实体**——不会积累垃圾。拒绝会被计数和报告,因此您知道何时需要扩展本体论。
每种类型都包含边界示例:
| 列 | 目的 |
|--------|---------|
| 典型 | 明确属于此类型 |
| 非典型 | 仍属于此类型的边缘情况 |
| 外部典型 | 看起来相似但*不属于*——显示它*属于*哪种类型 |
外部类型示例可以防止最常见的提取错误:所有内容都被放入一个包罗万象的类型中。
### 2. 嵌入——语义理解
文档被分块(1000 个字符,200 个字符重叠)并使用本地 AI 模型(Ollama + nomic-embed-text)转换为 768 维向量。这些支持语义搜索——按含义而非关键词查找文档。
嵌入直接存储在图数据库中作为 `FLOAT[768]` 列。不需要单独的向量数据库。一个数据库包含一切。
### 3. 提取——三阶段实体提取
每个文档都经过三个提取阶段:
**阶段 1——确定性**(即时、免费、始终运行):
- 日期、美元金额、电子邮件地址的正则表达式模式
- 从文档格式中结构化提取
- 置信度:0.85-0.90(高——这些是模式匹配)
**阶段 2——spaCy NER**(快速、本地、无需 GPU):
- 命名实体识别:人物、组织、地点
- 将 spaCy 标签映射到您的本体论类型(PERSON → person、ORG → organization 等)
- 置信度:0.70(好——NER 已成熟)
**阶段 3——LLM**(较慢、通过 Ollama 本地运行):
- 关系提取:谁与谁有关联,如何关联
- 类型细化:使用本体论上下文纠正阶段 2 的错误分类
- 生成带类型的边(EMPLOYED_BY、FUNDED_BY、CONTRACTED_WITH 等)
- 置信度:0.60(较低——LLM 提取需要人工审查)
- 受本体论约束:LLM 提示包含所有类型和边界示例
每个实体都记录其 `provenance`(哪个阶段提取的)和 `source_url`(来自哪个文档)。没有记录来源的内容不会进入图谱。
### 4. 质量控制——本体论验证
每个实体在进入图谱之前都会根据 `ONTOLOGY.md` 进行验证。如果提取管道产生一个类型为"weapon"的实体,但您的本体论不包含"weapon",它将被拒绝并且拒绝会被计数。
摄取后,您会看到:
```
Ontology rejections (types not in ONTOLOGY.md):
weapon: 3 rejections
vehicle: 2 rejections
Tip: Consider adding frequently rejected types to ONTOLOGY.md
```
这告诉您何时需要扩展本体论——数据正在告诉您它包含哪些类型。
### 5. 搜索和路径——追踪关联
三种搜索模式:
| 模式 | 工作方式 | 适用于 |
|------|-------------|----------|
| `keyword` | Cypher `CONTAINS` 匹配实体标签 | 按名称查找特定实体 |
| `semantic` | 查询嵌入与实体嵌入之间的余弦相似度 | 不知道名称时查找相关实体 |
| `hybrid` | 互惠秩融合(RRF)——按两个列表中的位置排名,无需权重调整 | 最佳通用搜索 |
**路径搜索**找到实体之间的类型化链。不只是"这些有关联",而是:
```
Jane Smith --[EMPLOYED_BY]--> Acme Corp --[CONTRACTED_WITH]--> Harbor Dev LLC
```
每条路径都有一个置信度分数(边置信度的乘积)。同一实体之间的多条路径通常意味着更强的关联。
### 6. 拓扑——发现缺失的内容
图分析运行确定性算法——没有 AI,只是对图结构进行数学运算:
| 算法 | 发现什么 | 为什么重要 |
|-----------|--------------|----------------|
| 连通分量 | 图中的独立集群 | 显示哪些调查是孤立的 |
| Louvain 社区 | 密集子群 | 自然的话题聚类 |
| 介数中心性 | 桥接社区的实体 | 安静的连接者——连接原本分离网络的人/组织 |
| 桥接检测 | 单点故障边 | 脆弱的连接,如果移除就会断裂 |
| 缺口检测 | 跨边较少的社区对 | **线索**——您的调查缺少什么 |
| 持续同调 | 图中的拓扑孔 | 更高阶的结构缺口(需要 Ripser) |
**缺口就是故事。** 两个大型社区之间没有跨边意味着您的文档覆盖了两个相关领域,但您还没有将它们连接起来。缺口问题告诉您下一步要找什么。
### 7. 每日简报——图谱发现了什么
仅从图结构生成的 markdown 文件。包含以下部分:
- **新实体**:过去 24 小时内添加的内容
- **矛盾**:来自不同来源的声明之间的 `CONTRADICTS` 边
- **结构缺口**:连接性低的社区对(作为调查问题)
- **出人意料的关联**:低度数实体上的高介数
- **未的实体**:超过 7 天且没有连接的实体(需要关注或删除)
可在 Obsidian、任何文本编辑器或终端中阅读。可选地自动复制到您的 Obsidian 保险库收件箱。
## 隐私
### 本地模式(默认)
一切都在您的机器上运行。没有网络连接。没有 API 密钥。没有账号。
| 组件 | 运行位置 |
|-----------|--------------|
| 文档文本 | 保留在磁盘上 |
| 实体提取 | 在您的 CPU/GPU 上运行 Ollama |
| 嵌入 | 在您的 CPU/GPU 上运行 Ollama |
| 知识图谱 | 磁盘上的 LadybugDB 目录 |
| 向量搜索 | LadybugDB 原生(同一数据库) |
| 分析 | Python (NetworkX) 在您的 CPU 上运行 |
| 每日简报 | 写入磁盘 |
**何时使用:** 敏感来源、泄露的文档、任何您不愿冒险传输的内容。
### 混合模式
嵌入保持在本地。实体提取可选择使用具有零数据保留(ZDR)的远程 LLM 处理非敏感文档。在复杂文档上获得更好的提取质量。
```
# 在 newsroom_graph/config.py 中
PRIVACY_MODE = "hybrid"
REMOTE_API_BASE = "https://api.anthropic.com/v1"
REMOTE_MODEL = "claude-haiku-4-5-20251001"
# 将 NEWSROOM_API_KEY 设置为环境变量 — 切勿硬编码
```
**何时使用:** 公共记录(非敏感)和机密材料(敏感)的混合。图谱、嵌入和分析始终保持在本地。
### 远程模式
一切通过远程 API。不建议用于敏感材料。
**何时使用:** 纯公共数据集的批量处理,在这种情况下速度和质量比机密性更重要。
请参阅 `docs/privacy-guide.md` 获取详细比较和提供商建议。
### 伦理:身份模糊性和来源保护
自动化提取为调查记者带来的两个风险:
**身份模糊性。** 管道会将"John Smith"、"J. Smith"和"John S. Smith"提取为三个单独的实体。它还可能将"BP US"和"British Petroleum"拆分为不同的组织。在基于图关联发布任何发现之前,**手动验证链接的实体实际上是同一个人或组织。** 自动图中的错误关联可能导致错误指控个人。`config.py` 中的去重阈值(`DEDUP_THRESHOLD = 0.92`)通过嵌入相似度捕获一些重复,但对于指代不同人的相似名称还不够。
**三角测量风险。** 结合多个数据集(公共记录 + 泄露的内部电子邮件 + 机密来源访谈)会创建一个图,其中实体的结构位置可能无意中泄露机密来源。如果您发布图的子集——即使已删除名称——来源周围独特的连接模式可能足以让对手识别谁泄露了信息。在分享任何图可视化或导出之前:
- 检查结构布局是否通过独特的关联位置泄露来源身份
- 考虑删除或泛化可追溯到机密来源的边
- 请记住,即使是聚合统计(社区成员资格、介数分数)也可以缩小候选人范围
**图谱是一种情报产品。** 以与您的来源列表相同的操作安全级别对待它。
## 配置
所有配置都在 `newsroom_graph/config.py` 中:
### 路径
```
GRAPH_DIR = Path("data/graph.lbug") # Where the graph database lives
INGEST_DIR = Path("ingest") # Where documents go for ingestion
BRIEFING_DIR = Path("briefings") # Where daily briefings are written
OBSIDIAN_VAULT = "" # Optional: Obsidian vault for briefing delivery
```
### 模型
```
EMBEDDING_MODEL = "nomic-embed-text" # Local embedding model (768 dimensions)
EMBEDDING_DIM = 768 # Must match model output dimension
LOCAL_EXTRACTION_MODEL = "llama3.2:3b" # Local LLM for entity/relationship extraction
```
默认提取模型(`llama3.2:3b`)小而快。要在速度代价下获得更好的提取质量,请尝试 `mistral`、`llama3:8b` 或 `gemma2`。
### 提取调优
```
MIN_CONFIDENCE = 0.5 # Minimum confidence to keep an entity (0.0-1.0)
MAX_ENTITIES_PER_DOC = 200 # Safety limit per document
DEDUP_THRESHOLD = 0.92 # Cosine similarity above this = likely duplicate
```
### 分析调优
```
AUTO_ANALYSIS = False # Run analysis after every ingestion
PRUNE_AGE_DAYS = 7 # Flag unlinked entities older than this
MIN_COMMUNITY_SIZE = 5 # Minimum community size for gap analysis
MAX_CROSS_EDGES_FOR_GAP = 3 # Below this = flagged as gap
TOP_BETWEENNESS = 10 # How many high-betweenness entities to report
```
### 每日简报部分
```
BRIEFING_SECTIONS = [
"new_entities", # Entities added in last 24h
"contradictions", # CONTRADICTS edges found
"structural_gaps", # Community pairs with low cross-connection
"surprising_connections", # High betweenness on low-frequency entities
"unlinked_entities", # Entities needing attention
]
```
删除部分名称以将其从简报中排除。
## 扩展本体论
编辑 `ONTOLOGY.md` 为您的报道领域添加实体类型和边类型。
**经验法则:** 只有当您看到 3 个以上不适合现有类型的实例时才添加类型。检查摄取后的拒绝日志——它会告诉您文档需要什么类型。
### 添加实体类型
在 `ONTOLOGY.md` 的实体类型表中添加一行:
```
| permit | A government permit, license, or approval | "Building permit #2024-087" | "Informal verbal approval" | "The permit office" → organization |
```
最后三列(典型、非典型、外部类型)提高提取准确性。外部类型列特别重要——它向 LLM 展示什么*不属于*此类型。
### 添加边类型
在边类型表中添加一行:
```
| ISSUED_BY | permit → organization | Government body that issued the permit | Regulatory authority, approval chain |
```
每种边类型都应该有明确的调查目的。如果您无法解释为什么一种关系对调查很重要,就不要添加它。
### 验证更改
```
python scripts/validate_ontology.py
```
这会检查:
- 所有类型在语法上有效
- 更新本体论后的图谱健康指标(ICR/CI/IPR)
- 哪些类型已填充,哪些为空
## 技术栈
全部开源。除 Ollama 外均可通过 pip 安装。
| 工具 | 版本 | 用途 | 为什么选择这个 |
|------|---------|---------|-------------|
| [LadybugDB](https://ladybugdb.com) | 0.15.3 | 图数据库 + 向量存储 | 嵌入式列式图数据库。Cypher 查询。原生的 `FLOAT[768]` 向量列,带有 `array_cosine_similarity`。无需服务器。一个目录 = 一个调查。KuzuDB 的延续。 |
| [PyArrow](https://arrow.apache.org) | 23.0+ | 批量数据加载 | LadybugDB 的 `COPY FROM` Parquet 比迭代插入快 25 倍。PyArrow 写入 Parquet 文件。 |
| [Pandas](https://pandas.pydata.org) | 3.0+ | 数据操作 | 用于 Parquet 导出前批量实体准备的数据框操作。 |
| [spaCy](https://spacy.io) | 3.8+ | NLP 提取(阶段 2) | 命名实体识别。`en_core_web_sm` 模型——小、快、足以处理人物/组织/地点。 |
| [NetworkX](https://networkx.org) | 3.6+ | 图分析 | Louvain 社区、介数中心性、桥接检测、连通分量。在提取的图上运行。 |
| [Ripser](https://ripser.scikit-tda.org) | 0.6+ | 持续同调(可选) | 发现拓扑孔——社区检测遗漏的更高阶结构缺口。 |
| [Ollama](https://ollama.com) | 0.3+ | 本地 AI 模型 | 在您的硬件上运行嵌入和提取模型。无 API 密钥。无云。 |
| [Obsidian](标签:AI风险缓解, LadybugDB, LLM评估, NetworkX, Ollama, 信息提取, 关系抽取, 图分析, 实体识别, 数据隐私, 文本挖掘, 文档分析, 新闻工作, 本地大模型, 本地部署, 特权检测, 知识管理, 突变策略, 结构化数据, 网络安全, 调查新闻, 调查记者, 逆向工具, 隐私优先, 隐私保护