跳到正文
DEV Community· Danilo O. Pinheiro, dopme.io·· 4 小時前AI 評分44

.NET 10 + Pinecone:如何整合向量資料庫

.NET 10 + Pinecone: como integrar com um banco de dados vetorial

AI 導讀

文章示範以 .NET 10 和 C# 透過 Pinecone REST API 整合向量資料庫,建立可按語義相似度儲存、搜尋及取回資訊的應用程式。嵌入向量模型負責將文字轉換為數值表示,Pinecone 則負責儲存及搜尋向量。文章指出,Pinecone 官方 C# SDK 的程式碼庫已於 2026 年 7 月封存。

正文

🧭 1. Introdução

A integração entre .NET 10 e Pinecone permite desenvolver aplicações capazes de armazenar, pesquisar e recuperar informações por similaridade semântica.

Essa abordagem é especialmente útil em projetos de:

  • Inteligência Artificial (IA).
  • RAG (Retrieval-Augmented Generation).
  • Assistentes virtuais e chatbots.
  • Busca semântica de documentos.
  • Sistemas de recomendação.
  • Recuperação inteligente de conhecimento.
  • Aplicações baseadas em LLMs (Large Language Models).

Diferentemente dos bancos relacionais tradicionais, que normalmente consultam dados por condições e valores exatos, o Pinecone foi projetado para trabalhar com vetores (embeddings).

Esses vetores representam o significado de textos, documentos e outros conteúdos em um espaço matemático. Dessa forma, conseguimos encontrar informações relacionadas pelo contexto, mesmo quando as palavras utilizadas são diferentes.

Neste artigo, vamos implementar uma integração utilizando:

  • .NET 10 e C#.
  • Pinecone Vector Database.
  • API REST do Pinecone.
  • HttpClient e IHttpClientFactory.
  • Injeção de dependência.
  • Arquitetura organizada em camadas.
  • Operações de inserção e pesquisa vetorial.

🧠 2. O que é Pinecone?

O Pinecone é um banco de dados vetorial gerenciado, desenvolvido para armazenar e consultar embeddings em larga escala.

Seu principal objetivo é permitir pesquisas por similaridade entre vetores, oferecendo uma infraestrutura especializada para aplicações de IA.

Como funciona?

Imagine uma aplicação que armazena documentos sobre desenvolvimento de software.

Podemos cadastrar os seguintes conteúdos:

  • Documento 1: Como implementar autenticação JWT com .NET.
  • Documento 2: Como proteger endpoints de uma API.
  • Documento 3: Como configurar um banco de dados SQL Server.
  • Documento 4: Como implementar autorização baseada em políticas.

Quando um usuário pesquisa:

"Como proteger uma API desenvolvida em C#?"

Uma busca tradicional pode depender da correspondência exata das palavras.

Já uma busca vetorial pode encontrar documentos semanticamente relacionados, como o documento sobre autenticação JWT ou autorização baseada em políticas.

Principais conceitos

Conceito Descrição
Vector Representação numérica de um conteúdo
Embedding Processo de transformar conteúdo em vetor
Index Estrutura onde os vetores são armazenados
Namespace Divisão lógica dos registros
Metadata Informações adicionais associadas ao vetor
Similarity Search Pesquisa por proximidade entre vetores
Upsert Inserção ou atualização de vetores
Query Consulta por similaridade

🔄 3. Arquitetura de funcionamento

Uma integração com Pinecone normalmente envolve três etapas principais:

  1. Transformar o conteúdo em embeddings.
  2. Armazenar os embeddings no Pinecone.
  3. Gerar um embedding para a pesquisa e consultar os vetores mais próximos.

É importante entender que Pinecone e o modelo de embeddings possuem responsabilidades diferentes.

O Pinecone armazena e pesquisa vetores. O modelo de embeddings transforma textos em representações numéricas.

Fluxo de inserção

flowchart TD
    A["Aplicação .NET 10"] --> B["Documento / Texto"]
    B --> C["Modelo de Embeddings"]
    C --> D["Vetor Numérico"]
    D --> E["Pinecone API"]
    E --> F["Vector Index"]
    F --> G["Armazenamento"]

Fluxo de pesquisa

flowchart TD
    A["Usuário realiza uma pesquisa"] --> B["API .NET 10"]
    B --> C["Modelo de Embeddings"]
    C --> D["Vetor da consulta"]
    D --> E["Pinecone Query"]
    E --> F["Pesquisa por similaridade"]
    F --> G["Documentos relacionados"]
    G --> H["Resposta para a aplicação"]

Em aplicações RAG, os documentos recuperados podem ser enviados posteriormente para um LLM, que utiliza esse contexto para construir uma resposta.


🏗️ 4. Criando o projeto .NET 10

Vamos começar criando uma Web API.

dotnet new webapi -n PineconeIntegration
cd PineconeIntegration

Para este exemplo, utilizaremos a API REST diretamente por meio do HttpClient.

Essa escolha evita acoplar a aplicação a uma biblioteca específica. O repositório do SDK oficial C# foi arquivado em julho de 2026, portanto vale avaliar cuidadosamente seu estado de manutenção antes de adotá-lo em novos projetos.

Estrutura sugerida

PineconeIntegration/
│
├── Controllers/
│   └── DocumentsController.cs
│
├── Application/
│   ├── DTOs/
│   │   ├── DocumentRequest.cs
│   │   └── SearchRequest.cs
│   │
│   └── Interfaces/
│       ├── IEmbeddingService.cs
│       └── IPineconeService.cs
│
├── Domain/
│   └── Entities/
│       └── Document.cs
│
├── Infrastructure/
│   ├── Embeddings/
│   │   └── EmbeddingService.cs
│   │
│   └── Pinecone/
│       ├── PineconeOptions.cs
│       └── PineconeService.cs
│
├── appsettings.json
└── Program.cs

A separação permite manter as responsabilidades organizadas:

  • Api: exposição dos endpoints HTTP.
  • Application: contratos, DTOs e regras de aplicação.
  • Domain: entidades e conceitos do negócio.
  • Infrastructure: integração com serviços externos, embeddings e Pinecone.

🔑 5. Criando uma conta e configurando o Pinecone

Para utilizar o serviço, acesse:

https://www.pinecone.io/

Após criar sua conta:

  1. Acesse o painel de controle.
  2. Crie um projeto.
  3. Gere uma API Key.
  4. Crie um Index.
  5. Configure a dimensão dos vetores.
  6. Selecione a métrica de similaridade.
  7. Aguarde o índice ficar disponível.

Configuração do Index

Para nosso exemplo, considere:

Configuração Valor
Nome dotnet-documents
Tipo Dense Vector
Dimension 384
Metric Cosine
Deployment Serverless

Atenção: a dimensão 384 é apenas ilustrativa. Ela precisa corresponder exatamente à dimensão produzida pelo modelo de embeddings escolhido.

A métrica Cosine é uma opção comum para pesquisas semânticas, mas a escolha deve considerar as características do modelo e da aplicação.

Obtendo o Index Host

Depois de criar o índice, obtenha seu endereço de acesso (host).

O formato é semelhante a:

https://dotnet-documents-xxxxx.svc.region.pinecone.io

O endereço real será fornecido pelo Pinecone. Não utilize um endereço fictício na aplicação.

Em produção, prefira utilizar o host específico do índice nas operações de dados.


⚙️ 6. Configurando as variáveis da aplicação

Vamos utilizar o padrão Options do .NET para centralizar as configurações.

PineconeOptions.cs

namespace PineconeIntegration.Infrastructure.Pinecone;

public sealed class PineconeOptions
{
    public const string SectionName = "Pinecone";

    public string ApiKey { get; set; } = string.Empty;

    public string IndexHost { get; set; } = string.Empty;

    public string Namespace { get; set; } = "documents";

    public int Dimension { get; set; } = 384;
}

appsettings.json

{
  "Pinecone": {
    "IndexHost": "",
    "Namespace": "documents",
    "Dimension": 384
  }
}

A API Key não deve ser armazenada no arquivo de configuração versionado.

Configure-a por variável de ambiente:

Pinecone__ApiKey="SUA_API_KEY"
Pinecone__IndexHost="https://SEU_INDEX_HOST"

Durante o desenvolvimento, também é possível utilizar o Secret Manager:

dotnet user-secrets init

dotnet user-secrets set "Pinecone:ApiKey" "SUA_API_KEY"

dotnet user-secrets set "Pinecone:IndexHost" "https://SEU_INDEX_HOST"

🔌 7. Criando o contrato de integração

Vamos definir uma interface para abstrair as operações do Pinecone.

IPineconeService.cs

namespace PineconeIntegration.Application.Interfaces;

public interface IPineconeService
{
    Task UpsertAsync(
        string id,
        float[] embedding,
        string content,
        CancellationToken cancellationToken);

    Task<string> SearchAsync(
        float[] embedding,
        int topK,
        CancellationToken cancellationToken);
}

Essa abstração permite trocar a implementação futuramente sem alterar os controllers ou as regras principais da aplicação.


🧮 8. Implementando o serviço de embeddings

Antes de enviar informações para o Pinecone, precisamos transformar o texto em um vetor.

Para isso, utilizamos um modelo de embeddings.

Podemos utilizar modelos de provedores externos ou executar um modelo localmente.

Neste exemplo, vamos definir um contrato independente do fornecedor.

IEmbeddingService.cs

namespace PineconeIntegration.Application.Interfaces;

public interface IEmbeddingService
{
    Task<float[]> GenerateAsync(
        string text,
        CancellationToken cancellationToken);
}

EmbeddingService.cs

using PineconeIntegration.Application.Interfaces;

namespace PineconeIntegration.Infrastructure.Embeddings;

public sealed class EmbeddingService : IEmbeddingService
{
    public Task<float[]> GenerateAsync(
        string text,
        CancellationToken cancellationToken)
    {
        // Exemplo didático.
        // Substitua pela chamada real ao modelo de embeddings.

        throw new NotImplementedException(
            "Configure um provedor real de embeddings.");
    }
}

Não devemos utilizar vetores aleatórios ou gerados artificialmente em uma aplicação real.

O modelo precisa produzir embeddings semanticamente consistentes tanto para os documentos armazenados quanto para as consultas.

Regra fundamental: utilize o mesmo modelo e a mesma configuração de dimensão nas duas operações.


📦 9. Criando o serviço Pinecone com HttpClient

Agora vamos implementar a comunicação com a API REST.

O Pinecone oferece operações de Data Plane para armazenar e pesquisar vetores.

Utilizaremos:

  • POST /vectors/upsert
  • POST /query

As chamadas serão direcionadas ao host específico do índice.

PineconeService.cs

using System.Net.Http.Json;
using System.Text.Json;
using PineconeIntegration.Application.Interfaces;
using PineconeIntegration.Infrastructure.Pinecone;

namespace PineconeIntegration.Infrastructure.Pinecone;

public sealed class PineconeService : IPineconeService
{
    private readonly HttpClient _httpClient;
    private readonly PineconeOptions _options;

    private static readonly JsonSerializerOptions JsonOptions =
        new(JsonSerializerDefaults.Web);

    public PineconeService(
        HttpClient httpClient,
        Microsoft.Extensions.Options.IOptions<PineconeOptions> options)
    {
        _httpClient = httpClient;
        _options = options.Value;
    }

    public async Task UpsertAsync(
        string id,
        float[] embedding,
        string content,
        CancellationToken cancellationToken)
    {
        ValidateEmbedding(embedding);

        var request = new
        {
            vectors = new[]
            {
                new
                {
                    id,
                    values = embedding,
                    metadata = new
                    {
                        content
                    }
                }
            },
            @namespace = _options.Namespace
        };

        using var response = await _httpClient.PostAsJsonAsync(
            "/vectors/upsert",
            request,
            JsonOptions,
            cancellationToken);

        response.EnsureSuccessStatusCode();
    }

    public async Task<string> SearchAsync(
        float[] embedding,
        int topK,
        CancellationToken cancellationToken)
    {
        ValidateEmbedding(embedding);

        if (topK is < 1 or > 1000)
        {
            throw new ArgumentOutOfRangeException(nameof(topK));
        }

        var request = new
        {
            vector = embedding,
            topK,
            includeMetadata = true,
            includeValues = false,
            @namespace = _options.Namespace
        };

        using var response = await _httpClient.PostAsJsonAsync(
            "/query",
            request,
            JsonOptions,
            cancellationToken);

        response.EnsureSuccessStatusCode();

        return await response.Content.ReadAsStringAsync(
            cancellationToken);
    }

    private void ValidateEmbedding(float[] embedding)
    {
        ArgumentNullException.ThrowIfNull(embedding);

        if (embedding.Length != _options.Dimension)
        {
            throw new ArgumentException(
                $"Expected {_options.Dimension} dimensions, " +
                $"but received {embedding.Length}.");
        }

        if (embedding.Any(value => !float.IsFinite(value)))
        {
            throw new ArgumentException(
                "Embedding contains invalid numeric values.");
        }
    }
}

Entendendo a implementação

UpsertAsync

Envia um vetor para o Pinecone com:

  • ID único.
  • Valores numéricos do embedding.
  • Metadata com o conteúdo original.
  • Namespace de armazenamento.

Se o ID já existir, a operação atualiza o registro.

SearchAsync

Recebe o vetor de pesquisa e solicita os registros mais próximos.

O parâmetro topK determina a quantidade máxima de resultados solicitados.

A propriedade includeMetadata permite recuperar os dados associados aos vetores.


🔐 10. Configurando autenticação e injeção de dependência

Vamos registrar o HttpClient e os serviços no container de dependências do .NET.

Program.cs

using PineconeIntegration.Application.Interfaces;
using PineconeIntegration.Infrastructure.Embeddings;
using PineconeIntegration.Infrastructure.Pinecone;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services
    .AddOptions<PineconeOptions>()
    .Bind(builder.Configuration.GetSection(
        PineconeOptions.SectionName))
    .Validate(
        options => !string.IsNullOrWhiteSpace(options.ApiKey),
        "Pinecone API Key is required.")
    .Validate(
        options =>
            Uri.TryCreate(
                options.IndexHost,
                UriKind.Absolute,
                out var uri) &&
            uri.Scheme == Uri.UriSchemeHttps,
        "A valid HTTPS Index Host is required.")
    .Validate(
        options => options.Dimension > 0,
        "Dimension must be greater than zero.")
    .ValidateOnStart();

builder.Services.AddHttpClient<IPineconeService, PineconeService>(
    (serviceProvider, client) =>
    {
        var options = serviceProvider
            .GetRequiredService<
                Microsoft.Extensions.Options.IOptions<PineconeOptions>>()
            .Value;

        client.BaseAddress = new Uri(
            options.IndexHost.TrimEnd('/') + "/");

        client.Timeout = TimeSpan.FromSeconds(30);

        client.DefaultRequestHeaders.Add(
            "Api-Key",
            options.ApiKey);

        client.DefaultRequestHeaders.Add(
            "X-Pinecone-Api-Version",
            "2025-10");
    });

builder.Services.AddScoped<IEmbeddingService, EmbeddingService>();

var app = builder.Build();

app.UseHttpsRedirection();

app.MapControllers();

app.Run();

A versão da API utilizada no exemplo é 2025-10, conforme a documentação de referência consultada. Antes de publicar em produção, verifique a versão estável mais recente e a compatibilidade dos endpoints utilizados.


📥 11. Criando o endpoint para cadastrar documentos

Vamos disponibilizar um endpoint para receber textos e armazenar seus respectivos embeddings.

DocumentRequest.cs

using System.ComponentModel.DataAnnotations;

namespace PineconeIntegration.Application.DTOs;

public sealed record DocumentRequest(
    [Required]
    [StringLength(10000, MinimumLength = 3)]
    string Content);

DocumentsController.cs

using Microsoft.AspNetCore.Mvc;
using PineconeIntegration.Application.DTOs;
using PineconeIntegration.Application.Interfaces;

namespace PineconeIntegration.Controllers;

[ApiController]
[Route("api/documents")]
public sealed class DocumentsController : ControllerBase
{
    private readonly IPineconeService _pineconeService;
    private readonly IEmbeddingService _embeddingService;

    public DocumentsController(
        IPineconeService pineconeService,
        IEmbeddingService embeddingService)
    {
        _pineconeService = pineconeService;
        _embeddingService = embeddingService;
    }

    [HttpPost]
    public async Task<IActionResult> Create(
        [FromBody] DocumentRequest request,
        CancellationToken cancellationToken)
    {
        var embedding = await _embeddingService.GenerateAsync(
            request.Content,
            cancellationToken);

        var id = Guid.NewGuid().ToString();

        await _pineconeService.UpsertAsync(
            id,
            embedding,
            request.Content,
            cancellationToken);

        return Created(
            $"/api/documents/{id}",
            new
            {
                Id = id,
                Message = "Document successfully indexed."
            });
    }
}

Exemplo de requisição

POST /api/documents
Content-Type: application/json
{
  "content": "O .NET 10 permite desenvolver APIs modernas utilizando C#."
}

O processo será:

  1. Receber o documento.
  2. Gerar seu embedding.
  3. Criar um identificador.
  4. Enviar o vetor para o Pinecone.
  5. Retornar o resultado da operação.

🔎 12. Criando o endpoint de pesquisa semântica

Agora vamos implementar a pesquisa.

SearchRequest.cs

using System.ComponentModel.DataAnnotations;

namespace PineconeIntegration.Application.DTOs;

public sealed record SearchRequest(
    [Required]
    [StringLength(2000, MinimumLength = 2)]
    string Query,

    [Range(1, 20)]
    int TopK = 5);

Adicione o endpoint ao DocumentsController:

[HttpPost("search")]
public async Task<IActionResult> Search(
    [FromBody] SearchRequest request,
    CancellationToken cancellationToken)
{
    var embedding = await _embeddingService.GenerateAsync(
        request.Query,
        cancellationToken);

    var results = await _pineconeService.SearchAsync(
        embedding,
        request.TopK,
        cancellationToken);

    return Ok(new
    {
        Query = request.Query,
        Results = results
    });
}

Exemplo de requisição

POST /api/documents/search
Content-Type: application/json
{
  "query": "Como desenvolver uma API moderna com C#?",
  "topK": 5
}

O Pinecone retornará os registros mais próximos do vetor da consulta.

Cada resultado pode conter:

  • ID do vetor.
  • Score de similaridade.
  • Metadata.
  • Conteúdo original, quando armazenado na metadata.

O score representa uma medida de similaridade conforme a métrica configurada no índice. Não deve ser interpretado automaticamente como uma porcentagem de confiança.


🤖 13. Integrando Pinecone com RAG

Uma das aplicações mais interessantes do Pinecone é a construção de soluções RAG.

RAG significa Retrieval-Augmented Generation e combina recuperação de informações com geração de respostas por modelos de linguagem.

Como funciona?

Imagine um chatbot corporativo que responde perguntas sobre documentos internos.

O fluxo pode ser:

  1. O usuário envia uma pergunta.
  2. A aplicação gera o embedding da pergunta.
  3. O Pinecone pesquisa os documentos mais relevantes.
  4. A aplicação recupera os conteúdos encontrados.
  5. Os conteúdos são adicionados ao contexto do LLM.
  6. O modelo gera uma resposta fundamentada nas informações recuperadas.

Arquitetura RAG

flowchart TD
    A["Usuário"] --> B["API .NET 10"]
    B --> C["Embedding da pergunta"]
    C --> D["Pinecone Vector Search"]
    D --> E["Documentos relevantes"]
    E --> F["Construção do contexto"]
    F --> G["LLM"]
    G --> H["Resposta contextualizada"]
    H --> A

Essa arquitetura pode ser aplicada em:

  • Chatbots de atendimento.
  • Assistentes de documentação técnica.
  • Sistemas de suporte interno.
  • Pesquisa de contratos.
  • Consulta de manuais.
  • Bases de conhecimento corporativas.

Importante: a recuperação de documentos não garante, por si só, que o LLM responderá corretamente. É necessário controlar o contexto, as permissões, as fontes e a qualidade das respostas.


🧱 14. Boas práticas de arquitetura e desenvolvimento

Para utilizar Pinecone em aplicações corporativas, recomendo observar alguns pontos.

🔐 Segurança

  • Nunca exponha a API Key no frontend.
  • Utilize variáveis de ambiente ou um cofre de segredos.
  • Restrinja as permissões das credenciais.
  • Utilize HTTPS.
  • Implemente autenticação e autorização na API .NET.
  • Separe os namespaces conforme as necessidades de isolamento.
  • Não permita que o usuário escolha livremente namespaces sem validação.

⚡ Performance

  • Utilize IHttpClientFactory.
  • Configure timeout adequado.
  • Avalie operações em lote para grandes volumes.
  • Evite gerar embeddings repetidos para conteúdos que não mudaram.
  • Utilize IDs estáveis quando precisar atualizar documentos.
  • Recupere apenas os metadados necessários.
  • Avalie o custo e a latência das consultas.

📊 Observabilidade

Monitore:

  • Tempo de geração dos embeddings.
  • Tempo de resposta do Pinecone.
  • Quantidade de vetores cadastrados.
  • Quantidade de resultados recuperados.
  • Erros de autenticação e comunicação.
  • Consumo e custos associados às operações.

Evite registrar API Keys, documentos confidenciais ou informações pessoais nos logs.

🧩 Consistência dos embeddings

O modelo de embeddings utilizado na indexação deve ser compatível com o modelo utilizado na consulta.

Se trocar o modelo, a dimensão ou a estratégia de geração, considere criar um novo índice e reprocessar os documentos.


🧪 15. Estratégia de testes

Uma integração bem estruturada também precisa de testes.

Sugestões:

Cenário Resultado esperado
Inserção de documento Vetor armazenado
Atualização de documento Registro atualizado
Consulta válida Resultados encontrados
Vetor com dimensão incorreta Validação rejeitada
API Key inválida Erro de autenticação
Index Host incorreto Falha de comunicação
Pinecone indisponível Tratamento de falha
Consulta sem resultados Lista vazia
Namespace incorreto Isolamento respeitado

Uma boa estratégia é utilizar mocks para testar os serviços de aplicação e testes de integração controlados para validar a comunicação real com o Pinecone.


🏛️ 16. Evoluindo a solução

Depois de implementar as operações básicas, podemos evoluir a arquitetura para suportar cenários mais complexos.

Funcionalidades futuras

  • Upload e processamento de PDF.
  • Divisão de documentos em chunks.
  • Pipeline assíncrono com RabbitMQ.
  • Processamento em lote.
  • Integração com Azure OpenAI ou outros provedores de LLM.
  • Busca híbrida: semântica e textual.
  • Filtros por metadata.
  • Controle de acesso por usuário e organização.
  • Versionamento de documentos.
  • Estratégias de reindexação.
  • Monitoramento com OpenTelemetry.

⚠️ 17. Pontos importantes antes de utilizar em produção

O exemplo apresentado demonstra o fluxo essencial, mas uma aplicação real precisa de algumas evoluções:

  1. Implementar um provedor real de embeddings.
  2. Criar uma estratégia de chunking para documentos extensos.
  3. Implementar políticas de retry e tratamento de erros transitórios, respeitando os limites do serviço.
  4. Definir limites de tamanho e quantidade de registros por requisição.
  5. Criar uma estratégia de atualização e exclusão de documentos.
  6. Garantir isolamento entre clientes e organizações.
  7. Validar a compatibilidade entre dimensão e modelo de embeddings.
  8. Implementar monitoramento de custos e latência.
  9. Tratar indisponibilidade do serviço externo.
  10. Avaliar a retenção e proteção dos dados armazenados.

Outro detalhe importante: Pinecone não substitui necessariamente um banco relacional. Uma arquitetura pode utilizar SQL Server ou PostgreSQL para os dados transacionais e Pinecone para a recuperação semântica.


🎯 18. Conclusão

A integração entre .NET 10 e Pinecone abre possibilidades interessantes para aplicações que precisam trabalhar com Inteligência Artificial, recuperação de conhecimento e pesquisas semânticas.

Com uma arquitetura organizada, utilizando interfaces, injeção de dependência e HttpClient, conseguimos manter a integração desacoplada e preparada para evoluções futuras.

O Pinecone pode atuar como uma camada especializada de recuperação vetorial, enquanto o .NET 10 permanece responsável pelas regras de negócio, exposição das APIs, segurança e orquestração dos serviços.

Mais do que integrar um banco de dados, estamos construindo uma base para aplicações inteligentes, capazes de recuperar informações pelo significado e não apenas pela correspondência exata de palavras.

O futuro das aplicações modernas não está somente em armazenar dados, mas também em conseguir interpretar, relacionar e recuperar conhecimento.


📚 Referências e documentação


🤝 Conecte-se Comigo

💼 LinkedIn

💻 Dev.to

✍️ Medium

📰 Substack

🚀 Compartilhando conhecimento sobre .NET, arquitetura de software, cloud computing, DevOps e Inteligência Artificial.

來源:DEV Community · dev.to