博客

利用 Elasticsearch 和 LlamaIndex 保护 RAG 中的敏感信息和 PII 信息

如何使用 Elasticsearch 和 LlamaIndex 保护 RAG 应用程序中的敏感数据和 PII 数据。

在本篇文章中,我们将探讨在 RAG(检索增强生成)流程中使用公共 LLM 时保护个人身份信息 (PII) 和敏感数据的方法。我们将探索使用开源库和正则表达式屏蔽 PII 和敏感数据,以及在调用公共 LLM 之前使用本地 LLM 屏蔽数据。

在开始之前,让我们回顾一下我们在本篇文章中使用的一些术语。

术语

LlamaIndex是用于构建 LLM(大型语言模型)应用程序的领先数据框架。LlamaIndex 为构建 RAG(检索增强生成)应用程序的各个阶段提供了抽象概念。像 LlamaIndex 和 LangChain 这样的框架提供了抽象,因此应用程序不会与任何特定 LLM 的应用程序接口紧密耦合。

ElasticsearchElastic 提供。Elastic 是 Elasticsearch 背后的行业领导者,Elasticsearch 是一个可扩展的数据存储和矢量数据库,支持精确的全文搜索、语义理解的矢量搜索以及两全其美的混合搜索。Elasticsearch 是一个分布式 RESTful 搜索和分析引擎、可扩展数据存储和矢量数据库。我们在本博客中使用的 Elasticsearch 功能在 Elasticsearch 的免费开放版本中提供。

检索增强生成(RAG)是一种人工智能技术/模式,在这种模式下,LLM 可以利用外部知识生成对用户查询的回复。这样,法律硕士的答复就可以根据具体情况量身定做,而不是泛泛而谈。

嵌入是文本/媒体意义的数字表示。它们是高维信息的低维表示。

RAG 和数据保护

一般来说,大型语言模型(LLM)善于根据模型中的可用信息生成响应,而这些信息可能是在互联网数据上训练出来的。然而,对于那些模型中没有信息的查询,则需要向法律硕士提供模型中没有的外部知识或具体细节。这些信息可能存在于您的数据库或内部知识系统中。检索增强生成(RAG)是一种技术,对于给定的用户查询,首先从外部(LLM)系统(如数据库)检索相关的上下文/信息,然后将上下文与用户查询一起发送给 LLM,以生成更具体、更相关的响应。

这使得 RAG 技术在问题解答、内容创建以及任何有利于深入理解上下文和细节的应用中都非常有效。

因此,在 RAG 管道中,您有可能将 PII(个人身份信息)等内部信息和敏感信息(如姓名、出生日期、账号等)暴露给公共 LLM。

虽然在使用 Elasticsearch 等矢量数据库时,数据是安全的(通过各种杠杆,如基于角色的访问控制文档级安全等),但在向外部公共 LLM 发送数据时必须小心谨慎。

在使用大型语言模型 (LLM) 时,出于多种原因,保护个人身份信息 (PII) 和敏感数据至关重要:

  • 隐私合规:许多地区都有严格的法规,如欧洲的《通用数据保护条例》(GDPR)或美国的《加利福尼亚消费者隐私法》(CCPA),这些法规都要求保护个人数据。要避免法律后果和罚款,就必须遵守这些法律。

  • 用户信任:确保敏感信息的保密性和完整性可建立用户信任。用户更愿意使用他们认为能保护其隐私的系统,并与之互动。

  • 数据安全:防止数据泄露至关重要。如果没有足够的保障措施,暴露在法律硕士面前的敏感数据很容易被窃取或滥用,从而导致身份被盗或金融欺诈等潜在危害。

  • 道德方面的考虑:从道德角度讲,尊重用户隐私并负责任地处理他们的数据非常重要。对 PII 处理不当会导致歧视、侮辱或其他负面社会影响。

  • 企业声誉:未能保护敏感数据的公司可能会声誉受损,这可能会对其业务造成长期负面影响,包括失去客户和收入。

  • 减少滥用风险:安全处理敏感数据有助于防止对数据或模型的恶意使用,例如在有偏见的数据上训练模型,或利用数据操纵或伤害个人。

总之,为了确保法律合规、维护用户信任、确保数据安全、坚持道德标准、保护企业声誉和降低滥用风险,必须对 PII 和敏感数据进行强有力的保护。

快速回顾

上一篇文章中,我们讨论了如何使用 RAG 技术,将 Elasticsearch 作为向量数据库,同时使用 LlamaIndex 和本地运行的 Mistral LLM 来实现 Q&A 体验。在此基础上,我们将继续努力。

阅读上一篇文章是可有可无的,因为我们现在将快速讨论/复述上一篇文章中的内容。

我们有一个样本数据集,内容是一家虚构的家庭保险公司的座席人员与客户之间的呼叫中心对话。我们开发了一个简单的 RAG 应用程序,可以回答 "客户因哪些与水有关的问题而提出索赔 "等问题。

从高度上看,流程是这样的。

RAG 流程

在索引阶段,我们使用 LlamaIndex 管道加载并索引文档。文档被分块并连同其嵌入一起存储在 Elasticsearch 向量数据库中。

在查询阶段,当用户提出问题时,LlamaIndex 会检索与查询相关的前 K 个相似文档。这些排名前 K 位的相关文档连同查询结果被发送到本地运行的 Mistral LLM,然后由其生成响应并发送回用户。请随时查看上一篇文章或探索代码

在上一篇文章中,我们在本地运行了 LLM。不过,在生产过程中,您可能希望使用OpenAIMistralAnthropic等公司提供的外部 LLM。这可能是因为您的使用案例需要一个更大的基础模型,或者由于企业生产的需要(如可扩展性、可用性、性能等),在本地运行不是一种选择。

在 RAG 管道中引入外部 LLM 会使您面临不慎将敏感和 PII 泄露给 LLM 的风险。在本篇文章中,我们将探讨如何在将文档发送给外部法律硕士之前,将 PII 信息屏蔽作为 RAG 管道的一部分。

持有公共法学硕士学位的 RAG

在讨论如何在 RAG 管道中保护 PII 和敏感信息之前,我们将首先使用 LlamaIndex、Elasticsearch 向量数据库和 OpenAI LLM 构建一个简单的 RAG 应用程序。

准备工作

我们需要以下材料

  • 运行Elasticsearch作为向量数据库来存储嵌入。按照上一篇文章中关于安装 Elasticsearch 的说明进行操作。

  • 开放人工智能应用程序接口密钥。

简单的 RAG 应用

整个代码可在Github Repository(branch:protection-pii)中找到,以供参考。克隆该 repo 是可选的,因为我们将在下文中详细介绍代码。

在您最喜欢的集成开发环境中,用以下 3 个文件创建一个新的 Python 应用程序。

  • index.py 与索引数据有关的代码的位置。

  • query.py 与查询和 LLM 交互相关的代码都放在这里。

  • .env 配置属性(如 API 密钥)的位置。

我们需要安装一些软件包。首先,我们要在应用程序的根文件夹中创建一个新的 python虚拟环境

python3 -m venv .venv

激活虚拟环境并安装以下所需软件包。

source .venv/bin/activate
pip install llama-index 
pip install llama-index-embeddings-openai
pip install llama-index-vector-stores-elasticsearch
pip install sentence-transformers
pip install python-dotenv
pip install openai

在 .env 中配置 OpenAI 和 Elasticsearch 连接属性锉刀

OPENAI_API_KEY="REPLACEME"
ELASTIC_CLOUD_ID="REPLACEME"
ELASTIC_API_KEY="REPLACEME"

索引数据

下载conversations.json文件,其中包含客户与我们虚构的房屋保险公司呼叫中心座席之间的对话。将该文件与 2 个 python 文件和 .env 文件一起放在应用程序的根目录中。文件。下面是该文件内容的示例。

{
"conversation_id": 103,
"customer_name": "Sophia Jones",
"agent_name": "Emily Wilson",
"policy_number": "JKL0123",
"conversation": "Customer: Hi, I'm Sophia Jones. My Date of Birth is November 15th, 1985, Address is 303 Cedar St, Miami, FL 33101, and my Policy Number is JKL0123.\nAgent: Hello, Sophia. How may I assist you today?\nCustomer: Hello, Emily. I have a question about my policy.\nCustomer: There's been a break-in at my home, and some valuable items are missing. Are they covered?\nAgent: Let me check your policy for coverage related to theft.\nAgent: Yes, theft of personal belongings is covered under your policy.\nCustomer: That's a relief. I'll need to file a claim for the stolen items.\nAgent: We'll assist you with the claim process, Sophia. Is there anything else I can help you with?\nCustomer: No, that's all for now. Thank you for your assistance, Emily.\nAgent: You're welcome, Sophia. Please feel free to reach out if you have any further questions or concerns.\nCustomer: I will. Have a great day!\nAgent: You too, Sophia. Take care.",
"summary": "A customer inquires about coverage for stolen items after a break-in at home, and the agent confirms that theft of personal belongings is covered under the policy. The agent offers assistance with the claim process, resulting in the customer expressing relief and gratitude."
}

index.py 中粘贴下面的代码,该代码负责索引数据。

# index.py
# pip install sentence-transformers
# pip install llama-index-embeddings-openai
# pip install llama-index-embeddings-huggingface

import json
import os
from dotenv import load_dotenv
from llama_index.core import Document
from llama_index.core import Settings
from llama_index.core.ingestion import IngestionPipeline
from llama_index.core.node_parser import SentenceSplitter
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.vector_stores.elasticsearch import ElasticsearchStore


def get_documents_from_file(file):
   """Reads a json file and returns list of Documents"""

   with open(file=file, mode='rt') as f:
       conversations_dict = json.loads(f.read())

   # Build Document objects using fields of interest.
   documents = [Document(text=item['conversation'],
                         metadata={"conversation_id": item['conversation_id']})
                for
                item in conversations_dict]
   return documents

# Load .env file contents into env
load_dotenv('.env')
Settings.embed_model = HuggingFaceEmbedding(
   model_name="BAAI/bge-small-en-v1.5"
)

def main():
   # ElasticsearchStore is a VectorStore that
   # takes care of Elasticsearch Index and Data management.
   es_vector_store = ElasticsearchStore(index_name="convo_index",
                                        vector_field='conversation_vector',
                                        text_field='conversation',
                                        es_cloud_id=os.getenv("ELASTIC_CLOUD_ID"),
                                        es_api_key=os.getenv("ELASTIC_API_KEY"))

   # LlamaIndex Pipeline configured to take care of chunking, embedding
   # and storing the embeddings in the vector store.
   llamaindex_pipeline = IngestionPipeline(
       transformations=[
           SentenceSplitter(chunk_size=350, chunk_overlap=50),
           Settings.embed_model
       ],
       vector_store=es_vector_store
   )

   # Load data from a json file into a list of LlamaIndex Documents
   documents = get_documents_from_file(file="conversations.json")
   llamaindex_pipeline.run(documents=documents)
   print(".....Indexing Data Completed.....\n")

if __name__ == "__main__":
   main()

运行上述代码可看到在 Elasticsearch 中创建了一个索引,将嵌入信息存储在名为convo_index 的 Elasticsearch 索引中。

如果您需要有关 LlamaIndex IngestionPipeline 的解释,请参阅上一篇文章中的创建 IngestionPipeline 部分。

查询

在上一篇文章中,我们使用了本地 LLM 进行查询

在本篇文章中,我们将使用公共 LLM OpenAI,如下所示。

# query.py
from llama_index.core import VectorStoreIndex, QueryBundle, Settings
from llama_index.llms.openai import OpenAI
from index import es_vector_store

# Public LLM where we send user query and Related Documents
llm = OpenAI()

index = VectorStoreIndex.from_vector_store(es_vector_store)

# This query_engine, for a given user query retrieves top 10 similar documents from
# Elasticsearch vector database and sends the documents along with the user query to the LLM.
# Note that documents are sent as-is. So any PII/Sensitive data is sent to the LLM.
query_engine = index.as_query_engine(llm, similarity_top_k=10)

query="Give me summary of water related claims that customers raised."
bundle = QueryBundle(query, embedding=Settings.embed_model.get_query_embedding(query))
result = query_engine.query(bundle)
print(result)

上述代码将打印 OpenAI 的响应如下。

客户提出了各种与水有关的索赔,包括地下室水渍、水管爆裂、冰雹对屋顶造成的损坏等问题,以及由于未及时通知、维护问题、逐渐磨损和原有损坏等原因造成的拒赔。在每个案例中,客户都对索赔被拒表示沮丧,并寻求对其索赔进行公平的评估和决定。

在 RAG 中屏蔽 PII

到目前为止,我们所做的工作是将文档原样连同用户查询一起发送给 OpenAI。

在 RAG 管道中,从矢量存储中检索到相关上下文后,我们有机会在将查询和上下文发送到 LLM 之前屏蔽 PII 和敏感信息。

在向外部法律硕士发送 PII 信息之前,有多种方法可以掩盖 PII 信息,每种方法都有自己的优点。下面我们来看看其中的一些选择

  1. 使用 spacy.io 或Presidio(微软维护的开源库)等 NLP 库。

  2. 使用开箱即用的 LlamaIndex NERPIINodePostprocessor.

  3. 通过 PIINodePostprocessor

使用上述任何一种方法实现屏蔽逻辑后,您就可以使用后处理器(您自己定制的后处理器或 LlamaIndex 开箱即用的后处理器)配置 LlamaIndex 的 IngestionPipeline。

使用 NLP 库

作为 RAG 管道的一部分,我们可以使用 NLP 库屏蔽敏感数据。我们将在本演示中使用 spacy.io 软件包。

创建一个新文件query_masking_nlp.py 并添加以下代码。

# query_masking_nlp.py

# pip install spacy
# python3 - m spacy download en_core_web_sm
import re
from typing import List, Optional

import spacy
from llama_index.core import VectorStoreIndex, QueryBundle, Settings
from llama_index.core.postprocessor.types import BaseNodePostprocessor
from llama_index.core.schema import NodeWithScore
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.llms.openai import OpenAI
from index import es_vector_store

# Load the spaCy model
nlp = spacy.load("en_core_web_sm")

# Compile regex patterns for performance
phone_pattern = re.compile(r'\b\d{3}[-.]?\d{3}[-.]?\d{4}\b')
email_pattern = re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b')
date_pattern = re.compile(r'\b(\d{1,2}[-/]\d{1,2}[-/]\d{2,4}|\d{2,4}[-/]\d{1,2}[-/]\d{1,2})\b')
dob_pattern = re.compile(
r"(January|February|March|April|May|June|July|August|September|October|November|December)\s(\d{1,2})(st|nd|rd|th),\s(\d{4})")
address_pattern = re.compile(r'\d+\s+[\w\s]+\,\s+[A-Za-z]+\,\s+[A-Z]{2}\s+\d{5}(-\d{4})?')
zip_code_pattern =  re.compile(r'\b\d{5}(?:-\d{4})?\b')
policy_number_pattern = re.compile(r"[A-Z]{3}\d{4}\.$")  # 3 characters followed by 4 digits, in our case e.g XYZ9876

Settings.embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-en-v1.5")

# match = re.match(policy_number_pattern, "XYZ9876")
# print(match)


def mask_pii(text):
   """
   Masks Personally Identifiable Information (PII) in the given
   text using pre-defined regex patterns and spaCy's named entity recognition.
   Args:
       text (str): The input text containing potential PII.
   Returns:
       str: The text with PII masked.
   """

   # Process the text with spaCy for NER
   doc = nlp(text)

   # Mask entities identified by spaCy NER (e.g First/Last Names etc)
   for ent in doc.ents:
       if ent.label_ in ["PERSON", "ORG", "GPE"]:
           text = text.replace(ent.text, '[MASKED]')

   # Apply regex patterns after NER to avoid overlapping issues
   text = phone_pattern.sub('[PHONE MASKED]', text)
   text = email_pattern.sub('[EMAIL MASKED]', text)
   text = date_pattern.sub('[DATE MASKED]', text)
   text = address_pattern.sub('[ADDRESS MASKED]', text)
   text = dob_pattern.sub('[DOB MASKED]', text)
   text = zip_code_pattern.sub('[ZIP MASKED]', text)
   text = policy_number_pattern.sub('[POLICY MASKED]', text)

   return text


class CustomPostProcessor(BaseNodePostprocessor):
   """
   Custom Postprocessor which masks Personally Identifiable Information (PII).
   PostProcessor is called on the Documents before they are sent to the LLM.
   """
   def _postprocess_nodes(
           self, nodes: List[NodeWithScore], query_bundle: Optional[QueryBundle]
   ) -> List[NodeWithScore]:
       # Masks PII
       for n in nodes:
          n.node.set_content(mask_pii(n.text))
       return nodes

   
# Use Public LLM to send user query and Related Documents
llm = OpenAI()
index = VectorStoreIndex.from_vector_store(es_vector_store)

# This query_engine, for a given user query retrieves top 10 similar documents from
# Elasticsearch vector database and sends the documents along with the user query to the LLM.
# Note that documents are masked based on custom logic defined in CustomPostProcessor._postprocess_nodes.
query_engine = index.as_query_engine(llm, similarity_top_k=10, node_postprocessors=[CustomPostProcessor()])



query = "Give me summary of water related claims that customers raised."
bundle = QueryBundle(query, embedding=Settings.embed_model.get_query_embedding(query))
response = query_engine.query(bundle)
print(response)

法律硕士的答复如下。

客户提出了各种与水有关的索赔,包括地下室水渍、水管爆裂、冰雹损坏屋顶以及暴雨期间的洪水等问题。这些索赔导致了基于缺乏及时通知、维护问题、逐渐磨损和预先存在的损坏等原因的拒赔而产生的挫折感。客户对这些拒赔表示失望、压力和经济负担,要求对其索赔进行公平评估和彻底审查。一些客户还面临索赔处理延迟的问题,这进一步引起了对保险公司服务的不满。

在上述代码中,当创建 Llama 索引查询引擎时,我们提供了一个 CustomPostProcessor。

QueryEngine 调用的逻辑在CustomPostProcessor_postprocess_nodes 方法中定义。我们正在使用 SpaCy.io 库来检测文件中的命名实体,然后使用一些正则表达式来替换这些名称以及敏感信息,然后再将文件发送到 LLM。

以下是自定义 PostProcessor 创建的原始会话和屏蔽会话的部分示例。

原文如此:

客户:你好,我是马修-洛佩兹(Matthew Lopez),出生日期是 1984 年 10 月 12 日,住在纽约州斯莫尔敦市雪松街 456 号,邮编 34567。我的保单号码是 TUV8901。探员下午好 马修我今天能为您提供什么帮助?客户:你好,我对贵公司拒绝我索赔的决定感到非常失望。

由 CustomPostProcessor 生成的屏蔽文本。

顾客:你好,我是 [蒙面],[蒙面] 是 [生日蒙面],我住在 34567 [蒙面] [蒙面] 的西达街 456 号。我的保单号码是 [屏蔽]。探员下午好,[蒙面]我今天能为您提供什么帮助?客户:你好,我对贵公司拒绝我索赔的决定感到非常失望。

请注意:

识别和屏蔽 PII 和敏感信息并不是一项简单的任务。要涵盖敏感信息的各种格式和语义,就必须充分了解自己的领域和数据。虽然上述代码可能适用于某些使用情况,但您可能需要根据自己的需求和测试情况进行修改。

使用开箱即用的 LlamaIndex NERPIINodePostprocessor

LlamaIndex 通过引入以下功能,使保护 RAG 管道中的 PII 信息变得更加容易 NERPIINodePostprocessor.

from llama_index.core import VectorStoreIndex, QueryBundle, Settings
from llama_index.core.postprocessor import NERPIINodePostprocessor
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.llms.openai import OpenAI
from index import es_vector_store

Settings.embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-en-v1.5")

# Use Public LLM to send user query and Related Documents
llm = OpenAI()

ner_processor = NERPIINodePostprocessor()
index = VectorStoreIndex.from_vector_store(es_vector_store)

# This query_engine, for a given user query retrieves top 10 similar documents from
# Elasticsearch vector database and sends the documents along with the user query to the LLM.
# Note that documents masked using the NERPIINodePostprocessor so that PII/Sensitive data is not sent to the LLM.
query_engine = index.as_query_engine(llm, similarity_top_k=10, node_postprocessors=[ner_processor])

query = "Give me summary of fire related claims that customers raised."
bundle = QueryBundle(query, embedding=Settings.embed_model.get_query_embedding(query))
response = query_engine.query(bundle)
print(response)

答复如下

客户提出了与火灾有关的财产损失索赔。在一个案例中,由于纵火被排除在承保范围之外,车库火灾损失索赔被拒绝。另一位客户就其住宅遭受的火灾损失提出索赔,该损失属于其保单的承保范围。此外,一位客户报告了厨房火灾,并得到了火灾损失赔偿的保证。

通过 PIINodePostprocessor

我们还可以利用本地或专用网络中运行的 LLM,在将数据发送到公共 LLM 之前完成屏蔽工作。

我们将使用运行在本地机器 Ollama 上的 Mistral 来进行屏蔽。

本地运行 Mistral

下载并安装Ollama。安装 Ollama 后,运行此命令下载并运行mistral

ollama run mistral

首次下载并在本地运行模型可能需要几分钟时间。通过提出类似下面 "写一首关于云的诗 "的问题来验证 mistral 是否在运行,并验证诗歌是否符合您的要求。保持 ollama 运行,因为我们稍后需要通过代码与 mistral 模型交互。

新建一个名为query_masking_local_LLM.py 的文件,并添加以下代码。

# pip install llama-index-llms-ollama
from llama_index.core import VectorStoreIndex, QueryBundle, Settings
from llama_index.core.postprocessor import PIINodePostprocessor
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.llms.ollama import Ollama
from llama_index.llms.openai import OpenAI
from index import es_vector_store

Settings.embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-en-v1.5")

# Use Public LLM to send user query and Related Documents and Local LLM to mask
public_llm = OpenAI()
local_llm = Ollama(model="mistral")

pii_processor = PIINodePostprocessor(llm=local_llm)
index = VectorStoreIndex.from_vector_store(es_vector_store)

# This query_engine, for a given user query retrieves top 10 similar documents from
# Elasticsearch vector database and sends the documents along with the user query to the public LLM.
# Note that documents are masked using the local llm via PIINodePostprocessor
# so that PII/Sensitive data is not sent to the public LLM.
query_engine = index.as_query_engine(public_llm, similarity_top_k=10, node_postprocessors=[pii_processor])


query = "Give me summary of fire related claims that customers raised."
bundle = QueryBundle(query, embedding=Settings.embed_model.get_query_embedding(query))
result = query_engine.query(bundle)
print(result)

响应如下所示

客户提出了与火灾有关的财产损失索赔。在一个案例中,由于纵火被排除在承保范围之外,车库火灾损失索赔被拒绝。另一位客户就其住宅遭受的火灾损失提出索赔,该损失属于其保单的承保范围。此外,一位客户报告了厨房火灾,并得到了火灾损失赔偿的保证。

结论

在这篇文章中,我们介绍了在 RAG 流程中使用公共 LLM 时如何保护 PII 和敏感数据。我们展示了实现这一目标的多种方法。强烈建议在采用这些方法之前,根据您的用例和需求对其进行测试。

相关内容

高级 RAG 技术第 2 部分:查询和测试

Han Xiang Choong

使用 Elasticsearch 解决实体问题,第 4 部分:终极挑战

Jessica Moszkowicz

在 Streams 中利用机器学习自动化日志解析

Nastia Havriushenko

利用弹性代理生成器和 GPT-OSS 构建人力资源人工智能代理

Tomás Murúa

高级 RAG 技术第 1 部分:数据处理

Han Xiang Choong

准备好打造最先进的搜索体验了吗?

足够先进的搜索不是一个人的努力就能实现的。Elasticsearch 由数据科学家、ML 操作员、工程师以及更多和您一样对搜索充满热情的人提供支持。让我们联系起来,共同打造神奇的搜索体验,让您获得想要的结果。

亲自试用