Back to "Recupero e analisi dei contenuti Web con LLM in 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

Recupero e analisi dei contenuti Web con LLM in C#

Friday, 19 December 2025

Nota di sintesi: Questo articolo insegna i fondamenti dell'estrazione dei contenuti web con LLM utilizzando l'approccio più semplice possibile. Per i casi di utilizzo della produzione (riepilogazione web, analisi dei documenti, strumenti di agente), vedere DocSummarizer Rilascio di GitHub - implementa l'architettura qui mostrata ma la produzione-grado: BERT embeddings, ricerca ibrida, Playwright per SPA, protezione SSRF, citazioni convalidate, e corretto monitoraggio della copertura.

Quando chiedi a ChatGPT di "leggere questo articolo e riassumerlo," cosa succede realmente? Se immagini l'intelligenza artificiale aprire un browser e leggere come si farebbe - non è così che funziona.

Gli LLM non navigano nel web. Ragionano su frammenti selezionati dal vostro codice.

Questo articolo ti mostra come costruire questo in C# con Ollama - nessun framework, solo codice pratico che puoi debug.

TL;DR: Il vostro codice prende → pulisce → pezzi → seleziona. LLM vede solo i frammenti che si iniettano. La selezione è lossy dal design - che è sia il vincolo che l'architettura.

L'unica cosa che rende questo lavoro

Ecco l'intuizione che cambia il modo in cui si costruiscono questi sistemi:

La selezione è la decisione del prodotto. E 'anche dove la maggior parte dei fallimenti hanno origine.

Il LLM è a valle della vostra selezione. Non può recuperare informazioni che non avete mostrato. Quando un agente "fails per trovare la risposta," il problema non è quasi mai il modello - è che la vostra logica di selezione ha scelto i pezzi sbagliati. (Questo è lo stesso principio dietro perché evito quadri come LangChain - astrattano la logica di selezione di cui hai bisogno per il debug.)

Questo è il motivo per cui "agente di navigazione" fallisce in silenzio. L'agente risponde fiduciosamente in base a ciò che ha visto. Non si sa mai che ha perso il contenuto giusto.

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

Il modello può solo ragionare su ciò che gli hai dato. Costruisci di conseguenza.


Obiettivi di progettazione prima del codice

Prima di scrivere qualcosa, questi sono i vincoli:

Velocità Perché ha importanza
Nessun rendering JS Solo HTML statico (Playwright per SPA)
Token bounded Hard budget per richiesta (2-4K token tipici)
Risposte legate alla fonte LLM non deve allucinare la conoscenza del web
Selezione deterministica Stesso ingresso → stesso pezzo (debuggabile)
Osservabile Log what you select, why, and what you widget

Se non riesci a spiegare perché è stato selezionato un pezzo, non puoi eseguire il debug dei guasti.


Il gasdotto

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

Ogni passo riduce i dati. Quando l'LLM lo vede, sei passato da 57KB di HTML a forse 2KB di testo rilevante. Ogni riduzione è lossy. Ogni riduzione può scartare la risposta. Questo è lo stesso modello che uso per analisi di grandi file CSV - le ragioni LLM, il vostro codice calcola e seleziona.


Esempio di esecuzione

In tutto questo articolo, useremo un URL:

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

E tre domande di maggiore specificità:

  1. "Cosa c'è di nuovo in .NET 10?"
  2. "Quali miglioramenti delle prestazioni sono menzionati?"
  3. "C'e' qualcosa su AOT?"

Questo mantiene gli esempi di base e mostra come la selezione conta di più come le domande diventano specifiche.


Configurazione

# 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 di OllamaSharp: Nella versione 5.x, GenerateAsync ritorna IAsyncEnumerable<GenerateResponseStream?> Si accumulano con... await foreach. I perni del progetto campione 5.1.5.


Passo 1: Preleva

Standard HTTP, ma con i dettagli che contano:

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();
}

Che cosa questo maneggia: Redirects (capped), compressione, timeout, User-Agent, convalida del tipo di contenuto.

Quello che non fa: Rendering JavaScript, autenticazione, limitazione dei tassi, robots.txt. Per la produzione, aggiungere ritardi per host e rispettare le politiche di crawl.


Fase 2: Pulire

L'HTML grezzo è per lo più rumore. Puliscilo o rifiuti gettoni sul layout.

Una pagina tipica:

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

La strategia

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

Pulizia è lossy - maniglia con cura

Il vostro pulitore può assolutamente eliminare la risposta.

  • Selettori aggressivi eliminano il contenuto legittimo
  • Il contenuto principale ha nomi di classe insoliti (.article-body, .post-text)
  • La risposta è in una barra laterale o in una casella di chiamata

Contenziosi:

  • Mantenere esplicitamente le voci (h1-h6)
  • Rimuovi da ruolo e Caldaia nota, non ampi modelli
  • Log: percorso dell'elemento estratto + lunghezza del testo
  • Ripiegamento: se il contenuto principale è sospettosamente breve, provare il corpo
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);

Estrazione di leggibilità Per ora, l'estrazione basata sul selettore funziona per la documentazione e i blog. Il progetto di esempio include un estrattore di punteggio se ne hai bisogno.


Fase 3: Chunk

Hai 6KB di testo pulito, perche' non mandare tutto?

  • Ago in pagliaio: LLMs eseguire peggio quando le informazioni rilevanti è sepolto in grande contesto
  • Costo: Più gettoni = più soldi e latenza
  • Focus: I pezzi selezionati producono un ragionamento migliore di tutto in una volta

La strategia di "Chunking" conta piu' di quanto ci si possa aspettare. Articolo sull'architettura RAG - gli stessi principi si applicano sia che tu stia scrivendo pagine web o documenti.

Chunking della pena ingenua

Questo è un punto di partenza, non un codice di produzione:

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);

Perché questo è ingenuo:

  • ". " rompe su "Dr. Smith," "v1.0," URL
  • La stima del token per numero di parole è ±20% di sconto
  • Nessuna sovrapposizione tra pezzi (perde il contesto ai confini)

Meglio: Chunking a base di testa

Per la documentazione, pezzo per sezione:

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;
}

Ciò preserva la struttura dei documenti e rende la selezione più significativa.


Passo 4: Selezionare

Questo è il punto in cui la maggior parte dei fallimenti accadono e dove il debugging dovrebbe iniziare.

Avete 5 pezzi. L'utente ha chiesto "Quali miglioramenti di prestazioni sono in .NET 10?" Solo 1-2 pezzi menzionano le prestazioni. Inviarli.

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

Parole chiave corrispondenti (semplice, debuggabile)

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" };

Principali miglioramenti rispetto al conteggio ingenuo:

  • Rimozione delle parole d'arresto (altrimenti "cosa" e "il" corrispondono sempre)
  • Amplificazione della rotta (segnale strutturale)
  • Registrazione delle parole chiave corrispondenti - Questa e' la tua linea di salvataggio per il debug.

Abbinamenti a base (semantica)

Parole chiave falliscono sui sinonimi. "perf" non corrisponde a "miglioramento delle prestazioni."

Le inserzioni trovano somiglianza semantica. Se si vuole andare più a fondo sulle inserzioni e la ricerca vettoriale, lo copro ampiamente nel Serie primer RAG e ricerca semantica 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();
}

Trade-off:

  • Parole chiave: Fast, debuggable, letteral matching
  • Abbinamenti: Semantico, ma più lento e ha bisogno di cache

Per la produzione, embeddings cache per (URL, hash chunk) in SQLite o un database vettoriale come QdrantCity name (optional, probably does not need a translation).


Invio alla LLM

Strutturare il prompt per forzare le risposte source-bounded con citazione:

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();
}

Questo passa da "sintesi di chiacchierata" a "analisi con provenienza."

Chiamare 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();
}

Questo è uso dello strumento

Quello che abbiamo costruito e' un modello di agente senza il framework. Perché preferisco questo approccio a LangChain - l'orchestrazione esplicita batte le astrazioni magiche quando il debug conta.

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

Il ciclo: se la fiducia è bassa, riprovare con più pezzi o parole chiave diverse.

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));
}

Cosa c'e' che non va?

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 regola di debug:

Se la risposta è sbagliata, è quasi sempre perché selezione errata, non perché il modello ha fallito.

Elenco di controllo di debug

  1. Registra gli ID e i punteggi di chunk selezionati
  2. Mostra quali parole chiave corrispondono (o accumulo di distanze)
  3. Riproduci il prompt esatto che è stato inviato
  4. Confronta: Era il contenuto giusto nell'HTML recuperato? Testo pulito? Qualche pezzo?

Di solito il fallimento non e' l'LLM, e' a monte.


Metterlo insieme

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();
}

Utilizzo con il nostro esempio corrente:

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);

L'output include citazioni e fiducia:

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

Quando funziona (e non funziona)

Funziona bene Non funziona
Documentazione JavaScript SPAs
Post di blog, articoli Contenuti dinamici/interattivi
Riferimenti tecnici Ricerca su più pagine
HTML statico Contenuto autenticato

Per i siti JS-heavy, è necessario Playwright per .NET.


Il principio chiave

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

L'LLM può solo ragionare su ciò che gli hai dato. La selezione è una tua responsabilità.

Non chiedere all'LLM di navigare. Chiedigli di ragionare.


Progetto campione completo

Completa attuazione del lavoro: Per lo più Lucid.LlmWebFetcher

Comprende:

  • WebFetcher - HTTP con una corretta gestione
  • HtmlCleaner - Rimozione del rumore + strategie di ripiego
  • ContentChunker - Condanna e intestazione
  • WebContentAnalyzer - Conduttura completa con registrazione
  • OllamaExtensions - Aiuto per le risposte in streaming
cd Mostlylucid.LlmWebFetcher
dotnet run

Risorse

Biblioteche

LLMsCity name (optional, probably does not need a translation)

Articoli correlati

Microsoft AI Stack

logo

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