Back to "Établissement d’un "Lawyer GPTM SK1 pour votre blog - Fine Tuning LLM Alternative: RAG with Qdrant and Generic Online LLMs"

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

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

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

AI C# Cloud LLM OpenAI Qdrant RAG

Établissement d’un "Lawyer GPTM SK1 pour votre blog - Fine Tuning LLM Alternative: RAG with Qdrant and Generic Online LLMs

Wednesday, 12 November 2025

Introduction

Dans mon 8-part "Lawyer GPTM SK2 série, Je vous ai montré comment construire un assistant d'écriture local complet fondé sur le RAG, - utilisant l'accélération GPU,, les LLM locaux,, et les bases de données vectorielles.

Mais 's être honnêtes - n'a pas tout le monde une station de travail avec un GPU NVIDIA, 96GB de RAMM SK4 et la patience pour mettre en place CUDAMSC5 cuDNNMST6 et se disputer des modèles GGUFMSSK7 Que faire si vous voulez simplement les avantages d'un assistant à la rédaction du blog sans l'investissement matérielMSL8

Cet article présente la solution de rechange fondée sur le cloud-, : la même approche RAGM SK2 la same Qdrant vector database, mais en utilisant les APIs LLM du cloud plutôt que l'inférence localeMSC4 Pensez à ce qu'il s'agit d'une solution MST5Lawyer GPT Lite

Divulgation complète: J’apprente encore quelle approche fonctionne le mieux dans la pratique , prenez donc mes estimations des coûts et mes allégations de rendement avec un pinch of salt. Je peux dire que cette approche par nuage s’est révélée remarquablement simple à mettre en place comparativement à la route GPUM SK4

NOTE: C'est une partie des expériences que j'ai réalisées avec l'A.I. M SK1 rédaction assistée) + ma propre éditionMST4 La même voixM ST5 le même pragmatismeM st6 quelques doigts plus rapidesMst7

Pourquoi une solution de rechange à la nuage?

L'approche originale

La série complète "Lawyer GPT" construit un système qui

  • Exécute 100% localement (privacy)
  • Aucun coût d'API
  • GPU rapide-inférence accélérée
  • Besoin de GPU NVIDIA (8GB+ VRAMM SK2
  • Établissement complexe (CUDAM SK1 cuDNN , gestion de modèles)
  • Limité aux modèles que vous pouvez intégrer à VRAM
  • Déploiement axé sur Windows-

L'alternative à la nuage

Cette approche vous donne

  • Aucun GPU n'est requis (exploite sur une machine quelconque
  • Établissement simple (no CUDA/cuDNNM SK2
  • Accès aux modèles frontaliers (GPT -4, ClaudeM SK2 etc.)
  • Cross-platform M SK1Windows,MacMST3LinuxMSC4
  • Amélioration de la qualité des produits (larger, modèles plus performantsM SK2
  • Coûts liés à l’API (mais raisonnable pour usage personnel)
  • Données envoyées aux API de tiers-
  • La disponibilité dépend du réseau

Quand utiliser lequel?

Utilisez l'approche locale Utilisez l'Approche de la nuage
La protection des renseignements personnels est cruciale.
Vous avez du matériel GPU Vous'Vous êtes sur Mac
Utilisation de volume élevé Utilise modérée
Vous aimez faire des travaux d’arpentage Vous voulez obtenir des résultats rapides

Aperçu de l'architecture

La version nulle conserve les mêmes fondations RAG mais échange l'inférence locale LLM pour les appels API:

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

Principales différences:

  • Modèle d'intégration: OpenAIM SK1s text-embedding-3-small API au lieu du modèle BGE local
  • LLM: Claude 3.5 Sonnet ou GPTM SK2 API au lieu du Mistral local/Llama
  • Aucun GPU n'est requis: Toute la programmation CPU-basée localementM SK2 se produit dans le nuage
  • Un déploiement plus simple: Unique exécutable, aucun fichier modèle à gérer

Je dois noter que je n’ai pas encore effectué de comparaisons approfondies entre les deux approches.

Équipe technologique

Cadre de base

  • .NET 9 - Même que la série originale
  • C# M SK1 - Caractéristiques linguistiques modernes

APIs de nuages

  • OpenAI API - Embeddings texte-embeddingMSC3smallM SK4 MSC5 LLM SGPTMST7
  • API anthropique - Alternative LLM Claude 3.5 SonnetMSC3
  • Les deux - Vous pouvez mélanger et joindre les deux

Base de données des vecteurs

  • Qdrant - Même que l'original, peut être exécuté localement par Docker ou en utilisant Qdrant Cloud
  • Solutions de rechange: PineconeM SK1 Weaviate Cloud (options gérées)

Options du client

  • Appuyer la console - Le plus simpleM SK1 parfait pour l'essai
  • Blazor WebAssembly - Web-basé sur , fonctionne partout
  • Avalonia -Cross+-Desktop de plate-forme

Établissement: Le chemin rapide

1. Installer Qdrant

Option A: Docker local M SK1Recommandé pour le développement)

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

Option B: Cloud Qdrant (Le plus facileM SK2

  1. Inscrivez-vous à nuages
  2. Créer un regroupement gratuit
  3. Obtenez votre clé API et un URL de regroupement

Aucune CUDA, aucun cuDNN , aucune installation de pilotes requise!

2. Obtenir les clés API

OpenAI (pour les embeddings + LLMM SK2

  1. Envoyer à platform.openai.com
  2. Créer la clé API
  3. Établir les limites d'utilisation (importantes!)

Anthropiques (optionnelM SK1 pour Claude):

  1. Envoyer à console.anthropic.com
  2. Créer la clé API

3. Configuration

Créer 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
    }
  }
}

Ce n’est pas le cas. Aucune mise en place de GPU, aucun téléchargement du modèle M SK1 fichiers GB), aucune gestion VRAMMSC3

Mise en œuvre

Services de base

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

Principaux avantages par rapport à la situation locale:

  • Aucune mise en place du temps d'exécution ONNX
  • Aucune gestion de mémoire GPU
  • Battage automatique par OpenAI
  • État de la qualité d'implantation conforme à l'art

Coût: ~$0.0001 par SSK2K de tokens MSC3excellentment bon marchéSMC4

  • Traitement des articles sur le blog 100 (~500K tokensM SK2 |
  • Usage quotidien (10 demandesM SK1 ~$0.001/jour SSK3 $0.30/mois

2. Service Cloud LLM

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

Avantages sur le plan local:

  • Aucun modèle à charger (démarrage instantané)
  • Aucune limite VRAM (utiliser 200Contexte de K si nécessaire)
  • Une meilleure qualité de la production (au moins en théorie - IM SK2m encore à l'essai
  • Les flux fonctionnent parfaitement

Coût:

  • Claude 3.5 Sonnet
  • Session typique d'écriture de blog (20K input, 2K outputM SK3 approximativement MSC4
  • Utilisation mensuelle (10 sessionsM SK1 approximativement $0.90/mois

Ces chiffres sont basés sur mes premiers expériences. - votre distance peut varier en fonction de la chatte avec l’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();
        }
    }
}

Le même API que la mise en place locale - indiquer simplement Docker local ou Qdrant Cloud

Pipeline d'ingestion

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

Service de production RAG

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

Client de console simple

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

Faire fonctionner le système

Établissement de la première fois

# 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

Temps de mise en place total: approximativement 15 minutes par rapport à approximatif 2 heures pour la mise en place de GPU locale M- supposant que tout se déroule sans heurt, ce qui, selon ma expérience, est une hypothèse dangereuse de faire

Utilisation quotidienne

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

Analyse des coûts

Estimation mensuelle des coûts (Blog personnelM SK1

Scénario: Écrire des articles de blog 4 par mois

Opération MSC1 Volume MSC2 Coût SMC3
Ingestion initiale
Embeddings queries, MSC3montantMSC4 5 6 Tokens 7 8 9
Les appels LLM (40 Suggestions) \800 Input de KM SK5 80 Outil de K
Total mensuel ~$3.65

Pour la comparaison:

  • Établissement local: M SK1mois (mais $800+GPU à l'avant-plan)
  • ChatGPT Plus: M SK1mois (sans RAG,génériqueMSC4
  • Premium Grammarly: M SK1mois (sans écriture par l'AI)

Fracture-sept points: Si vousM SK1d utiliser ce pour 18+ mois, le GPU local paye pour lui-mêmeMSC4 Autrement–, la nuage est moins coûteuseMST6 Même si jeMSSK7m toujours travaillant à déterminer si mes projections de coûts sont exactesMSL8 Je vais peut-être manger mes mots dans quelques mois lorsque les factures arrivent en

Conseils d'optimisation des coûts

  1. Utiliser des modèles plus petits pour les embeddings:

    • text-embedding-3-small: $0.00002/1K tokens
    • text-embedding-3-large: $0.00013/1K tokens
    • 6.5x différence de coûtsM SK1
  2. Batch API calls (50% moins cher pour les non--urgentsM SK2

    // Use OpenAI Batch API for ingestion
    var batch = await client.CreateBatchAsync(requests);
    // Wait hours, pay half price
    
  3. Emplacements de cache localement:

    // Don't re-embed identical text
    var cache = new Dictionary<string, float[]>();
    
  4. Utiliser des modèles moins coûteux pour les projets:

    • Claude 3.5 HaikuM SK1 $0.25/M entrées de (12x moins cher que Sonnet)
    • GPT-4oMSC1mini: M SK3M input (20x moins cher que GPT
  5. Limiter la fenêtre contextuelle:

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

Avantages par rapport à l'installation locale

1. Une meilleure qualité du modèle

Modèle
Mistral 7B MSC2 MSC3K USC4 Bon ESC5 Oui ESC6 Besoins OSC7GB VRAM ASC8
Llama MSC1 SMC2B MSC3 \MSC4K \ MSC5 Excellent Non MSC7 Nécessité MSC8 GB VRAM msc9 SMC10 Oui SMC11
GPTM SK1 Turbo MSC3K MSC4 Excellent SMC5 Non SMC6 Oui ESC7
Claude 3.5 Sonnet MSC2 MSC3K SMC4 Suivant ESC5 Non ESC6 Oui EESC7

La numérisation vous donne accès aux modèles 70B+ qui nécessiterait $10KM SK1Hardware GPU . Au moins, queMST4est la théorie MST5 J’apprente encore si les modèles plus grands produisent effectivement un contenu blog nettement meilleur en pratique.

2. Mise à jour immédiate

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

Aucun téléchargement de modèle, pas de conversions GGUF , aucune vérification de compatibilitéM SK2 C'est vraiment brillant lorsque vous experimentez avec différents modèles pour voir ce qui fonctionne le mieux.

3. CrossM SK1Platforme

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

L’approche locale est Windows + NVIDIA seulement.

4. Scalability

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

// Local? Limited by your single GPU

5. Déploiement plus simple

# 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

Limitations & CommerceM SK1déductions

1. Préoccupations liées à la protection des renseignements personnels

Le contenu de votre blog va à OpenAI/Anthropic.

Mesures d'atténuation

  • Utiliser uniquement pour le contenu du blog public
  • Vérifier la politique d'utilisation des données du fournisseur'
  • OpenAI: Données de l'API non utilisées pour la formation M SK1 comme des 2024)
  • Anthropiques: Le même engagement

Si vous ' rédigez des contenus confidentiels , utilisez l’approche locale . JeM SK3 me sens bien à l’aise avec cela pour mon blog public MSC4 mais je ne l’utiliserai pas pour quelque chose de très sensible

2. dépendance du réseau

Aucune connexion Internet = aucun assistant.

Mesures d'atténuation

  • Cache des suggestions antérieures localement
  • Mettre en œuvre le mode offline pour l'édition
  • Retourner à des modèles locaux plus petits

3. Durée de validité

Les appels API prennent 1-3 secondes par rapport à <1s local.

Examen de la réalité:

  • La génération locale: M SK1
  • génération de nuages: M SK1
  • Difference: M SK1s ( parfaitement acceptable pour l'aide à la rédaction dans ma expérience - même si je suppose qu'il dépend de votre impatience

4. Écluse du vendeurM SK1in

Changement d'API requiert des changements de code.

Mesures d'atténuation

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

Approche hybride: Le meilleur des deux mondes

Pouvez-vous mélanger local et nuage? Absolutement!

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

Utiliser local pour les embeddings (cheap,fasteM SK2cloud for LLM (quality mattersMSC4:

  • Embeddings localement: Économiser M SK1mois (moins d'argent, admittedlyMSC4
  • LLM en nuage: Obtenir la qualité GPT-4/Claude

C'est en fait mon approche recommandée, bien que j’experimente encore si ' est le bon équilibre.

  1. Run small embedding model locally (no GPU needed)
  2. Utiliser les APIs de nuage pour LLM
  3. Qdrant local pour le développement, nuage pour la production

Conclusion

L’alternative de nuage à "Lawyer GPT" vous donne approximativement 80% des avantages associés à |20% de la complexité.

Caractéristiques Locale
Temps de mise en place MSC1 SMC2 heures MSC3 4 minutes MSC5
Exigences matérielles GPU NVIDIA
Qualité du modèle MSC2BMSc3B SMC4 GPTS MC5 Claude SMC6
Coût mensuel $0 ≥ approximativement
Durée d’attente MSC2s MSC3
Protection des renseignements personnels
CrossM SK1platform Windows seulement MSC3 Mac/LinuxMSC5Windows MSC6
Maintenance Mise à jour du modèle, mises à jour de la norme CUDA MSC3 Aucune S

Quand utiliser la nuage:

  • Vous n’avez pas de GPU NVIDIA
  • Vous êtes sur Mac/Linux
  • Vous voulez le chemin le plus facile
  • Vous écrivez moins de 10 postes/mois
  • Vous faites confiance aux fournisseurs de nuages avec des contenus publics

Quand utiliser local:

  • Vous avez du matériel GPU
  • La protection des renseignements personnels est essentielle
  • Vous voulez $0 coût d'exploitation
  • Vous écrivez plus de 20 articles/montant
  • Vous aimez faire des tris.

La meilleure approche: Commencez par la nuvolat. Si vous l’aimez et que les coûts sont élevésM SK1 Limites de protection des renseignements personnels , migrer vers le local plus tard. L’architecture RAG est identique MSC4 vous échangez simplement le service LLMMST5 Même si je dois admettre que j’apprente encore les gottes iciMSL7

Prochaines étapes

  1. Essayez-le: Démarrer la version nulle ce matin
  2. Ingénèse: Processez vos articles de blog
  3. Écrire: Obtenez des suggestions d’AI fondées sur votre contenu antérieur
  4. Mesure: Coûts et utilité du tracé
  5. Décision: Garder la nuage ou migrer vers le local

La beauté du RAG est vous' n'êtes pas enfermé. La base de données vectorielle , le chunking, et la logique de recherche fonctionnent identiquement, que votre LLM soit sur votre GPU ou dans l'OpenAI

Commencez par simpler, prouver la valeurM SK1 puis optimiser. Je 'je suis toujours en train d’apprendre ce qui fonctionne le mieux pour moi-mêmeMSC4 donc je M SK5d être intéressé à entendre comment vous arrivez sur le plan de l’apprentissage.

Ressources

Articles connexes

Outils & Services

Code

  • Code d'exemple complet: [Référentiel GitHub] (à venir prochainementM SK2

Que vous soyez heureux de écrire avec votre nuage-assistant AI puissant.

logo

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