Bouwen van een "Lawyer GPT" voor uw blog - Fine Tuning LLM Alternatief: RAG met Qdrant en Generic Online LLMs (Nederlands (Dutch))

Bouwen van een "Lawyer GPT" voor uw blog - Fine Tuning LLM Alternatief: RAG met Qdrant en Generic Online LLMs

Wednesday, 12 November 2025

//

22 minute read

Inleiding

In mijn8-delige serie "Advocaat GPT"Ik heb je laten zien hoe je een complete lokale RAG-based schrijfassistent kunt bouwen met behulp van GPU acceleratie, lokale LLM's en vectordatabases.

Het is krachtig, privé en draait volledig op je hardware.

Maar laten we eerlijk zijn - niet iedereen heeft een werkplek met een NVIDIA GPU, 96GB RAM, en het geduld om CUDA, cuDNN, en wrangle GGUF modellen op te zetten.

Wat als je gewoon wilt de voordelen van een blog schrijven assistent zonder de hardware investering?

Dit artikel presenteert het cloud-gebaseerde alternatief: dezelfde RAG aanpak, dezelfde Qdrant vector database, maar met behulp van cloud LLM API's in plaats van lokale gevolgtrekkingen.

Beschouw het als "Advocaat GPT Lite" - eenvoudiger setup, lagere barrière voor toegang, en potentieel betere output kwaliteit met behulp van frontier modellen.

Volledige onthulling: Ik ben nog steeds aan het leren welke aanpak het beste werkt in de praktijk, dus neem mijn kostenramingen en prestatieclaims met een snufje zout.

Wat ik kan zeggen is dat deze cloud aanpak is gebleken opmerkelijk eenvoudig op te zetten in vergelijking met de GPU route.

  • OPMERKING: Dit maakt deel uit van mijn experimenten met AI (ondersteund opstellen) + mijn eigen bewerking.
  • Dezelfde stem, hetzelfde pragmatisme, gewoon snellere vingers.
  • Waarom een Cloud Alternative?
  • De oorspronkelijke aanpak
  • De volledige "Lawyer GPT" serie bouwt een systeem dat:
  • Loopt 100% lokaal (privacy)
  • Geen API-kosten

Snelle GPU-versnelde gevolgtrekking

Vereist NVIDIA GPU (8GB+ VRAM)

  • Complexe opstelling (CUDA, cuDNN, modelbeheer)
  • Beperkt tot modellen kunt u in VRAM passen
  • Windows-gerichte implementatie
  • The Cloud Alternative
  • Deze aanpak geeft u:
  • Geen GPU vereist (runs op een machine)
  • Eenvoudige installatie (geen CUDA/cuDNN)
  • Toegang tot grensmodellen (GPT-4, Claude, enz.)

Cross-platform (Windows, Mac, Linux)

Betere outputkwaliteit (grotere, meer capabele modellen) |--------------------|-------------------| API-kosten (hoewel redelijk voor persoonlijk gebruik) Gegevens verzonden naar API's van derden Latency hangt af van het netwerk Wanneer moet ik die gebruiken?

Gebruik een lokale aanpak Gebruik een cloudbenadering

Privacy is van cruciaal belang Gemak is het meest belangrijk

graph TB
    A[Markdown Files] -->|Ingest| B[Chunking Service]
    B -->|Text Chunks| C[Cloud Embedding API]
    C -->|Vectors| D[Qdrant Vector DB]

    E[User Writing] -->|Current Draft| F[Web/Desktop Client]
    F -->|Embed Context| C
    C -->|Query Vector| D
    D -->|Similar Content| G[Context Builder]

    G -->|Relevant Past Articles| H[Prompt Engineer]
    H -->|Prompt + Context| I[Cloud LLM API]
    I -->|Generated Suggestions| J[Response Handler]
    J -->|Suggestions + Citations| F

    F -->|Display| K[Editor with Suggestions]

    class C,I cloud
    class D,K local

    classDef cloud stroke:#f96,stroke-width:4px
    classDef local stroke:#333,stroke-width:2px

Je hebt GPU hardware Je bent op Mac/Linux/laptop

  • **Het gebruik van hoge volumes (enkele berichten/maand)**Je vindt het leuk om te knutselen. Je wilt snel resultaten.text-embedding-3-smallOverzicht architectuur
  • **De cloud versie houdt dezelfde RAG fundamentals maar wisselt lokale LLM-inferentie voor API-gesprekken:**Belangrijkste verschillen:
  • Inbeddingsmodel: OpenAI's
  • API in plaats van lokaal BGE modelLLM

: Claude 3.5 Sonnet of GPT-4 API in plaats van lokale Mistral/Lama

Geen GPU vereist

: Alles CPU-gebaseerd lokaal, rekent gebeurt in de cloud

  • Eenvoudigere inzet: Enkelvoudig uitvoerbaar, geen modelbestanden te beheren
  • **Ik moet opmerken dat ik nog geen uitgebreide benchmarks heb uitgevoerd om de twee benaderingen te vergelijken - ik zit zelf nog steeds in de verkennende fase.**Maar de eerste resultaten zijn veelbelovend genoeg om te delen.

Technologie Stack

  • Kernkader.NET 9
  • - Zelfde als de originele serieC# 13
  • - Moderne taaleigenschappenCloud API's

OpenAI API

  • **- Inbeddingen (tekst-inbedding-3-small) + LLM (GPT-4)**Antropische API
  • **- Alternative LLM (Claude 3.5 Sonnet)**Beide

- Je kunt mixen en matchen!

  • VectordatabaseQdrant
  • - Hetzelfde als origineel, kan lokaal draaien via Docker of Qdrant Cloud gebruikenAlternatief
  • **: Pinecone, Weaviate Cloud (beheerde opties)**Opties voor client

Console-app

- Eenvoudigste, geweldig voor het testen

Blazor WebAssembly

docker run -p 6333:6333 -p 6334:6334 \
    -v $(pwd)/qdrant_storage:/qdrant/storage:z \
    qdrant/qdrant

- Web-based, werkt overal

  1. Avalonia- Cross-platform bureaublad (Windows, Mac, Linux)
  2. Instellen: Het snelle pad

Qdrant installeren

Optie A: Lokale Docker (Aanbevolen voor ontwikkeling)

**Optie B: Qdrant Cloud (Gemakkelijkste)**Aanmelden bij

  1. cloud.qdrant.ioEen gratis cluster aanmaken
  2. Krijg je API-toets en cluster-URL
  3. Geen CUDA, geen cuDNN, geen driver installaties nodig!

**2.**API-sleutels ophalen

  1. OpenAI(voor inbeddingen + LLM):
  2. Ga naar

platform.openai.com

API-sleutel aanmakenappsettings.json:

{
  "BlogRAG": {
    "Embedding": {
      "Provider": "OpenAI",
      "Model": "text-embedding-3-small",
      "ApiKey": "sk-..."
    },
    "LLM": {
      "Provider": "Anthropic",
      "Model": "claude-3-5-sonnet-20241022",
      "ApiKey": "sk-ant-..."
    },
    "VectorStore": {
      "Type": "Qdrant",
      "Url": "http://localhost:6333",
      "ApiKey": "",
      "CollectionName": "blog_embeddings"
    },
    "Ingestion": {
      "MarkdownPath": "/path/to/your/blog/Markdown",
      "ChunkSize": 500,
      "ChunkOverlap": 50
    }
  }
}

**Gebruikslimieten instellen (belangrijk!)**Antropisch

(facultatief, voor Claude):

Ga naar

console.anthropic.com

using OpenAI;
using OpenAI.Embeddings;

namespace BlogRAG.Services
{
    public interface IEmbeddingService
    {
        Task<float[]> GenerateEmbeddingAsync(string text);
        Task<List<float[]>> GenerateBatchEmbeddingsAsync(List<string> texts);
    }

    public class OpenAIEmbeddingService : IEmbeddingService
    {
        private readonly OpenAIClient _client;
        private readonly string _model;
        private readonly ILogger<OpenAIEmbeddingService> _logger;

        public OpenAIEmbeddingService(
            string apiKey,
            string model,
            ILogger<OpenAIEmbeddingService> logger)
        {
            _client = new OpenAIClient(apiKey);
            _model = model;
            _logger = logger;
        }

        public async Task<float[]> GenerateEmbeddingAsync(string text)
        {
            var embeddings = await GenerateBatchEmbeddingsAsync(new List<string> { text });
            return embeddings.First();
        }

        public async Task<List<float[]>> GenerateBatchEmbeddingsAsync(List<string> texts)
        {
            _logger.LogInformation("Generating embeddings for {Count} texts", texts.Count);

            var request = new EmbeddingRequest
            {
                Input = texts,
                Model = _model
            };

            var response = await _client.CreateEmbeddingAsync(request);

            return response.Data
                .OrderBy(e => e.Index)
                .Select(e => e.Embedding.ToArray())
                .ToList();
        }
    }
}

API-sleutel aanmaken

  • Configuratie
  • Aanmaken
  • Dat is het.

**Geen GPU-instellingen, geen modeldownloads (12GB-bestanden), geen VRAM-beheer.**Uitvoering

  • Kerndiensten

Cloud Inbedding Service

using Anthropic.SDK;
using Anthropic.SDK.Messaging;

namespace BlogRAG.Services
{
    public interface ILLMService
    {
        Task<string> GenerateCompletionAsync(
            string systemPrompt,
            string userPrompt,
            float temperature = 0.7f);

        IAsyncEnumerable<string> GenerateStreamingCompletionAsync(
            string systemPrompt,
            string userPrompt,
            float temperature = 0.7f);
    }

    public class ClaudeLLMService : ILLMService
    {
        private readonly AnthropicClient _client;
        private readonly string _model;
        private readonly ILogger<ClaudeLLMService> _logger;

        public ClaudeLLMService(
            string apiKey,
            string model,
            ILogger<ClaudeLLMService> logger)
        {
            _client = new AnthropicClient(new APIAuthentication(apiKey));
            _model = model;
            _logger = logger;
        }

        public async Task<string> GenerateCompletionAsync(
            string systemPrompt,
            string userPrompt,
            float temperature = 0.7f)
        {
            _logger.LogInformation("Generating completion with temperature {Temp}", temperature);

            var messages = new List<Message>
            {
                new Message
                {
                    Role = RoleType.User,
                    Content = userPrompt
                }
            };

            var request = new MessageRequest
            {
                Model = _model,
                MaxTokens = 2048,
                Temperature = temperature,
                System = systemPrompt,
                Messages = messages
            };

            var response = await _client.Messages.CreateAsync(request);

            return response.Content.First().Text;
        }

        public async IAsyncEnumerable<string> GenerateStreamingCompletionAsync(
            string systemPrompt,
            string userPrompt,
            float temperature = 0.7f)
        {
            var messages = new List<Message>
            {
                new Message { Role = RoleType.User, Content = userPrompt }
            };

            var request = new MessageRequest
            {
                Model = _model,
                MaxTokens = 2048,
                Temperature = temperature,
                System = systemPrompt,
                Messages = messages,
                Stream = true
            };

            await foreach (var chunk in _client.Messages.StreamAsync(request))
            {
                if (chunk.Delta?.Text != null)
                {
                    yield return chunk.Delta.Text;
                }
            }
        }
    }
}

Belangrijkste voordelen vs lokale:

  • Geen ONNX Runtime setup
  • Geen GPU geheugenbeheer
  • Automatische batching door OpenAI
  • state-of-the-art inbeddingskwaliteit

Kosten:

  • : ~$0,0001 per 1K tokens (zeer goedkoop)
  • Verwerking 100 blogposten (~500K tokens): ~$0,05
  • Dagelijks gebruik (10 vragen): ~$0,001/dag = $0,30/maand

Cloud LLM-service

using Qdrant.Client;
using Qdrant.Client.Grpc;

namespace BlogRAG.Services
{
    public class QdrantVectorStore
    {
        private readonly QdrantClient _client;
        private readonly string _collectionName;
        private readonly ILogger<QdrantVectorStore> _logger;

        public QdrantVectorStore(
            string url,
            string apiKey,
            string collectionName,
            ILogger<QdrantVectorStore> logger)
        {
            _client = new QdrantClient(url, apiKey: apiKey);
            _collectionName = collectionName;
            _logger = logger;
        }

        public async Task CreateCollectionAsync(int vectorSize)
        {
            var collections = await _client.ListCollectionsAsync();

            if (collections.Any(c => c.Name == _collectionName))
            {
                _logger.LogInformation("Collection {Name} already exists", _collectionName);
                return;
            }

            await _client.CreateCollectionAsync(
                collectionName: _collectionName,
                vectorsConfig: new VectorParams
                {
                    Size = (ulong)vectorSize,
                    Distance = Distance.Cosine
                });

            _logger.LogInformation("Created collection {Name}", _collectionName);
        }

        public async Task UpsertAsync(
            Guid id,
            float[] vector,
            Dictionary<string, object> payload)
        {
            var point = new PointStruct
            {
                Id = id,
                Vectors = vector,
                Payload = payload
            };

            await _client.UpsertAsync(_collectionName, new[] { point });
        }

        public async Task<List<ScoredPoint>> SearchAsync(
            float[] queryVector,
            int limit = 10,
            float scoreThreshold = 0.7f)
        {
            var results = await _client.SearchAsync(
                collectionName: _collectionName,
                vector: queryVector,
                limit: (ulong)limit,
                scoreThreshold: scoreThreshold);

            return results.ToList();
        }
    }
}

**Voordelen ten opzichte van lokale:**Geen modelbelasting (instant opstarten)

Geen VRAM-limieten (gebruik indien nodig 200K-context)

namespace BlogRAG.Services
{
    public class IngestionService
    {
        private readonly IEmbeddingService _embedder;
        private readonly QdrantVectorStore _vectorStore;
        private readonly ILogger<IngestionService> _logger;

        public IngestionService(
            IEmbeddingService embedder,
            QdrantVectorStore vectorStore,
            ILogger<IngestionService> logger)
        {
            _embedder = embedder;
            _vectorStore = vectorStore;
            _logger = logger;
        }

        public async Task IngestMarkdownFilesAsync(string markdownPath)
        {
            var files = Directory.GetFiles(markdownPath, "*.md", SearchOption.AllDirectories);
            _logger.LogInformation("Found {Count} markdown files", files.Length);

            foreach (var file in files)
            {
                await IngestFileAsync(file);
            }
        }

        private async Task IngestFileAsync(string filePath)
        {
            var content = await File.ReadAllTextAsync(filePath);
            var metadata = ExtractMetadata(content);
            var chunks = ChunkContent(content);

            _logger.LogInformation("Processing {File}: {ChunkCount} chunks",
                Path.GetFileName(filePath), chunks.Count);

            // Batch embedding generation
            var texts = chunks.Select(c => c.Text).ToList();
            var embeddings = await _embedder.GenerateBatchEmbeddingsAsync(texts);

            // Upload to Qdrant
            for (int i = 0; i < chunks.Count; i++)
            {
                var chunk = chunks[i];
                var embedding = embeddings[i];

                var payload = new Dictionary<string, object>
                {
                    ["text"] = chunk.Text,
                    ["file_path"] = filePath,
                    ["blog_post_slug"] = metadata.Slug,
                    ["blog_post_title"] = metadata.Title,
                    ["chunk_index"] = i,
                    ["category"] = metadata.Category
                };

                await _vectorStore.UpsertAsync(Guid.NewGuid(), embedding, payload);
            }

            _logger.LogInformation("Ingested {File}", Path.GetFileName(filePath));
        }

        private List<TextChunk> ChunkContent(string content, int chunkSize = 500, int overlap = 50)
        {
            // Simple sentence-aware chunking
            var sentences = content.Split(new[] { ". ", ".\n", "!\n", "?\n" },
                StringSplitOptions.RemoveEmptyEntries);

            var chunks = new List<TextChunk>();
            var currentChunk = new StringBuilder();
            var currentLength = 0;

            foreach (var sentence in sentences)
            {
                if (currentLength + sentence.Length > chunkSize && currentChunk.Length > 0)
                {
                    chunks.Add(new TextChunk { Text = currentChunk.ToString() });

                    // Overlap: keep last sentence
                    currentChunk.Clear();
                    currentLength = 0;
                }

                currentChunk.Append(sentence).Append(". ");
                currentLength += sentence.Length;
            }

            if (currentChunk.Length > 0)
            {
                chunks.Add(new TextChunk { Text = currentChunk.ToString() });
            }

            return chunks;
        }

        private BlogMetadata ExtractMetadata(string content)
        {
            // Extract from markdown frontmatter or HTML comments
            var titleMatch = Regex.Match(content, @"^#\s+(.+)$", RegexOptions.Multiline);
            var categoryMatch = Regex.Match(content, @"");

            return new BlogMetadata
            {
                Title = titleMatch.Success ? titleMatch.Groups[1].Value : "Untitled",
                Category = categoryMatch.Success ? categoryMatch.Groups[1].Value : "General",
                Slug = Path.GetFileNameWithoutExtension(content)
            };
        }
    }

    public class TextChunk
    {
        public string Text { get; set; } = string.Empty;
    }

    public class BlogMetadata
    {
        public string Title { get; set; } = string.Empty;
        public string Category { get; set; } = string.Empty;
        public string Slug { get; set; } = string.Empty;
    }
}

Betere outputkwaliteit (minstens in theorie - Ik ben nog steeds aan het testen)

namespace BlogRAG.Services
{
    public class RAGGenerationService
    {
        private readonly IEmbeddingService _embedder;
        private readonly QdrantVectorStore _vectorStore;
        private readonly ILLMService _llm;
        private readonly ILogger<RAGGenerationService> _logger;

        public RAGGenerationService(
            IEmbeddingService embedder,
            QdrantVectorStore vectorStore,
            ILLMService llm,
            ILogger<RAGGenerationService> logger)
        {
            _embedder = embedder;
            _vectorStore = vectorStore;
            _llm = llm;
            _logger = logger;
        }

        public async Task<string> GenerateSuggestionAsync(
            string currentDraft,
            string requestType = "continue")
        {
            // 1. Generate embedding for current draft
            var draftEmbedding = await _embedder.GenerateEmbeddingAsync(currentDraft);

            // 2. Search for relevant past content
            var results = await _vectorStore.SearchAsync(
                queryVector: draftEmbedding,
                limit: 5,
                scoreThreshold: 0.7f);

            _logger.LogInformation("Found {Count} relevant chunks", results.Count);

            // 3. Build context from results
            var contextBuilder = new StringBuilder();
            foreach (var result in results)
            {
                var text = result.Payload["text"].ToString();
                var title = result.Payload["blog_post_title"].ToString();
                var score = result.Score;

                contextBuilder.AppendLine($"## From: {title} (relevance: {score:F2})");
                contextBuilder.AppendLine(text);
                contextBuilder.AppendLine();
            }

            // 4. Build prompt
            var systemPrompt = BuildSystemPrompt(requestType);
            var userPrompt = BuildUserPrompt(currentDraft, contextBuilder.ToString(), requestType);

            // 5. Generate with LLM
            var suggestion = await _llm.GenerateCompletionAsync(
                systemPrompt: systemPrompt,
                userPrompt: userPrompt,
                temperature: 0.7f);

            return suggestion;
        }

        private string BuildSystemPrompt(string requestType)
        {
            return requestType switch
            {
                "continue" => @"You are a technical blog writing assistant. Your role is to suggest
                    continuations for blog posts based on the author's past writing style and content.

                    Guidelines:
                    - Match the author's voice and technical depth
                    - Use similar patterns and structures from past posts
                    - Be specific and technical, not generic
                    - Include code examples when relevant
                    - Maintain consistency with past content",

                "improve" => @"You are a technical blog editor. Your role is to improve sections
                    of blog posts while maintaining the author's voice.

                    Guidelines:
                    - Preserve the author's style
                    - Improve clarity and flow
                    - Add technical depth where appropriate
                    - Suggest better examples from past posts
                    - Fix unclear explanations",

                "outline" => @"You are a technical blog outline generator. Your role is to suggest
                    outlines for new blog posts based on past structures.

                    Guidelines:
                    - Study the author's typical post structure
                    - Suggest sections based on successful past posts
                    - Include technical depth appropriate to topic
                    - Reference similar past articles",

                _ => "You are a helpful technical writing assistant."
            };
        }

        private string BuildUserPrompt(string currentDraft, string context, string requestType)
        {
            return $@"
# Current Draft
{currentDraft}

# Relevant Past Content
{context}

# Request
{GetRequestDescription(requestType)}

Please provide your suggestion based on the current draft and the relevant past content shown above.
Remember to maintain consistency with the author's past writing style and technical approach.
";
        }

        private string GetRequestDescription(string requestType)
        {
            return requestType switch
            {
                "continue" => "Continue writing from where the draft ends. Suggest the next 1-2 paragraphs.",
                "improve" => "Improve the current draft. Suggest specific edits and enhancements.",
                "outline" => "Create a detailed outline for completing this post.",
                _ => "Provide helpful suggestions."
            };
        }
    }
}

Streaming werkt perfect

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace BlogRAG.Console
{
    class Program
    {
        static async Task Main(string[] args)
        {
            // Setup DI and configuration
            var services = new ServiceCollection();

            var configuration = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .AddUserSecrets<Program>()  // For API keys
                .Build();

            services.AddLogging(builder => builder.AddConsole());

            // Register services
            var embeddingConfig = configuration.GetSection("BlogRAG:Embedding");
            services.AddSingleton<IEmbeddingService>(sp =>
                new OpenAIEmbeddingService(
                    embeddingConfig["ApiKey"]!,
                    embeddingConfig["Model"]!,
                    sp.GetRequiredService<ILogger<OpenAIEmbeddingService>>()));

            var llmConfig = configuration.GetSection("BlogRAG:LLM");
            services.AddSingleton<ILLMService>(sp =>
                new ClaudeLLMService(
                    llmConfig["ApiKey"]!,
                    llmConfig["Model"]!,
                    sp.GetRequiredService<ILogger<ClaudeLLMService>>()));

            var vectorConfig = configuration.GetSection("BlogRAG:VectorStore");
            services.AddSingleton(sp =>
                new QdrantVectorStore(
                    vectorConfig["Url"]!,
                    vectorConfig["ApiKey"] ?? "",
                    vectorConfig["CollectionName"]!,
                    sp.GetRequiredService<ILogger<QdrantVectorStore>>()));

            services.AddSingleton<IngestionService>();
            services.AddSingleton<RAGGenerationService>();

            var serviceProvider = services.BuildServiceProvider();

            // Run CLI
            await RunCLI(serviceProvider, configuration);
        }

        static async Task RunCLI(ServiceProvider serviceProvider, IConfiguration configuration)
        {
            System.Console.WriteLine("=== Blog RAG Assistant ===\n");
            System.Console.WriteLine("Commands:");
            System.Console.WriteLine("  ingest - Ingest markdown files");
            System.Console.WriteLine("  write - Start writing session");
            System.Console.WriteLine("  quit - Exit\n");

            while (true)
            {
                System.Console.Write("> ");
                var command = System.Console.ReadLine()?.Trim().ToLower();

                switch (command)
                {
                    case "ingest":
                        await IngestCommand(serviceProvider, configuration);
                        break;
                    case "write":
                        await WriteCommand(serviceProvider);
                        break;
                    case "quit":
                        return;
                    default:
                        System.Console.WriteLine("Unknown command");
                        break;
                }
            }
        }

        static async Task IngestCommand(ServiceProvider serviceProvider, IConfiguration configuration)
        {
            var ingestion = serviceProvider.GetRequiredService<IngestionService>();
            var markdownPath = configuration["BlogRAG:Ingestion:MarkdownPath"];

            System.Console.WriteLine($"Ingesting from {markdownPath}...");
            await ingestion.IngestMarkdownFilesAsync(markdownPath!);
            System.Console.WriteLine("Ingestion complete!\n");
        }

        static async Task WriteCommand(ServiceProvider serviceProvider)
        {
            var rag = serviceProvider.GetRequiredService<RAGGenerationService>();

            System.Console.WriteLine("\nEnter your draft (end with empty line):");
            var draft = new StringBuilder();
            string? line;

            while (!string.IsNullOrWhiteSpace(line = System.Console.ReadLine()))
            {
                draft.AppendLine(line);
            }

            System.Console.WriteLine("\nGenerating suggestion...\n");
            var suggestion = await rag.GenerateSuggestionAsync(draft.ToString());

            System.Console.WriteLine("=== Suggestion ===");
            System.Console.WriteLine(suggestion);
            System.Console.WriteLine("\n");
        }
    }
}

Kosten

Claude 3.5 Sonnet: $3 miljoen input tokens, $15/miljoen output

# 1. Clone/create project
dotnet new console -n BlogRAG
cd BlogRAG

# 2. Add packages
dotnet add package Qdrant.Client
dotnet add package OpenAI
dotnet add package Anthropic.SDK
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Configuration.UserSecrets

# 3. Set API keys (stored securely)
dotnet user-secrets init
dotnet user-secrets set "BlogRAG:Embedding:ApiKey" "sk-..."
dotnet user-secrets set "BlogRAG:LLM:ApiKey" "sk-ant-..."

# 4. Start Qdrant (local)
docker run -d -p 6333:6333 qdrant/qdrant

# 5. Run ingestion
dotnet run
> ingest

# 6. Start writing
> write

Typische blog schrijven sessie (20K input, 2K output): ruwweg $0,09Maandelijks gebruik (10 sessies): ongeveer $0,90/maand

Dit zijn ballpark cijfers gebaseerd op mijn vroege experimenten - je kilometers kunnen variëren afhankelijk van hoe spraakzaam je bent met de AI.

# Start Qdrant (if using local Docker)
docker start qdrant

# Run assistant
dotnet run
> write

# Enter your draft
I've been working on a new feature that uses Entity Framework Core...
[Ctrl+D or empty line]

# Get AI suggestion based on your past EF posts!

3.

Qdrant Vector Store (zelfde als Origineel!)

Zelfde API als lokale setup- wijs gewoon naar de lokale Docker of Qdrant Cloud!

Ingestiepijpleiding |-----------|--------|------| RAG Generation Service Eenvoudige console-client Het systeem draaien | Eerste tijd instellen | | ~$3.65 |

Totale installatietijd

  • : ongeveer 15 minuten vs ongeveer 2 uur voor lokale GPU setup - ervan uitgaande dat alles soepel verloopt, wat in mijn ervaring een gevaarlijke aanname is om te maken.
  • Dagelijks gebruik
  • Kostenanalyse

**Maandelijkse kostenraming (Persoonlijke blog)**Scenario

: Het schrijven van 4 blog berichten per maand

  1. Operatie Volume Kosten:

    • text-embedding-3-smallInitial intake (100 posts) One-time, 500K tokens
    • text-embedding-3-largeInbeddingen (queries, 40/maand) 40K tokens
    • LLM calls (40 suggesties) 800K input, 80K output
  2. Totaal maandelijksTer vergelijking:

    // Use OpenAI Batch API for ingestion
    var batch = await client.CreateBatchAsync(requests);
    // Wait hours, pay half price
    
  3. Lokale setup: $0/maand (maar $800+ GPU vooraf):

    // Don't re-embed identical text
    var cache = new Dictionary<string, float[]>();
    
  4. ChatGPT Plus: $20/maand (geen RAG, generiek):

    • Grammarly Premium: $12/maand (geen AI schrijven)
    • Break-even punt
  5. : Als je dit zou gebruiken voor 18+ maanden, lokale GPU betaalt voor zichzelf.:

    // Retrieve top 3 instead of top 10 chunks
    limit: 3  // 70% less input tokens
    

Anders is cloud goedkoper.

Hoewel ik nog steeds bezig ben uit te zoeken of mijn kostenprognoses kloppen - ik kan mijn woorden opeten in een paar maanden als de rekeningen binnen komen.

Kostenoptimalisatie Tips |-------|---------|---------|--------|--------| Gebruik kleinere modellen voor inbeddingen : $0.00002/1K tokens : $0.00013/1K tokens 6,5x kostenverschil!

Batch API-oproepen(50% goedkoper voor niet-urgent):

Cache-inbeddingen lokaal

// Switch models with one line
services.AddSingleton<ILLMService>(sp =>
    new ClaudeLLMService(  // Was GPT-4, now Claude
        config["ApiKey"],
        "claude-3-5-sonnet-20241022",  // Latest model
        sp.GetRequiredService<ILogger<ClaudeLLMService>>()));

Gebruik goedkopere modellen voor ontwerpenClaude 3.5 Haiku: $0,25/M input (12x goedkoper dan Sonnet)

GPT-4o-mini: $0,15/M input (20x goedkoper dan GPT-4)

# Works on Mac (no CUDA support)
dotnet run  # Just works!

# Works on Linux ARM (Raspberry Pi?)
dotnet run  # Just works!

# Works in Codespaces/Gitpod
dotnet run  # Just works!

Contextvenster beperken

Voordelen over lokale instellingen

// Handle 100 concurrent users? Easy with APIs
await Task.WhenAll(users.Select(u =>
    rag.GenerateSuggestionAsync(u.Draft)));

// Local? Limited by your single GPU

1.

# Deploy to Azure/AWS/GCP
dotnet publish -c Release
# Upload single binary, set env vars, done

# Local? Need to:
# - Include 12GB model files
# - Install CUDA on target machine
# - Ensure GPU drivers
# - Manage VRAM

Betere modelkwaliteit

Model Context Kwaliteit Local? Cloud?

Yes (needs 8GB VRAM)

Llama 3 70B 8K Uitstekend Nee (Heeft 48GB VRAM nodig) Ja

  • GPT-4 Turbo 128K Uitstekend Nee Ja
  • Sonnet Claude 3,5 200K Beste Nee Ja
  • Cloud geeft u toegang tot 70B+ modellen
  • dat $10K+ GPU hardware zou vereisen.

Tenminste, dat is de theorie - ik ben nog steeds te leren of de grotere modellen daadwerkelijk produceren merkbaar betere blog inhoud in de praktijk.

2.

Instant Updates

Geen modeldownloads

  • , geen GGUF conversies, geen compatibiliteitscontroles.
  • Dit is echt briljant als je experimenteert met verschillende modellen om te zien wat het beste werkt.

Cross-Platform

Lokale aanpak is alleen Windows + NVIDIA.

4.

  • Schaalbaarheid
  • Eenvoudigere implementatie

Beperkingen en trade-offs

1.

Privacybelangen

// Use abstraction layer
public interface ILLMService
{
    // Switch providers easily
}

// Factory pattern
services.AddSingleton<ILLMService>(sp =>
{
    return config["Provider"] switch
    {
        "OpenAI" => new OpenAILLMService(...),
        "Anthropic" => new ClaudeLLMService(...),
        "Cohere" => new CohereLLMService(...),
        _ => throw new Exception("Unknown provider")
    };
});

Uw bloginhoud gaat naar OpenAI/Anthropic.

**Mitigatie:**Alleen gebruiken voor publieke blog-inhoud

public class HybridEmbeddingService : IEmbeddingService
{
    private readonly LocalOnnxEmbedding _local;
    private readonly OpenAIEmbeddingService _cloud;
    private readonly bool _preferLocal;

    public async Task<float[]> GenerateEmbeddingAsync(string text)
    {
        if (_preferLocal && _local.IsAvailable())
        {
            return _local.GenerateEmbedding(text);  // Free, fast
        }

        return await _cloud.GenerateEmbeddingAsync(text);  // Fallback
    }
}

Controleer het gegevensgebruiksbeleid van de provider:

  • OpenAI: API-gegevens die niet voor opleiding worden gebruikt (vanaf 2024)
  • Antropisch: Zelfde inzet

Als je vertrouwelijke inhoud opstelt, gebruik dan de lokale aanpak.**Ik ben comfortabel met dit voor mijn openbare blog, maar ik zou het niet gebruiken voor iets op afstand gevoelig - en je moet niet geloven mijn woord voor wat "remotely sensitive" betekent voor uw use case.**2.

  1. Netwerkafhankelijkheid
  2. Geen internet = geen assistent.
  3. Mitigatie:

Cache vorige suggesties lokaal

Implementeer offline modus voor het bewerken

Terugvallen naar lokale kleinere modellen |---------|-------|-------| 3. Matigheid API oproepen duren 1-3 seconden vs <1s lokaal. Realiteitscontrole: Lokaal: 0.5s generatie Wolk: 2s generatie Verschil: 1.5s (perfect aanvaardbaar voor het schrijven van hulp in mijn ervaring - hoewel ik veronderstel dat het afhangt van hoe ongeduldig je bent) 4.

Leverancier Lock-in

  • Het schakelen van API's vereist codewijzigingen.
  • Mitigatie:
  • Hybride aanpak: beste van beide werelden
  • Kun je lokale en cloud mengen?
  • Absoluut!

Gebruik lokaal voor inbeddingen (goedkoop, snel), cloud voor LLM (kwaliteitskwesties)

  • Inbeddingen lokaal: Bespaar $ 0,004/maand (klein bedrag, toegegeven)
  • LLM in de cloud: Get GPT-4/Claude kwaliteit
  • Dit is eigenlijk mijn
  • aanbevolen aanpak
  • Hoewel ik nog steeds aan het experimenteren ben om te zien of het de juiste balans is:

**Voer kleine inbedding model lokaal (geen GPU nodig)**Gebruik cloud API's voor LLM

Qdrant lokaal voor ontwikkeling, cloud voor productie

  1. ConclusieDe cloud alternatief voor "Advocaat GPT" geeft je ongeveer 80% van de voordelen met 20% van de complexiteit - of althans dat is mijn ervaring tot nu toe geweest:
  2. Functionarie Local CloudInstellen 2-4 uur 15 minuten
  3. Behoefte aan hardware Behoefte aan NVIDIA GPU Eender welke computerModelkwaliteit: 7B-13B GPT-4, Claude 3.5
  4. Maandelijkse kosten: $0 ongeveer $3-5Latency .. 0.5s .. 2s . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
  5. Privacy 100% lokaal verzonden naar API'sAlleen Windows Mac/Linux/Windows

Onderhoud Modelupdates, CUDA-updates Geen**Wanneer de cloud moet worden gebruikt:**Je hebt geen NVIDIA GPU.

Je bent op Mac/Linux

Je wilt het makkelijkste pad

U schrijft minder dan 10 posten per maand

U heeft GPU hardware

Hoewel ik moet toegeven dat ik nog steeds de gotcha's leer.

  • Volgende stappen[Probeer het.

: Stel vanmiddag de cloudversie in

Finding related posts...
logo

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