Neste tutorial, você cria um assistente de solução de problemas de desenvolvimento em uma base de conhecimento do Alibaba Cloud Milvus que armazena referências de API, notas de arquitetura, Runbooks e postmortems. Pesquise por nome de API ou código de erro para obter etapas ordenadas de solução de problemas com citações das fontes.
Visão geral da solução
Este tutorial tem como base o Build an intelligent customer service Q&A application with the Alibaba Cloud Milvus knowledge base. O pipeline de ponta a ponta é idêntico: defina tags no console → importe arquivos de source em lote por tag → publique uma versão → recupere dados pelo SDK (com filtragem opcional de tags) → faça o LLM gerar respostas com citações de source → disponibilize a página de perguntas e respostas com Flask. Reutilize o código de engenharia (kb_client.py, upload.py, app.py, templates/index.html e start.sh) desse tutorial, com duas modificações descritas em Step 4: Adapt the reused code. Este documento aborda apenas as alterações necessárias para o cenário de solução de problemas de desenvolvimento: tags de módulo, parâmetros de recuperação para termos exatos, tratamento de arquivos de source HTML e restrições sobre comandos de alto risco e resultados vazios.
Para a primeira versão, selecione apenas de 5 a 20 arquivos de source de um único sistema e expanda o escopo após a validação. Todo o processo leva cerca de 20 a 30 minutos. O tempo de análise dos documentos depende da quantidade e do tamanho dos arquivos de source.
Pré-requisitos
Crie uma base de conhecimento do Milvus e registre o ID da base de conhecimento (por exemplo,
kd-803ae9b10cc31).Crie um usuário RAM com sua conta Alibaba Cloud, selecione Use Permanent AccessKey Access e anexe a política de sistema
AliyunMilvusFullAccess.Prepare um endpoint de LLM compatível com o protocolo OpenAI
chat/completionse sua chave de API.Instale o Python 3.8 ou posterior localmente.
Configure o diretório do projeto e os cinco arquivos reutilizados (
kb_client.py,upload.py,app.py,templates/index.htmlestart.sh) seguindo o tutorial referenciado em Solution overview.Garanta que os arquivos de source não contenham segredos, tokens ou contas de rede interna.
Etapa 1: Definir tags de módulo
Em Basic Information, na página de detalhes da base de conhecimento, escolha Tags > Gerencie e adicione as três tags a seguir. Defina o tipo de campo de cada tag como string. A lista suspensa de tipos fica no lado direito da caixa de entrada do nome da tag. As opções são string, int64, list, float32 e bool.
|
Nome da tag |
Descrição |
Valor de exemplo |
|
module |
Sistema ou service ao qual o arquivo de source pertence |
order-service, user-service |
|
docType |
Tipo de documento |
API, Runbook, ErrorCode, Postmortem |
|
version |
Versão da API ou do documento |
v2 |
A tag version tem o mesmo nome do parâmetro version, que representa a versão publicada da base de conhecimento em uma solicitação de recuperação. No entanto, um não afeta o outro: a tag é usada em tagFilter.field, enquanto a versão de recuperação fica no nível superior da solicitação. Se houver preocupação com confusão, nomeie a tag como apiVersion.
A caixa de diálogo de gerenciamento de tags é salva como um todo. Após abrir a caixa de diálogo, a lista de tags carrega de forma assíncrona. Aguarde até que todas as tags existentes sejam exibidas antes de adicionar novas tags e clique em OK. Caso contrário, o salvamento pode sobrescrever tudo com "lista vazia + novas tags" e apagar as definições de tags existentes.
A tag module é a decisão de design mais crítica neste cenário. Sistemas diferentes costumam ter caminhos de API com exatamente o mesmo nome. Sem a filtragem por módulo, eles interferem uns nos outros. Em nosso teste, tanto user-service quanto order-service possuíam o mesmo caminho GET /api/v2/orders/{orderId}. Ao pesquisarmos esse caminho:
|
Condição de recuperação |
Resultado |
|
Sem filtro |
O arquivo de source do user-service ficou em primeiro lugar (relevância 0,246) — o módulo errado foi atingido |
|
|
O arquivo de source do order-service ficou em primeiro lugar, e o arquivo de source do user-service foi completamente excluído |
Etapa 2: Preparar arquivos de source e o manifesto de importação
Nesta etapa, colete os arquivos de source, anote cada um deles no manifesto de importação e configure como a base de conhecimento os fragmentará.
Coloque os arquivos de source no diretório
documents/. Os formatos Markdown, HTML, PDF, DOCX e TXT são suportados. Mantenha códigos de erro completos, caminhos de API e números de versão nos arquivos de source. Em nossos testes, tanto códigos de erro quanto caminhos completos foram recuperados com exatidão.-
Crie o arquivo
documents.jsonle anote cada arquivo de source com seu módulo, tipo de documento e versão:{"path": "documents/order-api.md", "metadata": {"module": "order-service", "docType": "API", "version": "v2"}} {"path": "documents/order-timeout-runbook.md", "metadata": {"module": "order-service", "docType": "Runbook", "version": "v2"}} {"path": "documents/order-error-codes.html", "metadata": {"module": "order-service", "docType": "ErrorCode", "version": "v2"}} {"path": "documents/order-postmortem-2026-06.md", "metadata": {"module": "order-service", "docType": "Postmortem", "version": "v2"}} Importe a referência de API, o Runbook, a tabela de códigos de erro e o postmortem como um conjunto. Distinga-os com
docType. Em nosso teste, ao perguntarmos sobre um código de erro, todos os quatro tipos de arquivos de source foram atingidos simultaneamente. O modelo produziu uma resposta completa no formato "significado → etapas de solução de problemas → casos históricos".
Tratar arquivos de source HTML
Arquivos HTML podem ser carregados e analisados diretamente. O conteúdo dentro de <table> entra no índice. Em nosso teste, a consulta por ORD-42901, que existe apenas em uma tabela de códigos de erro em HTML, atingiu exatamente esse arquivo.
No entanto, o HTML tem uma granularidade de fragmentação mais grossa que o Markdown. Neste teste, um arquivo HTML contendo duas tabelas foi analisado em 1 fragmento. Isso está relacionado à forma como o HTML é processado. Antes de escolher um formato de arquivo de source, entenda dois pontos:
Blocos de código não preservam seu layout original. As tags
<pre>e<code>são reconhecidas como blocos, mas o texto é extraído recursivamente e unido com espaços. Indentação, quebras de linha e delimitadores de código são perdidos. Para arquivos de source com grandes quantidades de código ou comandos, converta-os para Markdown antes da importação. Caso contrário, os trechos de código recuperados podem não ser utilizáveis diretamente.Tabelas são indexadas como um todo. Uma
<table>é anexada como um único segmento independente em sua forma de string HTML original e não é dividida por linha. Portanto, tabelas grandes formam facilmente fragmentos grossos. Conclusão: textos descritivos simples e tabelas pequenas podem usar HTML diretamente. Quando precisar de fidelidade exata, recuperação por segmento de código ou divisão de tabelas grandes, prefira Markdown ou arquivos de source estruturados. Após a importação, verifique amostras dos fragmentos na página Data Management.
Configure a política de fragmentação
Etapas de solução de problemas e exemplos de código não devem ser cortados no meio. Na página de detalhes da base de conhecimento, clique em Create Policy em Processing Policy para ajustar a granularidade dos fragmentos. A unidade do comprimento máximo do segmento é caracteres (padrão 512). Defina entre 580 e 770 caracteres (cerca de 384 a 512 tokens) para que "uma etapa completa de solução de problemas" ou "um exemplo de código" caiba no mesmo fragmento. A proporção de caracteres para tokens varia conforme o idioma e o tokenizador dos seus arquivos de source. Use o valor em caracteres como base e verifique amostras dos fragmentos após a importação.
Etapa 3: Configure parâmetros de recuperação e o prompt
Consultas de solução de problemas de desenvolvimento consistem principalmente em códigos de erro, caminhos de API e comandos. Elas pertencem à categoria de correspondência exata de termos. Portanto, os parâmetros diferem muito de outros cenários.
{
"retrieval": {
"page_size": 6,
"candidate_count": 64,
"min_score": 0.05,
"semantic_weight": 0.15,
"enable_query_expansion": false,
"rerank_model_name": "",
"tag_filter": {
"relation": "and",
"conditions": [
{"field": "module", "op": "=", "value": "order-service"}
]
}
},
"scenario": {
"title": "Development docs and troubleshooting assistant",
"system_prompt": "You are a development documentation assistant. Answer only based on the retrieved materials. Preserve API names, error codes, commands, and code as-is. List troubleshooting steps in order and cite them as [Source N]. When write operations or high-risk commands are involved, explicitly state the risk level and require manual confirmation. When the retrieved materials are empty, reply only that the materials are insufficient and warn against performing any change operation.",
"image_enabled": false,
"sample_questions": [
"What are the required parameters for the order query API?",
"In what order should I troubleshoot a connection timeout?",
"In which documents does this error code appear?"
]
}
}
A lista a seguir descreve os parâmetros ajustados para este cenário. Os campos restantes no exemplo são mantidos do tutorial referenciado.
semantic_weight: 0.15: Fator de ponderação da pontuação final. A pontuação é calculada comoscore ≈ (1-semantic_weight) × keywordScore + semantic_weight × semanticScore(um recurso de classificação também pode ser adicionado). Omin_scorerealiza a pós-filtragem nessa pontuação final. Este cenário usa 0,15 para que a pontuação de palavras-chave de termos exatos, como códigos de erro e caminhos de API, domine a classificação. Comosemantic_weightinterage commin_score, ajuste os dois juntos conforme descrito em Tune semantic_weight and min_score together.enable_query_expansion: false: A expansão de consulta fica desativada para evitar a reescrita de códigos de erro e nomes de API. Em nosso teste, após ativá-la, okeywordScoreda consultaORD-50021caiu de 0,283 para 0,226. Além disso, o número de resultados recuperados para a consultaORD-42901caiu de 2 para 1. Mantenha essa opção desativada para recuperação de termos exatos.tag_filterfixa omodule: Isso impede que APIs com nomes idênticos de sistemas diferentes interfiram umas nas outras. Consulte Step 1: Define module tags para ver o efeito. Se um único ponto de entrada precisar abranger vários módulos, use{"field": "module", "op": "in", "value": ["order-service", "user-service"]}.Um
rerank_model_nameem branco significa que a reclassificação não está ativada. A correspondência exata para códigos de erro depende da pontuação de palavras-chave, então a reclassificação traz benefícios limitados.opsuporta apenas=,inenot in: Em nossos testes,≠,>,≥,<,≤,empty,not empty,start witheend withsão ignorados silenciosamente e todos os dados são retornados (nenhum erro é relatado).containsenot containssão meramente aliases dein/not in, não representando contenção de string; passar fragmentos de string retorna 0 resultados. Aliases comoeq,==elikeretornam400 Unsupported tag filter operator, e todas as perguntas falham. Após configurar as condições de filtro, compare o número total de resultados com os resultados não filtrados para confirmar que o filtro realmente entrou em vigor.
Ajustar semantic_weight e min_score conjuntamente
O parâmetro semantic_weight altera apenas a pontuação ponderada final; ele não modifica as duas subpontuações (keywordScore e semanticScore) em scoreDetails dos resultados de recuperação. A pontuação ponderada é aproximadamente:
score ≈ semantic_weight × semanticScore + (1 - semantic_weight) × keywordScore
Observe dois pontos antes de ajustar:
Reduzir
semantic_weightnão necessariamente diminui a pontuação total. A pontuação total cai apenas quando osemanticScoredo lote de resultados é maior que seukeywordScore. É por isso que este documento também reduzmin_scorepara 0,05. Calibre os dois parâmetros juntos verificandoscoreDetails.Ao definir
semantic_weightcomo 0, a implementação atual deixa de aplicar o limiarmin_score. Não use 0 para significar "recuperação pura por palavras-chave". Para materiais de desenvolvimento, okeywordScoregeralmente é muito menor que osemanticScore(em nossos testes, cerca de 0,16–0,28 e 0,64–0,78, respectivamente). Portanto, sob essa distribuição, reduzirsemantic_weightpuxa a pontuação total para baixo. Isso não é uma regra universal: somente quando osemanticScoredo lote de resultados é maior que seukeywordScoreé que reduzir o peso empurra a pontuação total para baixo; caso contrário, ela aumenta. Semin_scorenão for reduzido ao mesmo tempo, até mesmo resultados semanticamente relevantes serão filtrados:
|
Consulta |
semantic_weight=0.15 |
semantic_weight=0.7 |
|
|
8 resultados, pontuação máxima 0,269 |
8 resultados, pontuação máxima 0,602 |
|
|
4 resultados |
8 resultados |
|
Consulta em linguagem natural "Por que a API de pedidos desacelerou significativamente recentemente?" |
3 resultados |
8 resultados |
Ao usar semantic_weight=0.15, defina min_score para cerca de 0,05 (o exemplo acima está configurado dessa forma). Se você deixar min_score em 0,15 ou mais, a recuperação de consultas baseadas em comandos e em linguagem natural cai pela metade. Durante o ajuste, fixe um parâmetro e observe as duas subpontuações por meio de scoreDetails antes de decidir.
Restringir comandos de alto risco e resultados vazios
O assistente de solução de problemas gera comandos executáveis diretamente. Portanto, restrinja dois aspectos.
Comandos de alto risco. Marque o nível de risco e os requisitos de confirmação em uma tabela dentro dos arquivos de source. O modelo os transmitirá fielmente. Em nosso teste, um comando de reinicialização marcado como "alto risco, confirmação por duas pessoas necessária" no Runbook, quando consultado, foi retornado pelo modelo junto com as declarações explícitas "o nível de risco é alto", "confirmação por duas pessoas é necessária" e "não pule a solução de problemas e execute a reinicialização diretamente".
Resultados de recuperação vazios. O código deve interceptar esse caso, conforme descrito em Step 4: Adapt the reused code.
Etapa 4: Adaptar o código reutilizado
Adapte o projeto reutilizado com duas modificações antes de validar o assistente: um fallback para resultado vazio em app.py e uma correção de código de retorno em kb_client.py.
**Interceptar resultados de recuperação vazios em app.py**
No arquivo app.py reutilizado, a função llm_answer() ainda chama o LLM quando os resultados da recuperação estão vazios. Nesse ponto, o contexto é uma string vazia, e o modelo responde inteiramente com base em seu próprio conhecimento. Em nosso teste, ao perguntarmos "Como me recupero de um split-brain em cluster Redis?", algo que a base de conhecimento não cobre, o modelo gerou um plano operacional completo com parâmetros e não deu nenhum aviso. Em cenários de solução de problemas, os usuários podem copiar os comandos e executá-los diretamente no ambiente de produção. Portanto, intercepte isso no nível do código:
def llm_answer(question: str, results: list[dict[str, Any]]) -> str:
if not LLM.get("enabled", True):
return "The LLM is not enabled. Review the retrieval results below."
if not results:
return "No relevant document was retrieved from the knowledge base, so no troubleshooting steps can be provided. Do not perform any change operation based on this. We recommend that you contact the module owner."
...
Restringir isso apenas no prompt não é confiável. Mesmo que você declare "avise explicitamente quando os materiais não cobrirem a pergunta", o modelo ainda assim responderá.
O último critério de aceitação em Step 5: Upload, publish, and verify abrange esta modificação.
**Corrigir a verificação de código de retorno em kb_client.py**
No arquivo kb_client.py reutilizado, a função _check() avalia getattr(body, "code", 0) != 0. O code de uma resposta bem-sucedida é None, então recuperações bem-sucedidas são relatadas como "Failed: None". Altere a verificação para getattr(body, "code", None) not in (None, 0, "0"). Após a correção, recuperações bem-sucedidas não relatarão mais "Failed: None".
Etapa 5: Carregar, publicar e verificar
-
Carregue os arquivos de source. O campo
MetaFieldsaplica-se a todo o lote; o script primeiro agrupa por tag e depois envia em lotes.python upload.py --manifest documents.jsonl Na página Data Management do console, confirme se o status do arquivo de source é Completed. Uma resposta bem-sucedida da API de upload significa apenas que a análise assíncrona foi enviada.
-
Na página Version Management, clique em Publish Version. Após concluir o assistente, confirme se o status da nova versão é Published.
ImportantePodem existir no máximo 3 versões publicadas ao mesmo tempo. Depois que o limite é atingido, o botão Publish Version fica acinzentado, mas a página ainda mostra "There are currently N pending changes to publish". Exclua versões antigas desnecessárias nos registros de versão primeiro. Documentos de desenvolvimento mudam frequentemente (cada lançamento pode atualizar a referência de API e os Runbooks). Portanto, mantenha apenas "a versão atual + a versão histórica mais recente".
-
Inicie o service:
python app.py -
Envie uma pergunta de teste:
curl -sS http://127.0.0.1:7860/api/ask \ -H 'Content-Type: application/json' \ -d '{"question":"In what order should I troubleshoot a connection timeout?"}' -
Valide com base nos cinco critérios de aceitação a seguir:
A página abre normalmente e, após enviar uma pergunta, a resposta e as fontes de recuperação são retornadas juntas.
Cada
[Source N]na resposta pode ser associado ao fragmento correspondente do arquivo de source na seção de fontes abaixo.Ao perguntar com um código de erro completo, todos os arquivos de source nos quais o código de erro aparece são listados. Em nosso teste, a consulta por
ORD-50021listou corretamente quatro arquivos de source — a tabela de códigos de erro, a referência de API, o Runbook e o postmortem — juntamente com a localização em cada um.Ao perguntar sobre uma operação arriscada, a resposta inclui o nível de risco e o requisito de confirmação manual.
Ao perguntar sobre algo que a base de conhecimento não cobre, a resposta é "materials are insufficient" com um aviso para não realizar nenhuma operação de alteração, e nenhum comando fabricado aparece. Teste este caso na prática.
Observações para cenários de desenvolvimento
Ponto de entrada por módulo — Depois que
tag_filterfixa omodule, o ponto de entrada responde apenas a perguntas sobre aquele módulo. Mantenha as perguntas de exemplo dentro do mesmo módulo também.Termos exatos completos — Mantenha códigos de erro, caminhos de API, comandos e números de versão completos nos arquivos de source. Esses termos exatos são os principais pontos de entrada de recuperação neste cenário.
Marcação de risco — Marque níveis de risco e requisitos de confirmação nos arquivos de source. Não dependa do modelo para julgar quais comandos são perigosos.
Sem segredos nos uploads — Não carregue segredos, tokens, contas de rede interna ou strings de conexão de banco de dados de produção. O conteúdo dos fragmentos é enviado ao LLM como contexto.
Descrições de API obsoletas — Mantenha as descrições de operações de API obsoletas. Em nosso teste, depois que a referência de API declarou "a API v1 está obsoleta e seus nomes de parâmetros são incompatíveis", o modelo não misturou parâmetros antigos ao responder perguntas sobre os parâmetros da nova API.
O campo
tagsnos resultados de recuperação não reflete o módulo e a versão gravados. É um campo diferente dos MetaFields deAddDocuments, e nenhum parâmetro pode ativar seu retorno. Um valor vazio não significa que o upload falhou. Quando precisar anotar módulo e versão na resposta, una os resultados de recuperação comdocuments.jsonlpordocumentIde anexe as informações ao contexto.Implantação em produção — Não continue usando o servidor de desenvolvimento Flask para implantação online. Mude para um servidor WSGI de produção e adicione gerenciamento de segredos, autenticação, auditoria e limitação de taxa.
Perguntas frequentes
|
Sintoma |
Causa e solução |
|
A pergunta retorna 500, e o log mostra |
O operador |
|
Um filtro de tag foi adicionado, mas o número de resultados é exatamente o mesmo sem filtragem |
Um operador ineficaz (como |
|
APIs com nomes idênticos de outros sistemas são recuperadas |
O filtro |
|
Muito menos resultados de recuperação do que o esperado |
Quando |
|
A recuperação de código de erro é imprecisa |
Confirme se |
|
A filtragem de tags sempre retorna 0 resultados |
A ortografia do nome da tag não corresponde àquela escrita no momento do upload (a API não relata erro e simplesmente retorna 0 resultados). Verifique na página de detalhes da base de conhecimento escolhendo Tags > Gerencie. |
|
Uma pergunta não coberta pela base de conhecimento retornou comandos aparentemente executáveis |
A função |
|
Detalhes em arquivos de source HTML não podem ser recuperados |
O HTML tem uma granularidade de fragmentação mais grossa. Reduza o comprimento máximo do segmento. Converta HTML com grandes quantidades de código para Markdown primeiro. |
|
A recuperação relata "Failed: None" |
A verificação em |
|
A recuperação retorna |
Nenhuma versão foi publicada ainda, ou o número de versão codificado na configuração foi excluído. Use |
Próximas etapas
Expanda a cobertura módulo por módulo: crie um ponto de entrada independente para cada valor de
modulee mantenha as perguntas de exemplo dentro do mesmo módulo.Prepare-se para a produção: mude do servidor de desenvolvimento Flask para um servidor WSGI de produção e adicione gerenciamento de segredos, autenticação, auditoria e limitação de taxa.
Gerencie versões conforme seus documentos mudam: publique novas versões quando as referências de API ou Runbooks forem atualizados e mantenha apenas a versão atual mais a versão histórica mais recente.