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
Établissement d’un "Lawyer GPT" pour votre blog - Partie 7: Production de contenu M& Ingénierie rapide
Wednesday, 12 November 2025
Introduction
Bienvenue à la partie 7! Nous' avons construit tout ce qui est nécessaire pour un assistant de rédaction travaillant.Partie 4), UI (Partie 5), et l'intégration locale de LLM (Partie 6). Il est maintenant temps de réunir tous les éléments et de se concentrer sur ce qui importe le plus.
NOTE: C'est une partie des expériences que j'ai réalisées avec l'A.I. M SK1 rédaction assistée) + ma propre éditionMST4 La même voixM ST5 le même pragmatismeM st6 quelques doigts plus rapidesMst7
C’est là que le caoutchouc rencontre la route. Ingénierie rapide techniques, stratégies de gestion du contexte, et comment faire en sorte que le LLM génère des suggestions qui correspondent réellement à votre style d’écriture .
Le pipeline RAG complet
Voici le flux complet que nous mettons en œuvre.
sequenceDiagram
participant User
participant Editor
participant Embedder
participant VectorDB
participant ContextBuilder
participant PromptEngine
participant LLM
participant UI
User->>Editor: Types text
Editor->>Embedder: Generate embedding for context
Embedder->>VectorDB: Search similar content
VectorDB-->>ContextBuilder: Return top K chunks
ContextBuilder->>ContextBuilder: Rank & filter
ContextBuilder->>PromptEngine: Provide context
PromptEngine->>PromptEngine: Build structured prompt
PromptEngine->>LLM: Generate suggestion
LLM-->>UI: Stream response
UI->>User: Display suggestion
Service de constructeur du contexte
Premièrement, let, build a un constructeur de contexte sophistiqué qui sélectionne et classe les chunks récupérés
namespace Mostlylucid.BlogLLM.Core.Services
{
public interface IContextBuilder
{
Task<ContextResult> BuildContextAsync(string currentText, int maxTokens = 2000);
}
public class ContextResult
{
public List<ContextChunk> Chunks { get; set; } = new();
public int TotalTokens { get; set; }
public string CombinedContext { get; set; } = string.Empty;
}
public class ContextChunk
{
public string Text { get; set; } = string.Empty;
public string Source { get; set; } = string.Empty;
public float RelevanceScore { get; set; }
public int TokenCount { get; set; }
}
public class ContextBuilder : IContextBuilder
{
private readonly BatchEmbeddingService _embedder;
private readonly QdrantVectorStore _vectorStore;
private readonly ILogger<ContextBuilder> _logger;
public ContextBuilder(
BatchEmbeddingService embedder,
QdrantVectorStore vectorStore,
ILogger<ContextBuilder> logger)
{
_embedder = embedder;
_vectorStore = vectorStore;
_logger = logger;
}
public async Task<ContextResult> BuildContextAsync(string currentText, int maxTokens = 2000)
{
// Extract meaningful context from current text
var searchText = ExtractSearchContext(currentText);
// Generate embedding
var embedding = _embedder.GenerateEmbedding(searchText);
// Retrieve similar chunks
var searchResults = await _vectorStore.SearchAsync(
queryEmbedding: embedding,
limit: 20, // Get more than we need
languageFilter: "en"
);
// Re-rank results
var rankedChunks = ReRankResults(searchResults, currentText);
// Select chunks that fit in token budget
var selectedChunks = SelectChunks(rankedChunks, maxTokens);
// Build combined context
var combinedContext = BuildCombinedContext(selectedChunks);
return new ContextResult
{
Chunks = selectedChunks,
TotalTokens = selectedChunks.Sum(c => c.TokenCount),
CombinedContext = combinedContext
};
}
private string ExtractSearchContext(string currentText)
{
// Get last 3 paragraphs or 500 characters
var paragraphs = currentText.Split("\n\n");
var lastParagraphs = paragraphs.TakeLast(3);
var searchText = string.Join("\n\n", lastParagraphs);
if (searchText.Length > 500)
{
searchText = searchText.Substring(searchText.Length - 500);
}
return searchText;
}
private List<ContextChunk> ReRankResults(List<SearchResult> results, string currentText)
{
var chunks = new List<ContextChunk>();
foreach (var result in results)
{
var chunk = new ContextChunk
{
Text = result.Text,
Source = $"{result.BlogPostTitle} - {result.SectionHeading}",
RelevanceScore = result.Score,
TokenCount = EstimateTokens(result.Text)
};
// Boost score if from same category
if (SharesCategories(result, currentText))
{
chunk.RelevanceScore *= 1.2f;
}
// Reduce score if very recent (might be too similar)
if (IsVeryRecent(result))
{
chunk.RelevanceScore *= 0.9f;
}
chunks.Add(chunk);
}
return chunks.OrderByDescending(c => c.RelevanceScore).ToList();
}
private List<ContextChunk> SelectChunks(List<ContextChunk> rankedChunks, int maxTokens)
{
var selected = new List<ContextChunk>();
var totalTokens = 0;
foreach (var chunk in rankedChunks)
{
if (totalTokens + chunk.TokenCount > maxTokens)
{
break;
}
selected.Add(chunk);
totalTokens += chunk.TokenCount;
_logger.LogDebug("Selected chunk from {Source} ({Tokens} tokens, score: {Score:F3})",
chunk.Source, chunk.TokenCount, chunk.RelevanceScore);
}
_logger.LogInformation("Selected {Count} chunks totaling {Tokens} tokens",
selected.Count, totalTokens);
return selected;
}
private string BuildCombinedContext(List<ContextChunk> chunks)
{
var sb = new StringBuilder();
for (int i = 0; i < chunks.Count; i++)
{
sb.AppendLine($"=== Reference {i + 1}: {chunks[i].Source} ===");
sb.AppendLine(chunks[i].Text);
sb.AppendLine();
}
return sb.ToString();
}
private int EstimateTokens(string text)
{
// Rough estimate: 1 token ≈ 4 characters
return text.Length / 4;
}
private bool SharesCategories(SearchResult result, string currentText)
{
// Simple heuristic - check if category names appear in current text
// In production, extract actual categories from metadata
return false;
}
private bool IsVeryRecent(SearchResult result)
{
// Reduce score for posts from last 7 days to encourage diversity
// In production, parse actual publish dates
return false;
}
}
}
Ingénierie avancée rapide
La clé d’une bonne production est de bonnes prompts. Let's build a sophisticated prompt engineering system .
Modèles d'aide
namespace Mostlylucid.BlogLLM.Core.Services
{
public enum PromptType
{
ContinueWriting,
SuggestStructure,
GenerateCode,
ImproveSection,
AddExample
}
public class PromptTemplate
{
public PromptType Type { get; set; }
public string SystemPrompt { get; set; } = string.Empty;
public string UserPromptTemplate { get; set; } = string.Empty;
public float Temperature { get; set; } = 0.7f;
public int MaxTokens { get; set; } = 300;
}
public static class PromptTemplates
{
public static readonly Dictionary<PromptType, PromptTemplate> Templates = new()
{
[PromptType.ContinueWriting] = new PromptTemplate
{
Type = PromptType.ContinueWriting,
SystemPrompt = @"You are a helpful writing assistant for a technical blog about .NET, ASP.NET Core, and related technologies.
Your writing style is:
- Conversational but professional
- Technical and detailed
- Includes code examples when relevant
- Uses practical, real-world examples
- Occasionally uses dry humor
- Avoids marketing fluff
When continuing text, maintain consistency with the existing tone and technical depth.",
UserPromptTemplate = @"Based on these excerpts from similar blog posts:
{context}
---
Current draft:
{current_text}
Task: Suggest 2-3 sentences to naturally continue the current thought. Match the technical depth and conversational tone.
Continuation:",
Temperature = 0.75f,
MaxTokens = 200
},
[PromptType.SuggestStructure] = new PromptTemplate
{
Type = PromptType.SuggestStructure,
SystemPrompt = @"You are a technical blog editor helping to organize content effectively.
Your suggestions should:
- Create logical flow from simple to complex
- Group related concepts together
- Include practical examples sections
- Ensure each section has clear purpose",
UserPromptTemplate = @"Based on how similar topics were structured:
{context}
---
Proposed section: {section_title}
Task: Suggest 5-7 subsections or bullet points for what this section should cover.
Structure (as markdown list):",
Temperature = 0.6f, // Lower for more structured output
MaxTokens = 300
},
[PromptType.GenerateCode] = new PromptTemplate
{
Type = PromptType.GenerateCode,
SystemPrompt = @"You are a C# coding expert specializing in ASP.NET Core, Entity Framework, and modern .NET development.
Your code should:
- Follow C# conventions and best practices
- Include XML doc comments for public members
- Handle edge cases and errors
- Be production-ready, not just proof-of-concept
- Include explanatory comments for complex logic",
UserPromptTemplate = @"Relevant code patterns from past posts:
{context}
---
Task: {code_request}
Generate a complete, working C# code example with comments.
Code:",
Temperature = 0.4f, // Lower for more consistent code
MaxTokens = 500
},
[PromptType.ImproveSection] = new PromptTemplate
{
Type = PromptType.ImproveSection,
SystemPrompt = @"You are an editor helping to improve technical blog content.
Focus on:
- Clarity and readability
- Technical accuracy
- Adding concrete examples
- Removing redundancy",
UserPromptTemplate = @"Examples of well-written content:
{context}
---
Current version:
{current_section}
Task: Suggest improvements to make this clearer and more engaging while maintaining technical depth.
Improved version:",
Temperature = 0.7f,
MaxTokens = 400
},
[PromptType.AddExample] = new PromptTemplate
{
Type = PromptType.AddExample,
SystemPrompt = @"You are a technical writer who excels at creating practical, real-world examples.
Your examples should:
- Be realistic and relatable
- Demonstrate the concept clearly
- Include enough detail to be useful
- Reference common scenarios developers face",
UserPromptTemplate = @"Similar examples from past posts:
{context}
---
Concept to illustrate: {concept}
Task: Create a practical example that demonstrates this concept in a real-world scenario.
Example:",
Temperature = 0.8f, // Higher for more creative examples
MaxTokens = 350
}
};
}
}
Service de constructeur rapide
namespace Mostlylucid.BlogLLM.Core.Services
{
public interface IPromptBuilder
{
string BuildPrompt(PromptType type, Dictionary<string, string> variables, string context);
PromptTemplate GetTemplate(PromptType type);
}
public class PromptBuilder : IPromptBuilder
{
private readonly ILogger<PromptBuilder> _logger;
public PromptBuilder(ILogger<PromptBuilder> logger)
{
_logger = logger;
}
public string BuildPrompt(
PromptType type,
Dictionary<string, string> variables,
string context)
{
var template = PromptTemplates.Templates[type];
// Build full prompt with system message
var sb = new StringBuilder();
// System prompt
sb.AppendLine("<|system|>");
sb.AppendLine(template.SystemPrompt);
sb.AppendLine("</|system|>");
sb.AppendLine();
// User prompt with variables substituted
sb.AppendLine("<|user|>");
var userPrompt = template.UserPromptTemplate;
userPrompt = userPrompt.Replace("{context}", context);
foreach (var kvp in variables)
{
userPrompt = userPrompt.Replace($"{{{kvp.Key}}}", kvp.Value);
}
sb.AppendLine(userPrompt);
sb.AppendLine("</|user|>");
sb.AppendLine();
// Assistant prompt (model fills this in)
sb.AppendLine("<|assistant|>");
var fullPrompt = sb.ToString();
_logger.LogDebug("Built {Type} prompt ({Length} chars)",
type, fullPrompt.Length);
return fullPrompt;
}
public PromptTemplate GetTemplate(PromptType type)
{
return PromptTemplates.Templates[type];
}
}
}
Service de création de contenu
Maintenant, laissez-nous l’établir avec un service de production de contenu à haut niveau de -.
namespace Mostlylucid.BlogLLM.Core.Services
{
public interface IContentGenerationService
{
Task<GenerationResult> GenerateAsync(
GenerationRequest request,
CancellationToken cancellationToken = default);
IAsyncEnumerable<string> GenerateStreamAsync(
GenerationRequest request,
CancellationToken cancellationToken = default);
}
public class GenerationRequest
{
public string CurrentText { get; set; } = string.Empty;
public PromptType Type { get; set; } = PromptType.ContinueWriting;
public Dictionary<string, string> Variables { get; set; } = new();
public int MaxContextTokens { get; set; } = 2000;
}
public class GenerationResult
{
public string GeneratedText { get; set; } = string.Empty;
public List<string> SourcePosts { get; set; } = new();
public int ContextTokensUsed { get; set; }
public int GeneratedTokens { get; set; }
public TimeSpan GenerationTime { get; set; }
}
public class ContentGenerationService : IContentGenerationService
{
private readonly IContextBuilder _contextBuilder;
private readonly IPromptBuilder _promptBuilder;
private readonly ILlmService _llmService;
private readonly ILogger<ContentGenerationService> _logger;
public ContentGenerationService(
IContextBuilder contextBuilder,
IPromptBuilder promptBuilder,
ILlmService llmService,
ILogger<ContentGenerationService> logger)
{
_contextBuilder = contextBuilder;
_promptBuilder = promptBuilder;
_llmService = llmService;
_logger = logger;
}
public async Task<GenerationResult> GenerateAsync(
GenerationRequest request,
CancellationToken cancellationToken = default)
{
var startTime = DateTime.UtcNow;
_logger.LogInformation("Starting content generation for {Type}", request.Type);
// Build context from similar posts
var contextResult = await _contextBuilder.BuildContextAsync(
request.CurrentText,
request.MaxContextTokens
);
_logger.LogInformation("Built context from {Count} chunks ({Tokens} tokens)",
contextResult.Chunks.Count, contextResult.TotalTokens);
// Build prompt
var prompt = _promptBuilder.BuildPrompt(
request.Type,
request.Variables,
contextResult.CombinedContext
);
// Get template for parameters
var template = _promptBuilder.GetTemplate(request.Type);
// Generate with LLM
var generated = await _llmService.GenerateAsync(prompt, cancellationToken);
var result = new GenerationResult
{
GeneratedText = CleanGeneratedText(generated),
SourcePosts = contextResult.Chunks
.Select(c => c.Source)
.Distinct()
.ToList(),
ContextTokensUsed = contextResult.TotalTokens,
GeneratedTokens = EstimateTokens(generated),
GenerationTime = DateTime.UtcNow - startTime
};
_logger.LogInformation("Generated {Tokens} tokens in {Ms}ms",
result.GeneratedTokens, result.GenerationTime.TotalMilliseconds);
return result;
}
public async IAsyncEnumerable<string> GenerateStreamAsync(
GenerationRequest request,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Build context
var contextResult = await _contextBuilder.BuildContextAsync(
request.CurrentText,
request.MaxContextTokens
);
// Build prompt
var prompt = _promptBuilder.BuildPrompt(
request.Type,
request.Variables,
contextResult.CombinedContext
);
// Stream from LLM
await foreach (var token in _llmService.GenerateStreamAsync(prompt, cancellationToken))
{
yield return token;
}
}
private string CleanGeneratedText(string text)
{
// Remove common artifacts
text = text.Trim();
// Remove model's own tags if it added them
text = text.Replace("<|assistant|>", "");
text = text.Replace("</|assistant|>", "");
// Remove incomplete sentences at end
if (!text.EndsWith(".") && !text.EndsWith("!") && !text.EndsWith("?"))
{
var lastPeriod = text.LastIndexOf('.');
if (lastPeriod > text.Length / 2) // Only if more than halfway through
{
text = text.Substring(0, lastPeriod + 1);
}
}
return text.Trim();
}
private int EstimateTokens(string text)
{
return text.Length / 4;
}
}
}
Intégration avec l'UI
Mise à jour du modèle SuggestionsView pour utiliser le nouveau service:
public partial class SuggestionsViewModel : ViewModelBase
{
private readonly IContentGenerationService _generationService;
[RelayCommand]
private async Task GenerateContinuation()
{
IsGenerating = true;
try
{
var request = new GenerationRequest
{
CurrentText = GetCurrentText(),
Type = PromptType.ContinueWriting,
MaxContextTokens = 2000
};
var result = await _generationService.GenerateAsync(request);
AiSuggestion = result.GeneratedText;
SourcePosts.Clear();
foreach (var source in result.SourcePosts)
{
SourcePosts.Add(source);
}
StatusMessage = $"Generated in {result.GenerationTime.TotalSeconds:F1}s using {result.ContextTokensUsed} context tokens";
}
catch (Exception ex)
{
AiSuggestion = $"Error: {ex.Message}";
}
finally
{
IsGenerating = false;
}
}
[RelayCommand]
private async Task GenerateStructure()
{
var sectionTitle = PromptForSectionTitle(); // Show dialog
var request = new GenerationRequest
{
CurrentText = GetCurrentText(),
Type = PromptType.SuggestStructure,
Variables = new Dictionary<string, string>
{
["section_title"] = sectionTitle
}
};
// ... similar to above
}
}
Real-Time Streaming
Pour un meilleur UX,, les tokens de flux sont générés en fonction des valeurs suivantes :
[RelayCommand]
private async Task GenerateWithStreaming()
{
IsGenerating = true;
AiSuggestion = "";
try
{
var request = new GenerationRequest
{
CurrentText = GetCurrentText(),
Type = PromptType.ContinueWriting
};
await foreach (var token in _generationService.GenerateStreamAsync(request))
{
// Update UI in real-time
await Dispatcher.UIThread.InvokeAsync(() =>
{
AiSuggestion += token;
});
}
}
finally
{
IsGenerating = false;
}
}
Filtration de la qualité
Les générations ne sont pas toutes bonnes.
public class QualityFilter
{
public bool IsAcceptableQuality(string generatedText, string currentText)
{
// Too short
if (generatedText.Length < 50)
{
return false;
}
// Just repeats the prompt
if (generatedText.StartsWith(currentText.Substring(Math.Max(0, currentText.Length - 100))))
{
return false;
}
// Contains common failure modes
var badPatterns = new[]
{
"I cannot",
"I don't have",
"As an AI",
"I apologize"
};
if (badPatterns.Any(p => generatedText.Contains(p, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
// Contains markdown artifacts
if (generatedText.Contains("```") && !generatedText.Contains("```\n"))
{
return false; // Incomplete code block
}
return true;
}
public string SuggestImprovement(string generatedText)
{
if (generatedText.Length < 50)
return "Too short - try increasing max_tokens";
if (!generatedText.Contains("."))
return "No complete sentences - check for truncation";
return "Quality seems acceptable";
}
}
Exemples d'utilisation
Continuer à écrire
var request = new GenerationRequest
{
CurrentText = @"# Using Docker Compose for Development
Docker Compose makes it incredibly easy to manage multiple containers. In this post, I'll show you how to set up a complete development environment with",
Type = PromptType.ContinueWriting
};
var result = await _generationService.GenerateAsync(request);
// Result: "databases, caches, and message queues all defined in a single YAML file. We'll start with PostgreSQL and Redis, then add RabbitMQ for asynchronous messaging."
Suggérer une structure
var request = new GenerationRequest
{
CurrentText = "# Understanding Entity Framework Migrations",
Type = PromptType.SuggestStructure,
Variables = new Dictionary<string, string>
{
["section_title"] = "Best Practices for Production"
}
};
var result = await _generationService.GenerateAsync(request);
// Result:
// - Always review generated migrations before applying
// - Test migrations on staging environment first
// - Keep migration history in version control
// - Plan for rollback scenarios
// - Monitor migration performance on large tables
Générer le code
var request = new GenerationRequest
{
CurrentText = "We need to configure Entity Framework with PostgreSQL",
Type = PromptType.GenerateCode,
Variables = new Dictionary<string, string>
{
["code_request"] = "Create a DbContext setup with PostgreSQL connection"
}
};
var result = await _generationService.GenerateAsync(request);
// Result: Complete C# code example
Surveillance du rendement
Performance de la production de pistes:
public class GenerationMetrics
{
private readonly List<GenerationResult> _recentResults = new();
private readonly object _lock = new();
public void RecordResult(GenerationResult result)
{
lock (_lock)
{
_recentResults.Add(result);
if (_recentResults.Count > 100)
{
_recentResults.RemoveAt(0);
}
}
}
public GenerationStats GetStats()
{
lock (_lock)
{
if (_recentResults.Count == 0)
return new GenerationStats();
return new GenerationStats
{
AverageGenerationTimeMs = _recentResults.Average(r => r.GenerationTime.TotalMilliseconds),
AverageContextTokens = _recentResults.Average(r => r.ContextTokensUsed),
AverageGeneratedTokens = _recentResults.Average(r => r.GeneratedTokens),
TotalGenerations = _recentResults.Count
};
}
}
}
public class GenerationStats
{
public double AverageGenerationTimeMs { get; set; }
public double AverageContextTokens { get; set; }
public double AverageGeneratedTokens { get; set; }
public int TotalGenerations { get; set; }
}
Résumé
Nous avons construit un système complet de production de contenu.
- ✅ Constructeur de contexte avec sélection intelligente des tronçons
- ✅ Modèles de prompt avancés pour différents scénarios
- ✅ Constructeur rapide avec substitution variable
- ✅ Service de production de contenu orchestrant tout
- ✅ RealM SK1 temps de diffusion pour un meilleur UX
- ✅ Filtration de qualité pour rejeter les mauvais résultats
- ✅ Surveillance du rendement
- ✅ Intégration avec le client Windows
Qu’est-ce que's Suivant?
À l'intérieur Partie 8: Faits avancés & Déploiement de la production (partie finale!), nousM SK2nous couvrirons :
- Auto-lien vers des postes connexes
- Stratégies de déploiement
- Gestion de la configuration
- Amélioration continue
- Méthodes d'utilisation du monde réel
- Guide de résolution des problèmes
- Améliorations futures
Nous allons terminer la série par le déploiement et faire en sorte que cette production soit prête.
Navigation des séries
- Partie 1: Introduction & Architecture
- Partie 2: Établissement d'un GPU & CUDA en C#
- Partie 3: Comprendre les emplacements & Bases de données vectorielles
- Partie 4: Établissement du pipeline d’ingestion
- Partie 5: Le client Windows
- Partie 6: Intégration locale de LLM
- Partie 7: Production de contenu & Génie rapide (le présent articleM SK1
- Partie 8: Faits avancés & Déploiement de la production
Ressources
Nous vous ferons plaisir. Partie finale!