Back to "Costruire un "GPT avvocato" per il tuo blog - Fine Tuning LLM Alternativa: RAG con Qdrant e LLM online generico"

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

Costruire un "GPT avvocato" per il tuo blog - Fine Tuning LLM Alternativa: RAG con Qdrant e LLM online generico

Wednesday, 12 November 2025

Introduzione

Nella mia8-parte serie "Avvocato GPT", Vi ho mostrato come costruire un assistente di scrittura locale completo basato su RAG utilizzando l'accelerazione GPU, LLM locali, e database vettoriali.

E 'potente, privato, e funziona interamente sul vostro hardware.

Ma siamo onesti: non tutti hanno una workstation con una GPU NVIDIA, 96GB di RAM, e la pazienza di impostare modelli CUDA, cuDNN e wrangle GGUF.

Che cosa succede se volete solo i benefici di un assistente di scrittura del blog senza l'investimento dell'hardware?

Questo articolo presenta l'alternativa basata sul cloud: stesso approccio RAG, stesso database vettoriale Qdrant, ma utilizzando API cloud LLM invece di inferenza locale.

Immaginatelo come "Avvocato GPT Lite" - configurazione più facile, inferiore barriera all'ingresso, e potenzialmente migliore qualità di produzione utilizzando modelli di frontiera.

Piena divulgazione: Sto ancora imparando quale approccio funziona meglio nella pratica, in modo da prendere le mie stime dei costi e richieste di prestazioni con un pizzico di sale.

Quello che posso dire è che questo approccio cloud si è rivelato estremamente semplice da impostare rispetto al percorso GPU.

  • NOTA: Questo fa parte dei miei esperimenti con l'AI (elaborazione assistita) + il mio editing.
  • Stessa voce, stesso pragmatismo, solo dita piu' veloci.
  • Perché una Cloud Alternative?
  • L'approccio originale
  • La serie completa "Avvocato GPT" costruisce un sistema che:
  • Esegue il 100% localmente (privacy)
  • Nessun costo API

Inferenza accelerata GPU veloce

Richiede NVIDIA GPU (8GB+ VRAM)

  • Configurazione complessa (CUDA, cuDNN, gestione dei modelli)
  • Limitato ai modelli che puoi montare in VRAM
  • Implementazione focalizzata su Windows
  • L'alternativa cloud
  • Questo approccio vi dà:
  • Nessuna GPU richiesta (esegui su qualsiasi macchina)
  • Configurazione semplice (nessuna CUDA/cuDNN)
  • Accesso ai modelli di frontiera (GPT-4, Claude, ecc.)

Piattaforma trasversale (Windows, Mac, Linux)

Migliore qualità dell'output (modelli più grandi e capaci) |--------------------|-------------------| Costi API (sebbene ragionevoli per l'uso personale) Dati inviati a API di terze parti La latenza dipende dalla rete Quando usare quale?

| Usa l'approccio locale | Usa l'approccio cloud |

| La privacy è critica | La comodità è importante per la maggior parte |

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

| Hai hardware GPU | Sei su Mac/Linux/laptop |

  • | Uso ad alto volume | Uso moderato (pochi post/mese) || Ti piace armeggiare | Vuoi risultati veloci |text-embedding-3-smallPanoramica dell'architettura
  • **La versione cloud mantiene gli stessi fondamentali RAG ma scambia l'inferenza LLM locale per le chiamate API:**Principali differenze:
  • Modello di inserimento: OpenAI's
  • API invece del modello locale BGELLM

: Claude 3.5 Sonnet o API GPT-4 invece di locale Mistral/Llama

Nessuna GPU richiesta

: Tutto ciò che è basato sulla CPU localmente, il calcolo avviene in cloud

  • Implementazione più semplice: singolo eseguibile, nessun file modello da gestire
  • **Devo notare che non ho ancora eseguito un ampio benchmark comparando i due approcci - sono ancora in fase esplorativa me stesso.**Ma i primi risultati sono abbastanza promettenti da essere condivisi.

Stack tecnologico

  • Quadro centrale.NET 9
  • - Come la serie originaleC# 13
  • - Caratteristiche linguistiche moderneAPI cloud

API OpenAI

  • **- Inserzioni (testo-inserzione-3-piccolo) + LLM (GPT-4)**API antropiche
  • **- Alternative LLM (Claude 3.5 Sonnet)**Entrambi

- Puoi mischiare e abbinare!

  • Banca dati vettorialeQdrantCity name (optional, probably does not need a translation)
  • - Come originale, può essere eseguito localmente tramite Docker o utilizzare Qdrant CloudAlternative
  • **: Pinecone, Weaviate Cloud (opzioni gestite)**Opzioni client

App console

- Più semplice, ottimo per i test

Blazor WebAssembly

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

- Web-based, funziona ovunque

  1. AvaloniaCity name (optional, probably does not need a translation)- Desktop multipiattaforma (Windows, Mac, Linux)
  2. Impostazione: il percorso rapido

Installa Qdrant

Opzione A: Docker locale (consigliato per lo sviluppo)

**Opzione B: Qdrant Cloud (più semplice)**Iscriviti a

  1. nube.qdrant.ioCrea un cluster libero
  2. Ottieni la chiave API e l'URL del cluster
  3. Niente CUDA, niente cuDNN, niente driver!

**2.**Ottieni i tasti API

  1. OpenAICity name (optional, probably does not need a translation)(per inserti + LLM):
  2. Vai a

platform.openai.com

Crea chiave APIappsettings.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
    }
  }
}

**Impostare i limiti di utilizzo (importante!)**Antropico

(facoltativo, per Claude):

Vai a

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

Crea chiave API

  • Configurazione
  • Crea
  • Tutto qui.

**Nessuna configurazione GPU, nessun download di modelli (12GB file), nessuna gestione VRAM.**Attuazione

  • Servizi di base

Servizio di integrazione delle nuvole

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

Principali vantaggi rispetto a locali:

  • Nessuna configurazione di ONNX Runtime
  • Nessuna gestione della memoria GPU
  • Batching automatico da OpenAI
  • Qualità d'integrazione all'avanguardia

Costo:

  • : ~$0.0001 per gettoni 1K (molto economici)
  • Elaborazione di 100 post sul blog (~500K token): ~$0.05
  • Uso giornaliero (10 query): ~$0.001/giorno = $0.30/mese

Servizio Cloud LLM

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

**Vantaggi rispetto a quelli locali:**Nessun caricamento del modello (avvio istantaneo)

Nessun limite VRAM (usare il contesto 200K se necessario)

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

Migliore qualità dell'uscita (almeno in teoria - sto ancora testando)

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

Lo streaming funziona perfettamente

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

Costo

Claude 3.5 Sonnet: gettoni di ingresso da 3 milioni di dollari, uscita da 15 milioni di dollari

# 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

Tipica sessione di scrittura del blog (20K input, uscita 2K): circa $0.09Utilizzo mensile (10 sessioni): circa $0.90/mese

Queste sono figure di base sulla base dei miei primi esperimenti - il vostro chilometraggio può variare a seconda di quanto chiacchierate con l'IA.

# 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 (stesso come originale!)

Stessa API come impostazione locale- basta puntare a Docker locale o Qdrant Cloud!

Gasdotto per l'ingestione |-----------|--------|------| Servizio di generazione RAG Client semplice della console Eseguire il sistema | Impostazioni per la prima volta | | ~$3.65 |

Tempo totale di setup

  • : circa 15 minuti contro circa 2 ore per la configurazione GPU locale - supponendo che tutto vada liscio, che secondo la mia esperienza è un'ipotesi pericolosa da fare.
  • Uso giornaliero
  • Analisi dei costi

**Stima dei costi mensili (blog personale)**Scenario

: Scrivere 4 post sul blog al mese

  1. | Funzionamento | Volume | Costo |:

    • text-embedding-3-small| Ingestione iniziale (100 posti) | Una volta, token da 500K | $0.05 |
    • text-embedding-3-large| Embeddings (queries, 40/mese) | 40K tokens | $0.004 |
    • | Chiamate LLM (40 suggerimenti) | Ingresso 800K, uscita 80K | $3,60 |
  2. Totale mensilePer il confronto:

    // Use OpenAI Batch API for ingestion
    var batch = await client.CreateBatchAsync(requests);
    // Wait hours, pay half price
    
  3. Impostazione locale: $0/mese (ma $800+ GPU upfront):

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

    • Grammatica Premium: $12/mese (nessuna scrittura AI)
    • Punto di pareggio
  5. : Se lo si usa per 18+ mesi, la GPU locale si paga da sola.:

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

Altrimenti, il cloud è più economico.

Anche se sto ancora cercando di capire se le mie proiezioni di costo sono accurate - potrei mangiare le mie parole in pochi mesi, quando le bollette arrivano.

Suggerimenti per l'ottimizzazione dei costi |-------|---------|---------|--------|--------| Utilizzare modelli più piccoli per l'incorporamento : $0.00002/1K gettoni : $0.00013/1K gettoni 6.5x differenza di costo!

Chiamate API in batch(50% più economico per non urgenti):

Incorpora cache localmente

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

Utilizza modelli più economici per le bozzeClaude 3.5 Haiku: ingresso $0,25/M (12x più economico di Sonnet)

GPT-4o-mini: ingresso $0,15/M (20x più economico di 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!

Finestra del contesto limite

Vantaggi rispetto alla configurazione locale

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

Migliore qualità del modello

| Modello | Contesto | Qualità | Locale? | Nuvola? |

| Mistral 7B | 8K | Good | Yees (needs 8GB VRAM) | Yes |

| Llama 3 70B | 8K | Eccellente | No (bisogni VRAM da 48GB) | Sì |

  • | Turbo GPT-4 | 128K | Eccellente | No | Sì |
  • | Claude 3.5 sonnet | 200K | Best | No | Sì |
  • Cloud consente di accedere ai modelli 70B+
  • che richiederebbe $10K+ hardware GPU.

Almeno, questa è la teoria - Sto ancora imparando se i modelli più grandi in realtà producono contenuti blog notevolmente migliori in pratica.

2.

Aggiornamenti istantanei

Nessun modello di download

  • , nessuna conversione GGUF, nessun controllo di compatibilità.
  • Questo è veramente brillante quando si sta sperimentando con diversi modelli per vedere ciò che funziona meglio.

Cross-Platform

L'approccio locale è solo Windows + NVIDIA.

4.

  • Scalabilità
  • Semplificazione della distribuzione

Limitazioni e compromessi

1.

Preoccupazioni in materia di privacy

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

Il contenuto del tuo blog va a OpenAI/Anthropic.

**Mitigazione:**Uso solo per i contenuti del blog pubblico

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

Controlla la politica di utilizzo dei dati del provider:

  • OpenAI: dati API non utilizzati per la formazione (dal 2024)
  • Antropico: Stesso impegno

Se stai scrivendo contenuti riservati, usa l'approccio locale.**Sono a mio agio con questo per il mio blog pubblico, ma non lo userei per qualcosa di remotamente sensibile - e non dovresti credere alla mia parola per quello che significa "sensibile remoto" per il tuo caso d'uso.**2.

  1. Dipendenza dalla rete
  2. Niente internet = nessun assistente.
  3. Mitigazione:

Cache suggerimenti precedenti localmente

Implementa la modalità fuori rete per la modifica

Ripiegare ai modelli locali più piccoli |---------|-------|-------| 3. Latenza Le chiamate API durano 1-3 secondi vs <1s locali. Controllo della realtà: Locale: generazione 0.5s Cloud: generazione 2s Differenza: 1.5s (perfettamente accettabile per la scrittura di assistenza nella mia esperienza - anche se suppongo che dipende da quanto siete impazienti) 4.

Vendor Lock-in

  • Cambiare le API richiede modifiche di codice.
  • Mitigazione:
  • Approccio ibrido: il meglio di entrambi i mondi
  • Puoi mescolare locale e cloud?
  • Assolutamente!

Utilizzare locale per l'incorporamento (a basso costo, veloce), cloud per LLM (questioni di qualità)

  • Abbinamenti localmente: Salva $ 0,004/mese (piccolo importo, certo)
  • LLM in cloud: Ottieni la qualità GPT-4/Claude
  • In realta' questo e' il mio
  • Approccio raccomandato
  • , anche se sto ancora sperimentando per vedere se è il giusto equilibrio:

**Esegui localmente un piccolo modello di integrazione (nessuna GPU necessaria)**Usa le API cloud per LLM

Qdrant locale per lo sviluppo, cloud per la produzione

  1. ConclusioneL'alternativa cloud a "Avvocato GPT" ti dà circa l'80% dei vantaggi con il 20% della complessità - o almeno questa è stata la mia esperienza finora:
  2. |Caratteristiche |Locale |Cloud || Tempo di regolazione | 2-4 ore | 15 minuti |
  3. |Hardware require | NVIDIA GPU | Qualsiasi computer || Qualità del modello | 7B-13B | GPT-4, Claude 3.5 |
  4. | Costo mensile | $0 | circa $3-5 || Latenza | 0.5s | 2s |
  5. | Privacy | 100% locale | Inviato alle API || Cross-platform | Windows only | Mac/Linux/Windows |

| Manutenzione | Aggiornamenti del modello, aggiornamenti del CUDA | Nessuno |**Quando usare il cloud:**Non hai la GPU NVIDIA

Sei su Mac/Linux

Vuoi il sentiero più facile

Scrivi meno di 10 post/mese

Hai un hardware GPU

Anche se dovrei dire che sto ancora imparando i gotchas qui.

  • Passi successivi[Provaci.

: Impostare la versione cloud questo pomeriggio

logo

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