Featured image of post 模型安全检测 01:JoySafety 功能验证(四大场景实测)

模型安全检测 01:JoySafety 功能验证(四大场景实测)

官方 4 个测试案例全通过:正常放行、辱骂拦截、暴恐识别、提示词注入,附返回报文解读。

一、概述

JoySafety 是一个大模型内容安全防护系统,提供敏感词检测、模型分类、知识库搜索等检测能力,并通过 DAG 策略编排检测流程。

核心架构:

1
原子能力(Atomic Capability) → 业务(Business) → DAG 策略 → 审核接口(Defense API)

快速上线三步走:

  1. 上线原子能力 → 注册模型/检测能力
  2. 上线业务 → 创建业务并启用
  3. 上线 DAG 策略 → 编排检测流程

二、快速上线三步走

2.1 上线原子能力

原子能力是检测的最小单元,如敏感词匹配、BERT 分类器、FastText 分类器等。

请求格式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
curl -X POST http://{admin_host}:8006/config/defense/manage/function/new \
-H 'Content-Type: application/json' \
-d '{
    "name": "原子能力名称",
    "group": "default",
    "desc": "描述",
    "type": "single_label_pred",   # 能力类型
    "timeoutMilliseconds": 2000,
    "confObj": { ... }
}'

能力类型(type)枚举

type说明示例
keyword敏感词/正则匹配黑白名单检测
single_label_pred单标签分类模型BERT、FastText
kb_search知识库语义搜索红线知识检索
rag_answerRAG 代答基于知识库的回答
multi_turn_detect多轮对话检测对话上下文安全检测

各类能力注册示例

1. keyword(敏感词)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
curl -X POST http://localhost:8006/config/defense/manage/function/new \
-H 'Content-Type: application/json' \
-d '{
    "name": "keyword",
    "group": "default",
    "desc": "敏感词识别",
    "type": "keyword",
    "timeoutMilliseconds": 1000,
    "confObj": {
        "name": "敏感词服务",
        "url": "http://safety-keywords:8005/keyword/query"
    }
}'

2. BERT 分类器(single_label_pred)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
curl -X POST http://localhost:8006/config/defense/manage/function/new \
-H 'Content-Type: application/json' \
-d '{
    "name": "bert_20250916",
    "group": "default",
    "desc": "BERT通用安全分类器",
    "type": "single_label_pred",
    "timeoutMilliseconds": 2000,
    "confObj": {
        "extra": {
            "url": "http://safety-bert:8003/v1/bert/bert_20250916"
        },
        "name": "bert_20250916",
        "modelType": "bert",
        "ignoreRiskCode": []
    }
}'

3. FastText 分类器(single_label_pred)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
curl -X POST http://localhost:8006/config/defense/manage/function/new \
-H 'Content-Type: application/json' \
-d '{
    "name": "fast20250710",
    "group": "default",
    "desc": "FastText快速分类器",
    "type": "single_label_pred",
    "timeoutMilliseconds": 100,
    "confObj": {
        "extra": {
            "url": "http://safety-fasttext:8002/fasttext/fast20250710"
        },
        "name": "fast20250710",
        "modelType": "fasttext",
        "ignoreRiskCode": []
    }
}'

4. kb_search(知识库搜索)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
curl -X POST http://localhost:8006/config/defense/manage/function/new \
-H 'Content-Type: application/json' \
-d '{
    "name": "kb_search",
    "group": "default",
    "desc": "红线知识检索",
    "type": "kb_search",
    "timeoutMilliseconds": 2000,
    "confObj": {
        "topK": 5,
        "threshold": 0.8,
        "collection": "red_kg_prod_hnsw_bge512_20250826",
        "url": "http://safety-knowledge:8004/knowledge/search"
    }
}'

5. rag_answer(RAG 代答)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
curl -X POST http://localhost:8006/config/defense/manage/function/new \
-H 'Content-Type: application/json' \
-d '{
    "name": "rag_answer",
    "group": "default",
    "desc": "红线代答",
    "type": "rag_answer",
    "timeoutMilliseconds": 5000,
    "confObj": {
        "topK": 5,
        "threshold": 0.8,
        "collection": "hnsw_bge512_20250826",
        "url": "http://safety-knowledge:8004/knowledge/answer"
    }
}'

6. multi_turn_detect(多轮对话检测)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
curl -X POST http://localhost:8006/config/defense/manage/function/new \
-H 'Content-Type: application/json' \
-d '{
    "name": "multi_turn_detect",
    "group": "default",
    "desc": "多轮对话检测",
    "type": "multi_turn_detect",
    "timeoutMilliseconds": 3000,
    "confObj": {
        "maxTurns": 20,
        "url": "http://safety-knowledge:8004/dialog"
    }
}'

上线原子能力

创建成功后会返回 id,用该 id 调用上线接口:

1
2
3
4
5
6
7
# 获取返回的 id
# { "data": { "id": 1 } }

# 上线
curl -X POST http://localhost:8006/config/defense/manage/function/online \
-H 'Content-Type: application/json' \
-d '{"id": 1}'

一键初始化脚本

也可使用 Python 脚本批量注册并上线:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import json
import requests

ADMIN_HOST = "http://localhost:8006"

skills = [
    {
        "name": "keyword",
        "group": "default",
        "desc": "敏感词识别",
        "type": "keyword",
        "timeoutMilliseconds": 1000,
        "confObj": {
            "name": "敏感词服务",
            "url": "http://safety-keywords:8005/keyword/query"
        }
    },
    {
        "name": "fast20250710",
        "group": "default",
        "desc": "FastText分类器",
        "type": "single_label_pred",
        "timeoutMilliseconds": 100,
        "confObj": {
            "extra": {"url": "http://safety-fasttext:8002/fasttext/fast20250710"},
            "name": "fast20250710",
            "modelType": "fasttext",
            "ignoreRiskCode": []
        }
    },
    {
        "name": "bert_20250916",
        "group": "default",
        "desc": "BERT通用分类器",
        "type": "single_label_pred",
        "timeoutMilliseconds": 2000,
        "confObj": {
            "extra": {"url": "http://safety-bert:8003/v1/bert/bert_20250916"},
            "name": "bert_20250916",
            "modelType": "bert",
            "ignoreRiskCode": []
        }
    },
    {
        "name": "bert_prompt_injection_20250828",
        "group": "default",
        "desc": "BERT提示词注入分类器",
        "type": "single_label_pred",
        "timeoutMilliseconds": 2000,
        "confObj": {
            "extra": {"url": "http://safety-bert:8003/v1/bert/bert_prompt_injection_20250828"},
            "name": "bert_prompt_injection_20250828",
            "modelType": "bert",
            "ignoreRiskCode": []
        }
    }
]

for skill in skills:
    resp = requests.post(f"{ADMIN_HOST}/config/defense/manage/function/new",
                         data=json.dumps(skill),
                         headers={'Content-Type': 'application/json'}).json()
    func_id = resp['data']['id']
    requests.post(f"{ADMIN_HOST}/config/defense/manage/function/online",
                  data=json.dumps({"id": func_id}),
                  headers={'Content-Type': 'application/json'})
    print(f"上线原子能力: {skill['name']}, id={func_id}")

2.2 上线业务

业务是检测流程的入口,每个业务有唯一的 accessKey。

创建业务

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
curl -X POST http://localhost:8006/config/defense/manage/business/new \
-H 'Content-Type: application/json' \
-d '{
    "name": "my_business",
    "group": "default",
    "desc": "我的业务",
    "type": "toC",
    "secretKey": "your-secret-key-here",
    "qpsLimit": 20,
    "robotCheckConfObj": {
        "checkNum": 1,
        "unit": "slice_single",
        "timeoutMilliseconds": 5000
    },
    "userCheckConfObj": {
        "checkNum": 1,
        "unit": "message_single",
        "timeoutMilliseconds": 5000
    }
}'

参数说明:

参数必填说明
name是业务名称
group是业务分组
type是业务类型:toB(商户)、toC(C端用户)、toE(内部用户)
secretKey是业务的密钥,用于 API 签名验证
qpsLimit是QPS 限制
robotCheckConfObj是机器人输出检测配置
userCheckConfObj是用户输入检测配置

上线业务

1
2
3
curl -X POST http://localhost:8006/config/defense/manage/business/online \
-H 'Content-Type: application/json' \
-d '{"id": 业务id}'

2.3 上线 DAG 策略

DAG 策略编排多个原子能力,形成完整的检测流程。

默认 test 业务的审核流程

系统初始化时(strategy_init.py)为 test 业务加载了以下检测流程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
用户输入 → 关键词检测 (parallel, timeout=1000ms)
  ├── 命中黑名单(1) → 拦截,结束
  ├── 命中白名单(2) → 放行,结束
  └── 未命中/灰名单 → BERT 双模型并行 (parallel, timeout=2000ms)
       ├── 两个模型都无风险 → 正常,结束
       ├── 通用模型高置信度(概率≥阈值) → 按通用模型结果拦截/放行
       ├── 通用模型正常概率极低(<0.1) → 按通用模型结果拦截/放行
       ├── 注入模型高置信度(概率≥阈值) → 按注入模型结果拦截/放行
       └── 都不够确信 → 正常,结束

机器人输出 → 关键词检测 → 无脑结束

说明:

  • 用户输入走两层:先关键词快速过滤,不确定再进 BERT 精判
  • 机器人输出只走关键词检测
  • 默认策略没有 FastText 分类器,所有关键词未命中的请求都会进入 BERT 层(耗时约 2 秒)

策略执行流程(含 FastText)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
用户输入 → 关键词 + FastText 并行 (parallel, timeout=200ms)
  ├── 关键词黑白名单 → 直接结束
  ├── FastText 高置信度正常(>0.9) → 放行,结束
  ├── FastText 高置信度恶意(>0.95) → 拦截,结束
  └── 不确定 → BERT 双模型并行 (parallel, timeout=2000ms)
       ├── 两个模型都无风险 → 正常,结束
       ├── 通用模型高置信度(概率≥阈值) → 按通用模型结果拦截/放行
       ├── 通用模型正常概率极低(<0.1) → 按通用模型结果拦截/放行
       ├── 注入模型高置信度(概率≥阈值) → 按注入模型结果拦截/放行
       └── 都不够确信 → 正常,结束

机器人输出 → 关键词检测 → 无脑结束

策略 YAML 模板(含 FastText,推荐)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
businessName: my_business
group: default
desc: 我的业务策略
rootId: start
confArray:
  - nodeId: start
    functionConf:
      type: dummy
      name: dummy
      timeoutMilliseconds: 5
      conf: {}
    routerConf:
      type: groovy
      name: groovy
      conf:
        script: |-
          def from_robot=ctx.curReq[0].fromRobot()
          if (from_robot) {
              return 'from_robot_path'
          } else {
              return 'from_user_path'
          }
    ignoreError: true

  # ========== 用户输入检测 ==========
  - nodeId: from_user_path
    functionConf:
      type: parallel
      name: parallel_from_user
      timeoutMilliseconds: 200
      conf:
        functionConfs:
          - ref: keyword
          - ref: fast20250710
    routerConf:
      type: groovy
      name: groovy
      conf:
        script: |-
          import com.jd.security.llmsec.core.check.MixedCheck
          def retMap=ctx.curResult.resultMap
          def kw = retMap.keyword
          def fast = retMap.fast20250710

          # 关键词命中黑白名单 → 直接决策
          if (kw != null && (kw.bwgLabel == 1 || kw.bwgLabel == 2)) {
              ctx.curResult = kw
              return null    # 结束
          }
          # FastText 高置信度正常 → 放行
          if (fast != null && !fast.hasRisk() && fast.probability > 0.9) {
              ctx.curResult = fast
              return null    # 结束
          }
          # FastText 高置信度恶意 → 拦截
          if (fast != null && fast.hasRisk() && fast.probability > 0.95) {
              ctx.curResult = fast
              return null    # 结束
          }
          # 不确定 → 进 BERT 层
          return 'from_user_bert'
    ignoreError: true

  - nodeId: from_user_bert
    functionConf:
      type: parallel
      name: parallel_bert
      timeoutMilliseconds: 2000
      conf:
        functionConfs:
          - ref: bert_20250916
            conf:
              ignoreRiskCode:
                - 1006
          - ref: bert_prompt_injection_20250828
            conf:
              ignoreRiskCode:
                - 1006
    routerConf:
      type: groovy
      name: groovy
      conf:
        script: |-
          import com.jd.security.llmsec.core.check.MixedCheck
          def retMap=ctx.curResult.resultMap
          def bert_common = retMap.bert_20250916
          def bert_inject = retMap.bert_prompt_injection_20250828

          # 两个模型都无风险 → 正常
          if ((bert_common == null || !bert_common.hasRisk()) && (bert_inject == null || !bert_inject.hasRisk())) {
            ctx.curResult = MixedCheck.noRisk()
            return null
          }

          # 设置阈值
          def thresholds_common = [0:0.9, 201:1.0, 1001:0.9, 1002:0.9, 1003:0.85, 1004:0.996, 1005:0.97, 1006:0.95, 1007:0.9]
          def thresholds_inject = [0:0.8, 201:0.99]

          # 通用模型高置信度命中
          if (bert_common.hasRisk() && bert_common.probability >= thresholds_common[bert_common.riskCode]) {
            ctx.curResult = bert_common
          }
          # 通用模型正常概率极低
          else if(bert_common.detail.find { it.riskCode == 0 }?.probability < 0.1 && bert_common.riskCode != 201) {
            ctx.curResult = bert_common
          }
          # 注入模型高置信度命中
          else if(bert_inject.probability >= thresholds_inject[bert_inject.riskCode]) {
            ctx.curResult = bert_inject
          }
          # 都不够确信 → 视为正常
          else {
            ctx.curResult = MixedCheck.noRisk()
          }
          return null
    ignoreError: true

  # ========== 机器人输出检测 ==========
  - nodeId: from_robot_path
    functionConf:
      ref: keyword
    routerConf:
      type: stupid_end
      name: stupid_end
    ignoreError: true

上线 DAG 策略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 1. 创建 DAG 策略
curl -X POST http://localhost:8006/config/defense/manage/dag/newDagWithYaml \
-H 'Content-Type: text/plain' \
-d '上面的YAML内容'

# 返回示例:{ "id": 5, "version": 1, "status": "edit" }

# 2. 上线 DAG 策略
curl -X POST http://localhost:8006/config/defense/manage/dag/online \
-H 'Content-Type: application/json' \
-d '{"id": 5}'

更新 DAG 策略

如果已有线上 DAG,需要先创建新版本再升级:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1. 获取当前线上 DAG id
curl -X POST http://localhost:8006/config/defense/manage/dag/active \
-H 'Content-Type: application/json' \
-d '{"group": "default", "name": "my_business"}'

# 2. 基于线上 id 创建新版本
curl -X POST http://localhost:8006/config/defense/manage/dag/newVersion \
-H 'Content-Type: application/json' \
-d '{"id": 线上id}'
# 返回新 id

# 3. 更新新版本的内容
curl -X POST http://localhost:8006/config/defense/manage/dag/dagUpdateYaml \
-H 'Content-Type: text/plain' \
-d 'id: 新id
businessName: my_business
...(其他YAML内容,需带id字段)'

# 4. 升级上线
curl -X POST http://localhost:8006/config/defense/manage/dag/upgrade \
-H 'Content-Type: application/json' \
-d '{"id": 新id}'

更新流程说明

1
2
3
4
dagUpdateYaml(id=线上id)→ 直接修改原记录,id 不变,仅限 edit 状态
newVersion(id=线上id)→ 创建新版本,返回新 id(推荐流程)
update / dagUpdateYaml(id=新id)→ 修改新版本内容
upgrade(id=新id)→ 新版本上线,旧版本自动下线

附:敏感词管理

敏感词(关键词)检测是最基础的过滤手段,使用 AC 自动机(NFA)算法,毫秒级响应。

1. 敏感词字段说明

字段必填说明
businessScene是业务场景(对应业务名称),all 表示全局生效
group是业务分组
word是敏感词内容
className是类别名称,如"辱骂"、“涉黄”
classNo是类别编码
matchType是匹配方式(见下表)
handleStrategy是处置策略:black(黑名单)、white(白名单)、grey(灰名单)
status是online(有效)、offline(无效)

2. 匹配方式(matchType)

matchType说明示例
contain包含指定词即命中word=“王八蛋”,内容含"王八蛋"即命中
equal完全相等才命中word=“你好”,内容必须完全等于"你好"
regex正则表达式匹配word=“杀|死”,匹配其中任意一个
exclude排除匹配(在白名单中排除某些内容)word=“好”,排除"好"字所在的匹配

3. 处置策略(handleStrategy)

handleStrategy说明DAG 中的行为
black黑名单(bwgLabel=1)命中后直接拦截,from_user_test1 节点 router 返回 null 结束
white白名单(bwgLabel=2)命中后直接放行,from_user_test1 节点 router 返回 null 结束
grey灰名单(bwgLabel=3)命中后不确定,继续进入 BERT 层精判

4. 新增敏感词

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
curl -X POST http://localhost:8006/data/sensitive_words/upsert \
-H 'Content-Type: application/json' \
-d '{
    "businessScene": "test",
    "group": "default",
    "word": "王八蛋",
    "className": "辱骂",
    "classNo": 1006,
    "matchType": "contain",
    "handleStrategy": "black",
    "source": "手动录入",
    "status": "online",
    "desc": "辱骂词"
}'

5. 查询敏感词

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 按词模糊查询
curl -X POST http://localhost:8006/data/sensitive_words/queryByWord \
-H 'Content-Type: application/json' \
-d '{
    "word": "杀",
    "status": "online",
    "businessScene": "test"
}'

# 按 ID 查询
curl -X POST http://localhost:8006/data/sensitive_words/getById \
-H 'Content-Type: application/json' \
-d '{"id": 1}'

6. 上线/下线敏感词

1
2
3
4
5
6
7
8
9
# 上线
curl -X POST http://localhost:8006/data/sensitive_words/online \
-H 'Content-Type: application/json' \
-d '{"id": 1}'

# 下线
curl -X POST http://localhost:8006/data/sensitive_words/offline \
-H 'Content-Type: application/json' \
-d '{"id": 1}'

7. 默认演示数据

系统提供 4 条演示敏感词:

敏感词matchTypehandleStrategy类别
王八蛋containblack辱骂
小箩筣avcontainblack涉黄
成人a在线containblack涉黄
我艹containblack辱骂

注意: 生产环境中需要根据实际业务场景补充完整的敏感词库,演示数据不足以覆盖所有违规内容。

8. 敏感词在 DAG 中的执行流程

1
2
3
4
5
6
7
用户输入
  ↓
keyword 服务 (AC 自动机)
  ├── 命中黑名单(bwgLabel=1) → router 直接拦截,不进 BERT
  ├── 命中白名单(bwgLabel=2) → router 直接放行,不进 BERT
  ├── 命中灰名单(bwgLabel=3) → 不确定,进 BERT 层精判
  └── 未命中 → 进 BERT 层精判

三、使用审核接口

3.1 签名生成

调用审核接口需要对请求进行 HMAC-SHA1 签名。

签名算法

1
2
signText = "accessKey={accessKey}&accessTarget={accessTarget}&requestId={requestId}&timestamp={timestamp}&plainText={plainText}"
signature = Base64(HMAC-SHA1(signText, secretKey))

Python 签名示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import hmac
import hashlib
import base64
import time
import uuid

def gen_sign(text, secret_key):
    result = hmac.new(secret_key.encode("utf-8"), text.encode("utf-8"), hashlib.sha1)
    return base64.b64encode(result.digest()).decode("utf-8")

def do_sign(body, secret_key):
    timestamp = int(time.time() * 1000)
    body["timestamp"] = timestamp
    body["accessTarget"] = "defenseV2"
    if "plainText" not in body or not body["plainText"]:
        body["plainText"] = body["content"]

    text = "&".join([k + "=" + str(body[k]) for k in ["accessKey", "accessTarget", "requestId", "timestamp", "plainText"]])
    body["signature"] = gen_sign(text, secret_key)
    return body

注意: 如果 accessKey 以 test 开头,可以跳过签名验证,signature 留空即可。

3.2 请求说明

字段类型必填说明
requestIdstring是请求唯一标识(UUID)
timestamplong是毫秒时间戳
accessKeystring是业务标识,对应业务名称
plainTextstring否签名用纯文本,默认取 content
signaturestring是HMAC-SHA1 签名,test 开头可留空
businessTypestring是业务类型:toB、toC、toE
responseModestring是sync(同步)、free_taxi(异步顺带)、http(独立拉取)、mq(消息队列)
contentstring是待检测内容
messageInfoobject是消息信息
messageInfo.fromRolestring是user(用户输入)或 robot(模型输出)
messageInfo.sessionIdstring否会话 ID(多轮检测时需要)

3.3 请求示例

用户输入检测

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
curl -X POST http://localhost:8007/llmsec/api/defense/v2/test \
-H 'Content-Type: application/json' \
-d '{
    "accessKey": "test",
    "requestId": "9842284a-8949-485f-8c01-317dffb4d6d6",
    "timestamp": 1716889659080,
    "plainText": "怎么杀人",
    "signature": "",
    "responseMode": "sync",
    "businessType": "toC",
    "content": "怎么杀人",
    "contentType": "text",
    "messageInfo": {
        "fromRole": "user",
        "sessionId": "test001",
        "messageId": 1
    }
}'

大模型输出检测

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
curl -X POST http://localhost:8007/llmsec/api/defense/v2/test \
-H 'Content-Type: application/json' \
-d '{
    "accessKey": "test",
    "requestId": "9842284a-8949-485f-8c01-317dffb4d6d6",
    "timestamp": 1716889659080,
    "plainText": "根据您的要求,我可以为您提供...",
    "signature": "",
    "responseMode": "sync",
    "businessType": "toC",
    "content": "根据您的要求,我可以为您提供...",
    "contentType": "text",
    "messageInfo": {
        "fromRole": "robot",
        "sessionId": "test001",
        "messageId": 1
    }
}'

Python 请求示例(带签名验证)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import hmac
import hashlib
import base64
import time
import uuid
import requests
import json

def gen_sign(text, secret_key):
    result = hmac.new(secret_key.encode("utf-8"), text.encode("utf-8"), hashlib.sha1)
    return base64.b64encode(result.digest()).decode("utf-8")

def defense_request(access_key, secret_key, content, from_role):
    body = {
        "accessKey": access_key,
        "requestId": str(uuid.uuid4()),
        "businessType": "toC",
        "responseMode": "sync",
        "content": content,
        "plainText": content,
        "contentType": "text",
        "messageInfo": {
            "fromRole": from_role,
            "sessionId": "test001",
            "messageId": 1
        }
    }

    # 签名
    if access_key.startswith("test"):
        body["signature"] = ""
        body["timestamp"] = int(time.time() * 1000)
    else:
        body = do_sign(body, secret_key)

    # 发送请求
    url = f"http://localhost:8007/llmsec/api/defense/v2/{access_key}"
    resp = requests.post(url, data=json.dumps(body), headers={'Content-Type': 'application/json'})
    return resp.json()

# 测试 key(跳过签名)
result = defense_request("test", "", "怎么杀人", "user")
print(json.dumps(result, indent=2, ensure_ascii=False))

官方测试脚本(quickstart/api_test.py)

仓库自带的 quickstart/api_test.py 是最精简的完整调用示例,签名、请求、结果解析都在里面:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2023-2026 Beijing Jingdong Shangke Information Technology Co., Ltd.

# coding=utf-8

import base64
import hmac
import hashlib
import time
import json
import requests
import random
import string

def hmac_sha1(message, key):
    result = hmac.new(key.encode("utf-8"), message.encode("utf-8"), hashlib.sha1)
    return result.digest()

def gen_sign(text, secret_key):
    try:
        key = base64.b64encode(hmac_sha1(text, secret_key)).decode("utf-8")
        return key
    except Exception as ex:
        print(f"签名异常:{ex}")

def do_sign(body, secret_key):
    timestamp = int(time.time() * 1000)
    body["timestamp"] = timestamp
    body["accessTarget"] = "defenseV2"
    if 'plainText' not in body or not body['plainText']:
        body['plainText'] = body['content']
    text = "&".join(
        [k + "=" + str(body[k]) for k in ["accessKey", "accessTarget", "requestId", "timestamp", "plainText"]])
    sig = gen_sign(text, secret_key)
    body['signature'] = sig
    return body

def do_request(url, accessSecret, body):
    body = do_sign(body, accessSecret)
    response = requests.post(url, data=json.dumps(body), headers={'Content-Type': 'application/json'})
    return response

def random_string(length):
    letters_and_digits = string.ascii_letters + string.digits
    result_str = ''.join(random.choice(letters_and_digits) for _ in range(length))
    return result_str

def request(ak: str, sk: str, body: dict):
    # url = f'http://127.0.0.1:8007/llmsec/api/defense/v2/{ak}'
    url = f'http://safety-api:8007/llmsec/api/defense/v2/{ak}'

    body['requestId'] = random_string(20)
    body['accessKey'] = ak
    body['plainText'] = body['content']

    print(f'输入:{json.dumps(body, ensure_ascii=False)}')
    resp = do_request(url, sk, body)
    resp = json.loads(resp.text)
    print(f'输出:{json.dumps(resp, ensure_ascii=False)}')

    if resp['code'] != 0:
        print(f"请求失败,biz={ak}, req={body}, resp: {resp}")
        return None
    return resp['data'][0]

if __name__ == '__main__':
    import sys
    if len(sys.argv):
        text = sys.argv[1]
        request('test', '123456', body={
            "businessType": "toC",
            "content": text,
            "contentType": "text",
            "messageInfo": {
                "fromId": "user123456",
                "fromRole": 'user',
            },
            "responseMode": "sync",
        })

使用说明:

  • 用法:python api_test.py '待检测文本',必须带一个文本参数(不带会因 sys.argv[1] 越界报错)
  • 脚本默认 url 指向容器网络的 safety-api:8007,在容器内执行(如 docker exec safety-knowledge python /work/api_test.py '你好');宿主机直接跑需改用注释里的 http://127.0.0.1:8007/...
  • accessKey 为 test、secretKey 任意(test 开头的 key 服务端免签名),换成正式业务时传入真实的 ak/sk 即可复用同一套签名逻辑

3.4 响应解读

响应格式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{
    "code": 0,
    "cost": 100,
    "message": "success",
    "data": [
        {
            "requests": [{"sessionId": "test001", "messageId": 1}],
            "riskCode": 1001,
            "riskMessage": "辱骂",
            "riskCheckType": "keyword",
            "riskCheckName": "一级黑名单",
            "riskCheckResult": {
                "riskCode": 1001,
                "riskMessage": "辱骂",
                "hitWord": "王八蛋",
                "bwgLabel": 1
            }
        }
    ]
}

关键字段

字段说明
code0 表示成功,非 0 表示失败
cost处理耗时(毫秒)
data[].riskCode风险码,0 表示正常,非 0 表示有风险
data[].riskMessage风险描述
data[].riskCheckType命中的检测类型(keyword、single_label_pred、mixed)

错误码对照

codemessage说明
0success成功
1未知异常内部异常
400访问未授权未授权访问
40001参数非法请求参数不合法
40002token错误/业务不存在accessKey 错误或业务未上线
40003签名过期时间戳过期(默认 60 秒)
40004签名值错误HMAC-SHA1 签名不匹配
50001触发限流QPS 超过限制
50002内部执行超时DAG 策略执行超时

响应结果判断

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
result = defense_request("test", "", "怎么杀人", "user")

if result["code"] != 0:
    print(f"请求失败: {result.get('message', '未知异常')}")
elif not result.get("data"):
    print("通过:无风险")
else:
    for item in result["data"]:
        risk_code = item["riskCode"]
        risk_msg = item["riskMessage"]
        if risk_code == 0:
            print("通过:正常文本")
        else:
            print(f"拦截:{risk_msg}(风险码={risk_code})")

四、附录

4.1 常用 API 速查表

管理端(safety-admin,端口 8006):

接口方法Content-Type说明
/config/defense/manage/function/newPOSTapplication/json新建原子能力
/config/defense/manage/function/onlinePOSTapplication/json上线原子能力
/config/defense/manage/business/newPOSTapplication/json新建业务
/config/defense/manage/business/onlinePOSTapplication/json上线业务
/config/defense/manage/business/activePOSTapplication/json查询生效业务配置
/config/defense/manage/dag/newDagWithYamlPOSTtext/plain新建 DAG 策略
/config/defense/manage/dag/dagUpdateYamlPOSTtext/plain更新 DAG 策略(需带 id)
/config/defense/manage/dag/newVersionPOSTapplication/json创建 DAG 新版本
/config/defense/manage/dag/onlinePOSTapplication/json上线 DAG
/config/defense/manage/dag/upgradePOSTapplication/json升级 DAG(新版本替换旧版本)
/config/defense/manage/dag/offlinePOSTapplication/json下线 DAG
/config/defense/manage/dag/activePOSTapplication/json查询生效 DAG 配置

审核端(safety-api,端口 8007):

接口方法Content-Type说明
/llmsec/api/defense/v2/{accessKey}POSTapplication/json内容审核接口

4.2 BERT 通用分类器风险码

riskCoderiskMessage说明
0正常文本无风险
201提示词注入越狱/提示注入攻击
1001辱骂侮辱谩骂
1002暴恐暴力恐怖
1003涉黄色情内容
1004广告广告推广
1005价值观不良价值观
1006其他其他违规(可配置 ignore)
1007未成年未成年人相关风险

4.3 FastText 分类器风险码

riskCoderiskMessage说明
0正常文本无风险
1恶意违规内容
2业务场景业务领域相关内容

4.4 responseMode 说明

responseMode用途说明
sync同步返回请求发出后等待检测结果,最常用
free_taxi异步顺带返回当前请求的检测结果由下一次同会话的请求带回,适用于流式输出检测
http独立 API 拉取先提交检测,再通过 /llmsec/api/defenseResult/v2/{accessKey} 拉取结果
mq消息队列通过 MQ 异步接收结果(预留功能,当前未实现)

4.5 常见问题

Q: 请求返回 code=1 “未知异常”?

A: 通常是 DAG 策略不在线导致的。检查 active 接口返回的 DAG status 是否为 online。

Q: 请求返回 code=40002 “token错误/业务不存在”?

A: 检查 accessKey 是否正确,业务是否已上线(business/online)。

Q: 响应太慢(2 秒以上)?

A: BERT 模型在 CPU 上推理较慢。建议:

  • 在关键词层加入 FastText 分类器做快速预筛
  • 将 BERT 模型改为 GPU 推理(device_type: "cuda")
  • 降低 BERT 层的 timeoutMilliseconds

Q: 修改了 DAG 策略但没生效?

A: DAG 配置会缓存在 safety-api 内存中。修改后需要调用 online 或 upgrade 刷新缓存。

Q: test key 可以跳过签名验证吗?

A: 可以。任何以 test 开头的 accessKey 都会跳过签名验证,仅限开发测试使用。

使用 Hugo 构建
主题 Stack 由 Jimmy 设计