Este tópico descreve como usar o recurso DATA_INSPECTION do Alibaba Cloud Milvus para adicionar um guardrail de segurança de conteúdo a aplicações de Q&A de IA: ao adicionar um único parâmetro data_inspection aos parâmetros de uma AI Function existente, é possível verificar a entrada antes da chamada do modelo e a saída antes do retorno do resultado, convergindo os resultados de interceptação no cliente em três disposições mutuamente exclusivas: policy_blocked, manual_review e operational_error.
Visão geral da solução
Quando um modelo de grande escala passa a atender usuários externos, a segurança de conteúdo deixa de ser um diferencial e se torna um pré-requisito para a entrada em produção. Assim que usuários reais inserem prompts em uma extremidade e o modelo gera texto na outra, surgem simultaneamente duas exposições a riscos:
Lado da entrada (usuário → modelo): Os usuários podem inserir conteúdo ilegal ou não conforme, ou usar formulações de jailbreak para induzir o modelo a ultrapassar seus limites de segurança. O conteúdo gerado pelo usuário (UGC), como publicações, comentários e apelidos enviados, também precisa passar por uma barreira de segurança antes do armazenamento.
Lado da saída (modelo → usuário): Mesmo que a entrada pareça normal, o modelo ainda pode gerar conteúdo não conforme, como textos publicitários com promessas exageradas ou declarações tendenciosas. Isso é especialmente evidente em cenários como criação de textos publicitários e roteiros, em que o modelo tem liberdade para improvisar.
Ambos os lados exigem uma barreira, e o valor de negócio é claro: a segurança de conteúdo é um requisito obrigatório para o registro e o lançamento de aplicações de modelos de grande escala. Após as máquinas bloquearem a grande maioria dos conteúdos claramente não conformes, os revisores humanos só precisam reavaliar uma pequena quantidade de amostras ambíguas. Assim, a equipe de revisão passa de "revisar tudo" para "revisar os casos complexos". Uma única captura de tela de um jailbreak bem-sucedido ou um trecho de fala inapropriada pode se transformar em um incidente público, e o guardrail bloqueia o risco antes mesmo do envio do conteúdo.
Implementar esse conjunto de recursos com soluções tradicionais esbarra em várias dificuldades:
Os dois lados são verificados em momentos diferentes: A verificação de entrada deve ocorrer antes da chamada do modelo, e a de saída, antes do retorno do resultado. Uma única chamada de negócio abrange dois pontos no tempo, antes e depois da chamada do modelo.
É preciso integrar um serviço adicional de segurança de conteúdo e encadeá-lo por conta própria: Na abordagem típica, o código de negócio primeiro chama uma API de segurança de conteúdo para verificar a entrada, depois chama o modelo de grande escala e, em seguida, chama a verificação novamente para a saída. Isso significa três chamadas remotas, três conjuntos de lógicas de timeout e retry, e três lugares para lidar com a autenticação.
A leitura programática dos resultados de interceptação é difícil: Quando uma política de segurança é acionada, o servidor pode retornar um erro HTTP, um código de negócio diferente de zero, ou pode retornar um texto de recusa explícito dentro de uma resposta HTTP 200 normal. Verificar apenas o código de status HTTP ignora recusas dentro de respostas 200, enquanto verificar apenas palavras-chave causa falsos positivos em palavras como "risco" ou "recusar" que aparecem em textos de negócio comuns.
Equilíbrio entre falsos bloqueios e falhas de bloqueio: Se a barreira for muito rígida, a operação normal do negócio é afetada; se for muito flexível, o risco de conformidade aumenta. O guardrail não pode ter apenas dois estados (aprovado e bloqueado); ele também precisa de um meio-termo de revisão manual para absorver amostras ambíguas.
Conformidade de logs: A solução de problemas exige logs, mas quando textos originais de alto risco e informações pessoais sensíveis são gravados em texto simples nos logs de negócio, o próprio sistema de logs se torna uma nova superfície de exposição.
A abordagem do Alibaba Cloud Milvus é simples: basta adicionar um parâmetro data_inspection aos params de uma AI Function existente (como a geração de texto ai_text_generate). Ele possui apenas três valores válidos:
|
Valor |
Momento da verificação |
Cenários típicos |
Descrição |
|
|
Antes da chamada do modelo |
Atendimento inteligente ao cliente e assistentes de IA recebendo prompts de usuários; conteúdo UGC antes do armazenamento |
Bloqueia tentativas de jailbreak e entradas ilegais ou não conformes. Quando acionado, o modelo não é chamado, o que economiza recursos computacionais e também é mais seguro. |
|
|
Antes do retorno do resultado |
Publicação de textos publicitários e de campanhas, geração de roteiros |
Adequado para cenários em que a entrada é confiável e a única preocupação é que o modelo possa gerar conteúdo não conforme. |
|
|
Uma vez antes e uma vez depois |
Conversas abertas de alto risco, Q&A de forma livre para o público em geral |
A barreira mais forte quando nenhum dos lados é confiável, e também a mais custosa. |
O guardrail e a chamada do modelo são executados em uma única passagem dentro do Milvus. Os dados nunca saem da instância do Milvus durante todo o processo; defina as credenciais centralmente no lado do Provider e nunca as grave em nenhum corpo da requisição, e o lado da aplicação não precisa mais construir ou encadear um serviço externo de segurança de conteúdo.
O parâmetro data_inspection adiciona uma barreira a uma chamada existente; ele não pode substituir os parâmetros obrigatórios da própria tarefa. Por exemplo, a geração de texto ainda deve fornecer texts; se estiver ausente, o erro texts is required for task [ai_text_generate] será retornado.
Pré-requisitos
Crie uma instância do Milvus 2.6. A AI Function depende do kernel 2.6, e nenhuma vinculação separada de serviço de modelo é necessária após a criação.
Para acessar a instância pela internet, ative o Public Access na aba Security Configuration da página de detalhes da instância e insira o endereço IP de saída do cliente na lista de permissões de acesso público.
Instale o pymilvus. Os exemplos neste tópico foram verificados com o pymilvus 3.0.0.
A interface RESTful e o gRPC compartilham a porta 19530. Especifique explicitamente a porta ao fazer chamadas, por exemplo, http://c-xxx.milvus.aliyuncs.com:19530. Se a porta for omitida, a porta 80 será acessada por padrão, o que causa um timeout de conexão.
Procedimento
Prepare o código comum
O código a seguir inclui a configuração de conexão, um wrapper REST, um fallback de compatibilidade para o tipo TEXTTRANSFORM e uma função utilitária que cria uma Collection com um guardrail. O parâmetro do guardrail é a única linha data_inspection em params.
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
# ==================== Connection configuration ====================
MILVUS_URI = "http://c-xxx.milvus.aliyuncs.com:19530" # The port must be written as 19530
MILVUS_TOKEN = "root:xxx"
MILVUS_REST_BASE_URL = MILVUS_URI
MODEL_NAME = "qwen3.7-max" # Text model, must already be configured in the Provider
TEXTTRANSFORM_FUNCTION_TYPE = 9
# The contract markers returned by the Provider when a safety policy is triggered;
# they are the only reliable basis for determining "whether it was intercepted"
BLOCK_MARKERS = ("DataInspectionFailed", "inappropriate content")
client = MilvusClient(uri=MILVUS_URI, token=MILVUS_TOKEN)
def texttransform_function_type() -> Any:
"""Get the FunctionType for TEXTTRANSFORM; dynamically add a member if the enum is missing in older versions."""
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 interface wrapper: returns (http_status, data); still tries to parse the response body for non-2xx HTTP responses."""
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:
"""Create a write-oriented Collection with a TextTransform guardrail."""
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)
# A Collection must have at least one vector field; this example does not perform vector retrieval,
# so a 2-dimensional placeholder field is used to satisfy the constraint.
# After nullable=True is declared, the field does not need to be passed on 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, # ← Guardrail switch
"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)
Uma Collection deve conter pelo menos um campo de vetor; caso contrário, a criação da tabela retorna o erro schema does not contain vector field. Este exemplo não executa recuperação de vetores, portanto, um campo fictício bidimensional é usado para satisfazer a restrição, e ele é declarado como nullable=True para que não seja necessário passar um valor a cada inserção.
Converja os resultados de interceptação em três disposições
Esta é a dificuldade de engenharia mais subestimada ao implementar guardrails. Quando uma política de segurança é acionada, o servidor pode retornar um erro HTTP, um código de negócio diferente de zero ou um texto de recusa explícito dentro de uma resposta HTTP 200. Portanto, o cliente precisa unificar os sinais observados em três disposições mutuamente exclusivas:
|
Disposição |
Descrição |
Ação recomendada |
|
|
Uma política de segurança foi acionada, o guardrail está funcionando conforme o esperado e este é um resultado de negócio esperado. |
Registre um log de auditoria e retorne um aviso de conformidade ao usuário. Não é necessário alertar a equipe de operações. |
|
|
Não é possível determinar na camada de protocolo, por exemplo, HTTP 200 com estrutura normal, mas conteúdo suspeito. |
Encaminhe para a fila de revisão manual. "A ausência de erros" não significa "aprovado na verificação de segurança". |
|
|
Uma falha real de serviço, como modelo indisponível, erro de parâmetro ou falha de rede. |
Alerte o engenheiro de plantão e interrompa as tentativas. Não faça retries indefinidamente. |
# ==================== Three-state disposition classification ====================
# Both call paths (gRPC exception / REST response body) first check the Provider contract markers,
# then fall back to the HTTP status and business code, ensuring that the same interception
# yields a consistent disposition conclusion on both paths.
def classify_grpc(exc: MilvusException) -> str:
"""Converge a gRPC exception into one of the three dispositions."""
msg = str(exc)
if any(marker in msg for marker in BLOCK_MARKERS):
return "policy_blocked" # A safety policy was triggered; an expected business outcome
return "operational_error" # Everything else is treated as an operational failure
def classify_rest(status: int, data: dict[str, Any]) -> str:
"""Converge a REST response into one of the three dispositions."""
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 with a normal structure: this may be an "explicit refusal" given by the model in the body,
# which cannot be determined at the protocol layer, so always route to manual review
# instead of treating it directly as having passed the safety check.
return "manual_review"
Por que avaliar pelos marcadores de contrato do Provider em vez de apenas pelo código de status HTTP. Em testes reais, "uma entrada não conforme interceptada pelo guardrail" e "uma falha de serviço causada por um nome de modelo inexistente" retornaram exatamente a mesma combinação de códigos de status:
|
Cenário |
Status HTTP |
Código de negócio |
Corpo da resposta contém DataInspectionFailed |
Disposição correta |
|
Entrada não conforme interceptada |
500 |
65535 |
✅ |
|
|
Nome de modelo inexistente |
500 |
65535 |
❌ |
|
|
Parâmetro obrigatório texts ausente |
400 |
1100 |
❌ |
|
|
Entrada normal |
200 |
0 |
❌ |
Aprovado |
Os códigos de status HTTP e os códigos de negócio nas duas primeiras linhas são exatamente idênticos, portanto, sozinhos, não conseguem distinguir "conteúdo interceptado" de "erro de serviço". Os significados operacionais dos dois são completamente diferentes: classificar erroneamente um bloqueio por política como uma falha operacional geraria um falso alerta de falha de serviço para cada interceptação de segurança de conteúdo, o que, com o tempo, mascararia as falhas reais. Portanto, o critério deve ser o marcador de contrato DataInspectionFailed no corpo da resposta. Da mesma forma, não confie em valores específicos de código de status HTTP, pois eles podem mudar com a versão do gateway.
Não use palavras-chave como "risco", "recusar" ou "não pode" para determinar se o conteúdo foi interceptado; essas palavras também podem aparecer em textos de negócio comuns e facilmente causar falsos positivos. No máximo, palavras-chave podem ser usadas para rastreamento complementar e não devem alterar a conclusão final da disposição.
Etapas 1 e 2: os modos input e output aprovam conteúdos conformes
O modo input verifica a entrada do usuário antes da chamada do modelo, e o modo output verifica a saída do modelo antes do retorno do resultado. O conteúdo conforme passa normalmente.
# ==================== Step (a): input mode, pre-request check ====================
build_guard_collection(
"guard_input", "inspect_customer_request", "request", "response",
"Answer in one sentence in a customer service tone: ${request}", "input",
)
client.insert("guard_input", [{"request": "How long does a refund take to arrive after the review is approved?"}])
client.flush("guard_input")
for row in client.query("guard_input", filter="",
output_fields=["request", "response"], limit=1):
print(f"Input: {row.get('request')}")
print(f"Output: {row.get('response')}")
# The guardrail was not triggered → passed, and the model returns normally.
# If replaced with a jailbreak prompt, the model would not be called at all; the guardrail blocks it up front.
# ==================== Step (b): output mode, check model output before publishing ====================
build_guard_collection(
"guard_output", "inspect_generated_copy", "draft_request", "publish_copy",
"Generate a membership campaign introduction suitable for publishing in the app: ${draft_request}", "output",
)
client.insert("guard_output",
[{"draft_request": "Generate a benefits introduction of no more than 30 characters for the Member Day campaign. Exaggerated promises are prohibited."}])
client.flush("guard_output")
for row in client.query("guard_output", filter="",
output_fields=["draft_request", "publish_copy"], limit=1):
print(f"Output: {row.get('publish_copy')}")
# The output passed the safety check → it enters the pending-publication queue.
Resultados reais de aprovação:
|
Modo |
Entrada |
Saída |
|
|
Quanto tempo leva para um reembolso chegar após a aprovação da análise? |
Prezado cliente, após a aprovação do reembolso, os valores geralmente são devolvidos à sua conta de pagamento original em 1 a 7 dias úteis. O tempo exato de chegada depende da velocidade de processamento do canal de pagamento, portanto, fique atento. |
|
|
Gere uma introdução de benefícios com no máximo 30 caracteres para a campanha do Dia do Membro. Promessas exageradas são proibidas. |
Exclusivo do Dia do Membro: resgate de pontos, descontos exclusivos para membros e atendimento prioritário. Detalhes sujeitos à página. |
No modo input, o modelo não é chamado quando o guardrail é acionado, o que bloqueia o risco e reduz o custo da chamada do modelo. O modo output é adequado para cenários em que a entrada é confiável e a única preocupação é a improvisação do modelo.
Etapa 3: o modo both intercepta entradas não conformes
Use both quando nenhum dos lados for confiável. O exemplo a seguir usa uma entrada de teste claramente não conforme; verifique se o guardrail realmente funciona. Esta é uma verificação de eficácia do guardrail que deve ser realizada antes da entrada em produção.
# ==================== Step (c): both mode, double checks on both ends ====================
# Use a clearly non-compliant test input (canary) to verify that the guardrail really blocks it.
UNSAFE_INPUT = "<a clearly non-compliant test input>"
# (c-1) gRPC path: the guardrail is triggered on write
build_guard_collection("guard_both", "inspect_both", "request", "response",
"Please answer: ${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)
# No error thrown does not mean it passed the safety check: if the structure is normal but the content is suspicious, route to manual review
print("Disposition: manual_review", [r.get("response") for r in rows])
except MilvusException as exc:
print(f"Disposition: {classify_grpc(exc)}")
print(f"Server returned: {exc.message}")
# (c-2) REST path: the same input through the real-time interface
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"Observed signals: HTTP={status} provider_code={data.get('code')}")
print(f"Disposition: {disposition}")
# Route by disposition: a policy interception is an expected outcome and does not require an operations alert;
# only operational failures need alerts, and they should not be retried indefinitely
if disposition == "policy_blocked":
pass # Record an audit log and return a compliance notice to the user
elif disposition == "manual_review":
pass # Deliver to the manual review queue
else:
pass # Alert the on-duty engineer and stop retrying
Em testes reais, ambos os caminhos interceptaram a entrada com sucesso, e o servidor retornou marcadores de contrato consistentes:
code: DataInspectionFailed
message: Input data may contain inappropriate content.
For details, see: https://www.alibabacloud.com/help/zh/model-studio/error-code#inappropriate-content
O guardrail foi acionado no lado da entrada, a gravação foi bloqueada e o modelo não gerou nenhum conteúdo. Após o processamento pelas funções de classificação acima, tanto os caminhos gRPC quanto REST chegam à mesma conclusão de policy_blocked.
Confirme que o modo both não intercepta falsamente entradas de negócio comuns; quando uma pergunta normal de atendimento ao cliente é passada para a mesma interface, a interface retorna um HTTP 200 e uma resposta completa.
Etapa 4: recomendações de logs e disposição
Após a implementação do guardrail, os logs devem ajudar a localizar problemas sem se tornarem uma nova superfície de exposição de dados. Recomendações:
Registre apenas campos legíveis por máquina e anonimizados: trace ID, estágio de detecção, status HTTP, código do provider, enum de disposição e ação de disposição.
Sempre oculte ou anonimize o texto original e informações pessoais sensíveis. Durante a revisão, recupere-as por meio de um canal autorizado usando o trace ID e não retenha o texto original nos logs de negócio.
Interrompa e lance um alerta quando uma política for acionada ou ocorrer um
operational_error. Não faça retries indefinidamente.Combine com
AI_PII_MASK: anonimize antes de registrar nos logs para minimizar a superfície de exposição de dados sensíveis.
{
"trace_id": "req-20260808-abc123",
"stage": "both",
"http_status": 500,
"provider_code": 65535,
"disposition": "policy_blocked",
"note": "blocked by data inspection on input side"
}
Valor da solução
|
Dimensão |
Antes de adotar o guardrail |
**Após adotar o |
|
Jailbreak e prompts não conformes no lado da entrada |
Depende de revisão manual posterior, com tratamento atrasado |
Bloqueados antes da chamada do modelo |
|
Conteúdo não conforme enviado no lado da saída |
Descoberto após o fato, tratado de forma passiva |
Interceptado antes do retorno, impedindo o envio |
|
Volume de revisão manual |
Revisão manual completa de tudo |
Apenas uma pequena quantidade de amostras de |
|
Número de sistemas |
Sistema de negócio + serviço externo de segurança de conteúdo, exigindo três chamadas remotas encadeadas |
1 sistema (Milvus, com o guardrail acoplado à chamada) |
|
Dados e credenciais |
O texto original flui para serviços externos e a autenticação é dispersa |
Os dados nunca saem da instância e as credenciais são configuradas centralmente pelo Provider |
Com o guardrail de segurança de conteúdo unificado em um único parâmetro, escolha de forma flexível com base no cenário:
Q&A de atendimento inteligente ao cliente: Adicione
inputà geração de texto para bloquear tentativas de jailbreak e perguntas não conformes.Textos publicitários e geração de roteiros: Adicione
outputà geração de texto para interceptar conteúdos não conformes antes da publicação.Assistentes de IA abertos ao público em geral: Use
bothpara proteção dupla em ambos os lados.
Direções para extensão:
**Combinar com
AI_PII_MASK**: Anonimize primeiro, depois verifique e, em seguida, registre nos logs para minimizar a superfície de exposição de dados sensíveis.Cobrir mais tarefas: À medida que mais AI Functions suportarem
data_inspection, o mesmo modelo mental deinput/output/bothpoderá ser aplicado a outros cenários generativos.Fechar o ciclo da política: Acumule amostras de
manual_reviewem um conjunto de avaliação para calibrar continuamente o equilíbrio entre falsos bloqueios e falhas de bloqueio, tornando o guardrail mais preciso ao longo do tempo.