Att bygga en "Lawyer GPT" för din blogg - Fin Tuning LLM AlternativeM SK3 RAG med Qdrant och Generiska Online LLMs (Svenska (Swedish))

Att bygga en "Lawyer GPT" för din blogg - Fin Tuning LLM AlternativeM SK3 RAG med Qdrant och Generiska Online LLMs

Wednesday, 12 November 2025

//

23 minute read

Inleiding

I min 8-del "Lawyer GPTM SK2 serie, Jag visade er hur man bygger en komplett lokal RAG, med hjälp av GPU: s acceleration, som bygger upp skrivtecken från -, ,, lokala LLM och vektordatabaser.

Men låt oss vara ärliga. - alla har inte en arbetsstation med en NVIDIA GPU, ,, 96, GB RAM och tålamod med att installera CUDA, M SK5, cuDNN och , och knyta ihop GGUF-modeller, .. Tänk om du bara vill ha fördelarna med en bloggassistent utan att behöva hårdvarainvesteringar.

Detta artikel presenterar den molnbaserade alternativen - : samma RAG-metoden | , | samma Qdrant-vektordatabas | МSK3 | men med hjälp av moln LLM APIs istället för lokalt slutförstånd |. | Tänk på det som |

Full disclosure: Jag ' lär mig fortfarande vilket angreppssätt fungerar bäst i praktiken, så ta mina kostnadsskattningar och prestationer med en pinch saltM SK3 Vad jag kan säga är att den här molnmetoden har visat sig vara anmärkningsvärt enkelt att installera jämfört med GPU-routet

NOTET: Det här är en del av mina experiment med AI ( assisterad redigering ) + mitt eget redigerande M SK4 Samma röst, , samma pragmatism,; bara snabbare fingrar

Varför ett molnalternativ?

Den ursprungliga metoden

Den fulla "Lawyer GPT " serien bygger ett system som

  • Runner 100% lokalt (privacy)
  • Inga API-kostnader
  • snabb GPU-accelerated inference
  • Benötar NVIDIA GPU (8GB+ VRAMM SK2
  • Kompleks uppsättning (CUDA, cuDNNM SK2 modellstyrning )
  • Begränsat till modeller som kan passa i VRAM
  • Windows-fokussed deployment

Cloud Alternative

Den här metoden ger dig

  • Inget GPU krävs ( kör på vilken maskin som helst )
  • Enkel konfiguration (no CUDA/cuDNNM SK2
  • Tillgång till gränsmodeller (GPT -4, Claude, etcM SK3
  • Cross-platform | ( | Windows | , | Mac | Microsoft | Linux
  • Bättre utgångskvalitet ( större , mer kapabla modeller
  • API-kostnaden (men ändå rimlig för personlig användning )
  • Data skickad till tredje partens APIs
  • Tidslängden beror på nätverket

När man ska använda vilken?

Använd den lokala tillvägagångssättet
Privatsphäre är avgörande Möjligheterna är viktigast
Du har GPU-hardware Du' är på Mac
Hög volym tillförsel Mäktig tillfördel få poster
Du gillar att pyssla med saker Du vill ha resultat snabbt

Arkitektur överblick

Cloudversionen håller samma RAG fundamentaler men byter lokal LLM inferens för API sändningar

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

Nyckelskillnader:

  • Embeddingsmodell: OpenAI's text-embedding-3-small API istället för den lokala BGE-modellen
  • LLM: Claude 3.5 Sonnet eller GPT
  • Inget GPU krävs: Allting i CPUn - lokalt baserat , beräkningar händer i molnet
  • Enkelare utpostning: Enkelt utförbart, inga modellfiler att sköta

Jag borde lägga märke till att jag inte har gjort omfattande utvärderingar för att jämföra de två metoderna än. Jag är fortfarande i forskningsfasen. Men de första resultaten är lovande nog att dela med mig av

Teknologistack

Kernframework

  • .NET 9 - Samma som originalserien
  • C# 13 - Moderna språkdefinitioner

Cloud APIs

  • OpenAI-API - Embeddings | | text | - | embedding |-3- | small | МSK4 | LLM | MC6 | GPT | CRT
  • antropologisk API - Alternativ LLM (Claude 3.5 Sonnet)
  • Båda - Du kan blanda och matcha

Vektordatabas

  • Qdrant - Samma som originalt, kan köra lokalt via Docker eller använda Qdrant Cloud
  • Alternativ: PineconeM SK1 Viaviate Cloud (manade alternativ

Client-optioner

  • Konsoleapp - Den enklaste , bra att testa
  • Blazors webbyggande - Web -based, fungerar var som helst
  • Avalonia - Cross

Setup: Den snabba vägen

1. Installera Qdrant

Option A: Lokal Docker (Empvisad för utveckling)

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

Option B: Qdrant Cloud (EasiestM SK2

  1. Teka upp på: moln
  2. Skep en fri kluster
  3. få din API-kliv och kluster-URL

Ingen CUDA, ingen cuDNN , inga driverinstallationer behövs!

2. Ta API-kyler

OpenAI ( för inbäddar + LLM

  1. Gå till platform.openai.com
  2. Att skapa API-kliv
  3. Se användargränser (insignificant!)

antropologisk (optionell, för ClaudeM SK2

  1. Gå till konsol.anthropic.com
  2. Att skapa API-kliv

3. Konfiguration

Att skapa appsettings.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
    }
  }
}

Det är det. Inget GPU-uppsättning , inga modelldownloads (12 GB-filer M SK2 ingen VRAM-behandling .

Implementering

Kerntjänster

1. Cloud Embedding Service

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

Nyckliga fördelar jämfört med lokala

  • Ingen ONNX Runtime-uppsättning
  • Ingen GPU minneshantering
  • Automatisk slagning av OpenAI
  • Status: - av - och - lagd kvalitet

kostnaderna: ~$0.0001 per | 1 | K-token |( | Väldigt billigt

  • Processing 100 bloggposter (~500K tokens): ~$0.05
  • Daily usage (10 queries): ~$0.001/day | | 3 | 4 | månad

2. Cloud LLM 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;
                }
            }
        }
    }
}

Möjligheter över lokal nivå

  • Ingen modell laddad (instant startup)
  • Inga VRAM-gränser (använd 200K kontext om nödvändigt
  • Bättre utgångskvalitet (växmin i teorin
  • Streaming fungerar perfekt

kostnaderna:

  • Claude 3.5 Sonnet
  • En typisk bloggskrivningssession (20K input, 2K outputM SK3 ungefär $0.09
  • Montiellt användande (10 sestioner): grovt sett $0.90/ månad

Det här är sammanslagna siffror baserat på mina tidiga experiment. - din färdslängd kan variera beroende på hur pratlig du är med AI

3. Qdrant Vector Store

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

Samma API som lokal Konfiguration - bara peka på den lokala Docker eller Qdrant Cloud

Intagningskanal

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

RAG Generation Service

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

Enkel konsolklient

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

Att köra systemet

Första gången ställs upp

# 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

Hela inställningstid: grovt ♫ 15 ♫ minuter jämfört med ungefär ♫2 ♫ timmar för den lokala GPU-uppsättningen ♫

Varje dag

# 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!

kostnadsanalys

månadens kostnadsskattning (Personal Blog)

Scenario: Att skriva 4 bloggar om månaden

Operation Volumen kostnaderna
Initial ingestion (100 poster M SK2 En-timmar
Embeddings ( sökningar , МSK3 månad )
LLM-sändningar (40 förslag) 800 input
sammanlagt i månaden ~$3.65

För jämförelse:

  • Lokal ställning: M SK1månader ( men | $800+ | GPU uppfront
  • ChatGPT Plus
  • Grammarly Premium

Brek- sjupunkt: Om du ' skulle använda det här i ♫ 18+ månader ♫, ♫ så betalar lokala GPU sig själva | . ♫ Annars ♫ , ♫ är molnet billigare ♫

Försök optimera kostnaderna

  1. Använd mindre modeller för inbäddar:

    • text-embedding-3-small: $0.00002/1K tokens
    • text-embedding-3-large: $0.00013/1K tokens
    • 6.5x kostnadsskillnader
  2. Batch API ringer (50% billigare för icke-

    // Use OpenAI Batch API for ingestion
    var batch = await client.CreateBatchAsync(requests);
    // Wait hours, pay half price
    
  3. Cache-inbäddar lokalt:

    // Don't re-embed identical text
    var cache = new Dictionary<string, float[]>();
    
  4. Använd billigare modeller för ritningar:

    • Claude 3.5 Haiku : $0.25/ Input (12 x billigare än Sonnet )
    • GPT-4oM SK1mini: $0.15/M input MSC4x billigare än GPT
  5. Begränsa kontextvenster:

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

fördelar med att installera på plats

1. bättre modellkvalitet

Modell Kontext МSK2 Jakt Lokal ?
Mistral 7B S8K M Bra MPSK5 Ja Pbehov V8GB VRAM MVSK8 Ja
Llama ♫ ♫ 3 ♫
GPT
Claude ♫ ♫ 3.5 ♫ Sonnet ♫

Cloud ger dig tillgång till 70B+ modeller som skulle kräva $10K+ GPU-hardware . Åtminstone ♫ , ♫ att ♫' ♫ är teorin | - ♫ I ' ♫ lär jag mig fortfarande om huruvida de större modellerna faktiskt ger märkligt bättre blogginhoud i praktiken ♫

2. Omedelbart uppdaterade

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

Inga modeller ladda ner, inga GGUF-konverteringar , inga kompatibilitetskontroller

3. Cross-Platform

# 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!

Lokala tillvägagångssätt är Windows + NVIDIA bara.

4. Skalbarhet

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

// Local? Limited by your single GPU

5. Enklare användning

# 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

Begränsningar & Handel - avbetalningar

1. Privacy Concerns

Din blogginhoud går till OpenAI/Anthropic.

Mitigation:

  • Använd bara offentliga bloggar
  • Se providerns dataanvändningspolicy
  • OpenAI: API-data inte används för utbildning ( som i 2024)
  • Antropotiskt: Samma åtagande

Om du redigerar dollig innehåll, använder du den lokala metoden. Jag är bekväm med det här för min offentliga blogg, men jag skulle inte använda det för något avlägsna känsligt, och du borde inte använda mitt ord för vad det betyder för ditt användandefall,

2.nätverks beroende

Inget internet = ingen assistent .

Mitigation:

  • Cache tidigare förslag lokalt
  • Implementera avgränsad Modus för redigering
  • Falla tillbaka till lokala mindre modeller

3. Storlek

API sändningar tar 1-3 sekunder jämfört med <1s lokala sändning

Verklighetens kontroll:

  • Lokal generation
  • Generationen av moln
  • Skillnaden: 1.5s (mycket accepterat för hjälp i skrivandet i min erfarenhet | - | fast jag antar att det beror på hur utpatient du är

4. Verkoperlocka

Att byta API kräver kodförändringar.

Mitigation:

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

Hybrida tillvägagångssätt : Beste av båda världarna

Kan du blanda lokal och moln? Absolut!

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

Använd lokala embeddings ( billigt , snabbt M SK2 moln för LLM ( kvalitetproblem:

  • Inbäddar lokalt : spara $0.004/ månad | ( | liten summa
  • LLM i moln: få GPT-4/Claude kvalitet

Det här är faktiskt min tillämpningssätt, fast jag ' fortfarande experimenterar för att se om det är rätt balans

  1. Runna ett litet embeddingsmodell lokalt ( inget GPU behövs)
  2. Använd moln APIs för LLM
  3. Qdrant lokal för utveckling, moln för produktion

Slutsatsen

Den molnbaserade alternativen till "Lawyer GPT

Funktion Lokalt moln МSK3
Inställningstid
Hardwareanforderungen NVIDIA GPU Alla datorer
Modellkvalité 7B МSK3 B GPT
Måntalkostnaden
Fördröjning ♫ ♫ 0.5 ♫ s ♫
Privatsphäre 100% lokalt МSK3 skickad till APIs
Cross
Konservning Modelluppdateringar, CUDA-uppdateningar Ingen M

När man ska använda moln:

  • Du har inte NVIDIA GPU
  • Du är på Mac
  • Du vill ha den enklaste vägen
  • Du skriver mindre än 10 meddelanden /månader
  • Du litar på cloud-provider med offentliga innehåll

När man ska använda lokalt:

  • Du har GPU-hardware
  • Privatliv är viktigt
  • Du vill ha $0 operativ kostnad
  • Du skriver mer än 20 träffar
  • Du tycker om att pyssla

bästa tillvägagångssätt: Börja med moln.

Nästa steg

  1. Försök det.: montera upp molnversionen i eftermiddagen
  2. Ingest: Processera dina bloggar
  3. skriva: få AI-rådet baserat på din tidigare innehåll
  4. Määra: Beloppskostnader och nytta
  5. Bestämma: Håll molnet eller migrira till lokalt

skönheten med RAG är du' är inte inlåst. Vektordatabasen , chunkning , och återvinningslogiken fungerar likadant oavsett om din LLM är på din GPU eller i OpenAI

Börja enkelt , bevisa värdet, sedan optimera. JagM SK3 lär mig fortfarande vad som fungerar bäst självMska4 så jagMske5 skulle vilja höra hur du går på

resurser

verktyg & Services

Kod

  • Hela exemplet koden: [GitHubs arkiv

Lycka till att skriva med din moln-driven AI-assistent.

Finding related posts...
logo

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