Создание AI-копирайтера на PHP: от идеи до 200+ текстов в день

Создание AI-копирайтера на PHP: от идеи до 200+ текстов в день
Технические решения, проблемы интеграции OpenAI API и оптимизация работы с GPT-моделями
В этой статье разберу техническую реализацию веб-сервиса для генерации текстов с использованием OpenAI API. Покажу, с какими проблемами столкнулся при интеграции, как решал вопросы производительности и как построил систему умного выбора AI-моделей.
Почему не просто ChatGPT?
Задача была простая: автоматизировать создание однотипных текстов — описания товаров, посты, email-рассылки. ChatGPT для этого не подходил по нескольким причинам:
Отсутствие API без VPN — каждый запрос через прокси добавлял 2-5 секунд задержки
Нет переиспользования промптов — каждый раз формулировать запрос заново
Нет контроля над выбором модели — переплата за простые задачи
Невозможность встроить в свой workflow — копировать из интерфейса неудобно
Решил сделать обёртку над OpenAI API с системой шаблонов и автоматическим выбором модели.
Техническая архитектура
Стек технологий
Backend: PHP 8.1 + WordPress (как CMS для быстрого старта)
Database: MySQL 5.7
Frontend: Vanilla JS (без фреймворков для простоты)
API: OpenAI GPT-4o, GPT-4o-mini, o1-mini
Hosting: VPS в РФ (для работы без VPN)
Payment: ЮKassa через WooCommerce
Выбор WordPress был осознанным — нужна была готовая система пользователей, подписок и админ-панели. Написать всё с нуля заняло бы месяцы.
Схема работы
[Пользователь]
↓ (выбирает шаблон)
[Frontend: форма с параметрами]
↓ (AJAX-запрос)
[PHP: построение промпта]
↓ (выбор модели по правилам)
[OpenAI API]
↓ (streaming response)
[Frontend: отображение в реальном времени]
↓ (JS-анализ SEO)
[Результат + метрики]
Проблема 1: Интеграция с OpenAI API
Первая попытка — cURL
Начал с простого cURL-запроса:
function generate_text_simple($prompt) {
$api_key = get_option('openai_api_key');
$data = [
'model' => 'gpt-4o-mini',
'messages' => [
['role' => 'user', 'content' => $prompt]
],
'temperature' => 0.7
];
$ch = curl_init('https://api.openai.com/v1/chat/completions');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $api_key
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code !== 200) {
return ['error' => 'API request failed'];
}
$result = json_decode($response, true);
return $result['choices'][0]['message']['content'];
}
Проблемы:
Пользователь ждёт 30-60 секунд без обратной связи
Таймауты PHP (30 сек по умолчанию)
Невозможность отменить запрос
Решение — Server-Sent Events (SSE)
Переписал на streaming для отображения текста по мере генерации:
function generate_text_streaming($prompt, $model) {
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no'); // Для nginx
$api_key = get_option('openai_api_key');
$data = [
'model' => $model,
'messages' => [
['role' => 'system', 'content' => 'Ты профессиональный копирайтер...'],
['role' => 'user', 'content' => $prompt]
],
'stream' => true,
'temperature' => 0.7,
'max_tokens' => 4000
];
$ch = curl_init('https://api.openai.com/v1/chat/completions');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $api_key
]);
// Ключевой момент — функция обработки потока
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) {
$lines = explode("\n", $data);
foreach ($lines as $line) {
if (strpos($line, 'data: ') === 0) {
$json = substr($line, 6);
if ($json === '[DONE]') {
echo "data: [DONE]\n\n";
ob_flush();
flush();
continue;
}
$parsed = json_decode($json, true);
if (isset($parsed['choices'][0]['delta']['content'])) {
$content = $parsed['choices'][0]['delta']['content'];
echo "data: " . json_encode(['content' => $content]) . "\n\n";
ob_flush();
flush();
}
}
}
return strlen($data);
});
curl_exec($ch);
curl_close($ch);
}
Результат:
Текст появляется через 1-2 секунды
Пользователь видит прогресс
Можно отменить генерацию
Важный момент для nginx:
location /wp-admin/admin-ajax.php {
fastcgi_buffering off; # Отключаем буферизацию
proxy_buffering off;
# остальная конфигурация
}
Без этого nginx буферизует ответ и streaming не работает.
Проблема 2: Выбор оптимальной модели
GPT-4o стоит в 15 раз дороже GPT-4o-mini, но не всегда нужен. Для "Напиши пост на 200 символов" достаточно mini, а для "Создай SEO-статью на 10000 знаков с анализом конкурентов" нужна мощная модель.
Система умного выбора модели
Создал систему правил на основе параметров задачи:
class ModelSelector {
private $model_costs = [
'gpt-4o' => 5.00, // за 1M токенов
'gpt-4o-mini' => 0.15, // за 1M токенов
'o1-mini' => 3.00 // за 1M токенов
];
public function select_model($params) {
$length = intval($params['length']);
$complexity = $params['complexity']; // simple, medium, complex
$content_type = $params['content_type']; // post, article, description
$user_tier = $params['user_tier']; // free, start, pro, business
// Правило 1: Бесплатный тариф = только mini
if ($user_tier === 'free') {
return 'gpt-4o-mini';
}
// Правило 2: Короткие тексты (< 500 символов) = mini
if ($length < 500) {
return 'gpt-4o-mini';
}
// Правило 3: Сложные задачи = мощная модель
if ($complexity === 'complex') {
return ($user_tier === 'business') ? 'gpt-4o' : 'o1-mini';
}
// Правило 4: SEO-статьи > 3000 символов = o1-mini минимум
if ($content_type === 'seo_article' && $length > 3000) {
return ($user_tier === 'business') ? 'gpt-4o' : 'o1-mini';
}
// Правило 5: Средняя сложность = o1-mini
if ($complexity === 'medium') {
return 'o1-mini';
}
// По умолчанию
return 'gpt-4o-mini';
}
public function estimate_cost($prompt_tokens, $completion_tokens, $model) {
$total_tokens = $prompt_tokens + $completion_tokens;
$cost_per_million = $this->model_costs[$model];
return ($total_tokens / 1000000) * $cost_per_million;
}
}
Метрики эффективности выбора
Собрал статистику за месяц:
Запросов с gpt-4o-mini: 75% (средняя стоимость $0.002)
Запросов с o1-mini: 20% (средняя стоимость $0.015)
Запросов с gpt-4o: 5% (средняя стоимость $0.08)
Средняя стоимость запроса: $0.006
Экономия vs всегда gpt-4o: 92%
Важный инсайт: 75% задач отлично решаются дешёвой моделью. Переплачивать не имеет смысла.
Проблема 3: Система шаблонов
Каждый раз формулировать промпт с нуля — трата времени. Нужна библиотека готовых шаблонов.
Архитектура шаблонов
class TemplateManager {
private $templates = [];
public function __construct() {
$this->load_templates();
}
private function load_templates() {
// Базовый шаблон для описания товара
$this->templates['product_description'] = [
'name' => 'Описание товара для маркетплейса',
'category' => 'ecommerce',
'complexity' => 'simple',
'fields' => [
'product_name' => [
'label' => 'Название товара',
'type' => 'text',
'required' => true,
'placeholder' => 'Беспроводные наушники TWS'
],
'features' => [
'label' => 'Характеристики',
'type' => 'textarea',
'required' => true,
'placeholder' => 'ANC, 30 часов работы, IPX4'
],
'target_audience' => [
'label' => 'Целевая аудитория',
'type' => 'text',
'required' => false,
'placeholder' => 'Спортсмены 25-35 лет'
],
'keywords' => [
'label' => 'Ключевые слова (через запятую)',
'type' => 'text',
'required' => false,
'placeholder' => 'беспроводные наушники, TWS, спорт'
],
'marketplace' => [
'label' => 'Маркетплейс',
'type' => 'select',
'options' => ['Wildberries', 'Ozon', 'Яндекс.Маркет'],
'required' => true
]
],
'system_prompt' => 'Ты эксперт по написанию продающих описаний для маркетплейсов. Твоя задача — создать привлекательное и SEO-оптимизированное описание товара.',
'prompt_template' => 'Создай описание для товара "{product_name}" на маркетплейсе {marketplace}.
Характеристики: {features}
Целевая аудитория: {target_audience}
Ключевые слова для SEO: {keywords}
Требования:
- Длина 800-1200 символов
- Структура: заголовок, описание преимуществ, характеристики, призыв к действию
- Естественное вхождение ключевых слов (плотность 2-3%)
- Учитывай особенности маркетплейса {marketplace}'
];
// SEO-статья
$this->templates['seo_article'] = [
'name' => 'SEO-статья',
'category' => 'content',
'complexity' => 'complex',
'fields' => [
'topic' => [
'label' => 'Тема статьи',
'type' => 'text',
'required' => true
],
'keywords' => [
'label' => 'Ключевые запросы',
'type' => 'textarea',
'required' => true,
'placeholder' => 'Один ключ на строку'
],
'length' => [
'label' => 'Объём (символов)',
'type' => 'select',
'options' => ['3000', '5000', '7000', '10000'],
'required' => true
],
'tone' => [
'label' => 'Тон повествования',
'type' => 'select',
'options' => ['Деловой', 'Разговорный', 'Экспертный', 'Продающий'],
'required' => true
]
],
'system_prompt' => 'Ты SEO-копирайтер с 10-летним опытом. Специализируешься на текстах для Яндекса. Знаешь требования Баден-Баден и пишешь естественные, полезные тексты.',
'prompt_template' => 'Напиши SEO-статью на тему: "{topic}"
Ключевые запросы (используй естественно):
{keywords}
Объём: {length} символов
Тон: {tone}
Структура:
1. Введение с проблемой читателя (5-7% текста)
2. 3-5 подзаголовков H2 с раскрытием темы
3. Практические советы и примеры
4. Заключение с выводами
Требования:
- Плотность ключей 2-3% (не переспам!)
- Читабельность: индекс Флеша > 50
- Естественный язык без "воды"
- Структурированность: списки, таблицы где уместно
- Экспертность и глубина раскрытия темы'
];
// Ещё 20+ шаблонов...
}
public function build_prompt($template_id, $user_data) {
$template = $this->templates[$template_id];
$prompt = $template['prompt_template'];
// Замена плейсхолдеров на данные пользователя
foreach ($user_data as $key => $value) {
$prompt = str_replace('{' . $key . '}', $value, $prompt);
}
return [
'system' => $template['system_prompt'],
'user' => $prompt,
'complexity' => $template['complexity']
];
}
public function get_template_form($template_id) {
return $this->templates[$template_id]['fields'];
}
}
Frontend для работы с шаблоном
class TemplateForm {
constructor(templateId) {
this.templateId = templateId;
this.form = document.getElementById('template-form');
this.loadTemplate();
}
async loadTemplate() {
// Загружаем структуру шаблона
const response = await fetch('/wp-admin/admin-ajax.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: new URLSearchParams({
action: 'get_template_structure',
template_id: this.templateId
})
});
const template = await response.json();
this.renderForm(template.fields);
}
renderForm(fields) {
this.form.innerHTML = '';
for (const [key, field] of Object.entries(fields)) {
const wrapper = document.createElement('div');
wrapper.className = 'form-field';
const label = document.createElement('label');
label.textContent = field.label;
if (field.required) label.classList.add('required');
let input;
if (field.type === 'textarea') {
input = document.createElement('textarea');
input.rows = 4;
} else if (field.type === 'select') {
input = document.createElement('select');
field.options.forEach(opt => {
const option = document.createElement('option');
option.value = opt;
option.textContent = opt;
input.appendChild(option);
});
} else {
input = document.createElement('input');
input.type = field.type;
}
input.name = key;
input.id = `field-${key}`;
input.placeholder = field.placeholder || '';
input.required = field.required;
wrapper.appendChild(label);
wrapper.appendChild(input);
this.form.appendChild(wrapper);
}
// Кнопка генерации
const button = document.createElement('button');
button.type = 'submit';
button.textContent = 'Генерировать текст';
button.className = 'btn-generate';
this.form.appendChild(button);
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
}
async handleSubmit(e) {
e.preventDefault();
const formData = new FormData(this.form);
const data = Object.fromEntries(formData);
// Streaming генерация
this.startStreaming(data);
}
startStreaming(data) {
const resultDiv = document.getElementById('generated-text');
resultDiv.innerHTML = '';
const eventSource = new EventSource(
'/wp-admin/admin-ajax.php?action=generate_text_stream&' +
new URLSearchParams({
template_id: this.templateId,
...data
})
);
eventSource.onmessage = (event) => {
if (event.data === '[DONE]') {
eventSource.close();
this.analyzeText(resultDiv.textContent);
return;
}
const chunk = JSON.parse(event.data);
resultDiv.textContent += chunk.content;
// Автоскролл
resultDiv.scrollTop = resultDiv.scrollHeight;
};
eventSource. => {
eventSource.close();
console.error('Streaming error');
};
}
analyzeText(text) {
// SEO-анализ (об этом ниже)
const analyzer = new SEOAnalyzer();
const metrics = analyzer.analyze(text);
this.displayMetrics(metrics);
}
}
Проблема 4: SEO-анализ текста
Генерация текста — это полдела. Нужно проверить качество с точки зрения SEO.
Реализация SEO-анализатора на JavaScript
class SEOAnalyzer {
analyze(text) {
return {
length: this.getLength(text),
keywords: this.analyzeKeywords(text),
readability: this.calculateReadability(text),
water: this.calculateWater(text),
spam: this.calculateSpam(text)
};
}
getLength(text) {
return {
characters: text.length,
charactersNoSpaces: text.replace(/\s/g, '').length,
words: text.split(/\s+/).filter(w => w.length > 0).length,
sentences: text.split(/[.!?]+/).filter(s => s.trim().length > 0).length
};
}
analyzeKeywords(text, keywords = []) {
const words = text.toLowerCase().match(/\b[\wа-яё]+\b/gi) || [];
const totalWords = words.length;
const keywordStats = {};
keywords.forEach(keyword => {
const keywordLower = keyword.toLowerCase();
const regex = new RegExp('\\b' + keywordLower + '\\b', 'gi');
const count = (text.match(regex) || []).length;
const density = (count / totalWords) * 100;
keywordStats[keyword] = {
count: count,
density: density.toFixed(2),
status: this.getKeywordStatus(density)
};
});
return keywordStats;
}
getKeywordStatus(density) {
if (density < 1) return 'low'; // Мало
if (density <= 3) return 'good'; // Оптимально
if (density <= 5) return 'high'; // Много
return 'spam'; // Переспам
}
calculateReadability(text) {
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0);
const words = text.split(/\s+/).filter(w => w.length > 0);
const syllables = this.countSyllables(text);
if (sentences.length === 0 || words.length === 0) return 0;
// Индекс Флеша для русского языка
const ASL = words.length / sentences.length; // Average Sentence Length
const ASW = syllables / words.length; // Average Syllables per Word
const fleschScore = 206.835 - (1.015 * ASL) - (84.6 * ASW);
return {
score: Math.max(0, Math.min(100, fleschScore)).toFixed(1),
level: this.getReadabilityLevel(fleschScore),
avgWordsPerSentence: ASL.toFixed(1),
avgSyllablesPerWord: ASW.toFixed(2)
};
}
countSyllables(text) {
// Упрощённый подсчёт слогов для русского языка
const vowels = 'аеёиоуыэюя';
let count = 0;
let prevIsVowel = false;
for (let char of text.toLowerCase()) {
const isVowel = vowels.includes(char);
if (isVowel && !prevIsVowel) {
count++;
}
prevIsVowel = isVowel;
}
return count || 1;
}
getReadabilityLevel(score) {
if (score >= 80) return 'Очень лёгкий';
if (score >= 70) return 'Лёгкий';
if (score >= 60) return 'Средний';
if (score >= 50) return 'Сложноватый';
if (score >= 30) return 'Сложный';
return 'Очень сложный';
}
calculateWater(text) {
// "Водность" — процент стоп-слов
const stopWords = [
'а', 'без', 'более', 'бы', 'был', 'была', 'были', 'было',
'быть', 'в', 'вам', 'вас', 'весь', 'во', 'вот', 'все',
'всё', 'всего', 'всех', 'вы', 'где', 'да', 'даже', 'для',
'до', 'его', 'ее', 'её', 'ей', 'ему', 'если', 'есть',
'ещё', 'же', 'за', 'здесь', 'и', 'из', 'или', 'им',
'их', 'к', 'как', 'ко', 'когда', 'кто', 'ли', 'либо',
'мне', 'может', 'мы', 'на', 'надо', 'наш', 'не', 'него',
'неё', 'нет', 'ни', 'них', 'но', 'ну', 'о', 'об', 'один',
'он', 'она', 'они', 'оно', 'от', 'очень', 'по', 'под',
'при', 'с', 'со', 'так', 'также', 'такой', 'там', 'те',
'тем', 'то', 'того', 'тоже', 'той', 'только', 'том', 'ты',
'у', 'уже', 'хотя', 'чего', 'чей', 'чем', 'что', 'чтобы',
'чьё', 'эта', 'эти', 'это', 'я'
];
const words = text.toLowerCase().match(/\b[\wа-яё]+\b/gi) || [];
const stopWordCount = words.filter(w => stopWords.includes(w)).length;
const waterPercent = (stopWordCount / words.length) * 100;
return {
percent: waterPercent.toFixed(1),
status: waterPercent < 15 ? 'good' : waterPercent < 30 ? 'medium' : 'high',
label: waterPercent < 15 ? 'Низкая' : waterPercent < 30 ? 'Средняя' : 'Высокая'
};
}
calculateSpam(text) {
// "Тошнота" — частота самого частого слова
const words = text.toLowerCase().match(/\b[\wа-яё]{4,}\b/gi) || [];
const frequency = {};
words.forEach(word => {
frequency[word] = (frequency[word] || 0) + 1;
});
const maxFrequency = Math.max(...Object.values(frequency));
const spamPercent = (maxFrequency / words.length) * 100;
return {
percent: spamPercent.toFixed(2),
maxWord: Object.keys(frequency).find(k => frequency[k] === maxFrequency),
status: spamPercent < 3 ? 'good' : spamPercent < 5 ? 'medium' : 'high'
};
}
}
Отображение метрик
function displayMetrics(metrics) {
const container = document.getElementById('seo-metrics');
container.innerHTML = `
<div class="metrics-grid">
<div class="metric">
<h4>Объём текста</h4>
<p>${metrics.length.characters} символов</p>
<p>${metrics.length.words} слов</p>
</div>
<div class="metric">
<h4>Читаемость</h4>
<p class="score">${metrics.readability.score}</p>
<p>${metrics.readability.level}</p>
</div>
<div class="metric ${metrics.water.status}">
<h4>Водность</h4>
<p>${metrics.water.percent}%</p>
<p>${metrics.water.label}</p>
</div>
<div class="metric ${metrics.spam.status}">
<h4>Тошнота</h4>
<p>${metrics.spam.percent}%</p>
<p>Частое слово: ${metrics.spam.maxWord}</p>
</div>
</div>
<div class="keywords-analysis">
<h4>Плотность ключевых слов</h4>
${Object.entries(metrics.keywords).map(([key, data]) => `
<div class="keyword-stat ${data.status}">
<span class="keyword">${key}</span>
<span class="count">${data.count}x</span>
<span class="density">${data.density}%</span>
</div>
`).join('')}
</div>
`;
}
Проблема 5: Оптимизация затрат на API
За первую неделю работы потратил $25 на API. При 200 запросах в день это $750/месяц — неприемлемо.
Решение 1: Кэширование популярных запросов
class RequestCache {
private $cache_ttl = 86400; // 24 часа
public function get_cache_key($template_id, $params) {
// Убираем уникальные параметры типа "название компании"
$cache_params = $params;
unset($cache_params['company_name']);
unset($cache_params['product_name']);
return md5($template_id . serialize($cache_params));
}
public function get($cache_key) {
global $wpdb;
$cached = $wpdb->get_row($wpdb->prepare(
"SELECT content, created_at FROM {$wpdb->prefix}gpt_cache
WHERE cache_key = %s AND created_at > %s",
$cache_key,
date('Y-m-d H:i:s', time() - $this->cache_ttl)
));
return $cached ? $cached->content : null;
}
public function set($cache_key, $content) {
global $wpdb;
$wpdb->replace(
$wpdb->prefix . 'gpt_cache',
[
'cache_key' => $cache_key,
'content' => $content,
'created_at' => current_time('mysql')
],
['%s', '%s', '%s']
);
}
}
Результат: 40% запросов попали в кэш, экономия $300/месяц.
Решение 2: Ограничение max_tokens
function calculate_max_tokens($requested_length, $user_tier) {
// Запрашиваемая длина в символах → токены
$base_tokens = ceil($requested_length / 3); // ~3 символа на токен для русского
// Добавляем запас на промпт (обычно 200-500 токенов)
$prompt_tokens = 500;
// Ограничения по тарифу
$tier_limits = [
'free' => 800,
'start' => 1500,
'pro' => 4000,
'business' => 8000
];
$max_allowed = $tier_limits[$user_tier];
$calculated = min($base_tokens + $prompt_tokens, $max_allowed);
return $calculated;
}
Результат: снижение затрат на 30% за счёт отсутствия "лишних" токенов.
Решение 3: Rate limiting
class RateLimiter {
public function check_limit($user_id, $user_tier) {
$limits = [
'free' => ['requests' => 2, 'period' => 'day'],
'start' => ['requests' => 30, 'period' => 'month'],
'pro' => ['requests' => 100, 'period' => 'month'],
'business' => ['requests' => 200, 'period' => 'month']
];
$limit = $limits[$user_tier];
$count = $this->get_request_count($user_id, $limit['period']);
if ($count >= $limit['requests']) {
return [
'allowed' => false,
'remaining' => 0,
'reset_at' => $this->get_reset_time($limit['period'])
];
}
$this->increment_count($user_id);
return [
'allowed' => true,
'remaining' => $limit['requests'] - $count - 1,
'reset_at' => $this->get_reset_time($limit['period'])
];
}
private function get_request_count($user_id, $period) {
global $wpdb;
$since = ($period === 'day')
? date('Y-m-d 00:00:00')
: date('Y-m-01 00:00:00');
return $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}gpt_requests
WHERE user_id = %d AND created_at >= %s",
$user_id, $since
));
}
private function increment_count($user_id) {
global $wpdb;
$wpdb->insert(
$wpdb->prefix . 'gpt_requests',
[
'user_id' => $user_id,
'created_at' => current_time('mysql')
],
['%d', '%s']
);
}
private function get_reset_time($period) {
return ($period === 'day')
? strtotime('tomorrow midnight')
: strtotime('first day of next month midnight');
}
}
Итоговая архитектура
┌─────────────────────────────────────────────────────────┐
│ Пользователь │
└────────────────────┬────────────────────────────────────┘
│
┌───────────▼────────────┐
│ Frontend (JS) │
│ - Выбор шаблона │
│ - Заполнение формы │
│ - SSE для streaming │
│ - SEO-анализ │
└───────────┬────────────┘
│ AJAX
┌───────────▼────────────┐
│ WordPress Backend │
│ - TemplateManager │
│ - ModelSelector │
│ - RateLimiter │
│ - RequestCache │
└───────────┬────────────┘
│
┌───────────▼────────────┐
│ OpenAI API │
│ - GPT-4o │
│ - GPT-4o-mini │
│ - o1-mini │
└────────────────────────┘
Результаты и метрики
Производительность
После 3 месяцев работы и 2000+ сгенерированных текстов:
Среднее время генерации:
- Короткий текст (< 500 символов): 8 секунд
- Средний текст (500-2000): 18 секунд
- Длинный текст (2000-5000): 35 секунд
- SEO-статья (5000+): 60-90 секунд
Надёжность:
- Успешных запросов: 98.7%
- Ошибок API: 0.8%
- Таймаутов: 0.5%
Экономика:
- Средняя стоимость запроса: $0.006
- Затраты на API: $450/месяц (при 2500 запросах)
- Доход от подписок: ~$1800/месяц
- Маржинальность: 75%
Паттерны использования
Самые популярные шаблоны:
1. Описание товара (42%)
2. Пост для соцсетей (28%)
3. Email-письмо (15%)
4. SEO-статья (10%)
5. Остальные (5%)
Распределение по тарифам:
- Free: 60% пользователей, 15% запросов
- Start: 25% пользователей, 30% запросов
- Pro: 12% пользователей, 35% запросов
- Business: 3% пользователей, 20% запросов
Проблемы, которые ещё не решил
Fine-tuning моделей
Хочу обучить модель на успешных примерах, но OpenAI пока не открыл fine-tuning для GPT-4o.Мультиязычность
Сейчас только русский. Расширение на английский/казахский требует новых промптов.A/B тестирование промптов
Нет инструментов для сравнения эффективности разных формулировок.Автоматическая оценка качества
Сейчас оценка только от пользователей. Хочу автоматический scoring.
Технические выводы
Что сработало:
WordPress как основа — быстрый старт
SSE для streaming — отличный UX
Умный выбор модели — экономия 92%
Система шаблонов — основная ценность продукта
JS для SEO-анализа — работает быстро
Что не сработало:
Попытка использовать GPT для категоризации — слишком дорого
Автоматическое определение сложности текста — AI ошибается
Кэширование с учётом параметров — ключи коллизий
Технический долг:
Миграция с WordPress на чистый PHP (производительность)
Переписать frontend на React (сейчас 1500 строк ванильного JS)
Добавить очередь запросов (сейчас синхронная обработка)
Улучшить систему логирования
Полезные ссылки
Рабочий прототип: https://gpttext.ru
OpenAI API docs: https://platform.openai.com/docs
SSE: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
Если есть вопросы по реализации или идеи по улучшению — пишите в комментариях. Особенно интересно обсудить:
Опыт работы с OpenAI API и оптимизацию затрат
Альтернативные подходы к выбору модели
Методы оценки качества AI-текстов