Todos os produtos
Search
Central de documentação

Vector Retrieval Service for Milvus:Crie um pipeline de análise de voz e correspondência de FAQ para atendimento ao cliente com Alibaba Cloud Milvus

Última atualização: Aug 13, 2026

Neste tutorial, você encadeia as AI Functions do Alibaba Cloud Milvus em um pipeline de voz para contact center que opera inteiramente dentro de um único banco de dados vetorial. As gravações são transcritas no momento da escrita, o texto tem informações de identificação pessoal (PII) mascaradas antes de entrar no banco de dados, os FAQs são vetorizados na escrita e a recuperação semântica combinada com reranking associa uma pergunta coloquial à resposta padrão correta. Ao final, cada conversa registrada também recebe um rótulo de sentimento e uma categoria de ticket.

Visão geral da solução

Equipes de contact center acumulam gravações de chamadas e mensagens de voz diariamente, mas a maioria mantém esses dados apenas para arquivamento de conformidade. As gravações contêm solicitações de clientes, evidências da qualidade do atendimento dos agentes e sinais precoces de feedback negativo. Para ativar esse ativo de voz, são necessárias as seguintes capacidades:

  • Transcrição de fala para texto — Transcreva chamadas e mensagens de voz em massa para um formato textual pesquisável e analisável. Todas as demais etapas dependem desta.

  • Base de conhecimento pesquisável — Transforme respostas históricas de alta qualidade, manuais de products e FAQs em uma base de conhecimento com busca semântica, consultável por bots e agentes em tempo real.

  • Correspondência inteligente de FAQ — Associe perguntas coloquiais dos clientes ao FAQ padrão correto, em vez de depender da sorte com palavras-chave.

  • Reconhecimento de sentimento e intenção — Detecte mudanças emocionais durante a chamada para viabilizar alertas em tempo real e inspeção de qualidade pós-chamada.

  • Inspeção de qualidade e mascaramento de conformidade — Textos transcritos frequentemente contêm PII, como números de celular, documentos de identidade e cartões bancários. Mascare esses dados antes de armazenar, analisar ou compartilhar.

Montar esse conjunto de capacidades com uma abordagem tradicional geralmente exige o encadeamento de quatro ou mais sistemas: um service de ASR, um banco de dados vetorial, uma plataforma externa de NLP e um service de reranking. O código de integração cresce continuamente, e qualquer instabilidade em um único ponto afeta todo o pipeline. Além disso, enviar transcrições brutas para services externos aumenta o risco de exposição de PII.

As AI Functions do Alibaba Cloud Milvus consolidam essas capacidades em um único banco de dados vetorial. O Milvus aciona a inferência do modelo internamente durante a escrita e a busca, sem que os dados saiam da instância. A tabela a seguir descreve as seis AI Functions utilizadas neste tutorial.

Função

Finalidade

Papel no pipeline de voz do contact center

AI_AUDIO_TRANSCRIBE

Transcreve gravações em texto no momento da escrita, sem necessidade de chamada ASR pela aplicação.

Converte um grande volume de gravações de chamadas em texto pesquisável e analisável.

AI_PII_MASK

Mascara PII, como números de celular, documentos de identidade e cartões bancários, no texto.

Garante que apenas texto mascarado entre na base de conhecimento e no repositório analítico, aplicando conformidade desde o início.

AI_EMBEDDING

Converte texto de FAQ em vetores no momento da escrita.

Permite que solicitações coloquiais, como "Como conecto minha instância pela internet?", sejam atendidas por recuperação semântica.

AI_RERANK

Reordena os candidatos recuperados conforme sua relevância para a consulta.

Eleva o FAQ mais adequado à intenção para a primeira posição e corrige ruídos da recuperação vetorial.

AI_SENTIMENT

Determina o sentimento de uma conversa.

Agrega rótulos de sentimento para dar suporte à inspeção de qualidade abrangente e aos alertas de feedback negativo.

AI_CLASSIFY

Classifica uma conversa em uma categoria de ticket.

Dá suporte à classificação e ao roteamento automático de tickets.

Comparação das AI Functions com uma arquitetura multissistema

A tabela abaixo compara este pipeline com um equivalente montado a partir de services separados.

Dimensão

Abordagem tradicional (ASR + banco de dados vetorial + NLP + service de reranking)

Alibaba Cloud Milvus

Quantidade de sistemas

Quatro ou mais, com dados transitando entre eles

Um único sistema, sem saída de dados da instância

Transcrição

A aplicação chama o ASR primeiro e depois escreve no banco de dados

Transcrição no momento da escrita

Pipeline de vetorização

A aplicação chama o embedding primeiro e depois escreve

Vetorização no momento da escrita

Mascaramento de PII

Frequentemente posicionado ao final do pipeline e facilmente ignorado

Mascaramento na escrita, com conformidade aplicada preventivamente

Recuperação e reranking

É necessário implantar um service separado de modelo de reranking

AI_RERANK é nativo e executa dentro de uma única operação search

Sentimento e classificação

Requer uma plataforma externa de NLP

AI_SENTIMENT e AI_CLASSIFY são nativos

Pré-requisitos

  • Instância Milvus — Uma instância Milvus 2.6. As AI Functions exigem o kernel 2.6, e não é necessária vinculação adicional a services de modelo após a criação.

  • Acesso à internet — Para acessar a instância pela internet, ative o Public Endpoint na aba Security Configuration da página de detalhes da instância e adicione o IP de saída do cliente à lista de permissões de acesso público.

  • Porta do endpoint — Especifique explicitamente a porta 19530 na URI da instância, por exemplo http://c-xxx.milvus.aliyuncs.com:19530. A API RESTful e o gRPC compartilham essa porta. Se você omitir a porta, a conexão tentará usar a porta 80 e atingirá timeout.

  • Biblioteca cliente — pymilvus instalada. Os exemplos deste tópico foram validados com pymilvus 3.0.0.

  • Fonte de áudio — Gravações a serem transcritas carregadas em um endereço acessível para download pelo servidor. Em ambiente de produção, utilize uma URL assinada de curta duração do seu próprio bucket do Object Storage Service (OSS). Endereços fictícios ou expirados causarão falha na transcrição.

  • Credenciais de mídia — URLs assinadas de curta duração e com privilégio mínimo para todos os arquivos de mídia. Nunca incorpore credenciais de longa duração em uma URL.

  • Consentimento de gravação — Aviso de gravação e consentimento do cliente obtidos antes de processar qualquer áudio.

Prepare o código compartilhado

O código a seguir contém as configurações de conexão, um wrapper para chamadas REST e um fallback de compatibilidade para o tipo de função TEXTTRANSFORM. Todas as etapas subsequentes o reutilizam. Esta seção é apenas preparatória e não consome nenhum dos seis números de etapa.

Substitua MILVUS_URI e MILVUS_TOKEN pelos valores da sua instância.

from __future__ import annotations

import json
import time
from typing import Any
from urllib.error import HTTPError
from urllib.request import Request, urlopen

from pymilvus import DataType, Function, FunctionType, MilvusClient

# ==================== Connection settings ====================
MILVUS_URI = "http://c-xxx.milvus.aliyuncs.com:19530"  # the port must be 19530
MILVUS_TOKEN = "root:xxx"
MILVUS_REST_BASE_URL = MILVUS_URI

client = MilvusClient(uri=MILVUS_URI, token=MILVUS_TOKEN)

# AI_AUDIO_TRANSCRIBE / AI_PII_MASK / AI_SENTIMENT / AI_CLASSIFY are all Functions of the
# TEXTTRANSFORM type (function type value 9) and are distinguished by the task parameter.
# The FunctionType enum of some pymilvus versions has no such member, so add a fallback.
TEXTTRANSFORM_FUNCTION_TYPE = 9

def texttransform_function_type() -> Any:
    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,
              retries: int = 3) -> tuple[int, dict[str, Any]]:
    """Shared wrapper for the synchronous REST APIs. These calls go through an LLM, so retries make them more stable."""
    last: tuple[int, dict[str, Any]] | None = None
    for _ in range(retries):
        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:
                status, data = response.status, json.loads(response.read().decode("utf-8"))
        except HTTPError as exc:
            status, data = exc.code, json.loads(exc.read().decode("utf-8"))
        last = (status, data)
        if status == 200 and data.get("code") == 0:
            return status, data
        time.sleep(1)
    return last

Antes de executar qualquer etapa, considere as seguintes propriedades do código compartilhado:

  • Tipos de funçãoAI_AUDIO_TRANSCRIBE, AI_PII_MASK, AI_SENTIMENT e AI_CLASSIFY são funções do tipo TEXTTRANSFORM e se distinguem pelo parâmetro task, enquanto AI_EMBEDDING e AI_RERANK possuem suas próprias constantes FunctionType.

  • Escopo de nova tentativapost_json tenta repetir a chamada até três vezes quando a resposta é um erro HTTP ou o payload retorna um code diferente de zero. Timeouts de leitura e erros de rede não são capturados e propagam-se para o chamador.

  • Independência das etapas — Cada etapa cria sua própria collection e executa de forma autônoma. Para encadear as etapas, alimente o campo de saída de uma etapa no campo de entrada da próxima: o campo transcript da Etapa 1 torna-se a entrada de mascaramento na Etapa 2, e o texto mascarado torna-se a entrada content na Etapa 6.

Aviso

Cada etapa exclui qualquer collection existente com o mesmo nome antes de criar uma nova. Se sua instância já contiver uma collection chamada cs_transcribe, cs_pii_mask, cs_faq_kb ou cs_qc, essa collection e seus dados serão excluídos. Utilize nomes de collection que não estejam em uso na sua instância.

Etapa 1: Transcreva gravações de chamadas

Transcreva gravações de chamadas em texto em massa. Após anexar AI_AUDIO_TRANSCRIBE a uma collection, escrever um endereço de áudio gera a transcrição automaticamente, sem necessidade de chamada ASR pela aplicação.

# ==================== Step 1: AI_AUDIO_TRANSCRIBE transcription ====================
collection_name = "cs_transcribe"
if client.has_collection(collection_name):
    client.drop_collection(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("audio_url", DataType.VARCHAR, max_length=4096)   # audio input field
schema.add_field("transcript", DataType.VARCHAR, max_length=4096)  # transcript output field
# A Collection must contain at least one vector field. This step performs no vector search,
# so a 2-dimensional placeholder field satisfies the constraint. Declaring it as
# nullable=True means the field can be omitted at insert time.
schema.add_field("dummy_vector", DataType.FLOAT_VECTOR, dim=2, nullable=True)
schema.add_function(
    Function(
        name="transcribe_audio",
        function_type=texttransform_function_type(),
        input_field_names=["audio_url"],
        output_field_names=["transcript"],
        params={
            "provider": "aliyun_milvus",
            "model_name": "qwen3-asr-flash",
            "task": "ai_audio_transcribe",
            "language": "zh",
            "enable_itn": "true",     # normalize spoken numbers, such as one-three-eight to 138
        },
    )
)

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=collection_name, schema=schema,
                        index_params=index_params)

# audio_url must be a real address the server can download from. In production, use a
# short-lived signed URL from your own OSS bucket.
audio_urls = [
    "https://<your-bucket>.oss-cn-hangzhou.aliyuncs.com/calls/call_0001.mp3",
    "https://<your-bucket>.oss-cn-hangzhou.aliyuncs.com/calls/call_0002.wav",
]
client.insert(collection_name, [{"audio_url": u} for u in audio_urls])
client.flush(collection_name)

for row in client.query(collection_name, filter="",
                       output_fields=["audio_url", "transcript"], limit=10):
    print(f"{row['audio_url'].split('/')[-1]} -> {row['transcript']}")

Dois parâmetros neste exemplo controlam o comportamento da transcrição:

  • **language** — Este exemplo está definido como zh e, portanto, transcreve gravações em idioma chinês.

  • **enable_itn** — Ativa a normalização inversa de texto, simplificando a recuperação posterior e o processamento estruturado. O campo vetorial de placeholder e sua declaração nullable=True são ambos obrigatórios. Sem o campo de placeholder, a criação da collection falha; sem nullable=True, uma operação insert que omita o campo falhará. Para as mensagens de erro exatas, consulte Solução de problemas.

A consulta final imprime uma linha por gravação, no formato call_0001.mp3 -> <transcript>. Se uma transcrição estiver vazia, verifique o endereço de áudio antes de prosseguir para a Etapa 2.

Etapa 2: Mascare PII antes do armazenamento

Textos transcritos frequentemente contêm PII, como números de celular, documentos de identidade e cartões bancários. Mascare o texto antes de ele entrar na base de conhecimento ou no repositório analítico, movendo a conformidade para o início do pipeline em vez do final. Escolha um dos seguintes padrões:

  • (Recomendado para este pipeline) API REST síncrona — Utilize para processamento único antes do fluxo de dados. Apenas texto mascarado chega ao banco de dados, e a transcrição mascarada alimenta diretamente a Etapa 6.

  • Collection no momento da escrita — Utilize quando precisar manter o resultado mascarado junto ao texto original para auditoria.

Chame a API REST síncrona para mascarar o texto antes de escrevê-lo em qualquer lugar:

# ==================== Step 2: AI_PII_MASK masking ====================
# 2.1 Synchronous REST API: mask the text before it enters the database
status, data = post_json(
    "/v2/vectordb/ai/pii_mask",
    {
        "model_name": "qwen3.7-max",
        "texts": ["Hello, my phone number is 13800138000 and my ID card number is 110101199001010000. Please check my ticket."],
        "params": {
            "pii_types": ["PERSON", "PHONE", "ID_CARD"],
            "mask_char": "*",
            "preserve_length": True,   # keep the original length after masking
            "temperature": 0,
        },
    },
)
assert status == 200 and data.get("code") == 0, data
for item in data["data"]["output"]["outputs"]:
    print(item)

Quando preserve_length é true, o valor mascarado mantém o comprimento original de caracteres, preservando características de formatação sem expor o valor original. A seguinte saída é observada para a chamada REST acima:

Hello, my phone number is *********** and my ID card number is ******************. Please check my ticket.

O número de celular de 11 dígitos é mascarado com 11 asteriscos, e o documento de identidade de 18 dígitos com 18 asteriscos.

Como alternativa, anexe AI_PII_MASK a uma collection para que o campo mascarado seja gerado automaticamente na escrita:

# 2.2 Write-time Collection: the masked field is generated automatically when content is written
collection_name = "cs_pii_mask"
if client.has_collection(collection_name):
    client.drop_collection(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("content", DataType.VARCHAR, max_length=4096)
schema.add_field("masked", DataType.VARCHAR, max_length=4096)
schema.add_field("dummy_vector", DataType.FLOAT_VECTOR, dim=2, nullable=True)
schema.add_function(
    Function(
        name="mask_pii",
        function_type=texttransform_function_type(),
        input_field_names=["content"],
        output_field_names=["masked"],
        params={
            "provider": "aliyun_milvus",
            "model_name": "qwen3.7-max",
            "task": "ai_pii_mask",
            "pii_types": "PERSON,PHONE,ID_CARD",
            "mask_char": "*",
            "preserve_length": "true",
            "temperature": "0",
        },
    )
)
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=collection_name, schema=schema,
                        index_params=index_params)

client.insert(collection_name,
              [{"content": "Hello, my phone number is 13800138000. Please check my ticket."}])
client.flush(collection_name)
for row in client.query(collection_name, filter="", output_fields=["masked"], limit=1):
    print(row["masked"])

Esta alternativa escreve um texto de amostra mais curto contendo apenas um número de celular, portanto seu valor masked difere da saída REST mostrada anteriormente. Verifique se o valor impresso mantém a estrutura da frase e substitui os dígitos do número de celular por asteriscos.

Etapa 3: Construa a base de conhecimento de FAQ

Escreva perguntas de FAQ e suas respostas padrão em uma collection. AI_EMBEDDING as vetoriza no momento da escrita, eliminando a necessidade de a aplicação chamar previamente um modelo de embedding.

# ==================== Step 3: AI_EMBEDDING builds the FAQ knowledge base ====================
collection_name = "cs_faq_kb"
if client.has_collection(collection_name):
    client.drop_collection(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("content", DataType.VARCHAR, max_length=4096)   # FAQ question text
schema.add_field("answer", DataType.VARCHAR, max_length=4096)    # standard answer
schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=1024)
schema.add_function(
    Function(
        name="embed_content",
        function_type=FunctionType.TEXTEMBEDDING,
        input_field_names=["content"],
        output_field_names=["embedding"],
        params={
            "provider": "aliyun_milvus",
            "model_name": "text-embedding-v4",
            "dim": 1024,
            "max_client_batch_size": 10,
            "max_concurrency": 1,
        },
    )
)
index_params = client.prepare_index_params()
index_params.add_index(
    field_name="embedding",
    index_type="HNSW",
    metric_type="COSINE",
    params={"M": 16, "efConstruction": 200},
)
client.create_collection(collection_name=collection_name, schema=schema,
                        index_params=index_params)

faqs = [
    {"content": "How do I enable public network access for a Serverless Milvus instance?",
     "answer": "Enable public network access on the instance details page in the console and configure the whitelist."},
    {"content": "What do I do if I forget the console logon password?", "answer": "Reset it through the password recovery process in Account Center."},
    {"content": "Why is my bill higher than expected?", "answer": "Check the usage details, with a focus on compute and storage usage."},
    {"content": "How do I create a Collection and insert vectors?",
     "answer": "Define the schema with create_collection, then call insert."},
    {"content": "Does scaling up an instance affect online workloads?", "answer": "Scaling up is an online operation and usually does not interrupt service."},
]
client.insert(collection_name, faqs)
client.flush(collection_name)
client.load_collection(collection_name)

Este exemplo usa text-embedding-v4, que suporta múltiplos idiomas e dimensões personalizadas. O AI Center também fornece modelos como qwen3.7-text-embedding. Verifique os modelos disponíveis e suas dimensões na aba Model Service do AI Center no console e escolha um modelo com base na cobertura de idiomas e nos requisitos de qualidade. Independentemente do modelo escolhido, o dim do campo vetorial deve corresponder ao dim nos parâmetros da função.

Não prossiga para a Etapa 4 até que todas as cinco entradas de FAQ sejam escritas e load_collection retorne. Consultas contra uma collection não carregada não podem confirmar se a vetorização foi bem-sucedida.

Etapa 4: Recupere candidatos de FAQ por busca semântica

Envie a pergunta coloquial do cliente para a base de conhecimento de FAQ e obtenha os Top-N candidatos. A recuperação vetorial isolada é abrangente, mas não precisa; trate esta saída como um conjunto de candidatos, não como resposta final.

# ==================== Step 4: retrieve FAQs by semantic search ====================
query = "How can I get my instance connected from the internet?"
result = client.search(
    collection_name="cs_faq_kb",
    data=[query],
    anns_field="embedding",
    limit=3,
    output_fields=["content", "answer"],
)
for rank, hit in enumerate(result[0], 1):
    print(f"{rank}. [similarity {hit['distance']:.4f}] {hit['entity']['content']}")

A saída lista três candidatos de FAQ com suas pontuações de similaridade COSINE, classificados do maior para o menor. Mantenha essa classificação à mão: a Etapa 5 a compara com a classificação após reranking para a mesma consulta.

Etapa 5: Reordene os candidatos

Um modelo de reranking classifica os candidatos pela relevância real em relação à consulta. Somente a combinação de recuperação e reranking identifica o FAQ padrão corretamente de forma confiável. Escolha um dos seguintes padrões:

  • **(Recomendado para este pipeline) Ranker anexado ao search** — Funde recuperação e classificação refinada em uma única chamada, encurtando o pipeline.

  • API REST síncrona — Utilize quando a lista de candidatos já existir e apenas o reranking for necessário.

# ==================== Step 5: AI_RERANK reranking ====================
# 5.1 Synchronous REST API: rerank a given batch of candidates independently
status, data = post_json(
    "/v2/vectordb/ai/rerank",
    {
        "model_name": "qwen3-rerank",
        "query": query,
        "documents": [
            "How do I enable public network access for a Serverless Milvus instance?",
            "Does scaling up an instance affect online workloads?",
            "How do I create a Collection and insert vectors?",
        ],
        "params": {"max_concurrency": 2, "timeout_sec": 10},
    },
)
assert status == 200 and data.get("code") == 0, data
ranked = sorted(data["data"]["output"]["results"],
                key=lambda x: x["relevance_score"], reverse=True)
for rank, item in enumerate(ranked, 1):
    print(f"{rank}. [relevance {item['relevance_score']:.4f}] candidate index={item['index']}")

# 5.2 Attach a ranker to search so that retrieval and reranking finish in one call
reranker = Function(
    name="rerank_faq",
    function_type=FunctionType.RERANK,
    input_field_names=["content"],
    params={
        "reranker": "model",
        "provider": "aliyun_milvus",
        "model_name": "qwen3-rerank",
        "queries": [query],
        "max_concurrency": 2,
        "timeout_sec": 10,
    },
)
result = client.search(
    collection_name="cs_faq_kb",
    data=[query],
    anns_field="embedding",
    limit=3,
    output_fields=["content", "answer"],
    ranker=reranker,
)
for rank, hit in enumerate(result[0], 1):
    e = hit["entity"]
    print(f"{rank}. [rerank score {hit['distance']:.4f}] {e['content']} | answer: {e['answer']}")

A API REST não suporta top_n e retorna uma pontuação por candidato. Quando precisar truncar, ordene por pontuação e selecione os Top N no lado da aplicação.

Os seguintes dados são observados para a consulta "How can I get my instance connected from the internet?".

FAQ candidato

Etapa 4: recuperação vetorial

Etapa 5: após reranking

How do I enable public network access for a Serverless Milvus instance?

① 0,6141

0,5883

What do I do if I forget the console logon password?

② 0,5613

③ 0,2607

Does scaling up an instance affect online workloads?

③ 0,4757

② 0,3356

As duas colunas de pontuação não estão na mesma escala: a coluna da Etapa 4 reporta similaridade COSINE entre vetores, e a coluna da Etapa 5 reporta a pontuação de relevância do modelo de reranking. Compare as posições, não os valores absolutos.

A recuperação vetorial classificou "What do I do if I forget the console logon password?" em segundo lugar com 0,5613, embora sua intenção não tenha relação com conexão via internet. Essa é a fraqueza típica da recuperação puramente vetorial: a similaridade superficial de texto permanece alta enquanto a intenção real diverge. Após o reranking, esse candidato cai para a terceira posição, e a pergunta sobre dimensionamento de instância, semanticamente mais próxima, sobe para a segunda. Para um bot de atendimento ao cliente, a precisão do Top-1 determina diretamente a qualidade da resposta, tornando indispensável a etapa de reranking.

O reranking apenas de texto não requer o parâmetro is_multimodal. Esse parâmetro aplica-se somente ao reranking multimodal, que utiliza qwen3-vl-rerank para pontuar candidatos de imagem e vídeo. Passá-lo para candidatos apenas de texto não altera as pontuações.

Etapa 6: Rotule sentimento e categoria do ticket

Anexe duas funções ao texto da conversa: uma determina o sentimento para inspeção de qualidade e alertas de feedback negativo, e outra atribui uma categoria de ticket para distribuição automática de solicitações. Ambas executam automaticamente na escrita. No pipeline completo, o campo content recebe o texto mascarado produzido na Etapa 2; o exemplo a seguir escreve texto bruto para que possa ser executado de forma independente.

# ==================== Step 6: AI_SENTIMENT sentiment analysis + AI_CLASSIFY ticket classification ====================
collection_name = "cs_qc"
if client.has_collection(collection_name):
    client.drop_collection(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("content", DataType.VARCHAR, max_length=4096)     # masked conversation text
schema.add_field("sentiment", DataType.VARCHAR, max_length=64)     # sentiment output
schema.add_field("category", DataType.VARCHAR, max_length=64)      # ticket category output
schema.add_field("dummy_vector", DataType.FLOAT_VECTOR, dim=2, nullable=True)
schema.add_function(
    Function(
        name="analyze_sentiment",
        function_type=texttransform_function_type(),
        input_field_names=["content"], output_field_names=["sentiment"],
        params={"provider": "aliyun_milvus", "model_name": "qwen3.7-max",
                "task": "ai_sentiment", "categories": "positive,negative,neutral",
                "temperature": "0"},
    )
)
schema.add_function(
    Function(
        name="classify_ticket",
        function_type=texttransform_function_type(),
        input_field_names=["content"], output_field_names=["category"],
        params={"provider": "aliyun_milvus", "model_name": "qwen3.7-max",
                "task": "ai_classify", "labels": "account,inquiry,fault,billing",
                "prompt": "Classify by the topic of the customer question.", "temperature": "0"},
    )
)
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=collection_name, schema=schema,
                        index_params=index_params)

client.insert(collection_name, [
    {"content": "I have called three times about this problem and it is still not fixed. This is very disappointing!"},
    {"content": "How do I enable public network access for a Serverless Milvus instance?"},
])
client.flush(collection_name)
for row in client.query(collection_name, filter="",
                        output_fields=["content", "sentiment", "category"], limit=10):
    print(f"sentiment={row['sentiment']:<10} category={row['category']:<6} | {row['content'][:28]}")

Os seguintes resultados são observados para as duas conversas de amostra:

Texto da conversa

Sentimento

Categoria do ticket

I have called three times about this problem and it is still not fixed. This is very disappointing!

negative

fault

How do I enable public network access for a Serverless Milvus instance?

neutral

inquiry

Uma única collection pode conter múltiplas funções, desde que seus campos de saída não entrem em conflito. Neste exemplo, duas funções preenchem sentiment e category separadamente, de modo que uma única escrita produz tanto o rótulo de sentimento quanto a categoria do ticket.

Importante

Resultados de sentimento e classificação são julgamentos do modelo, não fatos estabelecidos. Mantenha uma etapa de revisão humana antes de ações de alto impacto, como escalonamento de tickets ou suspensão de service.

Solução de problemas

A tabela a seguir lista os erros mais frequentemente retornados pelos exemplos deste tutorial.

Mensagem de erro

Causa

Solução

Timeout de conexão ao conectar com MilvusClient

A URI da instância omite a porta, fazendo a conexão recair na porta 80.

Especifique explicitamente a porta 19530, por exemplo http://c-xxx.milvus.aliyuncs.com:19530.

Failed to download multimodal content

O endereço de áudio é um placeholder ou expirou, impedindo o servidor de baixar a gravação.

Informe um endereço que o servidor possa realmente baixar. Em produção, utilize uma URL assinada de curta duração do seu próprio bucket OSS.

schema does not contain vector field

A collection não define nenhum campo vetorial.

Adicione um campo vetorial. Quando a etapa não realiza busca vetorial, um campo de placeholder bidimensional satisfaz a restrição.

Insert missed an field dummy_vector

O campo vetorial de placeholder não está declarado como nullable=True e a chamada insert o omite.

Declare o campo de placeholder com nullable=True ou passe um valor para ele no momento da inserção.

Limpeza

Os exemplos criam quatro collections: cs_transcribe, cs_pii_mask, cs_faq_kb e cs_qc. Essas collections e seus índices permanecem na instância, e cs_faq_kb permanece carregada, até que você as exclua.

Exclua as collections após concluir o tutorial:

for name in ("cs_transcribe", "cs_pii_mask", "cs_faq_kb", "cs_qc"):
    if client.has_collection(name):
        client.drop_collection(name)

Verifique se client.has_collection(name) retorna False para cada um dos quatro nomes antes de considerar a limpeza concluída.

Aviso

Excluir uma collection remove a collection juntamente com os dados que ela contém, incluindo as transcrições e textos mascarados produzidos pelos exemplos.

Retenção de dados e conformidade

Dados de gravação permanecem sensíveis mesmo após a conclusão do pipeline. Trate os artefatos armazenados da seguinte forma:

  • Período de retenção — Defina um período de retenção explícito para áudios brutos, transcrições, rótulos de sentimento e registros de revisão. Exclua ou anonimize-os quando o período expirar.

  • Dados derivados — Trate dados derivados em conjunto com a exclusão dos dados de source ou a retirada de consentimento.

  • Revisão de saída do modelo — Resultados de sentimento e classificação exigem uma etapa de revisão humana antes de ações de alto impacto. Para detalhes, consulte Etapa 6: Rotule sentimento e categoria do ticket. Requisitos de consentimento, aviso prévio e URLs assinadas aplicam-se antes de processar qualquer gravação. Para detalhes, consulte Pré-requisitos.

Próximos passos

Os cenários a seguir estendem este pipeline:

  • Assistência ao agente em tempo real — Conecte a recuperação e o reranking ao workspace do agente para fornecer os scripts e conhecimentos mais relevantes durante uma chamada ativa.

  • Inspeção de qualidade em lote — Combine gravações históricas com AI_BATCH para transcrição offline completa, mascaramento e rotulagem de sentimento, elevando a cobertura da inspeção de qualidade de amostragem para cobertura total. AI_BATCH está fora do escopo deste tutorial.

  • Alertas de feedback negativo — Dispare alertas e escalonamentos em tempo real para tickets cuja saída AI_SENTIMENT seja negative.