当时我给自己做的AI客服接上Dify后,还没高兴两天,就发现一个特别闹心的问题:用户只要在对话里稍微绕一下,模型就可能顺着话头输出一些不合适的内容。提示词里写再多“注意安全”,LLM也不一定真当回事。后来我把重点放到Dify平台自身,补了一套关键词审核机制,才算是把内容安全这条线彻底管住了。
这篇文章就围绕Dify里的关键词审核展开,从审核关口怎么选、工作流节点怎么写、审核API怎么封装,到敏感词库性能优化和上线后的各种坑,一步步完整拆解。不管你是刚开始接触Dify,还是已经在跑生产环境的项目,这套方案都能直接拿去改、拿去用。
1. 为什么内容审核偏偏要从“关键词”做起
很多人一听说内容审核,第一反应是上大模型语义审核。但实际做下来你会发现,关键词审核才是性价比最高、最容易在Dify里落地的那一层。
1.1 大模型输出天然带“失控概率”
大模型生成内容的特点是:每次生成都不一样,同一个问题换个说法可能就是另一个结果。你可以在系统提示词里写“你是合规助手,不得输出违规内容”,模型大概率会配合,但遇到对抗性输入、角色扮演指令或长文本诱导时,仍然有概率绕过去。
这不是模型不聪明,而是提示词约束本质上属于“软约束”,它依赖模型的语义理解能力。对于内容安全要求明确、违规形态相对固定的场景,你需要一种“硬约束”——不管模型怎么想,只要文本里出现了不该出现的东西,就必须拦截。关键词审核就是这种硬约束。
关键词方案的另一个优势是可解释性极强。模型审核返回一句“疑似违规”,你很难向客户解释为什么;关键词审核可以精确到“命中了哪个词、出现在哪个位置”,处理流程透明,用户也服气。
1.2 关键词审核能管住什么,管不住什么
关键词审核的效率很高,适合以下场景:
- 管控明确违禁词,比如平台规则里白纸黑字列出来的违规类型;
- 拦截广告营销类内容,比如导流话术、替人刷单、假冒商品等描述;
- 对重复垃圾信息做快速过滤,比如连续发送相同内容;
- 作为模型审核的前置粗筛,把明显不合规的内容挡在模型调用之前,省下推理成本。
但它的短板也同样明显:关键词没办法识别语义层面的变体。用户把敏感词拆开写、加入表情符号、或者用完全不含关键词的句式表达同一个意思,关键词匹配就很难生效。所以成熟的做法是“关键词先行,模型兜底”,关键词负责挡掉大多数确定性违规,模型审核负责处理语义模糊的疑似内容。
1.3 在Dify里做关键词审核的独特优势
Dify本身是LLM应用开发平台,工作流编排里刚好提供了代码节点、判断节点、变量聚合这些能力。意味着一套完整的关键词审核逻辑,不需要单独写服务、不需要另搞数据库,就在Dify工作流里可以全部编排出来。
这对于已经有Dify环境的团队来说,省掉了一步基础设施成本。你不需要把审核逻辑放进业务后端里,也不需要在模型网关层额外挂一层代理,审核规则可以直接跟应用走。后续想要调整关键词列表,也只需要改工作流里的关键词配置,不用重新部署业务服务。这正是我推荐在Dify里做关键词审核的核心原因:审核与应用在同一个编排体系里,规则即配置,迭代成本最低。
2. 审核模块在大模型应用里到底卡在哪道关口
设计审核模块的时候,先别急着写代码。想清楚一个问题:审核要发生在什么位置?位置选错了,后面全白搭。
从API请求到最终用户看到内容,整条链路大致有三个可插入审核的关口。
2.1 三个可插入审核的位置对比
位置A:用户输入进模型之前。用户发出的Query先过一遍关键词,不合格的直接拦截,不让LLM处理。这个位置适合拦截明显的恶意输入、隐私探测、违法违规请求。好处是能省下模型调用成本;坏处是只能审输入,审不了模型输出。
位置B:模型生成结果之后、返回给用户之前。模型把内容吐出来了,先跑关键词审核,命中违规就替换成预设的提示文案,不命中则原样放行。这个位置能覆盖模型自身的输出风险,是内容安全最关键的一道关卡。
位置C:应用出口处做事后日志审计。对话已经完成,审核逻辑放在日志系统或回调服务里,识别出问题内容后做告警和分析。位置C不拦截用户,只用于复盘和持续优化规则。
对Dify工作流来说,最实用的是走“先审输出、再审输入”的组合:输入先粗筛,输出再细查。但如果你只想先做最核心的一步,优先把输出审核做好,模型可能产生的不合规内容才是你真正要防的东西。
2.2 为什么审核节点要放在LLM节点之后
有朋友问过我:把审核放在前面还是后面,是不是都行?我的建议很明确——需要审核模型输出时,节点必须放在LLM节点之后。放前边的话,你审的是用户输入,根本没碰到模型生成的结果,模型照样可能吐出问题内容。
一个典型的工作流编排顺序是:
- 开始节点接收用户Query;
- 条件判断节点或代码节点先对Query做快速粗筛;
- 通过后进入LLM节点生成回答;
- LLM节点之后紧跟关键词审核节点,对完整回答做匹配;
- 根据命中结果走放行分支或拦截分支。
这样的顺序保证审核覆盖面最完整:用户输入有防护,模型输出也有筛查,两侧都管住了。
2.3 关键词审核放在独立应用还是主应用内
这里有两种选择:审核逻辑直接嵌入业务应用的工作流里;或者单独建一个“审核工作流应用”,用API方式对外提供审核服务。
我建议这样判断:如果Dify只跑一个应用,审核节点直接嵌进主应用工作流就行,简单直接。但如果平台里有多个应用都要过审,或者外部系统也想调用Dify来审内容,那最好单独做一个“内容审核API应用”,把审核逻辑封装成标准接口。这样各类应用都来调它,规则统一维护,不会出现每个应用一套词表的混乱局面。
后面第4章我会详细讲这个“审核API应用”怎么做。这里你只需要记住:审核模块做在哪个应用里不影响核心逻辑,影响的是后续维护半径。
3. 最轻量的落地方案:Dify工作流里的关键词审核模块
接下来进入正题,实现一个可以跑起来的关键词审核工作流。这里用Dify工作流的三个核心节点搞定:代码节点、判断节点、结束节点。
3.1 用代码节点还是规则节点,这是个关键选择
Dify的规则节点里其实提供了“字符串包含”这类判断操作,很多人一开始会用它来做关键词匹配。但我建议直接上代码节点,原因有三个:
- 规则节点的字符串包含是单个关键词的精确包含判断,要做多关键词、分词、归一化,你得串十几个条件节点,编排看起来很松散;
- 规则节点很难处理大小写、全半角、空白字符这类变体;
- 代码节点内可以做循环、正则、归一化,一套逻辑集中在一个节点里,后续改配置只动一处。
代码节点的使用门槛其实不高,Dify支持Python,把一段匹配逻辑写进去,设置好输入变量和输出变量就行。
3.2 一份可直接落地的Python审核脚本
以下是我在Dify代码节点里常用的审核脚本,简化版,方便你看懂结构:
def main(query: str) -> dict: # 敏感词表,实际使用中按业务场景维护 sensitive_words = ["违规链接", "违禁品", "刷单", "假冒商品"] normalized_words = [normalize_word(w) for w in sensitive_words] normalized_text = normalize_word(query) hit_words = [] for word, origin in zip(normalized_words, sensitive_words): if word in normalized_text: hit_words.append(origin) if hit_words: return { "status": "block", "hit_keywords": hit_words, "message": "内容包含违禁关键词,请修改后重试" } return { "status": "pass", "hit_keywords": [], "message": "" } def normalize_word(text: str) -> str: import unicodedata import re # 全角转半角、大小写折叠 text = unicodedata.normalize("NFKC", text).casefold() # 去掉所有空白字符和零宽字符 text = re.sub(r"[\s\u200b-\u200d\uFEFF]+", "", text) return text这个脚本的匹配逻辑不复杂:先把输入和词库都做归一化(NFKC全半角转换、casefold大小写折叠、去空白),再用子串匹配判断命中。输出的status字段是关键,后续判断节点靠它分流。
为什么把敏感词直接写在代码里?发布前期关键词只有几十个,写死在脚本里最直观,谁都能维护。等词库大到几百上千之后,再考虑后文讲的外部化方案。
3.3 判断节点分流:放行、拦截、还是走人工
代码节点输出status后,后面接一个判断节点。判断条件很简单:
- status 等于 “pass”:内容合规,走正常流程;
- status 等于 “block”:内容违规,走拦截分支。
拦截分支的结束节点里,直接返固定提示文本给用户,比如“抱歉,当前内容未通过审核,请调整后重新提交”。这里要注意:拦截信息要友好,不要暴露审核规则细节,也不要列出具体命中的敏感词,避免用户针对性地变体绕行。命中词只记录到日志里,给运营同学看。
如果你需要“疑似违规走人工”的第三态,脚本里可以增加一个“review”状态,判断节点再拉一个分支,把内容转给人工处理队列。对于内容生成类应用,三态比二态更实用,宁可多转人工也不想误杀。
3.4 敏感词表维护:写死在代码里还是拉出去
词表放代码里确实方便,但随着运营频次上来,问题也来了:每次改几个词,都要进Dify改工作流,重新发布。为此我建议按阶段选方案:
- 百级以内关键词:直接写代码节点里,最省事;
- 千级关键词:放到环境变量或外部文件,通过HTTP请求节点读取词库服务;
- 大量且持续变动的词库:用Dify知识库管理词表,工作流里加知识检索节点,把用户内容拿去检索比对。不过这个方案检索延迟略高,而且语义检索可能漏召回,适合作为辅助而不是唯一手段。
我目前线上环境用的是“代码节点内置基础词表+外部审核API补充高级规则”的组合。基础词表负责百来个高确定性违禁词,外部API负责正则表达式、复杂变体和人工名单,两边各司其职。这也是Dify做内容审核最灵活的地方:核心逻辑和外部能力可以无缝打通。
4. 把审核能力封装成接口:自建审核API的完整姿势
如果你的审核逻辑只在Dify应用内部用,到这一步就算完了。但更多场景下,审核能力要被多个业务系统调用——这就涉及把审核工作流做成一个独立的API服务。
4.1 审核工作流怎么对外暴露成“内容审核API”
在Dify里,任何工作流应用都可以开启API访问。把上面第3章说的审核工作流单独发布成一个应用,然后到“API访问”页面拿API密钥。调用方只要发一个HTTP请求,传待审核文本,就能拿到审核结果。
这个工作流应用里不要放LLM节点,纯代码节点+判断节点足够。这样每次审核调用不会消耗模型Token,成本极低,响应速度也快。实际测试中,一次纯代码审核通常在几百毫秒内就能返回,完全可以承担高并发审核压力。
4.2 请求与响应参数设计
对外接口建议就设计两个核心入参:
{ "inputs": { "text": "需要审核的内容", "scene": "customer_service" }, "response_mode": "blocking", "user": "audit-system" }scene参数用于区分业务场景,比如客服对话、内容生成、用户昵称,不同场景可以用不同词表。这个参数在工作流里要传给代码节点使用。
响应的返回结构关键就三个字段:
{ "audit_status": "block", "hit_keywords": ["违禁品"], "message": "内容包含违禁关键词,请修改后重试" }audit_status对应对标接口调用方直接判断,hit_keywords给运营留审计信息。我把这套输出设计成和Dify节点内部一致的结构,这样工作流内部调用外部API时,数据也能无缝衔接。
4.3 鉴权问题的典型排查:403与Credentials Validation
把审核API给外部系统接入时,最常遇到两类报错:一类是Dify调用第三方服务报“credentials validation”失败,另一类是外部调你的Dify接口拿403。
“credentials validation”失败,多半是第三方服务配置里的API Key或密钥填错了,或者是自定义工具Schema里的鉴权字段名和服务端要求不一致。排查时先去看Dify的自定义工具配置页,确认鉴权方式选的是API Key还是OAuth,重新粘贴密钥;还不行,就抓一下请求看实际发出去的头是否带上了正确的Authorization字段。
403问题的排查链路我建议按这个顺序走:
- 确认API密钥是否正确,是不是从目标应用里复制的;
- 确认请求头带的设备标识,Dify用Authorization: Bearer方式传API Key,不要自定义Header名;
- 确认网络出口有没有被Dify的访问控制拦截;
- 如果启用了多租户,确认这个API Key属于当前空间,而不是别的空间。
很多403不是密钥错了,是请求头传的方式不对,或者复制时把前后空格带上了,这个问题出的次数最多。
4.4 审核结果对外返回时,注意不要泄漏规则
审核API返回给调用方的信息,与返回给用户的提示文案要分开。对调用方,可以返回命中的关键词、分类、审核时间;对终端用户,只返回一句友好提示。否则用户知道你是靠“违规链接”这个词拦他的,他可以换个说法继续试探。
我在接口层面用一个简单映射处理:内部返回完整审核结果给调用系统,调用系统再根据audit_status自己决定怎么展示。这样既保证了面向运营的透明度,也保持了对终端用户的克制。
5. 从能用到好用:敏感词库进化与匹配性能优化
如果你的审核词表还停留在几十个词,这段可能觉得用不上。但要真上线跑几个月,词表规模一定会涨,性能优化的价值就出来了。
5.1 敏感词库大了之后:从逐个匹配到Trie/正则
Python里用“for循环+in子串判断”匹配几十个词是完全没问题的。词表加到几百上千时,匹配次数成了瓶颈,因为每个词都要扫一遍文本。两个优化方向:
一是把所有词组合成一个大正则。Python的re引擎底层做了优化,多个词用|连接后,一次扫描就能找出所有命中项。
import re pattern = re.compile("|".join(map(re.escape, sensitive_words))) matches = pattern.findall(text)注意需要用re.escape处理词表里的特殊字符,避免正则语法冲突。这种方案适合几千词级别,代码改动最小。
二是用Aho-Corasick自动机。它一次遍历文本就能找到全部命中的敏感词,时间复杂度O(n)不随词表大小线性恶化。如果Dify代码节点的沙箱环境允许装第三方库,pyahocorasick是首选;沙箱装不了,就把这个匹配逻辑挪到外部审核API服务里,再用HTTP请求节点去调用。
5.2 变体对抗:大小写、全半角、数字替换、拆字
内容对抗的核心手法不外乎几种:大小写混写、全半角切换、数字代替字母、字符中间插空格或零宽字符。第3章的normalize_word函数已经处理了全半角、大小写和空白,但对抗更激进时,还需要加两个处理:
- 数字和字母替换:把“0”归一为“o”,“1”归一为“l”,“3”归一为“e”,这样“l0gin”这类写法可以被打回原形;
- 拼音变体和同音字:这个没法靠简单归一化覆盖,只能配合外部规则表做映射或让模型审核兜底。
归一化是双刃剑。全局把“0”替换成“o”会误伤正常内容,例如订单号“No.1024”会被改成“nolole”。所以我的策略是:归一化只应用在敏感词匹配环节,够匹配用就行,不要把归一化后的文本展示给用户。也就是必须在独立算法里做归一化,不要污染原始数据。
5.3 审核结果缓存与降级策略
审核服务响应要快到什么程度?对在线对话场景,建议把审核命中结果做缓存。同一内容如果短时间内重复进来,直接返回第一次的审核结果,减轻匹配压力。缓存可以用外部Redis,也可以只在代码节点里用一个简单的过期字典,看你们的基础设施情况。
比缓存更重要的是降级策略。我在生产环境里遇到过一个事故:外部审核API服务偶发超时,直接把所有内容都拦截了,用户体验崩了。后来明确了降级原则:
- 内容安全要求极高的场景(金融、医疗、未成年相关),审核服务不可用时默认拦截并告警;
- 一般性内容生成场景,审核服务不可用时默认放行,同时把审核异常写入监控日志,人工介入处理。
没有一个降级策略是完美的,你要结合业务容忍度决定是“宁可错杀”还是“宁可漏放”。我的建议是设置开关位,把默认动作做成可配置,不要写死在代码里。
6. 上线四个月踩过的坑:Dify审核配置里的环境与兼容问题
最后这部分是实战里最容易踩坑的地方。很多问题不是审核逻辑本身,而是Dify平台的配置和环境细节。我把遇到过的典型问题列成一张排查表,再展开讲几个影响最大的。
6.1 代码节点“明明匹配了却不生效”的排查链路
这是初学者最容易卡壳的地方。脚本逻辑在本地跑没问题,贴到Dify代码节点里就是不出结果。排查点有三个:
- 代码节点的输入变量名是不是和你函数参数名完全一致。Dify代码节点里的输入映射是大小写敏感的,参数名对不上,变量就是空值;
- 代码节点一定要把结果用return以dict形式返回,然后在后续节点里用“node_name.output”这种变量引用方式取值,别漏掉节点ID;
- 判断节点里比较的字段名和代码节点输出的字段名是否一致,多一个下划线都会导致条件走不进去。
这些问题本质上都是Dify节点间的数据流没有打通。所以我的习惯是先加一个空的LLM节点或调试输出节点把代码结果打印出来,确认字段有值,再继续往下接。
6.2 换行符、编码与敏感词漏检
敏感词表从Excel里复制出来,经常带着\r或不可见字符。词表里有隐藏的换行符,匹配词库时会发现“词根本比对不上”。解决办法是在写词表时统一做清洗,去掉首尾空白和不可见字符。UTF-8编码也是一样的道理,词表文件乱码时先检查编码,不要直接往代码里粘贴。
所有从外部导入词表的环节,我都会在脚本里跑一遍清洗函数:逐条strip,跳过空字符串,输出词条数做校验。这个操作看着简单,能挡掉大部分“词表没生效”的幻觉问题。
6.3 部分匹配导致误杀正常内容
关键词子串匹配最大的误杀场景是“部分匹配”。“客服”这个词如果进了词表,那“客户服务”四个字也可能被误伤。比如有些业务里,“发票”是正常词,但在另一些场景里和违规刷单强相关。完全一刀切的词表一定会误杀。
我的经验是把词表按“命中类型”分级:
- 级词(高确定):只要出现就拦截;
- 级词(弱确定):需要满足额外条件才拦截,比如同一个词连续出现多次,或和另一个词同时出现;
- 白名单词:覆盖正常业务词,优先于敏感词判断。
这套分级可以在代码节点里用一个字典实现,也可以在外部审核API里维护规则。逻辑不复杂,但对降低误杀率帮助极大。
6.4 多租户、版本升级与SSL等平台级问题
审核工作流跑久了,还会遇到一批平台层面的问题,这是普通教程不会写的:
- 多租户隔离问题:Dify社区版新版本支持多租户后,注意每个空间的API Key和工作流是隔离的。你在这个空间配置好的审核规则,不会自动同步到其他空间。升级最好先导出工作流DSL备份,在新版本导入验证,再切流量。
- Dify升级后工作流不兼容:有次升级后,我的代码节点输出字段没有被后续节点正确识别,排查了一圈发现是旧的判断节点配置和版本不兼容。升级前一定先看发布说明,升级后跑一遍审核测试集,别只跑一个正常用例就上线。
- 审核API调用外部服务遇到SSL错误:内网环境用自签名证书时,Dify容器默认不信任该证书,会报SSL握手错误。正规做法是把CA证书挂载进Dify容器;临时做法是让外部服务暴露HTTP内网地址,但别在生产环境允许未加密传输。
- 遇到底层报错先看服务日志:Dify部署在Docker里时,
docker logs是排障第一入口。代码节点的Python异常、节点执行超时、外部API不通,基本都能在服务日志里看到具体堆栈,不要只盯着前端页面的错误提示。
踩过这一圈之后我的体会是:Dify里做关键词审核,逻辑本身一天能写完,真正花时间的是让它在自己的部署环境里稳定跑起来,并且不被误杀、漏判、平台升级这些事反复折腾。这套方案现在是我的标准做法,每次新项目要过内容安全,直接拿这套关键词审核工作流改词表就上。
如果后续有条件,建议把审核逻辑做一层更细的“分级处置”和“命中分析报表”出来,给运营同学提供决策依据,而不是只给一个pass/block的结果。你会发现,内容审核这件事,玩到后面拼的不是拦截,是治理。