Back to "Obtención y análisis de contenido web con LLMs en C#"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

Agents AI C# LLM Systems Design

Obtención y análisis de contenido web con LLMs en C#

Friday, 19 December 2025

Nota: Este artículo enseña los fundamentos de la extracción de contenido web con LLMs utilizando el enfoque más simple posible. Para casos de uso de producción (resumación web, análisis de documentos, herramientas de agente), ver DocSummarizer Liberación de GitHub - implementa la arquitectura mostrada aquí pero de grado de producción: incrustaciones BERT, búsqueda híbrida, Playwright para SPAs, protección SSRF, citas validadas y seguimiento de cobertura adecuado.

Cuando le pides a ChatGPT que "lea este artículo y lo resuma", ¿qué sucede realmente? Si te imaginas la IA abriendo un navegador y leyendo como lo harías - así no es como funciona.

Los LLMs no navegan por la web. Razonan sobre fragmentos que selecciona tu código.

Este artículo te muestra cómo construir esto en C# con Ollama - sin marcos, sólo código práctico que se puede depurar.

TL;DR: Tu código se obtiene → limpia → trozos → selecciona. El LLM solo ve los fragmentos que inyectas. La selección es perdida por diseño - eso es tanto la restricción como la arquitectura.

La única cosa que hace que esto funcione

Aquí está la visión que cambia cómo se construyen estos sistemas:

La selección es la decisión del producto. También es donde se originan la mayoría de las fallas.

El LLM es aguas abajo de su selección. No puede recuperar la información que no lo mostró. Cuando un agente "no encuentra la respuesta", el problema es casi nunca el modelo - es que su lógica de selección eligió los trozos equivocados. (Este es el mismo principio detrás de por qué evito marcos como LangChain - abstraen la lógica de selección que necesitas para depurar.)

Esta es la razón por la "navegación del agente" falla silenciosamente. El agente responde con confianza basado en lo que vio. Nunca se sabe que se perdió el contenido correcto.

flowchart LR
    URL[Full Page] --> Select[Your Selection]
    Select --> LLM[LLM Sees This]
    LLM --> Answer[Answer]
    
    URL -.->|"50KB"| Select
    Select -.->|"2KB"| LLM
    
    Miss[Missed Content] -.->|"Never seen"| X[❌]
    
    style Select stroke:#e74c3c,stroke-width:3px
    style Miss stroke:#95a5a6,stroke-width:2px,stroke-dasharray: 5 5

El modelo sólo puede razonar sobre lo que le diste. Construir en consecuencia.


Objetivos de diseño antes del código

Antes de escribir nada, estas son las limitaciones:

Limitación # # Por qué importa

|------------|----------------| | Sin renderizado JS HTML estático solamente (Playwright para SPAs) | Bonos consolidados Presupuesto duro por petición (2-4K tokens tipica) | Respuestas basadas en fuentes LLM no debe alucinar el conocimiento web | Selección determinista La misma entrada → los mismos trozos (debugable) | Observable Registra lo que seleccionaste, por qué, y lo que descartaste

Si no puedes explicar por qué se seleccionó un trozo, no puedes depurar fallos.


El oleoducto

flowchart TB
    URL[URL] --> Fetch[1. Fetch]
    Fetch --> Clean[2. Clean]
    Clean --> Chunk[3. Chunk]
    Chunk --> Select[4. Select]
    Select --> LLM[LLM]
    LLM --> Answer[Answer]
    
    Fetch -.->|"57KB HTML"| Clean
    Clean -.->|"6KB text"| Chunk
    Chunk -.->|"5 chunks"| Select
    Select -.->|"2 chunks"| LLM
    
    style Select stroke:#e74c3c,stroke-width:3px
    style Clean stroke:#f39c12,stroke-width:3px

Cada paso reduce los datos. Para cuando el LLM lo ve, has pasado de 57KB de HTML a tal vez 2KB de texto relevante. Cada reducción es con pérdidas. Cada reducción puede descartar la respuesta. Este es el mismo patrón que utilizo para análisis de archivos CSV de gran tamaño - las razones LLM, su código computa y selecciona.


Ejemplo de ejecución

A lo largo de este artículo, usaremos una URL:

https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-10/overview

Y tres cuestiones de cada vez mayor especificidad:

  1. "¿Qué hay de nuevo en .NET 10?"
  2. "¿Qué mejoras de rendimiento se mencionan?"
  3. "¿Hay algo sobre AOT?"

Esto mantiene los ejemplos fundamentados y muestra cómo la selección importa más a medida que las preguntas se hacen específicas.


Configuración

# Install Ollama from https://ollama.ai
ollama pull llama3.2:3b

# NuGet packages
dotnet add package AngleSharp      # HTML parsing
dotnet add package OllamaSharp     # Ollama client (5.1.x)

Nota de OllamaSharp: En la versión 5.x, GenerateAsync devuelve IAsyncEnumerable<GenerateResponseStream?> - fluye fichas a medida que se generan. await foreach. Los pines del proyecto de la muestra 5.1.5.


Paso 1: Obtener

HTTP estándar, pero con los detalles que importan:

public class WebFetcher : IDisposable
{
    private readonly HttpClient _http;
    
    public WebFetcher()
    {
        var handler = new HttpClientHandler
        {
            AllowAutoRedirect = true,
            MaxAutomaticRedirections = 5,  // Cap redirects
            AutomaticDecompression = DecompressionMethods.All
        };
        
        _http = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) };
        _http.DefaultRequestHeaders.Add("User-Agent", 
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
    }
    
    public async Task<string> FetchAsync(string url)
    {
        var response = await _http.GetAsync(url);
        
        // Bail if not HTML
        var contentType = response.Content.Headers.ContentType?.MediaType ?? "";
        if (!contentType.Contains("html") && !contentType.Contains("text"))
            throw new InvalidOperationException($"Not HTML: {contentType}");
        
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    }
    
    public void Dispose() => _http.Dispose();
}

¿Qué maneja esto?: Redireccionamientos (encapsulados), compresión, timeouts, User-Agent, validación del tipo de contenido.

Lo que no hace: Renderización de JavaScript, autenticación, limitación de tarifas, robots.txt. Para la producción, añada retrasos por host y respete las políticas de rastreo.


Paso 2: Limpiar

El HTML crudo es mayormente ruido. Límpialo o desperdicia tokens en el diseño.

Una página típica:

57KB HTML → 6KB useful text (90% reduction)

La Estrategia

flowchart LR
    HTML[Raw HTML] --> Remove[Remove Noise]
    Remove --> Find[Find Main Content]
    Find --> Extract[Extract Text + Headings]
    Extract --> Normalize[Normalize]
    
    Remove -.->|"script, style, nav, ads"| Find
    Find -.->|"main, article, .content"| Extract
    
    style Remove stroke:#e74c3c,stroke-width:3px
    style Find stroke:#f39c12,stroke-width:3px

Limpieza es la pérdida - manejar con cuidado

Su limpiador puede eliminar absolutamente la respuesta. Errores comunes:

  • Los selectores agresivos eliminan el contenido legítimo
  • El contenido principal tiene nombres de clases inusuales (.article-body, .post-text)
  • La respuesta está en una barra lateral o caja de llamada

Mitigaciones:

  • Mantener los encabezamientos explícitamente (h1-h6)
  • Eliminar por función y Placa de caldera conocida, no patrones amplios
  • Registro: ruta del elemento extraído + longitud del texto
  • Retroceso: si el contenido principal es sospechosamente corto, pruebe cuerpo
public class HtmlCleaner
{
    private readonly HtmlParser _parser = new();
    
    // Known noise - remove these entirely
    private static readonly string[] NoiseElements = 
        { "script", "style", "nav", "footer", "aside", "iframe", "noscript" };
    
    // Boilerplate patterns - remove by role, not aggressive wildcards
    private static readonly string[] NoiseSelectors = 
    {
        "[role='navigation']", "[role='banner']", "[role='complementary']",
        "[class*='cookie']", "[class*='newsletter']", "[aria-hidden='true']"
    };
    
    // Where to find content - order matters (most specific first)
    private static readonly string[] ContentSelectors = 
        { "main", "article", "[role='main']", ".content", ".post-content" };
    
    public CleanResult Clean(string html)
    {
        var doc = _parser.ParseDocument(html);
        
        // Remove noise
        foreach (var tag in NoiseElements)
            foreach (var el in doc.QuerySelectorAll(tag).ToList())
                el.Remove();
        
        foreach (var selector in NoiseSelectors)
            foreach (var el in doc.QuerySelectorAll(selector).ToList())
                el.Remove();
        
        // Find main content
        IElement? main = null;
        string? matchedSelector = null;
        
        foreach (var selector in ContentSelectors)
        {
            main = doc.QuerySelector(selector);
            if (main != null) { matchedSelector = selector; break; }
        }
        
        // Fallback to body if main content is suspiciously short
        var text = main?.TextContent ?? "";
        if (text.Length < 500 && doc.Body != null)
        {
            main = doc.Body;
            matchedSelector = "body (fallback)";
            text = main.TextContent;
        }
        
        return new CleanResult
        {
            Text = NormalizeWhitespace(text),
            MatchedSelector = matchedSelector ?? "none",
            OriginalLength = html.Length
        };
    }
    
    private string NormalizeWhitespace(string text)
    {
        text = Regex.Replace(text, @"[ \t]+", " ");
        text = Regex.Replace(text, @"\n\s*\n+", "\n\n");
        return text.Trim();
    }
}

public record CleanResult(string Text, string MatchedSelector, int OriginalLength);

Extracción de legibilidad (puntuando párrafos, densidad de texto) es un agujero de conejo entero. Por ahora, la extracción basada en selector trabaja para la documentación y blogs. El proyecto de muestra incluye un extractor de puntuación si lo necesita.


Paso 3: Chunk

Tienes 6KB de texto limpio. ¿Por qué no enviarlo todo?

  • Aguja en pajar: Los LLM funcionan peor cuando la información relevante está enterrada en un contexto amplio
  • Costo: Más fichas = más dinero y latencia
  • Enfoque: Los trozos seleccionados producen mejor razonamiento que todo a la vez

Recorte de estrategia importa más de lo que usted esperaría. Cubro esto en profundidad en el Artículo de arquitectura RAG - los mismos principios se aplican si usted está troceando páginas web o documentos.

Falso castigo

Este es un punto de partida, no un código de producción:

public List<string> ChunkBySentence(string text, int maxTokens = 2000)
{
    var chunks = new List<string>();
    
    // WARNING: This breaks on abbreviations, decimals, URLs, code samples
    var sentences = text.Split(new[] { ". ", ".\n", "! ", "? " }, 
        StringSplitOptions.RemoveEmptyEntries);
    
    var current = new StringBuilder();
    var tokens = 0;
    
    foreach (var sentence in sentences)
    {
        var sentenceTokens = EstimateTokens(sentence);
        
        if (tokens + sentenceTokens > maxTokens && current.Length > 0)
        {
            chunks.Add(current.ToString().Trim());
            current.Clear();
            tokens = 0;
        }
        
        current.Append(sentence).Append(". ");
        tokens += sentenceTokens;
    }
    
    if (current.Length > 0)
        chunks.Add(current.ToString().Trim());
    
    return chunks;
}

// Rough estimate - OK for demos, not for billing
private int EstimateTokens(string text) 
    => (int)(text.Split(' ').Length * 1.3);

¿Por qué esto es ingenuo?:

  • ". " saltos en "Dr. Smith", "v1.0", URLs
  • Estimación de token por recuento de palabras es ±20% de descuento
  • No hay solapamiento entre trozos (perde contexto en los límites)

Mejor: Recorte basado en encabezados

Para la documentación, pedazo por sección:

public List<ContentChunk> ChunkByHeadings(string html)
{
    var doc = new HtmlParser().ParseDocument(html);
    var chunks = new List<ContentChunk>();
    
    var headings = doc.QuerySelectorAll("h1, h2, h3");
    
    foreach (var heading in headings)
    {
        var content = new StringBuilder();
        content.AppendLine(heading.TextContent);
        
        var sibling = heading.NextElementSibling;
        while (sibling != null && !sibling.TagName.StartsWith("H"))
        {
            content.AppendLine(sibling.TextContent);
            sibling = sibling.NextElementSibling;
        }
        
        chunks.Add(new ContentChunk
        {
            Heading = heading.TextContent.Trim(),
            Content = content.ToString().Trim(),
            HeadingLevel = int.Parse(heading.TagName[1..])
        });
    }
    
    return chunks;
}

Esto preserva la estructura de los documentos y hace que la selección sea más significativa.


Paso 4: Seleccionar

Aquí es donde ocurren la mayoría de los fracasos y donde debe comenzar la depuración.

Usted tiene 5 trozos. El usuario preguntó "¿Qué mejoras de rendimiento hay en .NET 10?" Sólo 1-2 trozos mencionan el rendimiento. Envíelos.

flowchart TB
    Q["Question: What perf improvements?"] --> Score[Score Each Chunk]
    
    subgraph Chunks
        C1["Chunk 1: Overview..."] 
        C2["Chunk 2: Runtime perf..."]
        C3["Chunk 3: Libraries..."]
        C4["Chunk 4: SDK changes..."]
    end
    
    Score --> C1
    Score --> C2
    Score --> C3
    Score --> C4
    
    C2 -->|"score: 3"| Top[Selected]
    C3 -->|"score: 1"| Top
    
    style C2 stroke:#27ae60,stroke-width:3px
    style C1 stroke:#95a5a6,stroke-width:2px,stroke-dasharray: 5 5
    style C4 stroke:#95a5a6,stroke-width:2px,stroke-dasharray: 5 5

Coincidencia de palabras clave (simple, debagueable)

public record ScoredChunk(string Content, string? Heading, int Score, List<string> MatchedKeywords);

public List<ScoredChunk> SelectByKeywords(
    List<ContentChunk> chunks, 
    string question, 
    int topK = 3)
{
    // Normalize and filter stopwords
    var keywords = question.ToLower()
        .Split(' ', StringSplitOptions.RemoveEmptyEntries)
        .Where(w => w.Length > 3)
        .Where(w => !Stopwords.Contains(w))
        .Select(w => w.Trim(',', '.', '?', '!'))
        .Distinct()
        .ToList();
    
    var scored = chunks.Select(chunk =>
    {
        var text = (chunk.Heading + " " + chunk.Content).ToLower();
        var matched = keywords.Where(kw => text.Contains(kw)).ToList();
        
        // Boost if keyword appears in heading
        var headingBoost = chunk.Heading != null && 
            keywords.Any(kw => chunk.Heading.ToLower().Contains(kw)) ? 2 : 0;
        
        return new ScoredChunk(
            chunk.Content, 
            chunk.Heading, 
            matched.Count + headingBoost,
            matched
        );
    })
    .OrderByDescending(x => x.Score)
    .Take(topK)
    .ToList();
    
    // LOG THIS - it's your debugging lifeline
    foreach (var s in scored)
        Console.WriteLine($"  [{s.Score}] {s.Heading ?? "(no heading)"}: {string.Join(", ", s.MatchedKeywords)}");
    
    return scored;
}

private static readonly HashSet<string> Stopwords = new()
    { "what", "how", "does", "the", "are", "is", "in", "for", "of", "to", "and" };

Mejoras clave con respecto al recuento ingenuo:

  • Eliminación de stopword (de lo contrario "qué" y "el" siempre coinciden)
  • Potenciación de la dirección (señal estructural)
  • Registro de palabras clave coincidentes - Este es tu salvavidas de depuración.

Basado en el empotrado (semántico)

Las palabras clave fallan en sinónimos. "perf" no coincide con "mejoras de rendimiento".

Si quieres profundizar en las incrustaciones y la búsqueda de vectores, cubrir esto ampliamente en el Serie de imprimación RAG y búsqueda semántica con ONNX.

public async Task<List<ScoredChunk>> SelectByEmbedding(
    List<ContentChunk> chunks, 
    string question, 
    int topK = 3)
{
    var questionEmbed = await EmbedAsync(question);
    
    // Cache these per URL in production
    var scored = new List<(ContentChunk Chunk, double Score)>();
    
    foreach (var chunk in chunks)
    {
        var chunkEmbed = await EmbedAsync(chunk.Content);
        var similarity = CosineSimilarity(questionEmbed, chunkEmbed);
        scored.Add((chunk, similarity));
    }
    
    return scored
        .OrderByDescending(x => x.Score)
        .Take(topK)
        .Select(x => new ScoredChunk(x.Chunk.Content, x.Chunk.Heading, (int)(x.Score * 100), new()))
        .ToList();
}

private async Task<double[]> EmbedAsync(string text)
{
    var request = new EmbedRequest { Model = "nomic-embed-text", Input = [text] };
    var response = await _ollama.EmbedAsync(request);
    return response.Embeddings.First().ToArray();
}

Negociaciones:

  • Palabras clave: Fast, debuggable, literal emparejamiento
  • Incrustaciones: Semántica, pero más lenta y necesita caché

Para la producción, incrustaciones de caché por (URL, hachís) en SQLite o una base de datos vectorial como Qdrant.


Enviando a la LLM

Estructurar el prompt para forzar respuestas basadas en fuentes con citación:

public string BuildPrompt(string url, List<ScoredChunk> chunks, string question)
{
    var sb = new StringBuilder();
    
    sb.AppendLine("You are answering a question using ONLY the content below.");
    sb.AppendLine("Rules:");
    sb.AppendLine("- Answer ONLY from the provided sources");
    sb.AppendLine("- Cite which SOURCE number supports each claim");
    sb.AppendLine("- Include 1-2 brief quotes as evidence");
    sb.AppendLine("- If the answer isn't in the sources, say 'Not enough information'");
    sb.AppendLine("- End with Confidence: High/Medium/Low");
    sb.AppendLine();
    
    for (int i = 0; i < chunks.Count; i++)
    {
        sb.AppendLine($"=== SOURCE {i + 1} ===");
        if (chunks[i].Heading != null)
            sb.AppendLine($"Section: {chunks[i].Heading}");
        sb.AppendLine($"From: {url}");
        sb.AppendLine(chunks[i].Content);
        sb.AppendLine();
    }
    
    sb.AppendLine($"Question: {question}");
    sb.AppendLine();
    sb.AppendLine("Answer (with citations and confidence):");
    
    return sb.ToString();
}

Esto pasa de "resumen hablador" a "análisis con procedencia".

Llamando a Ollama

public async Task<string> AskAsync(string prompt)
{
    var request = new GenerateRequest { Model = "llama3.2:3b", Prompt = prompt };
    
    var response = new StringBuilder();
    await foreach (var chunk in _ollama.GenerateAsync(request))
    {
        if (chunk?.Response != null)
            response.Append(chunk.Response);
    }
    
    return response.ToString().Trim();
}

Esto es el uso de la herramienta

Lo que hemos construido es un patrón de agente sin el marco. por qué prefiero este enfoque sobre LangChain - orquestación explícita supera abstracciones mágicas cuando la depuración importa.

flowchart LR
    subgraph Tools["Tools (Deterministic)"]
        T1[fetch_url]
        T2[clean_html]
        T3[chunk_text]
        T4[select_relevant]
    end
    
    subgraph LLM["LLM (Reasoning)"]
        R[Interpret + Answer]
    end
    
    T1 --> T2 --> T3 --> T4 --> R
    
    R -->|"Low confidence"| Retry[Retry with different selection]
    Retry --> T4
    
    style T4 stroke:#e74c3c,stroke-width:3px
    style R stroke:#3498db,stroke-width:3px

El bucle: si la confianza es baja, reinténtalo con más trozos o palabras clave diferentes.

var answer = await AskAsync(prompt);

if (answer.Contains("Not enough information") || answer.Contains("Confidence: Low"))
{
    // Retry with more chunks
    var moreChunks = SelectByKeywords(allChunks, question, topK: 5);
    answer = await AskAsync(BuildPrompt(url, moreChunks, question));
}

Lo que va mal

flowchart TB
    subgraph Failures["Failure Modes"]
        F1[Cleaner removes content]
        F2[Chunking breaks mid-thought]
        F3[Selection picks wrong chunks]
        F4[LLM hallucinates connections]
    end
    
    F1 --> R1["'Not enough info' - answer existed"]
    F2 --> R2["Partial answer - context lost"]
    F3 --> R3["Wrong answer - right content skipped"]
    F4 --> R4["Confident but wrong"]
    
    style F3 stroke:#e74c3c,stroke-width:3px

La regla de depuración:

Si la respuesta es incorrecta, es casi siempre porque La selección estaba equivocada., no porque el modelo falló.

Lista de verificación de depuración

  1. Registrar los IDs y puntuaciones de trozos seleccionados
  2. Mostrar las palabras clave coincidentes (o distancias de inserción)
  3. Reproduzca el prompt exacto que fue enviado
  4. Comparar: ¿El contenido correcto en el HTML obtenido? ¿Texto limpio? ¿Algún trozo?

El fallo generalmente no es el LLM. Es aguas arriba.


Poniéndolo juntos

public class WebAnalyzer : IDisposable
{
    private readonly WebFetcher _fetcher = new();
    private readonly HtmlCleaner _cleaner = new();
    private readonly OllamaApiClient _ollama = new(new Uri("http://localhost:11434"));
    
    public async Task<AnalysisResult> AnalyzeAsync(string url, string question)
    {
        // 1. Fetch
        var html = await _fetcher.FetchAsync(url);
        
        // 2. Clean (with observability)
        var cleaned = _cleaner.Clean(html);
        Console.WriteLine($"Cleaned: {cleaned.OriginalLength} → {cleaned.Text.Length} bytes ({cleaned.MatchedSelector})");
        
        // 3. Chunk
        var chunks = ChunkByHeadings(html);
        Console.WriteLine($"Chunks: {chunks.Count}");
        
        // 4. Select (with logging)
        Console.WriteLine("Selection scores:");
        var selected = SelectByKeywords(chunks, question, topK: 3);
        
        // 5. Prompt + LLM
        var prompt = BuildPrompt(url, selected, question);
        var answer = await AskAsync(prompt);
        
        return new AnalysisResult
        {
            Answer = answer,
            ChunksUsed = selected.Count,
            SelectionScores = selected.Select(s => s.Score).ToList()
        };
    }
    
    public void Dispose() => _fetcher.Dispose();
}

Uso con nuestro ejemplo de ejecución:

using var analyzer = new WebAnalyzer();

var result = await analyzer.AnalyzeAsync(
    "https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-10/overview",
    "What performance improvements are in .NET 10?"
);

Console.WriteLine(result.Answer);

La producción incluye citas y confianza:

Based on SOURCE 1 and SOURCE 2:

.NET 10 includes several performance improvements:
- JIT improvements including better inlining and method devirtualization (SOURCE 1)
- "Enhanced loop inversion for better optimization" (SOURCE 1)
- NativeAOT enhancements for improved code generation (SOURCE 2)

Confidence: High

Cuando esto funciona (y no funciona)

Funciona bien # # No funciona

|------------|--------------| Documentación JavaScript SPAs Publicaciones en el blog, artículos Contenido dinámico/interactivo Referencias técnicas Investigación de varias páginas HTML estático Contenido autenticado

Para sitios de JS-pesados, usted necesita Guionista para .NET.


El principio fundamental

flowchart LR
    subgraph Your["Your Code's Job"]
        direction TB
        F[Fetch reliably]
        C[Clean carefully]
        S[Select correctly]
        O[Observe everything]
    end
    
    subgraph LLM["LLM's Job"]
        direction TB
        R[Reason over what you gave it]
        A[Admit when it doesn't know]
    end
    
    Your --> LLM
    
    style S stroke:#e74c3c,stroke-width:3px
    style R stroke:#3498db,stroke-width:3px

El LLM sólo puede razonar sobre lo que le diste. La selección es tu responsabilidad.

No le pidas a la LLM que navegue. Pídele que razone.


Proyecto de muestra completa

Ejecución completa del trabajo: Mayormentelucid.LlmWebFetcher

Incluye:

  • WebFetcher - HTTP con manejo adecuado
  • HtmlCleaner - Eliminación de ruido + estrategias de recuperación
  • ContentChunker - Frase y troceado basado en encabezados
  • WebContentAnalyzer - Gasoducto completo con tala
  • OllamaExtensions - Ayudante para la transmisión de respuestas
cd Mostlylucid.LlmWebFetcher
dotnet run

Recursos

Bibliotecas

LLMs

Artículos relacionados

Microsoft AI Stack

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.