拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Dify关键词审核实战:从工作流节点到API封装的内容安全方案

Dify关键词审核实战:从工作流节点到API封装的内容安全方案

当时我给自己做的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节点之后。放前边的话,你审的是用户输入,根本没碰到模型生成的结果,模型照样可能吐出问题内容。

一个典型的工作流编排顺序是:

  1. 开始节点接收用户Query;
  2. 条件判断节点或代码节点先对Query做快速粗筛;
  3. 通过后进入LLM节点生成回答;
  4. LLM节点之后紧跟关键词审核节点,对完整回答做匹配;
  5. 根据命中结果走放行分支或拦截分支。

这样的顺序保证审核覆盖面最完整:用户输入有防护,模型输出也有筛查,两侧都管住了。

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问题的排查链路我建议按这个顺序走:

  1. 确认API密钥是否正确,是不是从目标应用里复制的;
  2. 确认请求头带的设备标识,Dify用Authorization: Bearer方式传API Key,不要自定义Header名;
  3. 确认网络出口有没有被Dify的访问控制拦截;
  4. 如果启用了多租户,确认这个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的结果。你会发现,内容审核这件事,玩到后面拼的不是拦截,是治理。

返回列表