本文介紹如何用阿里雲 Milvus 的 DATA_INSPECTION 能力為 AI 問答類應用加一道Alibaba Content Security Service護欄:在已有 AI Function 的參數中加一個 data_inspection 開關,即可在模型調用前檢查輸入、返回前檢查輸出,並在用戶端把攔截結果收斂為policy_blocked、manual_review、operational_error 三種互斥處置。
方案概述
大模型一旦對外提供服務,Alibaba Content Security Service就從加分項變成上線門檻。只要有真實使用者在一端輸入 prompt、模型在另一端輸出文本,就同時開啟了兩個風險敞口:
輸入側(使用者 → 模型):使用者可能輸入違法違規內容,或用越獄話術誘導模型突破安全邊界。使用者提交的文章、評論、暱稱等 UGC 內容,也需要先過一道安全門再落庫。
輸出側(模型 → 使用者):即便輸入看起來正常,模型也可能產生不合規內容,例如誇大承諾的營銷文案、帶有偏見的表述。文案產生、指令碼創作這類讓模型自由發揮的情境尤其明顯。
兩端都需要門禁,其業務價值很直接:Alibaba Content Security Service是大模型應用備案與上線的硬性要求;機器先擋掉絕大多數明確違規內容後,人工只需複核少量模糊樣本,審核團隊從「全量看」變成「看疑難」;一條越獄成功的截圖或一段不當言論都可能演變成公開輿情,護欄把風險擋在內容發出去之前。
傳統方案要湊齊這套能力會遇到幾個痛點:
兩端檢查時機不同:輸入檢查必須在模型調用之前完成,輸出檢查必須在結果返回之前完成。一次業務調用橫跨模型調用的前後兩個時點。
要額外接Alibaba Content Security Service服務並自己串聯:典型做法是業務代碼先調Alibaba Content Security Service API 檢查輸入、再調大模型、再調一次檢查輸出。三次遠程調用、三套逾時重試、三處鑒權。
攔截結果不好機讀:命中安全性原則時,服務端可能返回 HTTP 錯誤、可能返回非零業務碼,也可能在 HTTP 200 裡正常返回一段明確拒答的文本。只判 HTTP 狀態代碼會漏掉 200 裡的拒答,只判關鍵詞又會被正常業務文本裡的「風險」「拒絕」等詞誤傷。
誤攔與漏攔的權衡:門收得太緊影響正常業務,太松則合規風險上升。護欄不能只有允許存取與攔截兩態,還需要一條轉人工複核的中間地帶來吸收模糊樣本。
日誌合規:排查問題需要日誌,但高風險原文與個人敏感資訊一旦明文落進業務日誌,日誌系統本身就成了新的泄露面。
阿里雲 Milvus 的做法是:在已有 AI Function(例如文本產生 ai_text_generate)的 params 中加一個 data_inspection 開關即可,取值只有三種:
取值 | 檢查時機 | 典型情境 | 說明 |
| 模型調用前 | 智能客服、AI 助手接收使用者 prompt;UGC 內容落庫前 | 擋住越獄與違法違規輸入。命中時模型不被調用,既省算力也更安全。 |
| 結果返回前 | 營銷與活動文案發布、指令碼產生 | 適用於輸入可信、只擔心模型產生不合規內容的情境。 |
| 前後各一次 | 高風險開放式對話、面向公眾的自由問答 | 兩端都不可信時的最強門禁,成本也最高。 |
護欄與模型調用在 Milvus 內部一次完成,資料全程不離開 Milvus 執行個體,憑據由管理員在 Provider 側統一配置、不寫進任何請求體,業務側不需要再自建或串聯外部Alibaba Content Security Service服務。
data_inspection 是給已有調用加的一層門,不能替代原任務本身的必填參數。例如文本產生仍必須提供 texts,缺失會報 texts is required for task [ai_text_generate]。
前提條件
已建立 Milvus 2.6 版本執行個體。AI Function 依賴 2.6 版本核心,建立後無需單獨綁定模型服務。
如需從公網訪問執行個體,已在執行個體詳情頁的 安全配置 頁簽開啟 公網訪問 並將用戶端出口 IP 加入公網訪問白名單。
已安裝 pymilvus,本文樣本基於 pymilvus 3.0.0 驗證。
RESTful 介面與 gRPC 共用 19530 連接埠,調用時必須顯式帶連接埠,例如 http://c-xxx.milvus.aliyuncs.com:19530;省略連接埠會預設訪問 80 連接埠並導致連線逾時。
操作步驟
準備公用代碼
以下程式碼封裝含串連配置、REST 封裝、TEXTTRANSFORM 類型相容兜底,以及一個建帶護欄 Collection 的工具函數。護欄開關就是 params 裡那一行 data_inspection。
from __future__ import annotations
import json
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
from pymilvus import DataType, Function, FunctionType, MilvusClient
from pymilvus.exceptions import MilvusException
# ==================== 串連配置 ====================
MILVUS_URI = "http://c-xxx.milvus.aliyuncs.com:19530" # 連接埠必須寫 19530
MILVUS_TOKEN = "root:xxx"
MILVUS_REST_BASE_URL = MILVUS_URI
MODEL_NAME = "qwen3.7-max" # 文本模型,須已在 Provider 中配置
TEXTTRANSFORM_FUNCTION_TYPE = 9
# Provider 命中安全性原則時返回的契約標識,是判斷「是否被攔」的唯一可靠依據
BLOCK_MARKERS = ("DataInspectionFailed", "inappropriate content")
client = MilvusClient(uri=MILVUS_URI, token=MILVUS_TOKEN)
def texttransform_function_type() -> Any:
"""取 TEXTTRANSFORM 的 FunctionType;老版本枚舉缺失時動態補一個成員。"""
for type_name in ("TEXTTRANSFORM", "TEXT_TRANSFORM", "TextTransform"):
ft = getattr(FunctionType, type_name, None)
if ft is not None:
return ft
existing = getattr(FunctionType, "_value2member_map_", {}).get(TEXTTRANSFORM_FUNCTION_TYPE)
if existing is not None:
return existing
extension = int.__new__(FunctionType, TEXTTRANSFORM_FUNCTION_TYPE)
extension._name_ = "TEXTTRANSFORM"
extension._value_ = TEXTTRANSFORM_FUNCTION_TYPE
FunctionType._value2member_map_[TEXTTRANSFORM_FUNCTION_TYPE] = extension
FunctionType._member_map_["TEXTTRANSFORM"] = extension
return extension
def post_json(path: str, body: dict[str, Any], timeout: int = 120) -> tuple[int, dict[str, Any]]:
"""REST 介面封裝:返回 (http_status, data),HTTP 非 2xx 時仍嘗試解析響應體。"""
request = Request(
f"{MILVUS_REST_BASE_URL.rstrip('/')}{path}",
data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
headers={"Authorization": f"Bearer {MILVUS_TOKEN}", "Content-Type": "application/json"},
method="POST",
)
try:
with urlopen(request, timeout=timeout) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
return exc.code, json.loads(exc.read().decode("utf-8"))
def build_guard_collection(name: str, func_name: str, in_field: str, out_field: str,
prompt: str, data_inspection: str) -> None:
"""建一個帶 TextTransform 護欄的寫入型 Collection。"""
if client.has_collection(name):
client.drop_collection(name)
schema = MilvusClient.create_schema(auto_id=True, enable_dynamic_field=False)
schema.add_field("id", DataType.INT64, is_primary=True)
schema.add_field(in_field, DataType.VARCHAR, max_length=1024)
schema.add_field(out_field, DataType.VARCHAR, max_length=4096)
# Collection 必須至少有一個向量欄位;本例不做向量檢索,用 2 維佔位欄位滿足約束。
# 聲明 nullable=True 後,insert 時無需再傳該欄位。
schema.add_field("dummy_vector", DataType.FLOAT_VECTOR, dim=2, nullable=True)
schema.add_function(
Function(
name=func_name,
function_type=texttransform_function_type(),
input_field_names=[in_field],
output_field_names=[out_field],
params={
"provider": "aliyun_milvus",
"model_name": MODEL_NAME,
"task": "ai_text_generate",
"prompt": prompt,
"data_inspection": data_inspection, # ← 護欄開關
"temperature": "0.2",
"enable_thinking": "false",
"timeout_sec": "45",
},
)
)
index_params = client.prepare_index_params()
index_params.add_index(field_name="dummy_vector", index_type="AUTOINDEX",
metric_type="COSINE")
client.create_collection(collection_name=name, schema=schema, index_params=index_params)Collection 必須至少包含一個向量欄位,否則建表報 schema does not contain vector field。本例不做向量檢索,用一個 2 維佔位欄位滿足約束,並聲明為 nullable=True 以免每次寫入都要傳值。
把攔截結果收斂為三種處置
這是護欄落地時最容易被低估的工程痛點。命中安全性原則時,服務端可能返回 HTTP 錯誤、非零業務碼,也可能在 HTTP 200 裡返回一段明確拒答的文本。因此用戶端需要把觀察到的訊號統一收斂為三種互斥處置:
處置 | 含義 | 建議動作 |
| 命中安全性原則,護欄正常工作,屬業務預期內的結果。 | 記錄審計日誌,向使用者返回合規提示。不必警示營運。 |
| 協議層無法判定,例如 HTTP 200 且結構正常但內容可疑。 | 投遞到人工複核隊列。「未拋錯」不等於「安全通過」。 |
| 真實的服務故障,如模型不可用、參數錯誤、網路失敗。 | 警示值班並停止重試,不要無限重試。 |
# ==================== 三態處置分類 ====================
# 兩條調用路徑(gRPC 異常 / REST 響應體)都先按 Provider 契約標識判斷,
# 再按 HTTP 狀態與業務碼兜底,確保同一次攔截在兩條路徑上得到一致的處置結論。
def classify_grpc(exc: MilvusException) -> str:
"""把 gRPC 異常收斂為三態處置。"""
msg = str(exc)
if any(marker in msg for marker in BLOCK_MARKERS):
return "policy_blocked" # 命中安全性原則,屬業務預期結果
return "operational_error" # 其餘視為營運故障
def classify_rest(status: int, data: dict[str, Any]) -> str:
"""把 REST 響應收斂為三態處置。"""
raw = json.dumps(data, ensure_ascii=False)
if any(marker in raw for marker in BLOCK_MARKERS):
return "policy_blocked"
if status >= 400 or data.get("code", 0) != 0:
return "operational_error"
# HTTP 200 且結構正常:有可能是模型在本文裡給出的「明確拒答」,
# 無法從協議層判定,一律轉人工複核,不直接當作安全通過。
return "manual_review"為什麼必須按 Provider 契約標識判斷,而不能只看 HTTP 狀態代碼。實測中「一條違規輸入被護欄攔截」與「模型名不存在導致的服務故障」返回了完全相同的狀態代碼組合:
情境 | HTTP 狀態 | 業務 code | 響應體含 DataInspectionFailed | 正確處置 |
違規輸入被攔截 | 500 | 65535 | ✅ |
|
模型名不存在 | 500 | 65535 | ❌ |
|
缺少必填參數 texts | 400 | 1100 | ❌ |
|
正常輸入 | 200 | 0 | ❌ | 允許存取 |
前兩行的 HTTP 狀態代碼與業務碼一模一樣,僅靠它們無法區分「內容被攔」和「服務出錯」。兩者的營運含義完全不同——把策略攔截誤判成營運故障,會讓每次Alibaba Content Security Service攔截都產生一條虛假的服務故障警示,長期會淹沒真實故障。因此判據必須是響應體中的 DataInspectionFailed 契約標識。同理,也不要依賴具體的 HTTP 狀態代碼取值,它可能隨網關版本變化。
不要用「風險」「拒絕」「無法」這類關鍵詞去判定是否被攔——正常業務文本裡也可能出現這些詞,容易誤傷。關鍵詞最多用於輔助打點,不應改變最終處置結論。
步驟一與步驟二:input 與 output 模式允許存取合規內容
input 模式在模型調用前檢查使用者輸入,output 模式在結果返回前檢查模型輸出。合規內容正常允許存取。
# ==================== 步驟 (a):input 模式,請求前置檢查 ====================
build_guard_collection(
"guard_input", "inspect_customer_request", "request", "response",
"請以客服口吻用一句話回答:${request}", "input",
)
client.insert("guard_input", [{"request": "退款審核後多久到賬?"}])
client.flush("guard_input")
for row in client.query("guard_input", filter="",
output_fields=["request", "response"], limit=1):
print(f"輸入: {row.get('request')}")
print(f"輸出: {row.get('response')}")
# 護欄未命中 → 允許存取,模型正常返回。
# 若換成越獄 prompt,模型根本不會被調用,護欄在最前面就擋下了。
# ==================== 步驟 (b):output 模式,發布前檢查模型輸出 ====================
build_guard_collection(
"guard_output", "inspect_generated_copy", "draft_request", "publish_copy",
"請產生一條適合 App 發布的會員活動簡介:${draft_request}", "output",
)
client.insert("guard_output",
[{"draft_request": "為會員日活動產生一條不超過30字的權益簡介,禁止誇大承諾。"}])
client.flush("guard_output")
for row in client.query("guard_output", filter="",
output_fields=["draft_request", "publish_copy"], limit=1):
print(f"輸出: {row.get('publish_copy')}")
# 輸出通過安全檢查 → 進入待發布隊列。實測允許存取結果:
模式 | 輸入 | 輸出 |
| 退款審核後多久到賬? | 親,退款審核通過後,款項通常會在1-7個工作日內原路退回您的支付賬戶,具體到賬時間取決於支付渠道的處理速度,請您留意查收哦。 |
| 為會員日活動產生一條不超過30字的權益簡介,禁止誇大承諾。 | 會員日專享積分兌換、專屬折扣及優先客服權益,詳情以頁面為準。 |
input 模式命中時模型不會被調用,因此既攔住了風險也節省了模型調用開銷;output 模式適用於輸入可信、只擔心模型自由發揮的情境。
步驟三:both 模式攔截違規輸入
兩端都不可信時用 both。下面用一條明確違規的測試輸入驗證護欄是否真的生效——這是上線前應當做的護欄有效性驗證。
# ==================== 步驟 (c):both 模式,兩端雙重檢查 ====================
# 用一條明確違規的測試輸入(canary)驗證護欄是否真的攔得住。
UNSAFE_INPUT = "<一條明確違規的測試輸入>"
# (c-1) gRPC 路徑:寫入時觸發護欄
build_guard_collection("guard_both", "inspect_both", "request", "response",
"請回答:${request}", "both")
try:
client.insert("guard_both", [{"request": UNSAFE_INPUT}])
client.flush("guard_both")
rows = client.query("guard_both", filter="",
output_fields=["request", "response"], limit=1)
# 未拋錯不等於安全通過:結構正常但內容可疑時轉人工複核
print("處置: manual_review", [r.get("response") for r in rows])
except MilvusException as exc:
print(f"處置: {classify_grpc(exc)}")
print(f"服務端返回: {exc.message}")
# (c-2) REST 路徑:同一條輸入走即時介面
status, data = post_json("/v2/vectordb/ai/text_generate", {
"model_name": MODEL_NAME,
"texts": [UNSAFE_INPUT],
"params": {"data_inspection": "both"},
})
disposition = classify_rest(status, data)
print(f"觀察訊號: HTTP={status} provider_code={data.get('code')}")
print(f"處置: {disposition}")
# 按處置分流:策略攔截屬預期結果,不必警示營運;營運故障才需要警示且不應無限重試
if disposition == "policy_blocked":
pass # 記錄審計日誌,向使用者返回合規提示
elif disposition == "manual_review":
pass # 投遞到人工複核隊列
else:
pass # 警示值班,停止重試實測兩條路徑都成功攔截,服務端返回一致的契約標識:
code: DataInspectionFailed
message: Input data may contain inappropriate content.
For details, see: https://www.alibabacloud.com/help/zh/model-studio/error-code#inappropriate-content護欄在輸入側即命中,寫入被阻斷,模型未產生任何內容。經上面的分類函數處理後,gRPC 與 REST 兩條路徑都得到 policy_blocked 的一致結論。
同時實測確認:both 模式對正常業務輸入不會誤攔——同一介面傳入正常客服問題時返回 HTTP 200 與完整回覆。
步驟四:日誌與處置建議
護欄落地後,日誌既要能定位問題,又不能成為新的資料泄露面。建議:
只記錄可機讀、已脫敏的欄位:trace id、檢測階段、HTTP 狀態、provider code、處置枚舉、處置動作。
原文與個人敏感資訊一律刪減或脫敏;複核時憑 trace id 由授權通道調取,不在業務日誌中留存原文。
命中策略或
operational_error時停止並警示,不要無限重試。可與
AI_PII_MASK組合:入日誌前先脫敏,把敏感性資料暴露面壓到最低。
{
"trace_id": "req-20260808-abc123",
"stage": "both",
"http_status": 500,
"provider_code": 65535,
"disposition": "policy_blocked",
"note": "blocked by data inspection on input side"
}方案價值
維度 | 接入護欄前 | 接入 |
輸入側越獄與違規 prompt | 依賴後置人工審核,處置滯後 | 模型調用前即阻斷 |
輸出側不合規內容外發 | 事後發現、被動處置 | 返回前攔截,發不出去 |
人工審核量 | 全量人工過審 | 僅複核 |
系統數量 | 業務系統 + 外部Alibaba Content Security Service服務,需串三次遠程調用 | 1 套(Milvus,護欄隨調用附帶) |
資料與憑據 | 原文流出到外部服務,鑒權分散 | 資料不出執行個體,憑據由 Provider 統一配置 |
把Alibaba Content Security Service護欄收斂為一個開關之後,可以按情境靈活選擇:
智能客服問答:給文本產生加
input,擋住越獄與違規提問。營銷文案與指令碼產生:給文本產生加
output,發布前攔下不合規內容。面向公眾的開放式 AI 助手:用
both做兩端雙保險。
可延伸的方向:
與
AI_PII_MASK組合:先脫敏、再檢查、再入日誌,把敏感性資料暴露面壓到最低。覆蓋更多任務:隨著支援
data_inspection的 AI Function 增多,同一套input/output/both心智可以平移到更多產生式情境。策略閉環:把
manual_review樣本沉澱成評測集,持續校準誤攔與漏攔的平衡點,讓護欄越用越准。