Back to "Construyendo un "Abogado GPT" para su blog - Fine Tuning LLM Alternativa: RAG con Qdrant y Genérico LLMs en línea"

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

Construyendo un "Abogado GPT" para su blog - Fine Tuning LLM Alternativa: RAG con Qdrant y Genérico LLMs en línea

Wednesday, 12 November 2025

Introducción

En miSerie "Abogado GPT" de 8 partes, les mostré cómo construir un completo asistente de escritura local basado en RAG usando aceleración GPU, LLM locales y bases de datos vectoriales.

Es poderoso, privado, y se ejecuta completamente en su hardware.

Pero seamos honestos - no todo el mundo tiene una estación de trabajo con una GPU NVIDIA, 96 GB de RAM, y la paciencia para establecer CUDA, CuDNN, y Wrangle GGUF modelos.

¿Qué pasa si sólo quieres los beneficios de un asistente de escritura de blog sin la inversión de hardware?

Este artículo presenta la alternativa basada en la nube: el mismo enfoque RAG, la misma base de datos de vectores Qdrant, pero usando API LLM en la nube en lugar de inferencia local.

Piense en ello como "Abogado GPT Lite" - configuración más fácil, menor barrera a la entrada, y potencialmente mejor calidad de salida utilizando modelos fronterizos.

Divulgación completa: Todavía estoy aprendiendo qué enfoque funciona mejor en la práctica, así que toma mis estimaciones de costos y reclamaciones de rendimiento con una pizca de sal.

Lo que puedo decir es que este enfoque cloud ha demostrado ser notablemente sencillo de configurar en comparación con la ruta GPU.

  • NOTA: Esto es parte de mis experimentos con IA (redacción asistida) + mi propia edición.
  • La misma voz, el mismo pragmatismo; sólo los dedos más rápidos.
  • ¿Por qué una alternativa en la nube?
  • El enfoque original
  • La serie completa de "Abogado GPT" construye un sistema que:
  • Funciona al 100% localmente (privacidad)
  • Sin costos de API

Inferencia acelerada de la GPU

Requiere NVIDIA GPU (8GB+ VRAM)

  • Configuración compleja (CUDA, cuDNN, gestión de modelos)
  • Limitado a los modelos que puede caber en VRAM
  • Implementación centrada en Windows
  • La alternativa de la nube
  • Este enfoque le da:
  • No se requiere GPU (se ejecuta en cualquier máquina)
  • Configuración sencilla (no CUDA/cuDNN)
  • Acceso a modelos fronterizos (GPT-4, Claude, etc.)

Multiplataforma (Windows, Mac, Linux)

Mejor calidad de salida (modelos más grandes y capaces) |--------------------|-------------------| Costos API (aunque razonables para uso personal) Datos enviados a API de terceros La latencia depende de la red ¿Cuándo usar cuál?

Usa el enfoque local Usa el enfoque de la nube

La privacidad es crítica # # La comodidad es importante

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

Tienes hardware de la GPU Estás en Mac/Linux/Laptop

  • Uso de alto volumen Uso moderado (pocos puestos/mes)# Disfrutas jugando # # Quieres resultados rápido #text-embedding-3-smallDescripción general de la arquitectura
  • **La versión en la nube mantiene los mismos fundamentos RAG pero intercambia inferencia LLM local para llamadas API:**Diferencias fundamentales:
  • Modelo de empotrado: OpenAI's
  • API en lugar del modelo local de BGELLM

: Claude 3.5 Sonnet o GPT-4 API en lugar de local Mistral/Llama

No se requiere GPU

: Todo lo que se basa en CPU localmente, calcular sucede en la nube

  • Despliegue más sencillo: Ejecutable único, no hay archivos de modelo para administrar
  • **Debo tener en cuenta que no he ejecutado extensos puntos de referencia comparando los dos enfoques todavía - todavía estoy en la fase exploratoria yo mismo.**Pero los resultados iniciales son lo suficientemente prometedores como para compartirlos.

Apilador de tecnología

  • Marco básico.NET 9
  • - Lo mismo que la serie originalC# 13
  • - Características del lenguaje modernoAPI en la nube

API de OpenAI

  • **- Incrustaciones (text-embedding-3-pequeño) + LLM (GPT-4)**API Antrópica
  • **- LLM alternativa (Claude 3.5 Sonnet)**Ambas cosas.

- ¡Puedes mezclar y combinar!

  • Base de datos vectorialQdrant
  • - Igual que el original, puede ejecutarse localmente a través de Docker o utilizar Qdrant CloudAlternativa
  • **: Pinecone, Weaviate Cloud (opciones administradas)**Opciones del cliente

Aplicación de consola

- Más simple, ideal para las pruebas

Blazor WebAssembly

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

- Basado en la web, funciona en cualquier lugar

  1. Avalonia- Escritorio multiplataforma (Windows, Mac, Linux)
  2. Configuración: El camino rápido

Instalar Qdrant

Opción A: Docker local (recomendado para el desarrollo)

**Opción B: Nube de Qdrant (más fácil)**Regístrese en

  1. cloud.qdrant.ioCrear un clúster libre
  2. Obtenga su clave de API y URL de clúster
  3. ¡No hay CUDA, no hay cuDNN, no se necesitan instalaciones de conductor!

**2.**Obtener claves de API

  1. OpenAI(para incrustaciones + LLM):
  2. Ir a

platform.openai.com

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

**Establecer límites de uso (¡importante!)**Antrópico

(optativo, para Claude):

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

Crear clave API

  • Configuración
  • Crear
  • Eso es.

**Sin configuración de GPU, sin descargas de modelos (12GB de archivos), sin administración de VRAM.**Aplicación

  • Servicios básicos

Servicio de empotrado en la nube

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

Beneficios clave frente a los locales:

  • No hay configuración de tiempo de ejecución ONNX
  • Sin gestión de memoria GPU
  • Loting automático por OpenAI
  • Calidad de incrustación de última generación

Costo:

  • : ~0,0001 por 1K tokens (muy barato)
  • Procesamiento de 100 entradas de blog (~500K tokens): ~$0,05
  • Uso diario (10 consultas): ~0,001/día = 0,30/mes

Servicio LLM en la nube

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

**Beneficios sobre locales:**Sin carga de modelos (inicio instantáneo)

No hay límites VRAM (utilizar el contexto 200K si es necesario)

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

Mejor calidad de salida (al menos en teoría - todavía estoy probando)

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

La transmisión funciona a la perfección

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 Soneto: 3 dólares por millón de tokens de entrada, 15 dólares por millón de productos

# 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

Sesión típica de escritura de blog (20K de entrada, salida 2K): aproximadamente $0,09Uso mensual (10 sesiones): aproximadamente $0,90 por mes

Estas son figuras de estadio basado en mis experimentos tempranos - su kilometraje puede variar dependiendo de lo hablado que usted es con la 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.

Tienda de vectores Qdrant (¡Igual que Original!)

La misma API que la configuración local- sólo señalar a Docker local o Qdrant Cloud!

Gasoducto de ingestión |-----------|--------|------| Servicio de generación RAG Cliente de consola simple Ejecutar el sistema | Configuración de la primera vez | | ~$3.65 |

Tiempo total de instalación

  • : aproximadamente 15 minutos frente a aproximadamente 2 horas para la configuración local de la GPU - asumiendo que todo va sin problemas, lo cual en mi experiencia es una suposición peligrosa a hacer.
  • Uso diario
  • Análisis de costos

**Estimación mensual de costos (Blog personal)**Hipótesis

: Escribir 4 entradas de blog por mes

  1. # Operación # # Volumen # # Coste #:

    • text-embedding-3-smallIngestión inicial (100 puestos) Una sola vez, 500K tokens $0,05
    • text-embedding-3-largeIncrustaciones (consultas, 40/mes) tokens de 40K $0.004
    • llamadas LLM (40 sugerencias) 800K entrada, salida 80K $3,60
  2. Total mensualA efectos de comparación:

    // Use OpenAI Batch API for ingestion
    var batch = await client.CreateBatchAsync(requests);
    // Wait hours, pay half price
    
  3. Configuración local: $0/mes (pero $800+ GPU por adelantado):

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

    • Grammarly Premium: $12/mes (sin escritura de AI)
    • Punto de equilibrio
  5. : Si usas esto durante más de 18 meses, la GPU local se paga por sí misma.:

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

De lo contrario, la nube es más barata.

Aunque todavía estoy averiguando si mis proyecciones de costos son exactas, puede que me esté comiendo mis palabras en unos meses cuando lleguen las facturas.

Consejos de optimización de costos |-------|---------|---------|--------|--------| Utilizar modelos más pequeños para incrustar : $0,00002/1K tokens : $0,00013/1K tokens 6.5x diferencia de costos!

Llamadas API por lotes(50% más barato para los no urgentes):

Incrustaciones de caché a nivel local

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

Utilizar modelos más baratos para los borradoresClaude 3.5 Haiku: $0,25/M de entrada (12x más barato que Sonnet)

GPT-4o-mini: $0,15/M de entrada (20x más barato que 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!

Limite la ventana de contexto

Ventajas sobre la configuración local

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

Mejor calidad del modelo

¿Contexto de la calidad local?

Mistral 7B 8K Bueno Sí (necesita 8GB VRAM) Sí

Llama 3 70B 8K Excelente No (necesita 48GB VRAM) Sí

  • GPT-4 Turbo 128K Excelente No Sí
  • Claude 3.5 Soneto 200K Mejor No Sí
  • Cloud le da acceso a los modelos 70B+
  • que requeriría $ 10K + GPU hardware.

Al menos, esa es la teoría - todavía estoy aprendiendo si los modelos más grandes realmente producen contenido de blog notablemente mejor en la práctica.

2.

Actualizaciones instantáneas

Sin descargas de modelos

  • , sin conversiones GGUF, sin comprobaciones de compatibilidad.
  • Esto es realmente brillante cuando estás experimentando con diferentes modelos para ver qué funciona mejor.

Plano transversal

El enfoque local es sólo Windows + NVIDIA.

4.

  • Escalabilidad
  • Despliegue más sencillo

Limitaciones y compensaciones

1.

Inquietudes en materia de privacidad

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

El contenido de tu blog va a OpenAI/Anthropic.

**Mitigación:**Uso sólo para el contenido público del blog

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

Comprobar la política de uso de datos del proveedor:

  • OpenAI: Datos API no utilizados para la formación (a 2024)
  • Antrópico: Mismo compromiso

Si estás redactando contenido confidencial, usa el enfoque local.**Me siento cómodo con esto para mi blog público, pero no lo usaría para nada remotamente sensible - y no deberías tomar mi palabra por lo que "sensitivamente sensible" significa para tu caso de uso.**2.

  1. Dependencia de la red
  2. No hay internet = no hay asistente.
  3. Mitigación:

Caché sugerencias anteriores a nivel local

Implementar modo fuera de línea para editar

Volver a los modelos locales más pequeños |---------|-------|-------| 3. Latencia Las llamadas API tardan 1-3 segundos frente a <1s locales. Comprobación de la realidad: Local: generación de 0,5s Nube: generación de 2s Diferencia: 1.5s (perfectamente aceptable para escribir asistencia en mi experiencia - aunque supongo que depende de lo impaciente que eres) 4.

Bloqueo del vendedor

  • Cambiar API requiere cambios de código.
  • Mitigación:
  • Enfoque híbrido: lo mejor de ambos mundos
  • ¿Se puede mezclar local y nube?
  • ¡Por supuesto!

Utilice local para incrustar (más barato, rápido), nube para LLM (cuestiones de calidad)

  • Incrustaciones locales: Ahorre $0.004/mes (cantidad pequeña, es cierto)
  • LLM en la nube: Obtenga la calidad GPT-4/Claude
  • Esta es en realidad mi
  • enfoque recomendado
  • , aunque todavía estoy experimentando para ver si es el equilibrio correcto:

**Ejecutar un pequeño modelo de incrustación local (no se necesita GPU)**Usar API en la nube para LLM

Qdrant local para el desarrollo, nube para la producción

  1. ConclusiónLa alternativa cloud a "Abogado GPT" le da aproximadamente el 80% de los beneficios con el 20% de la complejidad - o al menos esa ha sido mi experiencia hasta ahora:
  2. Característica local NubeTiempo de instalación 2-4 horas 15 minutos
  3. **# Requisito de hardware # # NVIDIA GPU # # Cualquier computadora #**Calidad del modelo 7B-13B GPT-4, Claude 3.5
  4. **# Costo mensual # # # # # # aproximadamente # # # # # # # # # # # # # # # # # # # # cuesta mensual # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # cuesta mensual # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #**Latencia 0,5s 2s
  5. Privacidad 100% local Enviado a APISólo Windows Mac/Linux/Windows

Mantenimiento Actualizaciones de modelos, actualizaciones de CUDA Ninguna**Cuándo usar la nube:**Usted no tiene NVIDIA GPU

Estás en Mac/Linux

Quieres el camino más fácil.

Escribes menos de 10 posts/mes

Usted tiene hardware GPU

Aunque debo advertir que todavía estoy aprendiendo las gotchas aquí.

  • Próximos pasos[Pruébalo.

: Configure la versión en la nube esta tarde

logo

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