在我的8部分“律师GPT”系列,我教你如何使用 GPU 加速、 本地LLM 和矢量数据库, 建立一个完整的本地RAG 写作助理。
它是强大的,私人的, 运行完全在你的硬件上。
但老实说, 不是每个人都有工作站 拥有NVIDIA GPU,96GB的RAM, 以及建立CUDA,CUDNN, 和折叠GGUF模型的耐心。
若你只想得到博客写作助理的好处,
本文介绍了基于云的替代方法:同样的RAG方法,相同的Qdrant矢量数据库,但使用云 LLM API, 而不是当地推论。
我可以说的是,与GPU路线相比,这种云层方法已经证明是相当直截了当的。
需要 NVIDIA GPU (8GB+ VRAM)
跨平台(窗口、Mac、Linux)
提高产出质量(大、更有能力的模型) |--------------------|-------------------| API费用(尽管个人使用合理) 发送到第三方API的数据 延迟取决于网络 何时使用哪一种?
隐私是最重要的 方便的事情
graph TB
A[Markdown Files] -->|Ingest| B[Chunking Service]
B -->|Text Chunks| C[Cloud Embedding API]
C -->|Vectors| D[Qdrant Vector DB]
E[User Writing] -->|Current Draft| F[Web/Desktop Client]
F -->|Embed Context| C
C -->|Query Vector| D
D -->|Similar Content| G[Context Builder]
G -->|Relevant Past Articles| H[Prompt Engineer]
H -->|Prompt + Context| I[Cloud LLM API]
I -->|Generated Suggestions| J[Response Handler]
J -->|Suggestions + Citations| F
F -->|Display| K[Editor with Suggestions]
class C,I cloud
class D,K local
classDef cloud stroke:#f96,stroke-width:4px
classDef local stroke:#333,stroke-width:2px
你有GPU硬件 你在Mac/Linux/笔记本电脑上
text-embedding-3-small建筑结构概览克洛德·3.5索内特或GPT-4 API,而不是当地Mistral/Llama
布拉佐尔网络大会
docker run -p 6333:6333 -p 6334:6334 \
-v $(pwd)/qdrant_storage:/qdrant/storage:z \
qdrant/qdrant
- 以网络为基础,在任何地方工作
安裝 Qdrant
**备选办法B: 云层(最容易)**注册时间
2. 目标获取 APP 密钥
创建 API 密钥appsettings.json:
{
"BlogRAG": {
"Embedding": {
"Provider": "OpenAI",
"Model": "text-embedding-3-small",
"ApiKey": "sk-..."
},
"LLM": {
"Provider": "Anthropic",
"Model": "claude-3-5-sonnet-20241022",
"ApiKey": "sk-ant-..."
},
"VectorStore": {
"Type": "Qdrant",
"Url": "http://localhost:6333",
"ApiKey": "",
"CollectionName": "blog_embeddings"
},
"Ingestion": {
"MarkdownPath": "/path/to/your/blog/Markdown",
"ChunkSize": 500,
"ChunkOverlap": 50
}
}
}
**设定使用限制( 重要 ! )**人类
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;
}
}
}
}
}
关键效益与本地效益 :
成本成本成本成本成本:
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();
}
}
}
**地方效益超过:**没有模式装入( 即时启动)
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");
}
}
}
# 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
典型博客写作课(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!
与本地设置相同的 API- 只要指向本地的多克或昆特云!
摄入管管 |-----------|--------|------| RAG 生成处 简单控制台客户端 运行系统 | 首次时间设置 | | ~$3.65 |
设置总时间
**每月成本估算(个人博客)**假设情景
· · · · · · · · · · · · · · · · · · · · · · · · · 操作 · · · · · · · · · · · · · · · · · · · · · · 费用 · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · ·:
text-embedding-3-small初次摄入(100个员额) 一次性,500K象征性 0.05美元text-embedding-3-large* 嵌入(盘口,40/月) * 40K象征性品 * 0.004美元 *月每月共计共计比较:
// Use OpenAI Batch API for ingestion
var batch = await client.CreateBatchAsync(requests);
// Wait hours, pay half price
本地设置 : $0 / 月( 但800+ GPU 前期):
// Don't re-embed identical text
var cache = new Dictionary<string, float[]>();
加:每月20美元(无RAG,通用):
:如果你用它18个月以上, 当地GPU自己付费。:
// Retrieve top 3 instead of top 10 chunks
limit: 3 // 70% less input tokens
费用优化提示 |-------|---------|---------|--------|--------| 使用较小的嵌入模型 : 00002/1K美元象征性物 : 0.0013/1K 象征性物 6.5x 成本差异!
批次APP电话(非紧急需求更便宜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.25M 输入(比Sonnet便宜12x)
# 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
# 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
是的(需要8GBVRAM)
Llama 3 70B = 8K = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
至少,这是理论我仍在学习 大型模型是否实际上 产生了显著更好的博客内容。
即时更新
没有模式下载
本地方法仅为 Windows + NVIDIA 。
1. 目标 1. 目标
隐私问题
// 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")
};
});
**缓解:**仅用于公共博客内容
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
}
}
检查提供者的数据使用政策:
如果您正在起草机密内容, 请使用本地方法 。但我不会用它来做任何 远程敏感的东西 你不应该用我的话 来形容"远为敏感"的用法2. 目标
执行脱机编辑模式
返回当地小型模型 |---------|-------|-------| 3 个 时间间隔 API 电话需要 1-3秒与 < 1s 当地通话 。 真实检查 : 当地:0.5代 云云:2代 差异:1.5(根据我的经验,书面协助完全可以接受----尽管我想这取决于你有多不耐烦) 4. 4个。
供应商锁定
当地用于嵌入(便宜、快),云用于LLM(质量事项)
**本地运行小型嵌入模型( 不需要 GPU )**LLM 使用云 API
维护**何时使用云 :**你没有NVIDIA GPU(荷兰语)
你在麦克莱克斯号上
: 设置今天下午的云版
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.