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

<!--category-- AI, LLM, RAG, C#, Cloud, Qdrant, OpenAI -->
<datetime class="hidden">1973-02-08T1:59</datetime>

## Introducción

En mi[Serie "Abogado GPT" de 8 partes](/blog/building-a-lawyer-gpt-for-your-blog-part1), 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?

[TOC]

## Usa el enfoque local Usa el enfoque de la nube

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

```mermaid
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-small`Descripció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 BGE**LLM

: 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 original**C# 13
- **- Características del lenguaje moderno**API 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 vectorial**Qdrant
- **- Igual que el original, puede ejecutarse localmente a través de Docker o utilizar Qdrant Cloud**Alternativa
- **: Pinecone, Weaviate Cloud (opciones administradas)**Opciones del cliente

## Aplicación de consola

### - Más simple, ideal para las pruebas

**Blazor WebAssembly**

```bash
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)](https://cloud.qdrant.io)
2. Configuración: El camino rápido
3. 1.

**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.io[Crear un clúster libre](https://platform.openai.com)
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):](https://console.anthropic.com)
2. Ir a

### platform.openai.com

Crear clave API`appsettings.json`:

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

```csharp
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**

- 3.
- 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
- 1.

#### Servicio de empotrado en la nube

```csharp
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

2.

#### Servicio LLM en la nube

```csharp
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)

```csharp
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, @"<!--category--\s+(.+)-->");

            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)

```csharp
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

```csharp
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

```bash
# 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,09**Uso 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.

```bash
# 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-small`Ingestión inicial (100 puestos)  Una sola vez, 500K tokens  $0,05
   - `text-embedding-3-large`Incrustaciones (consultas, 40/mes)  tokens de 40K  $0.004
   - llamadas LLM (40 sugerencias)  800K entrada, salida 80K  $3,60

2. **Total mensual**A efectos de comparación:
   
   ```csharp
   // 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)**:
   
   ```csharp
   // 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.**:
   
   ```csharp
   // 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

```csharp
// 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 borradores**Claude 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)

```bash
# 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

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

// Local? Limited by your single GPU
```

### 1.

```bash
# 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.
- 3.

### Plano transversal

**El enfoque local es sólo Windows + NVIDIA.**

4.

- Escalabilidad
- 5.
- Despliegue más sencillo

### Limitaciones y compensaciones

**1.**

Inquietudes en materia de privacidad

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

```csharp
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ón**La 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 Nube**Tiempo 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 API**Só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

- [Confía en los proveedores de cloud con contenido público](/blog/building-a-lawyer-gpt-for-your-blog-part1)
- [Cuándo usar local:](/blog/building-a-lawyer-gpt-for-your-blog-part1)

### Usted tiene hardware GPU

- [La privacidad es fundamental](https://qdrant.tech/)Usted quiere $0 costo de operación
- [Escribes más de 20 posts/mes](https://cloud.qdrant.io)Disfrutas jugando
- [Mejor enfoque:](https://platform.openai.com)
- [Empieza con la nube.](https://console.anthropic.com)
- [Si te encanta y alcanzas los límites de costo/privacidad, migra a la localidad más tarde.](https://github.com/openai/openai-dotnet)
- [La arquitectura RAG es idéntica: ¡sólo cambia el servicio LLM!](https://github.com/tghamm/Anthropic.SDK)

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