M0nkeyFl0wer/investigative-journalism-kg

GitHub: M0nkeyFl0wer/investigative-journalism-kg

面向调查记者的隐私优先知识图谱工具包,可在本地完成文档摄取、实体关系提取、图谱构建和结构缺口分析。

Stars: 0 | Forks: 0

# open-newsroom-graph 面向调查记者的隐私优先知识图谱工具包。摄取文档、提取实体和关系、构建可搜索图谱,并发现结构缺口以寻找线索。完全在笔记本电脑上运行。 **无需云端。无需账号。数据不会离开您的设备。** LadybugDB    NetworkX    Ollama ## 这是做什么的 您有一堆文档——法庭文件、公司注册信息、泄露的电子邮件、公共记录。您需要找到关联,更重要的是,找到*缺失*的内容。 此工具包可以: 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) 联系。
标签:AI风险缓解, LadybugDB, LLM评估, NetworkX, Ollama, 信息提取, 关系抽取, 图分析, 实体识别, 数据隐私, 文本挖掘, 文档分析, 新闻工作, 本地大模型, 本地部署, 特权检测, 知识管理, 突变策略, 结构化数据, 网络安全, 调查新闻, 调查记者, 逆向工具, 隐私优先, 隐私保护