yemrealtanay/llm-hasher

GitHub: yemrealtanay/llm-hasher

llm-hasher 是一个本地运行的 PII 脱敏中间件,在文本发送给 LLM 前自动检测敏感信息并替换为加密 token,返回时再还原原始值,全程零云端依赖。

Stars: 24 | Forks: 0

# 🔏 llm-hasher **面向 LLM pipeline 的隐私优先 PII tokenization 中间件。** 在发送给任何 LLM 之前,将敏感数据替换为不透明的 token。 在返回时恢复原始值。你的 vault,你的密钥。 [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![Go Version](https://img.shields.io/badge/Go-1.25+-00ADD8?logo=go)](go.mod) [![Docker](https://img.shields.io/badge/Docker-ready-2496ED?logo=docker)](docker-compose.yml) [![Ollama](https://img.shields.io/badge/Powered%20by-Ollama-black)](https://ollama.ai) [快速开始](#quick-start) · [API 参考](#api-reference) · [工作原理](#how-it-works) · [贡献指南](#contributing)
## 问题所在 你将客户记录、支持聊天或文档发送给 LLM 进行摘要、分析或分类。这些文本包含真实的信用卡号、家庭住址、姓名和国家身份证号——它们以明文形式发送给第三方 API,并可能存储在你自己的数据库中。 **llm-hasher 位于你的应用程序和 LLM 之间。**它会剔除敏感数据,发送干净的 token,并在响应中将真实值还原。LLM 永远不会看到真实的 PII。你的数据库存储的是 token,而不是明文。 ## 工作原理 ``` Your App ──► POST /v1/tokenize ──► llm-hasher ──► tokenized text │ detects PII locally (Ollama, no cloud) stores in encrypted vault Your App ──► [your LLM call with tokenized text] Your App ──► POST /v1/detokenize ──► llm-hasher ──► original text restored ``` **示例:** ``` Input: "Hi, my card is 4111-1111-1111-1111 and email is john@example.com" Output: "Hi, my card is CREDIT_CARD_john12_4f8a2b and email is EMAIL_john12_9c3d1a" ``` LLM 接收到的是 tokenization 后的版本——它仍然理解上下文(“用户提供了一个 CREDIT_CARD”),但永远看不到真实的号码。当 LLM 的响应返回时,你将其通过 `/v1/detokenize` 处理,就能找回真实的值。 ## 功能 - **本地 PII 检测** —— 使用在你自己的服务器上运行的 [Ollama](https://ollama.ai),检测过程中没有任何数据离开你的基础设施 - **混合检测** —— 针对结构化类型(信用卡、电子邮件、电话号码、IBAN、IP)使用快速 regex,针对上下文相关类型(姓名、地址、国家身份证、护照)使用 LLM - **加密 vault** —— token 与值的映射存储在 SQLite 中,并采用 AES-256-GCM 加密,密钥永远不会离开进程 - **使用你自己的 ID** —— 使用你业务领域中的 `context_id`(`"zoom_call_789"`、`"contact_123"`),而无需跟踪外部的 UUID - **去重** —— 同一上下文中的相同 PII 始终映射到同一个 token,因此 LLM 能够一致地进行推理 - **性能** —— 大段文本会被分块并由并行的 goroutine 处理;detokenization 采用单次多字符串替换 - **双重用途** —— 作为独立的 HTTP 服务运行,或作为 Go 库导入 - **零运行依赖** —— 单一二进制文件 + SQLite;Docker Compose 已包含 Ollama ## 支持的 PII 类型 | 类型 | 检测方法 | |---|---| | `CREDIT_CARD` | Regex (Visa, Mastercard, Amex, Discover) | | `EMAIL` | Regex | | `PHONE_NUMBER` | Regex (国际格式) | | `IP_ADDRESS` | Regex (IPv4) | | `BANK_ACCOUNT` | Regex (IBAN) | | `PERSON_NAME` | Ollama LLM (上下文感知) | | `HOME_ADDRESS` | Ollama LLM (上下文感知) | | `NATIONAL_ID` | Ollama LLM (SSN, TC Kimlik, NIN 等) | | `PASSPORT` | Ollama LLM | | `DATE_OF_BIRTH` | Ollama LLM | ## 快速开始 ### Docker (推荐) ``` git clone https://github.com/yemrealtanay/llm-hasher cd llm-hasher make docker-up ``` 就是这样。Docker Compose 会启动 Ollama,自动拉取 `llama3.2:3b` (~2GB),并在端口 `8080` 上启动服务。 ``` # 检查其是否正在运行 curl http://localhost:8080/healthz # {"status":"ok"} ``` ### 裸金属 ``` git clone https://github.com/yemrealtanay/llm-hasher cd llm-hasher make setup # installs Ollama if missing, pulls model, builds binary make run ``` ## API 参考 ### `POST /v1/tokenize` 检测文本中的 PII,并使用有 vault 支持的 token 进行替换。 **请求:** ``` { "text": "My card is 4111-1111-1111-1111 and I live at 123 Main St, Boston", "context_id": "zoom_call_789", "ttl": "24h" } ``` | 字段 | 必填 | 描述 | |---|---|---| | `text` | 是 | 需要进行 tokenization 的文本 | | `context_id` | 否 | 用于限定映射范围的你自己的 ID。如果省略,系统会生成并返回一个随机 ID。请使用你业务领域中的稳定 ID 以便后续进行 detokenization。 | | `ttl` | 否 | Token 过期持续时间(例如 `"24h"`、`"7d"`)。`"0"` 或省略 = 永不过期。 | **响应:** ``` { "tokenized_text": "My card is CREDIT_CARD_zoomc7_4f8a2b and I live at HOME_ADDRESS_zoomc7_9c3d1a", "context_id": "zoom_call_789", "entities": [ { "token": "CREDIT_CARD_zoomc7_4f8a2b", "pii_type": "CREDIT_CARD" }, { "token": "HOME_ADDRESS_zoomc7_9c3d1a", "pii_type": "HOME_ADDRESS" } ] } ``` ### `POST /v1/detokenize` 在包含 token 的文本中恢复原始值。 **请求:** ``` { "text": "The user provided CREDIT_CARD_zoomc7_4f8a2b as payment.", "context_id": "zoom_call_789" } ``` **响应:** ``` { "original_text": "The user provided 4111-1111-1111-1111 as payment." } ``` ### `POST /v1/tokenize/batch` 在单个请求中对多个文本进行 tokenization(并行处理)。 **请求:** ``` { "items": [ { "text": "Call with John, card 4111-1111-1111-1111", "context_id": "call_1" }, { "text": "Support ticket from jane@example.com", "context_id": "ticket_42" } ] } ``` ### `DELETE /v1/contexts/{context_id}` 硬删除某个上下文的所有 token 映射。适用于合规/被遗忘权场景。 ``` curl -X DELETE http://localhost:8080/v1/contexts/zoom_call_789 # 204 No Content ``` ## 真实用例模式 ### 模式 1:LLM 代理 (记录分析) ``` # 1. 发送到 LLM 前进行 tokenize resp = requests.post("http://localhost:8080/v1/tokenize", json={ "text": transcript, "context_id": f"zoom_{call_id}" }) tokenized = resp.json() # 2. 将 tokenize 后的文本发送到你的 LLM llm_response = openai.chat.completions.create( messages=[ {"role": "system", "content": "Summarize this call transcript."}, {"role": "user", "content": tokenized["tokenized_text"]} ] ) # 3. 对 LLM 响应进行 detokenize final = requests.post("http://localhost:8080/v1/detokenize", json={ "text": llm_response.choices[0].message.content, "context_id": f"zoom_{call_id}" }) print(final.json()["original_text"]) ``` ### 模式 2:数据库存储 ``` # 在保存到 DB 之前 — 进行无过期时间的 tokenize resp = requests.post("http://localhost:8080/v1/tokenize", json={ "text": transcript, "context_id": f"contact_{contact_id}", "ttl": "0" # no expiry — lives as long as the record }) db.save(contact_id=contact_id, transcript=resp.json()["tokenized_text"]) # 向用户显示时 — 实时进行 detokenize row = db.get(contact_id) resp = requests.post("http://localhost:8080/v1/detokenize", json={ "text": row.transcript, "context_id": f"contact_{contact_id}" }) show_to_user(resp.json()["original_text"]) ``` ### 模式 3:Go 库 ``` import "github.com/yemrealtanay/llm-hasher/pkg/hasher" h, err := hasher.New( hasher.WithOllama("http://localhost:11434", "llama3.2:3b"), hasher.WithVault("data/vault.db", ""), ) defer h.Close() result, err := h.Tokenize(ctx, transcript, "zoom_call_789", nil) // result.Text contains tokenized transcript original, err := h.Detokenize(ctx, llmResponse, "zoom_call_789") ``` ## 配置 将 `configs/config.example.yaml` 复制到 `configs/config.yaml` 并进行调整: ``` ollama: model: "llama3.2:3b" # or llama3.1:8b for higher recall confidence_threshold: 0.7 # lower = more aggressive detection chunk_size: 800 # words before parallel chunking kicks in vault: default_ttl: "24h" # "0" for no expiry ``` **加密密钥**(生产环境):将 `VAULT_KEY` 环境变量设置为 64 位十六进制字符(32 字节)。如果未设置,系统会自动生成一个密钥并保存到 `data/vault.key`。 ``` # 生成密钥 openssl rand -hex 32 # 添加到 .env: # VAULT_KEY= ``` ## 性能 | 场景 | 典型延迟 | |---|---| | 短文本 (< 800 字),仅 regex PII | < 5ms | | 带有 LLM 检测的短文本 | 2–8秒 (取决于模型) | | 长文本 (5000 字),6 个并行分块 | 3–10秒 | | Detokenize (任意大小) | < 5ms | 检测延迟主要受 Ollama 推理影响。在现代笔记本电脑上使用 `llama3.2:3b` 每个分块通常需要 2–4 秒。GPU 或更快的模型可以显著缩短这一时间。 ## 路线图 - [x] **v1** —— 带有本地 Ollama 检测的 Tokenize / Detokenize API - [ ] **v2** —— 内置 LLM 代理 endpoint (兼容 OpenAI、Anthropic) - [ ] **v2** —— 针对大型文档的流式 tokenization (SSE) - [ ] **v2** —— OpenTelemetry 链路追踪 - [ ] **v3** —— 用于检查 vault 的 Web UI - [ ] **v3** —— 多租户支持 ## 许可证 [MIT](LICENSE) —— 可自由使用、修改和分发。
标签:AI风险缓解, DLL 劫持, Docker, EVTX分析, Go, LLM评估, Ollama, Ruby工具, 中间件, 大语言模型, 安全防御评估, 数据脱敏, 日志审计, 网络安全, 请求拦截, 隐私保护