AI时代,敏感词过滤,如何精准且高效,方法+代码实现

前言

自从我开始搞大模型应用,就一直有一个头疼的问题困扰着我的团队,那就是避免敏感信息

传统的做法是通过一些匹配算法,过滤掉敏感词,这个后面我们再讲。

但大模型的对话中,想要防止他做一些不合法的事情,就比较困难了。

传统做法&代码实现

原理

比较经典的算法有kmp 字典树,ac自动机,或者配合起来使用。

我们这里用一种字节树(我起的名)的方法。就是将所有敏感词,构建成一颗树,如下图:

需要对一段文本过滤时,则将文本依次从左向右,在这颗树中匹配。

代码实现

github 传送门

树节点的结构:

rust 复制代码
pub struct Node<V>{
    key:u8,
    data:Option<V>,
    next:Vec<Node<V>>
}

敏感词的抽象:

rust 复制代码
pub trait AsBytes{
    fn as_byte(&self)-> &[u8];
}

功能实现:

rust 复制代码
impl<V> ByteMap<V>
{
    pub fn new()->Self{
        ByteMap{root:Node::default(0)}
    }

    pub fn insert<K:AsBytes>(&mut self,key:&K,value:V){
        let keys = key.as_byte().to_vec();
        self.root.insert(keys.into_iter(),value);
    }

    pub fn get<K:AsBytes>(&self,key:K)->Option<&V>{
        let keys = key.as_byte();
        self.root.get(keys.iter())
    }
    
    // 找到第一个匹配的项
    pub fn match_first<K:AsBytes>(&self,keys:K) ->Option<&V>{
        let keys = keys.as_byte();
        return self.root.match_first(keys.iter())
    }

    // 匹配所有子集
    pub fn match_all<'a, K: AsBytes>(&'a self, keys:&'a K) ->Vec<&'a V> {
        let path = keys.as_byte();
        let vals = vec![];
        return self.root.match_all(path.iter(),vals);
    }

    pub fn remove<K:AsBytes>(&mut self, key:K) ->Option<V>{
        let keys = key.as_byte();
        self.root.remove(keys.iter())
    }
}

敏感词抽象的默认实现

我们需要给各种类型实现AsByte的默认实现,方便后续使用。

rust 复制代码
// 数值类型,节省代码
macro_rules! number_default_for_as_bytes {
    ($($b:tt,$n:tt);*) => {
        $(
        impl AsBytes for $b
        {
            fn as_byte(&self) -> &[u8] {
                unsafe {
                    &*(self as *const $b as *const [u8;$n])
                }
            }
        }
        )*

    };
}
number_default_for_as_bytes!(u8,1;u16,2;u32,4;u64,8;u128,16;i8,1;i16,2;i32,4;i64,8;i128,16);

// 对usize 和isize的特殊处理,略,详细请看上面github传送门

// 对u8 list的一系列实现

// char的实现,方便中文的处理
impl AsBytes for &[char]{
    fn as_byte(&self) -> &[u8] {
        unsafe {
            std::mem::transmute(*self)
        }
    }
}
impl AsBytes for Vec<char>{
    fn as_byte(&self) -> &[u8] {
        let cs = self.as_slice();
        unsafe {
            std::mem::transmute(cs)
        }
    }
}

// 编译器不保证所有情况都解引用,我们给引用类型也加一个自实现
impl<T> AsBytes for &T
where T: AsBytes
{
    fn as_byte(&self) -> &[u8] {
        (*self).as_byte()
    }
}

注意: 在rust中想按中文字符切割,需要转成charchar长度是4 固定的,表示utf8时不会压缩空间。

使用

rust 复制代码
#[test]
fn byte_map_chinese(){
    let mut map = ByteMap::new();
    map.insert(&("你好".chars().collect::<Vec<char>>()),"你好");
    map.insert(&("hello".chars().collect::<Vec<char>>()),"hello");
    map.insert(&("123".chars().collect::<Vec<char>>()),"123");

    let target = "飞流123hello你好飞流直下三千尺".chars().collect::<Vec<char>>();

    for i in 0..target.len(){
        if let Some(s) = map.match_first(&target[i..]){
            println!("match ok ---> {}",s);
        }
    }
}

ai 相关的思考

分词

上面的匹配方式过于死板,随便加个符号,都能够轻松的被绕开。

为了加快速度和准确程度,我们可以对输入文本先过滤,再分词,再匹配。类似下面的方式

python 复制代码
import jieba
import re

input = "我来-到北京a清华大*学"
input = re.sub(r'[-a*]','',input)

seg_list = jieba.cut(input, cut_all=True)
print("Full Mode: " + "/ ".join(seg_list)) 

#> 我/ 来到/ 北京/ 清华/ 清华大学/ 华大/ 大学

语法纠正

因为汉语言的博大精深,有些话词序颠倒,正常人也能够理解,但是想找出他们,殊为不易。

这种情况,需要先做词法纠正,如纠正错别字,纠正字序错误,再找敏感词。

这种方法不常用,方案复杂,收益低,缺点明显。

机器学习

利用一些传统的机器学习算法,做分类。

对一段文本进行分类,存在敏感词 or不存在敏感词

这种方式需要衡量准确度,既容易误伤,又容易遗漏

NLP

利用NLP处理。

举个简单的思路:分词之后,计算目标词的向量,然后召回敏感词。

如果最相近敏感词和目标词的相似度超过某个阈值,则视为存在敏感信息。

也可以训练一个分类器 进行辨别。

这种方法速度稍慢,容易误伤。

集成

实际工程上,通常的做法是多个方法结合在一起使用。针对自己的业务,衡量耗时 效果 成本 等等因素,灵活的变通才是王道。

AIGC & 大模型

上面提到的过滤方法,都建立在你已知有哪些敏感词,有哪些敏感场景的情况下,进行防御的。

而现在的时代是,ai是生产者,ai是创造者,ai是消费者等等 多身份于一体的,那敏感信息,不合法的信息就更加难以防御。

看一下GPT是如何犯罪的。

如果你直接问它,如下图:

但咱们可以迂回一下:

看到这里,你大概是否理解了,我在前言中提到的头疼的问题,尤其是我们所处的互联网环境更加严格。

敏感词拦截

我们还是可以用传统的方式拦截,但有几个问题。

  1. 文本是流式的,那个要求过滤方式也是流式的。

    • 看到这,就解释了,有那么多现成的库,为啥我们要自己实现一套byte式的过滤方法。
    • 基于上面的我们的代码,你能否封装成一个流式处理库?
  2. 因为回复是实时模式,也就是你发现敏感词的时候,人已经看到了前面的内容,这种情况又要如何处理?

    • 数据加密,本文不讨论,以后再说

大模型安全

解决问题的最好方法,就是铲除源头。不要让模型产出有危害的信息是最好的方式。

这好像已经脱离了敏感词的范畴,以后我们单开一篇讲。

尾语

时代变了,我们面临的问题也变了,多思考,才不会落伍。

相关推荐
蚂蚁背大象1 小时前
Rust 所有权系统是为了解决什么问题
后端·rust
布列瑟农的星空2 小时前
前端都能看懂的rust入门教程(五)—— 所有权
rust
子玖2 小时前
go实现通过ip解析城市
后端·go
Java不加班3 小时前
Java 后端定时任务实现方案与工程化指南
后端
心在飞扬3 小时前
RAG 进阶检索学习笔记
后端
Moment3 小时前
想要长期陪伴你的助理?先从部署一个 OpenClaw 开始 😍😍😍
前端·后端·github
Das1_3 小时前
【Golang 数据结构】Slice 底层机制
后端·go
得物技术3 小时前
深入剖析Spark UI界面:参数与界面详解|得物技术
大数据·后端·spark
古时的风筝3 小时前
花10 分钟时间,把终端改造成“生产力武器”:Ghostty + Yazi + Lazygit 配置全流程
前端·后端·程序员