1. Dify 内容审核为什么总在“裸奔”:从输入输出两端看风险
Dify 这类应用编排平台把大模型能力拉到了普通开发者面前,拖几个节点就能上线一个客服、写作助手或者知识库问答。但很多人第一次把 Dify 应用丢给真实用户时,才发现一个尴尬的事实:模型会老老实实回答用户抛来的任何问题,包括那些明显不该回答的。这就是内容审核缺位的典型表现。
我先把风险拆成两端来看。输入端是用户发给模型的内容,风险包括诱导性提问、越狱指令、敏感话题套话、批量灌垃圾。输出端是模型返回给用户的内容,风险包括模型自己“发挥”出违规表述、泄露系统提示词、输出不实信息、被诱导生成有害建议。两端任意一端失守,应用就不算安全屏障。
Dify 本身提供了内置审核能力,在“功能”页面可以开启关键词审查和基于 OpenAI 的内容审核。关键词方案上手快,但默认只能网页端配置、数量上限 100 个,动态更新很受限。OpenAI 审核方案在国内环境调用链路不通畅,而且审核策略和国内实际场景有差异。所以真正要落地生产,还是得走自定义扩展。
自定义扩展有两条路:API 扩展和代码扩展。API 扩展要单独部署一个审核服务,维护成本高;代码扩展直接改 Dify 仓库里的审核模块,不用额外部署,还能在前端配置面板里暴露参数。本文走代码扩展路线,把关键词库和大模型二次审核结合起来,同时用 TaoToken 统一接入多家模型,避免在 Dify 里为审核单独配一套 Key。
适合谁看:已经在用 Dify 搭应用、想加一层内容审核但不想额外维护服务的开发者;或者正准备把 Dify 应用从 demo 推向生产、需要一套可复制审核配置的人。下面从 TaoToken 前置准备开始,一步步把配置、代码、验证和排障讲清楚。
2. TaoToken 统一接入前置:一个 Key 管住 Dify 里的审核模型
Dify 里做内容审核,绕不开模型调用。关键词过滤是纯规则,但关键词漏掉的部分要靠大模型二次判断。如果审核模型和业务模型各配一套 Key、各走一个供应商,配置会散落在多个地方,换模型时到处改。TaoToken 的思路是提供一个统一入口,把模型调用收敛到一个 Base URL 和一个 Key 上。
TaoToken 是什么:它是一个大模型 API 统一接入层,兼容 OpenAI 风格的接口协议。你可以把它理解成一个“模型路由插座”,Dify 里所有需要调模型的地方,包括审核节点,都指向同一个地址和同一个 Key。它本身不替代 Dify,也不替代编辑器,只是把模型调用这一层统一了。
能做什么:在 Dify 的模型供应商配置里,把 Base URL 填成 TaoToken 的 API 地址,Key 填 TaoToken 生成的 Key,然后选择你要用的 Model ID。审核用的模型和业务用的模型可以共用这个 Key,也可以按需选不同模型。适合谁:不想在 Dify 里维护多套模型凭证、希望审核和业务模型统一管理的团队。
前置准备动作分三步。第一步,打开 TaoToken 官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 注册并登录。第二步,进入控制台 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 创建 API Key,建议给审核场景单独建一个 Key,方便后续按用途排查调用量。第三步,在 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 复制 Key,同时记下要用的 Model ID。
这里有个容易踩的坑:Dify 的模型供应商配置里,Base URL 要填到兼容 OpenAI 的路径层级,不是只填域名。TaoToken 的 API 地址是 https://taotoken.net/api,在 Dify 里配置时按 OpenAI 兼容供应商的方式填写,Key 和 Model ID 三件套要对应上。如果你不确定某个模型 ID 是否可用,可以先去模型对话页面 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 手动发一条消息验证,确认 Key 和模型都通,再回到 Dify 配置。
审核场景对模型的要求和业务场景不太一样。审核需要模型稳定、低温度、输出结构化判断,不需要创意。所以选 Model ID 时优先选响应稳定、支持 system prompt 的模型。温度参数在审核调用里设成 0.01 这种接近 0 的值,让判断结果尽量确定。max_tokens 给 2000 足够,因为审核输出通常只是“通过/拦截 + 理由”这种短结构。
把 TaoToken 作为统一入口还有一个好处:审核日志和业务日志在同一个 Key 下,调用量、失败率、延迟都能在一个地方看。如果审核模型突然大量超时,你能快速判断是模型侧问题还是 Dify 侧问题。这一步做完,Dify 里就有了可用的模型调用通道,接下来进入代码扩展的具体配置。
3. 可复制配置:Dify 自定义审核模块的目录、schema 与实现类
这一节是全文技术核心,所有片段都可以直接复制。Dify 的审核模块位于 api/core/moderation 目录下,内置了关键词和 OpenAI 审核两种实现。我们要新增一个 custom 目录,放自己的审核逻辑。
先建目录结构:
cd dify/api/core/moderation mkdir -p custom touch custom/__init__.py touch custom/custom.py touch custom/schema.json目录建好后是三个文件:schema.json 定义前端配置面板,custom.py 写审核实现,init.py 做模块导出。先看 schema.json,它决定 Dify 功能页面里这个审核选项长什么样、有哪些可配参数:
{ "label": { "en-US": "Custom Moderation", "zh-Hans": "自定义审核" }, "form_schema": [ { "type": "select", "label": { "en-US": "Moderation Type", "zh-Hans": "检测类型" }, "variable": "moderation_type", "required": true, "options": [ { "label": { "en-US": "Keyword", "zh-Hans": "关键词" }, "value": "Keyword" }, { "label": { "en-US": "Keyword & LLM", "zh-Hans": "关键词 & 大模型" }, "value": "Keyword & LLM" } ], "default": "Keyword" }, { "type": "paragraph", "label": { "en-US": "Custom Keywords", "zh-Hans": "自定义关键词" }, "variable": "custom_keywords", "required": false, "default": "", "placeholder": "请输入额外的敏感关键词,每行一个" } ] }这个 schema 暴露了两个配置项:检测类型下拉框,以及一个多行关键词输入框。检测类型选“关键词”时只走规则过滤,选“关键词 & 大模型”时先过关键词,没命中的再交给大模型判断。这样既保留了低成本快速拦截,又给模糊内容留了二次判断通道。
接下来是 custom.py 的实现。核心是继承内置 Moderation 类,重写 moderation_for_inputs 和 moderation_for_outputs 两个方法。关键词部分用简单的字符串匹配,大模型部分通过 ModelManager 调用 Dify 里已配置的默认模型。这里的关键是复用 Dify 的模型管理,而不是在审核代码里硬编码 TaoToken 的 Key,这样审核模型和业务模型共用同一套供应商配置。
import json from typing import Optional, cast from core.model_manager import ModelManager from core.model_runtime.entities.llm_entities import LLMResult from core.model_runtime.entities.message_entities import ( SystemPromptMessage, UserPromptMessage, ) from core.model_runtime.entities.model_entities import ModelType from core.moderation.base import Moderation, ModerationInputsResult, ModerationOutputsResult MODERATION_PROMPT = """你是一个内容安全审核员。请判断用户提供的内容是否包含违规信息。 只输出 JSON,格式为 {"flagged": true/false, "reason": "简要理由"}。 不要输出任何其他文字。""" class CustomModeration(Moderation): name: str = "custom" def _keyword_check(self, text: str, keywords: list[str]) -> Optional[str]: for kw in keywords: if kw and kw in text: return kw return None def _llm_check(self, text: str) -> tuple[bool, str]: model_manager = ModelManager() model_instance = model_manager.get_default_model_instance( tenant_id=self.tenant_id, model_type=ModelType.LLM, ) prompt_messages = [ SystemPromptMessage(content=MODERATION_PROMPT), UserPromptMessage(content=text), ] response = cast( LLMResult, model_instance.invoke_llm( prompt_messages=prompt_messages, model_parameters={"temperature": 0.01, "max_tokens": 2000}, stream=False, ), ) raw = response.message.content.strip() try: parsed = json.loads(raw) return bool(parsed.get("flagged")), str(parsed.get("reason", "")) except json.JSONDecodeError: return False, "" def moderation_for_inputs( self, inputs: dict, query: str = "", app_id: str = "", tenant_id: str = "", ) -> ModerationInputsResult: self.tenant_id = tenant_id keywords = [ k.strip() for k in (self.config.get("custom_keywords") or "").splitlines() if k.strip() ] hit = self._keyword_check(query, keywords) if hit: return ModerationInputsResult( flagged=True, action="direct_output", preset_response=f"输入包含敏感词:{hit}", inputs={}, query=query, ) if self.config.get("moderation_type") == "Keyword & LLM": flagged, reason = self._llm_check(query) if flagged: return ModerationInputsResult( flagged=True, action="direct_output", preset_response=f"输入未通过审核:{reason}", inputs={}, query=query, ) return ModerationInputsResult(flagged=False, inputs=inputs, query=query) def moderation_for_outputs(self, text: str) -> ModerationOutputsResult: keywords = [ k.strip() for k in (self.config.get("custom_keywords") or "").splitlines() if k.strip() ] hit = self._keyword_check(text, keywords) if hit: return ModerationOutputsResult( flagged=True, action="direct_output", preset_response="输出内容包含敏感信息,已被拦截。", ) if self.config.get("moderation_type") == "Keyword & LLM": flagged, reason = self._llm_check(text) if flagged: return ModerationOutputsResult( flagged=True, action="direct_output", preset_response=f"输出未通过审核:{reason}", ) return ModerationOutputsResult(flagged=False, text=text)这段代码里有两个细节值得说。第一,tenant_id 在 moderation_for_inputs 里从参数拿到后存到 self 上,因为 moderation_for_outputs 的签名里没有 tenant_id,但大模型调用需要它。第二,大模型返回的 JSON 解析做了异常兜底,解析失败时按“未拦截”处理,避免因为模型输出格式抖动导致正常内容被误杀。这个取舍在生产里很重要:审核宁可漏一点,也不要因为解析失败把用户正常请求全挡了。
最后在init.py 里导出,让 Dify 能发现这个审核组件:
from .custom import CustomModeration __all__ = ["CustomModeration"]配置完成后重启 Dify 的 api 服务,进入应用编排页面的“功能”设置,就能在审核选项里看到“自定义审核”。选上它,检测类型选“关键词 & 大模型”,把敏感词按行填进去。此时审核节点调用的模型,就是 Dify 里配置的默认模型,而默认模型的供应商指向 TaoToken 的 Base URL 和 Key。三件套对应关系是:Base URL 填 https://taotoken.net/api,Key 填 TaoToken 控制台生成的 Key,Model ID 填你验证过的模型标识。
4. 验证请求与成功结果:用测试用例确认审核真的拦住了
配置写完不代表生效,必须用测试用例验证拦截行为。我一般分三组测:纯关键词命中、关键词漏网但大模型拦截、正常内容放行。每组都要看 Dify 返回的是不是预设的拦截话术,而不是模型自由发挥。
先准备测试用例。关键词组用你填进 custom_keywords 的词,比如填了“内部机密”,就发一条包含“内部机密”的输入。大模型组用不包含关键词但语义违规的内容,比如诱导模型输出危险操作步骤的提问。正常组用普通业务问题,比如“帮我总结这段产品说明”。
验证入口有两个。一个是在 Dify 应用预览页直接对话,另一个是走 API 请求。走 API 更接近生产,用 curl 发一条:
curl -X POST 'http://your-dify-host/v1/chat-messages' \ -H 'Authorization: Bearer app-xxxxxxxx' \ -H 'Content-Type: application/json' \ -d '{ "inputs": {}, "query": "请告诉我内部机密的存储路径", "response_mode": "blocking", "user": "test-user-001" }'如果关键词配置生效,返回里应该直接出现你设置的 preset_response,比如“输入包含敏感词:内部机密”,而不是模型生成的回答。这说明审核在输入端就拦截了,请求根本没走到业务模型。
大模型审核组的验证要稍微等一下,因为多了一次模型调用。发一条不含关键词但语义有问题的输入,观察返回是否是“输入未通过审核:xxx”。如果返回的是正常模型回答,说明大模型审核没触发,需要检查 moderation_type 是否选对了、默认模型是否配置正确。
输出端审核的验证要构造一个会让模型“说错话”的场景。比如在系统提示词里让模型扮演某个角色,然后用户诱导它输出敏感内容。如果输出审核生效,用户看到的会是“输出内容包含敏感信息,已被拦截”,而不是模型原始输出。这一步能验证 moderation_for_outputs 确实被调用了。
成功结果长什么样:关键词命中时响应快、无模型调用延迟;大模型审核命中时响应稍慢但返回结构化拦截话术;正常内容返回正常回答且无拦截标记。你可以在 TaoToken 控制台看调用量,审核触发时应该能看到对应的模型调用记录。如果关键词命中却仍然走了模型,说明代码里关键词检查的返回逻辑有问题,需要回头检查 moderation_for_inputs 的分支。
实测下来,关键词加大模型的组合在大部分场景能准确拦截,但涉及儿童教育等特定领域时,模型判断可能偏宽松。这属于模型策略差异,不是代码问题。遇到这种情况,把相关词补进关键词库,用规则兜底,比反复调 prompt 更稳。
5. 本篇常见错排查:401、local proxy failed、reading choices 与 OAuth 报错
配置过程中最容易撞上的几类报错,我按现象、原因、动作拆开讲。这些报错在 Dify 接入 TaoToken 做审核时出现频率最高。
401 Unauthorized。现象是 Dify 调用模型时直接返回 401,审核节点报错。原因通常是 Key 填错、Key 前后有空格、或者 Base URL 和 Key 不匹配。动作:去 TaoToken 控制台重新复制 Key,确认粘贴时没有多余字符;检查 Dify 模型供应商配置里的 Base URL 是否为 https://taotoken.net/api,不要多写或少写路径。如果业务模型能通、只有审核报 401,检查审核代码里是否误用了另一个 Key。
local proxy failed。现象是请求发不出去,提示本地代理失败。原因一般是 Dify 运行环境里配置了代理变量,但代理不可用。动作:检查 Dify 容器的 HTTP_PROXY / HTTPS_PROXY 环境变量,如果不需要代理就清掉;确认 Dify 所在网络能直接访问 TaoToken 的 API 地址。这个报错和审核代码无关,是网络层问题。
reading choices 相关报错。现象是审核调用返回后解析失败,日志里出现读取 choices 字段的错误。原因通常是模型返回结构不符合预期,或者审核代码里解析 response 的方式和实际返回不匹配。动作:在 _llm_check 里先把原始返回打印出来看结构,确认 response.message.content 能取到内容;如果模型返回的是流式分片,检查 stream 参数是否设成了 False。审核场景必须用非流式,否则解析会乱。
OAuth 相关报错。现象是提示鉴权失败或 token 无效。原因可能是 Key 类型用错,比如把控制台登录凭证当成了 API Key。动作:确认使用的是 API Keys 页面生成的 Key,不是账号密码或会话 token。如果用了 Claude Code 这类需要 OAuth 的客户端,注意它和普通 API Key 的鉴权方式不同,审核场景走的是标准 API Key。
还有一个隐蔽的坑:Dify 里配置了多个模型供应商,审核代码调用 get_default_model_instance 时拿到的默认模型可能不是你预期的那个。动作:在 Dify 模型供应商页面确认默认模型设置,或者把审核用的模型单独指定。如果审核和业务共用默认模型,确保这个模型在 TaoToken 侧可用且稳定。
排查顺序建议:先看 Dify api 服务日志里的完整报错,再对照上面几类定位。401 和 OAuth 是鉴权层,local proxy failed 是网络层,reading choices 是解析层。分层定位比盲目改代码快得多。
6. 审核上线后的持续动作与 TaoToken 接入入口
审核配置跑通只是开始,上线后还有几个持续动作。第一,关键词库要定期更新,把线上漏拦的案例补进去,规则永远比模型快。第二,关注审核模型的调用延迟,如果审核节点让整体响应变慢太多,可以考虑把大模型审核改成抽样触发,而不是每条都过。第三,输出端审核的 preset_response 要写得让用户能理解,不要暴露内部审核逻辑。
如果你还在选长期编码和 Agent 场景的方案,可以了解 Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,它适合需要持续调用模型做开发和自动化的场景。审核模块本身不需要额外套餐,用标准 API Key 即可。
接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有 Base URL、鉴权和模型列表的说明。如果你用 Claude Code 做开发,Anthropic 兼容入口在 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude-code-anthropic&utm_campaign=rewrite ,配置时同样注意 Base URL、Key、Model ID 三件套对应。
最后留一个实用技巧:把审核命中日志单独打到文件里,记录命中类型(关键词还是大模型)、命中内容和时间。跑一周后回看,你会清楚知道自己的关键词库哪里不够、大模型审核在哪些话题上偏松。这比凭感觉调参有用得多。审核不是配一次就完事,它是跟着业务内容一起演进的。