Back to "Κατασκευή ενός "δικηγόρου GPT" για το blog σας - Fine Tuning LLM Εναλλακτική: RAG με Qdrant και Generic Online LLMs"

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

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

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

AI C# Cloud LLM OpenAI Qdrant RAG

Κατασκευή ενός "δικηγόρου GPT" για το blog σας - Fine Tuning LLM Εναλλακτική: RAG με Qdrant και Generic Online LLMs

Wednesday, 12 November 2025

Εισαγωγή

ΣτοΣειρά 8-μερών "Δικηγόρος GPT", σας έδειξα πώς να χτίσει μια πλήρη τοπική RAG-based βοηθός γραφής χρησιμοποιώντας GPU επιτάχυνση, τοπικές LLMs, και διανυσματική βάση δεδομένων.

Είναι πανίσχυρο, ιδιωτικό, και τρέχει εντελώς στο υλικό σας.

Αλλά ας είμαστε ειλικρινείς - δεν έχουν όλοι μια θέση εργασίας με μια NVIDIA GPU, 96GB της RAM, και την υπομονή να δημιουργήσει CUDA, cudNN, και wangle GGUF μοντέλα.

Κι αν θέλεις τα οφέλη ενός βοηθού στο blog χωρίς την επένδυση υλικού;

Το άρθρο αυτό παρουσιάζει την εναλλακτική λύση με βάση το σύννεφο: ίδια προσέγγιση RAG, ίδια Qdrant διανυσματική βάση δεδομένων, αλλά χρησιμοποιώντας cloud LLM APIs αντί για τοπικά συμπεράσματα.

Σκεφτείτε το ως "δικηγόρος GPT Lite" - ευκολότερη εγκατάσταση, χαμηλότερο εμπόδιο στην είσοδο, και δυνητικά καλύτερη ποιότητα παραγωγής χρησιμοποιώντας τα μοντέλα συνόρων.

Πλήρης αποκάλυψη: Εξακολουθώ να μαθαίνω ποια προσέγγιση λειτουργεί καλύτερα στην πράξη, οπότε πάρτε τις εκτιμήσεις κόστους και τις απαιτήσεις απόδοσης μου με μια πρέζα αλάτι.

Αυτό που μπορώ να πω είναι ότι αυτή η προσέγγιση σύννεφο έχει αποδειχθεί εξαιρετικά απλή για να συσταθεί σε σύγκριση με τη διαδρομή GPU.

  • ΣΗΜΕΙΩΣΗ: Αυτό είναι μέρος των πειραμάτων μου με AI (βοηθητική σύνταξη) + δικό μου μοντάζ.
  • Ίδια φωνή, ίδιος πραγματισμός, απλά γρηγορότερα δάχτυλα.
  • Γιατί ένα Cloud Alternative;
  • Η αρχική προσέγγιση
  • Η πλήρης σειρά "Δικηγόρος GPT" δημιουργεί ένα σύστημα που:
  • Λειτουργεί 100% τοπικά (ιδιωτικότητα)
  • Δεν υπάρχει κόστος API

Fast GPU-επιταχυνόμενο συμπέρασμα

Απαιτεί NVIDIA GPU (8GB+ VRAM)

  • Σύνθετη εγκατάσταση (CUDA, cudNN, διαχείριση μοντέλων)
  • Περιορισμένη σε μοντέλα μπορείτε να χωρέσει σε VRAM
  • Επίκεντρη ανάπτυξη των Windows
  • Το Cloud Alternative
  • Η προσέγγιση αυτή σας δίνει:
  • Δεν απαιτείται GPU (ροές σε οποιοδήποτε μηχάνημα)
  • Απλή ρύθμιση (όχι CUDA/cuDNN)
  • Πρόσβαση σε μοντέλα συνόρων (GPT-4, Claude κ.λπ.)

Cross-platform (Windows, Mac, Linux)

Καλύτερη ποιότητα παραγωγής (μεγαλύτερη, πιο ικανή μοντέλα) |--------------------|-------------------| Κόστος API (αν και εύλογο για προσωπική χρήση) Δεδομένα που αποστέλλονται σε τρίτους APIs Η πληρότητα εξαρτάται από το δίκτυο Πότε να χρησιμοποιήσετε ποια;

Χρησιμοποίησε την τοπική προσέγγιση Χρησιμοποίησε την προσέγγιση σύννεφο

Η προστασία της ιδιωτικής ζωής είναι ζωτικής σημασίας.

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

□ Έχετε GPU υλικό Είστε σε Mac / Linux / laptop

  • **• Χρήση μεγάλου όγκου • Μέτρια χρήση (λίγες θέσεις/μήνα) •**Μπορείτε να απολαύσετε tinkering Θέλετε αποτελέσματα γρήγοραtext-embedding-3-smallΑρχιτεκτονική επισκόπηση
  • **Η έκδοση σύννεφο διατηρεί τα ίδια βασικά RAG, αλλά swap τοπικά LLM συμπεράσματα για API κλήσεις:**Βασικές διαφορές:
  • Μοντέλο προσθήκης: OpenAI's
  • API αντί του τοπικού μοντέλου BGELLM

: Claude 3.5 Sonnet ή GPT-4 API αντί της τοπικής Mistral/Llama

Δεν απαιτείται GPU

: Όλα τα CPU που βασίζονται τοπικά, compute συμβαίνουν στο σύννεφο

  • Απλή ανάπτυξη: Μονό εκτελέσιμο, χωρίς αρχεία μοντέλων για τη διαχείριση
  • **Θα πρέπει να σημειώσω ότι δεν έχω τρέξει εκτεταμένα σημεία αναφοράς συγκρίνοντας τις δύο προσεγγίσεις ακόμα - είμαι ακόμα σε διερευνητική φάση ο ίδιος.**Αλλά τα αρχικά αποτελέσματα είναι αρκετά υποσχόμενα για να μοιραστούν.

Στοίβα τεχνολογίαςName

  • Βασικό πλαίσιοNET 9
  • - Ίδια με την αρχική σειράC# 13
  • - Σύγχρονα γλωσσικά χαρακτηριστικάCloud APIs

OpenAI API

  • **- Ενσωμάτωση (κείμενο-ενσωμάτωση-3-μικρό) + LLM (GPT-4)**Ανθρωπιστικό API
  • **- Εναλλακτική LLM (Claude 3.5 Sonnet)**Και τα δύο.

- Μπορείς να ανακατέψεις και να ταιριάξεις!

  • Βάση δεδομένων VectorQdrantCity name (optional, probably does not need a translation)
  • - Το ίδιο με το πρωτότυπο, μπορεί να τρέξει τοπικά μέσω Docker ή να χρησιμοποιήσει Qdrant CloudΕναλλακτική λύση
  • **: Pinecone, Weaviate Cloud (managed options)**Επιλογές πελάτη

Εφαρμογή κονσόλας

- Απλή, μεγάλη για τις δοκιμές

Blazor WebAssembley

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

- Web-based, λειτουργεί οπουδήποτε

  1. ΑβαλόνιαCity name (optional, probably does not need a translation)- Cross-platform επιφάνεια εργασίας (Windows, Mac, Linux)
  2. Ρύθμιση: Το γρήγορο μονοπάτι

Εγκατάσταση Qdrant

Επιλογή Α: Τοπικό Docker (Συνιστάται για ανάπτυξη)

**Επιλογή Β: Σύννεφο Qdrant (Ευκολότερο)**Εγγραφείτε στο

  1. cloud.qdrant.ioΔημιουργία δωρεάν συστάδας
  2. Πάρτε το κλειδί API και το URL συστάδας
  3. Δεν CUDA, δεν CudNN, δεν χρειάζονται εγκαταστάσεις οδήγησης!

**2.**Πάρτε τα κλειδιά API

  1. OpenAI(για ενσωμάτωση + LLM):
  2. Πήγαινε στο

πλατφόρμα.opnai.com

Δημιουργία κλειδιού 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
    }
  }
}

**Ορισμός ορίων χρήσης (σημαντικό!)**Ανθρωπιστικό

(προαιρετικά, για τον Claude):

Πήγαινε στο

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

Δημιουργία κλειδιού API

  • Ρύθμιση
  • Δημιουργία
  • Αυτό είναι.

**Καμία ρύθμιση GPU, καμία λήψη μοντέλου (12GB αρχεία), καμία διαχείριση VRAM.**Εφαρμογή

  • Κεντρικές Υπηρεσίες

Υπηρεσία Ενσωμάτωσης Σύννεφων

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

Βασικά οφέλη έναντι τοπικών:

  • Δεν ρύθμιση ONNX Runtime
  • Καμία διαχείριση μνήμης GPU
  • Αυτόματη παρτίδα από την OpenAI
  • Ποιότητα προσθήκης τελευταίας τεχνολογίας

Κόστος:

  • ~ $0.0001 ανά 1K μάρκες (πολύ φθηνά)
  • Επεξεργασία 100 αναρτήσεων blog (~ 500K μάρκες): ~ $ 0.05
  • Καθημερινή χρήση (10 ερωτήματα): ~$0,001/ημέρα = $0.30/μήνα

Cloud LLM Service

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

**Οφέλη έναντι τοπικών:**Κανένα μοντέλο φόρτωσης (άμεση εκκίνηση)

Δεν όρια VRAM (χρησιμοποιήστε το πλαίσιο 200K εάν χρειάζεται)

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

Καλύτερη ποιότητα εξόδου (τουλάχιστον θεωρητικά - Εξακολουθώ να δοκιμάζω)

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

Το ρεύμα λειτουργεί τέλεια.

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

Κόστος

Claude 3.5 Sonnet: $ 3/εκατομμύρια σημεία εισόδου, $15 εκατομμύρια παραγωγή

# 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

Τυπική συνεδρία γραφής blog (20K εισόδου, 2K εξόδου): περίπου $0.09Μηνιαία χρήση (10 συνεδρίες): περίπου $ 0,90 / μήνα

Αυτές είναι φιγούρες με βάση τα πρώτα μου πειράματα - τα μίλια σας μπορεί να διαφέρουν ανάλογα με το πόσο φλύαροι είστε με την τεχνητή νοημοσύνη.

# 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 (Το ίδιο ως Original!)

Ίδιο API με την τοπική εγκατάσταση- απλά δείξε το τοπικό Ντόκερ ή Qdrant Cloud!

Αγωγός κατάποσης |-----------|--------|------| RAG Generation Service Απλή Κονσόλα Πελάτης Εκτέλεση του συστήματος | Ρύθμιση πρώτης φοράς | | ~$3.65 |

Συνολικός χρόνος εγκατάστασης

  • : περίπου 15 λεπτά έναντι περίπου 2 ωρών για την τοπική εγκατάσταση GPU - υποθέτοντας ότι όλα πηγαίνουν ομαλά, η οποία από την εμπειρία μου είναι μια επικίνδυνη υπόθεση που πρέπει να κάνουμε.
  • Καθημερινή χρήση
  • Ανάλυση κόστους

**Μηνιαία εκτίμηση κόστους (Personal Blog)**Σενάριο

: Γράφοντας 4 blog posts ανά μήνα

  1. Δραστηριότητα - Τόμος - Κόστος:

    • text-embedding-3-smallΑρχική κατάποση (100 δημοσιεύσεις) Μια φορά, 500K μάρκες... 0.05 δολάρια...
    • text-embedding-3-largeΕνδυμασίες (κατασχέσεις, 40/μήνα)
    • Η LLM τηλεφωνεί (40 προτάσεις) Η είσοδος 800K, η έξοδος 80K $3.60
  2. Σύνολο μηνιαίουΓια σύγκριση:

    // Use OpenAI Batch API for ingestion
    var batch = await client.CreateBatchAsync(requests);
    // Wait hours, pay half price
    
  3. Τοπική ρύθμιση: $0/μήνα (αλλά $ 800+ GPU προκαταβολικά):

    // Don't re-embed identical text
    var cache = new Dictionary<string, float[]>();
    
  4. ChatGPT Plus: $20/μήνα (χωρίς RAG, γενικό):

    • Grammarly Premium: $12/μήνα (χωρίς εγγραφή AI)
    • Σημείο διάρρηξης-εμβέλειας
  5. : Αν το χρησιμοποιείτε αυτό για 18+ μήνες, η τοπική GPU πληρώνει για τον εαυτό της.:

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

Διαφορετικά, το σύννεφο είναι φθηνότερο.

Αν και εξακολουθώ να επεξεργάζομαι αν οι προβλέψεις κόστους μου είναι ακριβείς - μπορεί να τρώω τα λόγια μου σε λίγους μήνες όταν έρθουν οι λογαριασμοί.

Συμβουλές βελτιστοποίησης κόστους |-------|---------|---------|--------|--------| Χρήση μικρότερων μοντέλων για την ενσωμάτωση : $0.00002/1K μάρκες : $0.00013/1K μάρκες 6,5x διαφορά κόστους!

Η παρτίδα API καλεί(50% φθηνότερο για μη-επείγον):

Κρυφές παρεμβολές σε τοπικό επίπεδο

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

Χρησιμοποιήστε φθηνότερα μοντέλα για τα σχέδιαClaude 3.5 Haiku: $0,25/M εισόδου (12x φθηνότερα από Sonnet)

GPT-4o-mini: $0.15 M εισόδου (20x φθηνότερο από 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!

Όριο παραθύρου πλαισίου

Πλεονεκτήματα πάνω από την τοπική ρύθμιση

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

logo

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