# Hämta och analysera webbinnehåll med LLMs i C#

<!--category-- AI, LLM, Agents, C#, Systems Design -->
<datetime class="hidden">2025-12-19T10:00</datetime>

> **på grundval av de uppgifter som avses i punkt 1 i denna artikel,**: Denna artikel lär ut grunderna för webbinnehållsextraktion med LLMs med hjälp av den enklaste möjliga metoden. För användningsfall (web summering, dokumentanalys, agentverktyg), se **[DocSummarizer Ordförande](/blog/building-a-document-summarizer-with-rag)** [![Utgivning från GitHub](https://img.shields.io/github/v/release/scottgal/mostlylucidweb?filter=docsummarizer*&label=docsummarizer)](https://github.com/scottgal/mostlylucidweb/releases?q=docsummarizer) - det implementerar arkitekturen som visas här men produktionsgrad: BERT inbäddningar, hybrid sökning, Playwright för SPAs, SSRF skydd, validerade citeringar, och korrekt täckning spårning.

När du ber ChatGPT att "läsa denna artikel och sammanfatta den", vad händer egentligen? Om du föreställer dig AI öppna en webbläsare och läsa som du skulle - det är inte hur det fungerar.

**LLMs surfar inte på webben. De resonerar över fragment din kod väljer.**

Den här artikeln visar hur du bygger detta i C# med Ollama - inga ramar, bara praktisk kod du kan felsöka.

> **TL;DR**: Din kod hämtar → renar → bitar → väljer. LLM bara ser de fragment du injicerar. Val är förlust genom design - det är både begränsningen och arkitekturen.

[TOC]

## Den enda sak som får detta att fungera

Här är insikten som förändrar hur du bygger dessa system:

**Urvalet är produktbeslutet. Det är också där de flesta misslyckanden har sitt ursprung.**

LLM är nedströms ditt val. Det kan inte återvinna information du inte visade det. När en agent "underlåter att hitta svaret", problemet är nästan aldrig modellen - det är att din urval logik valde fel bitar. (Detta är samma princip bakom [varför jag undviker ramar som LangChain](/blog/why-i-dont-use-langchain) - de absorberar bort urvalslogiken du behöver för att felsöka.)

Det är därför "agent surfar" misslyckas tyst. Agenten förtroende svar baserat på vad den såg. Man vet aldrig att det missade rätt innehåll.

```mermaid
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
```

Modellen kan bara resonera om vad du gav den. Bygg därefter.

---


## Designmål före koden

Innan du skriver något, är dessa begränsningar:

"Konstrikta dig" Varför det betyder något "
|------------|----------------|
| **Ingen JS- återgivning** på Statisk HTML endast (Playwright för SPAs)
| **Avgränsade polletter** till Hård budget per begäran (2-4K tokens typiska)
| **Källkodsbundna svar** till LLM får inte hallucinera webbkunskapen
| **Deterministiskt urval** på samma ingång → samma bitar (debuggable)
| **Observerbar** Logga in vad du valt, varför och vad du kasserade

Om du inte kan förklara varför en bit valdes, kan du inte felsöka fel.

---


## Rörledningen

```mermaid
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
```

Varje steg minskar data. När LLM ser det, har du gått från 57KB HTML till kanske 2KB relevant text. Varje minskning är förlorad. Varje minskning kan kasta svaret. Det är samma mönster jag använder för att [Analys av stora CSV-filer](/blog/analysing-large-csv-files-with-local-llms) - LLM skäl, din kod beräknar och väljer.

---


## Exempel på körning

Under hela artikeln använder vi en webbadress:

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

Och tre frågor av ökande specificitet:

1. "Vad är nytt i NET 10?"
2. "Vilka prestandaförbättringar nämns?"
3. "Finns det något om AOT?"

Detta håller exemplen grundade och visar hur urval betyder mer när frågor blir specifika.

---


## Ställ in

```bash
# 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)
```

> **OllamaSharp-not**: I version 5.x, `GenerateAsync` returer `IAsyncEnumerable<GenerateResponseStream?>` - Det strömmar polletter som de genereras. `await foreach`. Provet projekt pins 5.1.5.

---


## Steg 1: Hämta

Standard HTTP, men med de detaljer som är viktiga:

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

**Vad det här hanterar**: Redirects (cappad), komprimering, timeouts, User-Agent, validering av innehållstyp.

**Vad det inte gör**: JavaScript rendering, autentisering, hastighetsbegränsning, robots.txt. För produktion, lägga till per-host förseningar och respektera krypning politik.

---


## Steg 2: Rensa

Raw HTML är mestadels buller. Rengör det eller du slösa polletter på layout.

En typisk sida:

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

### Strategin

```mermaid
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
```

### Rengöring är förlorad - handtag med omsorg

Din städare kan absolut ta bort svaret. Vanliga misstag:

- Aggressiva väljare tar bort legitimt innehåll
- Huvudinnehåll har ovanliga klassnamn (`.article-body`, `.post-text`)
- Svaret finns i en sidebar eller callout box

**Begränsningar**:

- Behåll rubrikerna uttryckligen (`h1`-`h6`)
- Ta bort genom *roll* och *känd pannplåt*, inte breda mönster
- Logg: extraherad elementsökväg + textlängd
- Fallback: om huvudinnehållet är misstänkt kort, prova kroppen

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

> **Utvinning av läsbarhet** (Scoring punkter, text densitet) är en hel kanin hål. För närvarande, selector-baserad extraktion fungerar för dokumentation och bloggar. Sampleprojektet innehåller en scoring extraktor om du behöver det.

---


## Steg 3: Chunk

Du har 6KB av ren text. Varför inte skicka allt?

- **Nål i höstack**: LLMs presterar sämre när relevant information är begravd i stora sammanhang
- **Kostnad**: Fler polletter = mer pengar och latens
- **Fokus**: Utvalda bitar ger bättre resonemang än allt på en gång

Det är viktigare än man kan förvänta sig. [Artikeln om RAG-arkitektur](/blog/rag-architecture) - samma principer gäller oavsett om du klipper webbsidor eller dokument.

### Naivdomsnedläggelse

Detta är en utgångspunkt, inte en produktionskod:

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

**Varför detta är naivt**:

- `". "` bryter på "Dr Smith", "v1.0", webbadresser
- Uppskattning av antalet toner med ord är ±20 % lägre än
- Ingen överlappning mellan bitar (förlorar sammanhang vid gränser)

### Bättre: Rubrikbaserad chunking

För dokumentation, bit för bit:

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

Detta bevarar dokumentstrukturen och gör urvalet mer meningsfullt.

---


## Steg 4: Välj

Det är här de flesta misslyckanden sker och där felsökningen bör börja.

Du har 5 bitar. Användaren frågade "Vilka prestandaförbättringar finns i .NET 10?" Endast 1-2 bitar nämner prestanda. Skicka dessa.

```mermaid
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
```

### Nyckelordsmatchning (enkelt, felsökningsbart)

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

**Viktiga förbättringar jämfört med naiv beräkning**:

- Stoppord borttagning (annars "vad" och "den" alltid matchar)
- Rubrikökning (strukturell signal)
- **Loggning matchade sökord** - Det här är din felsökningslivlina.

### Inbäddning baserad (semantisk)

Nyckelord misslyckas med synonymer. "perf" matchar inte "prestandaförbättringar".

Inbäddningar finner semantisk likhet. Om du vill gå djupare på inbäddningar och vektorsökning, Jag täcker detta i stor utsträckning i [RAG-primerserien](/blog/rag-primer) och [semantisk sökning med ONNX](/blog/semantic-search-with-onnx-and-qdrant).

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

**Avräkning**:

- Nyckelord: Snabb, avstuggbar, bokstavlig matchning
- Inbäddningar: Semantisk, men långsammare och behöver caching

För produktion, cacheinbäddningar per (URL, bit hash) i SQLite eller en vektordatabas som [Qdrant Ordförande](/blog/building-a-lawyer-gpt-for-your-blog-part4).

---


## Skickar till LLM

Strukturera uppmaningen att tvinga källbundna svar med hänvisning:

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

Detta går från "chatty summering" till "analys med härkomst".

### Att ringa till Ollama

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

---


## Det här är verktygsanvändning

Vi har byggt upp ett agentmönster utan ramen. [varför jag föredrar detta tillvägagångssätt framför LangChain](/blog/why-i-dont-use-langchain) - explicit orkestrering slår magiska abstraktioner vid felsökning.

```mermaid
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
```

Loopen: om självförtroendet är lågt, försök igen med fler bitar eller olika nyckelord.

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

---


## Vad som går fel

```mermaid
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
```

**Felsökningsregeln**:

> Om svaret är fel, är det nästan alltid för att **Urvalet var fel**, inte för att modellen misslyckades.

### Avlusning av checklista

1. **Logga markerade bit- ID och poäng**
2. **Visa vilka nyckelord som matchas** (eller inbäddade avstånd)
3. **Spela upp den exakta prompten igen** som skickades
4. **Jämför**: Var rätt innehåll i hämtad HTML? Rengjord text? Någon bit?

Misslyckandet är oftast inte LLM, det är uppströms.

---


## Att sätta ihop det

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

Användning med vårt körexempel:

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

Utdata inkluderar citeringar och förtroende:

```
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
```

---


## När detta fungerar (och inte gör det)

Det fungerar bra, det fungerar inte.
|------------|--------------|
Dokumentation  på JavaScript SPAs
på blogginlägg, artiklar  på Dynamiskt/interaktivt innehåll
Tekniska referenser  med flera sidors forskning
på Statisk HTML-text Autentiserat innehåll

För JS-tunga webbplatser, behöver du [Spelrätt för .NET](https://playwright.dev/dotnet/).

---


## Huvudprincipen

```mermaid
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
```

LLM kan bara resonera om vad du gav det. Urvalet är ditt ansvar.

Be inte LLM att bläddra.

---


## Fullständigt urvalsprojekt

Fullständigt genomförande av arbetet: [Mestlylucid.LlmWebFetcher](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.LlmWebFetcher)

Innehåller:

- `WebFetcher` - HTTP med korrekt hantering
- `HtmlCleaner` - Bullerborttagning + reservstrategier
- `ContentChunker` - Dom och rubrikbaserad styckning
- `WebContentAnalyzer` - Full pipeline med loggning
- `OllamaExtensions` - Hjälpare för streaming-svar

```bash
cd Mostlylucid.LlmWebFetcher
dotnet run
```

---


## Resurser

**Bibliotek**

- [Vinkelsharp](https://anglesharp.github.io/) - HTML-tolkning
- [OllamaSharp Ordförande](https://github.com/awaescher/OllamaSharp) - Ollama-klienten.

**LLM-minnen**

- [Ollama Ordförande](https://ollama.ai/) - Lokal LLM runtime
- [Detta anslag är avsett att täcka utgifter för tjänstemän och tillfälligt anställda som arbetar inom politikområdet Migration och inrikes frågor samt administrativa utgifter för tjänstemän och tillfälligt anställda som arbetar inom politikområdet Migration och inrikes frågor.](https://ollama.ai/library/llama3.2) - Snabb, kapabel modell
- [nomic-embed-text](https://ollama.ai/library/nomic-embed-text) - Inbäddningar (~275MB)

**Relaterade artiklar**

- **[DocSummarizer-serien](/blog/building-a-document-summarizer-with-rag)** - **FAR BETTER-metoden** för sammanfattning av webbinnehåll. Den här artikeln visar det enklaste sättet att hämta och analysera; DocSummarizer visar produktionssättet med BERT-extraktion, hybridsökning, validerade citeringar och korrekt täckningsspårning.
- [Analysera stora CSV-filer med lokala LLM](/blog/analysing-large-csv-files-with-local-llms) - Samma mönster: LLM skäl, kod beräkningar
- [Varför jag inte använder LangChain](/blog/why-i-dont-use-langchain) - Ramlös agentdesign
- [RAG Primer Ordförande](/blog/rag-primer) - Förstå hämtning-augmented generation
- [Semantisk sökning med ONNX och Qdrant](/blog/semantic-search-with-onnx-and-qdrant) - Lokal semantisk sökning
- [Att bygga en advokat GPT](/blog/building-a-lawyer-gpt-for-your-blog-part1) - Fullständig RAG genomförandeserie

**Microsoft AI Stack**

- [Semantisk kernel](https://github.com/microsoft/semantic-kernel) - Om du vill ha ett ramverk
- [Microsoft.Extensions.AI](https://devblogs.microsoft.com/dotnet/introducing-microsoft-extensions-ai-preview/) - Sammanfattningar