Compare commits
No commits in common. "9f616aaf1dc2c5c4065b723a3a93ec1e9f6006cd" and "4cc97efebacba596db745a27a82f36fd41da2ea9" have entirely different histories.
9f616aaf1d
...
4cc97efeba
346
README.md
346
README.md
|
@ -6,349 +6,3 @@
|
||||||
|
|
||||||
Сервис для генерации персонализированных холодных писем с использованием RAG и LangGraph.
|
Сервис для генерации персонализированных холодных писем с использованием RAG и LangGraph.
|
||||||
|
|
||||||
## О программе
|
|
||||||
|
|
||||||
- Персонализация писем под роль, индустрию и компанию лида
|
|
||||||
- RAG-подход с базой знаний реальных кейсов
|
|
||||||
- LangGraph пайплайн обработки запросов
|
|
||||||
- Векторный поиск в ChromaDB
|
|
||||||
- Поддержка OpenAI и Google Gemini
|
|
||||||
- RESTful API для интеграции с CRM
|
|
||||||
|
|
||||||
### Логика работы
|
|
||||||
|
|
||||||
Система обрабатывает запросы через 10-этапный LangGraph пайплайн:
|
|
||||||
|
|
||||||
1. **Валидация входа** - проверка обязательных полей лида
|
|
||||||
2. **Извлечение признаков** - нормализация роли, индустрии из входных данных
|
|
||||||
3. **Построение запроса** - формирование поискового запроса на основе профиля лида
|
|
||||||
4. **Векторный поиск** - поиск релевантных кейсов в ChromaDB (top-30)
|
|
||||||
5. **Ранжирование контекста** - отбор лучших кейсов (top-6) и создание bullets
|
|
||||||
6. **Построение промпта** - формирование системного и пользовательского промптов
|
|
||||||
7. **LLM генерация** - создание письма через OpenAI/Gemini
|
|
||||||
8. **Парсинг ответа** - извлечение JSON с темой и телом письма
|
|
||||||
9. **Проверка качества** - валидация длины, структуры, наличия CTA
|
|
||||||
10. **Формирование результата** - финальная структура ответа с метаданными
|
|
||||||
|
|
||||||
## Промпт-инжиниринг: обоснование подхода
|
|
||||||
|
|
||||||
### Почему именно такая структура письма?
|
|
||||||
|
|
||||||
Структура **Приветствие -> Хук -> Ценность -> Кейс -> CTA -> Подпись** выбрана на основе исследований конверсии холодных писем:
|
|
||||||
|
|
||||||
**1. Приветствие по имени**
|
|
||||||
- Персонализация создает впечатление индивидуального подхода
|
|
||||||
- Снижает восприятие письма как спама
|
|
||||||
|
|
||||||
**2. Хук-вопрос в первых строках**
|
|
||||||
- Привлекает внимание и активирует любопытство
|
|
||||||
- Касается специфики индустрии лида -> создает ощущение релевантности
|
|
||||||
|
|
||||||
**3. Конкретная ценность с цифрами**
|
|
||||||
- Мозг лучше воспринимает конкретные числа vs абстракции
|
|
||||||
- "15 минут" vs "быстро", "95%" vs "значительно"
|
|
||||||
- Снижает скептицизм -> повышает доверие
|
|
||||||
|
|
||||||
**4. Социальное доказательство с кейсом**
|
|
||||||
- Название реальной компании + численный результат
|
|
||||||
- Психология: "если помогли похожей компании, помогут и нам"
|
|
||||||
- Снижает воспринимаемый риск принятия решения
|
|
||||||
|
|
||||||
**5. Мягкий CTA без давления**
|
|
||||||
- "Если интересно" vs "Давайте встретимся завтра"
|
|
||||||
- Оставляет ощущение выбора -> снижает сопротивление
|
|
||||||
- 15-минутный формат -> низкий барьер входа
|
|
||||||
|
|
||||||
**6. Эмоциональные триггеры**
|
|
||||||
- "Спокойствие", "контроль" -> позитивные эмоции от решения проблем
|
|
||||||
- "Избежите", "снизьте риски" -> страх потерь (loss aversion)
|
|
||||||
- Комбинация мотивирует к действию
|
|
||||||
|
|
||||||
### Ограничения для максимальной эффективности
|
|
||||||
|
|
||||||
- **≤ 1000 символов**: оптимальная длина письма для цифрового потребления. Согласно Nielsen Norman Group, пользователи читают в среднем 20–28% текста на веб-странице и быстро теряют внимание после первых 200–250 слов (~1000–1200 символов).
|
|
||||||
Источник: [How Users Read on the Web](https://www.nngroup.com/articles/how-users-read-on-the-web/)
|
|
||||||
- **Микро-абзацы 1-2 предложения**: улучшают восприятие текста на экранах смартфонов. Люди сканируют информацию взглядом, и короткие абзацы помогают быстрее вычленять ключевые мысли.
|
|
||||||
Источник: [F-Shaped Pattern of Reading Web Content](https://www.nngroup.com/articles/f-shaped-pattern-reading-web-content/)
|
|
||||||
- **1 конкретный CTA**: письма с одним призывом к действию получают на **371 % больше кликов** и могут увеличить доход на **1617 %** по сравнению с письмами с несколькими CTA.
|
|
||||||
Источник: [Unlayer - Call to Action in Emails](https://unlayer.com/blog/call-to-action-in-emails)
|
|
||||||
|
|
||||||
### Настройка промптов
|
|
||||||
|
|
||||||
Системные промпты: `src/services/prompt_templates.py`
|
|
||||||
|
|
||||||
- Тон: деловой, лаконичный, дружелюбный
|
|
||||||
- Длина: до 1000 символов
|
|
||||||
- Структура: Приветствие -> Хук -> Ценность -> Кейс -> CTA -> Подпись
|
|
||||||
|
|
||||||
## Быстрый старт
|
|
||||||
|
|
||||||
### 1. Подготовка окружения
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://git.itqop.pw/itqop/ai-email-assistant.git
|
|
||||||
cd ai-email-assistant
|
|
||||||
|
|
||||||
# Создание конфигурации
|
|
||||||
cp .env.example .env
|
|
||||||
# Или создайте .env файл сами
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Базовая конфигурация .env
|
|
||||||
|
|
||||||
```bash
|
|
||||||
LLM_PROVIDER=openai
|
|
||||||
OPENAI_API_KEY=your_openai_api_key_here
|
|
||||||
API_SECRET_KEY=your_admin_token
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Запуск через Docker
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose up --build
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Загрузка базы знаний
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST "http://localhost:8000/api/v1/admin/ingest" \
|
|
||||||
-H "Authorization: Bearer your_admin_token"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Использование
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST "http://localhost:8000/api/v1/generate_email" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"contact": "Помящий Никита",
|
|
||||||
"position": "Технический директор",
|
|
||||||
"company_name": "FIVE",
|
|
||||||
"segment": "маркетинговое агентство"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Альтернативный запуск (без Docker)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Создание виртуального окружения
|
|
||||||
python -m venv .venv
|
|
||||||
.venv\Scripts\activate # Windows
|
|
||||||
source .venv/bin/activate # Linux/Mac
|
|
||||||
|
|
||||||
# Установка зависимостей
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# Загрузка данных
|
|
||||||
python -m src.ingest.ingest_cli --data-dir articles_konsol_pro --recreate
|
|
||||||
|
|
||||||
# Запуск сервиса
|
|
||||||
python -m src.app.main
|
|
||||||
```
|
|
||||||
|
|
||||||
## API документация
|
|
||||||
|
|
||||||
### Основные эндпоинты
|
|
||||||
|
|
||||||
- `GET /healthz` - Проверка здоровья
|
|
||||||
- `GET /readiness` - Готовность к работе
|
|
||||||
- `GET /docs` - Swagger документация
|
|
||||||
- `POST /api/v1/generate_email` - Генерация письма
|
|
||||||
|
|
||||||
### Формат запроса
|
|
||||||
|
|
||||||
**Обязательные поля:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"contact": "string",
|
|
||||||
"position": "string",
|
|
||||||
"company_name": "string",
|
|
||||||
"segment": "string"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Полный формат:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"contact": "Помящий Никита",
|
|
||||||
"position": "Технический директор",
|
|
||||||
"company_name": "FIVE",
|
|
||||||
"segment": "маркетинговое агентство",
|
|
||||||
"email": "nikita@five.agency",
|
|
||||||
"locale": "ru",
|
|
||||||
"notes": "Дополнительная информация"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Формат ответа
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"subject": "Как упростить работу с самозанятыми в FIVE?",
|
|
||||||
"body": "Здравствуйте, Никита!....",
|
|
||||||
"meta": {
|
|
||||||
"locale": "ru",
|
|
||||||
"lead_normalized": {
|
|
||||||
"contact_name": "Помящий Никита",
|
|
||||||
"contact_first_name": "Никита",
|
|
||||||
"contact_last_name": "Помящий",
|
|
||||||
"role_title": "Технический директор",
|
|
||||||
"role_category": "tech",
|
|
||||||
"company_name": "FIVE",
|
|
||||||
"industry_segment": "маркетинговое агентство",
|
|
||||||
"industry_tag": "marketing_agency",
|
|
||||||
"email": null,
|
|
||||||
"locale": "ru",
|
|
||||||
"notes": null
|
|
||||||
},
|
|
||||||
"used_chunks": [
|
|
||||||
"sds_podkluchenie_k_konsoli#sds_podkluchenie_k_konsoli#c7",
|
|
||||||
"vysotnik_rental#vysotnik_rental#c2"
|
|
||||||
],
|
|
||||||
"model": "gpt-4o",
|
|
||||||
"tokens_prompt": 1132,
|
|
||||||
"tokens_completion": 289,
|
|
||||||
"guardrails_violations": 0,
|
|
||||||
"context_chunks_used": 5,
|
|
||||||
"context_quality_score": 0.87
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Структура meta:**
|
|
||||||
- `locale` - Язык, на котором сгенерировано письмо (например, ru)
|
|
||||||
- `lead_normalized` - Объект с нормализованной и обогащенной информацией о лиде:
|
|
||||||
- `contact_first_name/contact_last_name` - Распознанные имя и фамилия
|
|
||||||
- `role_category/industry_tag` - Машинно-читаемые теги для должности и сегмента компании (например, tech, marketing_agency)
|
|
||||||
- `used_chunks` - Массив с ID фрагментов из базы знаний, которые были использованы для генерации ответа
|
|
||||||
- `model` - Использованная LLM модель
|
|
||||||
- `tokens_prompt/tokens_completion` - Расход токенов на запрос и ответ для мониторинга затрат
|
|
||||||
- `guardrails_violations` - Количество нарушений правил безопасности (guardrails). 0 означает, что все проверки пройдены
|
|
||||||
- `context_chunks_used` - Общее количество фрагментов контекста, которые были использованы для генерации
|
|
||||||
- `context_quality_score` - Оценка релевантности найденного контекста (0-1):
|
|
||||||
- 1.0 - отличное качество контекста
|
|
||||||
- 0.8+ - хорошее качество (высокая релевантность)
|
|
||||||
- 0.6+ - приемлемое качество (достаточная релевантность)
|
|
||||||
- <0.6 - низкое качество (слабая релевантность)
|
|
||||||
|
|
||||||
## Конфигурация
|
|
||||||
|
|
||||||
### Переменные окружения
|
|
||||||
|
|
||||||
| Параметр | Описание | По умолчанию |
|
|
||||||
|----------|----------|--------------|
|
|
||||||
| `LLM_PROVIDER` | openai или gemini | openai |
|
|
||||||
| `LLM_MODEL` | Модель генерации | gpt-4o |
|
|
||||||
| `EMBEDDING_MODEL` | Модель векторизации | text-embedding-3-large |
|
|
||||||
| `TOP_K` | Чанков для поиска | 30 |
|
|
||||||
| `TOP_N_CONTEXT` | Чанков в контексте | 6 |
|
|
||||||
| `CHUNK_SIZE` | Размер чанка (токены) | 500 |
|
|
||||||
| `CHUNK_OVERLAP` | Перекрытие чанков | 100 |
|
|
||||||
|
|
||||||
## Мониторинг
|
|
||||||
|
|
||||||
### Качество контекста
|
|
||||||
|
|
||||||
Метрика `context_quality_score` вычисляется на основе:
|
|
||||||
- **Средний similarity score** (40%) - релевантность найденных кейсов
|
|
||||||
- **Доля высококачественных chunks** (25%) - процент кейсов с similarity > 0.7
|
|
||||||
- **Консистентность результатов** (20%) - равномерность качества chunks
|
|
||||||
- **Фактор количества** (15%) - достаточность объема контекста
|
|
||||||
|
|
||||||
**Интерпретация для мониторинга:**
|
|
||||||
- `context_quality_score < 0.5` - критически низкое качество, требует обновления базы знаний
|
|
||||||
- `context_quality_score < 0.7` - предупреждение о качестве контекста
|
|
||||||
- `context_quality_score >= 0.8` - хорошее качество генерации
|
|
||||||
|
|
||||||
### Логи
|
|
||||||
|
|
||||||
Все запросы логируются в JSON:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"request_id": "1641234567890",
|
|
||||||
"method": "POST",
|
|
||||||
"url": "http://localhost:8000/api/v1/generate_email",
|
|
||||||
"status_code": 200,
|
|
||||||
"process_time": 2.341
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Проверка состояния
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Статус сервиса
|
|
||||||
curl http://localhost:8000/healthz
|
|
||||||
|
|
||||||
# Статистика базы знаний
|
|
||||||
curl -H "Authorization: Bearer secret" \
|
|
||||||
http://localhost:8000/api/v1/admin/knowledge-base/stats
|
|
||||||
|
|
||||||
# Docker логи
|
|
||||||
docker-compose logs -f ai-email-assistant
|
|
||||||
```
|
|
||||||
|
|
||||||
## Устранение ошибок
|
|
||||||
|
|
||||||
### Частые проблемы
|
|
||||||
|
|
||||||
**404 No relevant knowledge found**
|
|
||||||
```bash
|
|
||||||
# Загрузите базу знаний
|
|
||||||
curl -X POST -H "Authorization: Bearer secret" \
|
|
||||||
"http://localhost:8000/api/v1/admin/ingest"
|
|
||||||
```
|
|
||||||
|
|
||||||
**502 External service error**
|
|
||||||
- Проверьте API ключи LLM провайдера в .env
|
|
||||||
|
|
||||||
**400 Validation failed**
|
|
||||||
- Убедитесь что все обязательные поля заполнены
|
|
||||||
|
|
||||||
## Обновление базы знаний
|
|
||||||
|
|
||||||
### Добавление кейсов
|
|
||||||
|
|
||||||
1. Поместите `.md` файлы в `data`
|
|
||||||
2. Файлы должны содержать:
|
|
||||||
- Заголовки для структуры
|
|
||||||
- Числовые метрики
|
|
||||||
- Ключевые слова индустрии
|
|
||||||
- Упоминания ролей
|
|
||||||
|
|
||||||
3. Дополните базу (инкрементально):
|
|
||||||
```bash
|
|
||||||
curl -X POST -H "Authorization: Bearer secret" \
|
|
||||||
"http://localhost:8000/api/v1/admin/ingest"
|
|
||||||
```
|
|
||||||
4. (альтернативно) Пересоздайте базу:
|
|
||||||
```bash
|
|
||||||
curl -X POST -H "Authorization: Bearer secret" \
|
|
||||||
"http://localhost:8000/api/v1/admin/ingest?recreate=true"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Формат кейсов
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# Кейс компании FIVE
|
|
||||||
|
|
||||||
Маркетинговое агентство автоматизировало работу с подрядчиками.
|
|
||||||
|
|
||||||
## Результаты
|
|
||||||
- Онбординг: с 2 дней до 15 минут
|
|
||||||
- Снижение ошибок: на 95%
|
|
||||||
- Выплаты: мгновенные
|
|
||||||
|
|
||||||
Технический директор Никита отмечает эффективность.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Тестирование
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Все тесты
|
|
||||||
pytest src/tests/
|
|
||||||
|
|
||||||
# Только API
|
|
||||||
pytest src/tests/test_api.py
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
|
@ -27,12 +27,7 @@ def context_rank_node(state: EmailGenerationState) -> EmailGenerationState:
|
||||||
|
|
||||||
ranked_context = retrieval_service.build_context(ranked_chunks)
|
ranked_context = retrieval_service.build_context(ranked_chunks)
|
||||||
|
|
||||||
context_quality_score = retrieval_service.calculate_context_quality_score(
|
|
||||||
ranked_chunks
|
|
||||||
)
|
|
||||||
|
|
||||||
state["ranked_context"] = ranked_context
|
state["ranked_context"] = ranked_context
|
||||||
state["context_quality_score"] = context_quality_score
|
|
||||||
return state
|
return state
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
@ -40,7 +40,6 @@ def guardrails_node(state: EmailGenerationState) -> EmailGenerationState:
|
||||||
llm_output = state.get("llm_output")
|
llm_output = state.get("llm_output")
|
||||||
lead_model = state.get("lead_model")
|
lead_model = state.get("lead_model")
|
||||||
ranked_context = state.get("ranked_context")
|
ranked_context = state.get("ranked_context")
|
||||||
context_quality_score = state.get("context_quality_score")
|
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
"locale": lead_model.locale if lead_model else "ru",
|
"locale": lead_model.locale if lead_model else "ru",
|
||||||
|
@ -51,9 +50,6 @@ def guardrails_node(state: EmailGenerationState) -> EmailGenerationState:
|
||||||
"tokens_completion": llm_output.tokens_completion if llm_output else 0,
|
"tokens_completion": llm_output.tokens_completion if llm_output else 0,
|
||||||
"guardrails_violations": len(violations),
|
"guardrails_violations": len(violations),
|
||||||
"context_chunks_used": len(ranked_context.chunks) if ranked_context else 0,
|
"context_chunks_used": len(ranked_context.chunks) if ranked_context else 0,
|
||||||
"context_quality_score": (
|
|
||||||
context_quality_score if context_quality_score is not None else 0.0
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
email_clean = EmailDraftClean(
|
email_clean = EmailDraftClean(
|
||||||
|
|
|
@ -20,7 +20,6 @@ class EmailGenerationState(TypedDict):
|
||||||
retrieval_query: Optional[RetrievalQuery]
|
retrieval_query: Optional[RetrievalQuery]
|
||||||
retrieved_chunks: Optional[List[DocChunk]]
|
retrieved_chunks: Optional[List[DocChunk]]
|
||||||
ranked_context: Optional[RankedContext]
|
ranked_context: Optional[RankedContext]
|
||||||
context_quality_score: Optional[float]
|
|
||||||
prompt_payload: Optional[PromptPayload]
|
prompt_payload: Optional[PromptPayload]
|
||||||
llm_output: Optional[LLMRawOutput]
|
llm_output: Optional[LLMRawOutput]
|
||||||
email_draft: Optional[EmailDraft]
|
email_draft: Optional[EmailDraft]
|
||||||
|
|
|
@ -254,7 +254,7 @@ class MarkdownLoader:
|
||||||
r"hello@konsol\.pro",
|
r"hello@konsol\.pro",
|
||||||
r"\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}",
|
r"\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}",
|
||||||
r"125047.*?дом \d+",
|
r"125047.*?дом \d+",
|
||||||
r"\[Разработка - SKDO\]\(\)",
|
r"\[Разработка — SKDO\]\(\)",
|
||||||
r"\[Подключиться к Консоли\]\(\)",
|
r"\[Подключиться к Консоли\]\(\)",
|
||||||
r"\[Кейсы наших клиентов\]\(\)",
|
r"\[Кейсы наших клиентов\]\(\)",
|
||||||
r"\[Делимся экспертизой\]\(\)",
|
r"\[Делимся экспертизой\]\(\)",
|
||||||
|
|
|
@ -3,7 +3,7 @@ from src.models.email import RankedContext
|
||||||
from src.app.config import settings
|
from src.app.config import settings
|
||||||
|
|
||||||
|
|
||||||
SYSTEM_PROMPT = """Вы - AI-ассистент отдела продаж платформы «Консоль.Про». Ваша задача: на основе профиля лида и предоставленного контекста сформировать одно персонализированное холодное письмо (первое касание) на языке лида. Вы пишете кратко, уважительно, без давления. Вы показываете ценность через конкретные результаты клиентов и цифры. Вы не придумываете факты; используете только контекст. Если нет данных или они не релевантны - говорите общими преимуществами платформы.
|
SYSTEM_PROMPT = """Вы — AI-ассистент отдела продаж платформы «Консоль.Про». Ваша задача: на основе профиля лида и предоставленного контекста сформировать одно персонализированное холодное письмо (первое касание) на языке лида. Вы пишете кратко, уважительно, без давления. Вы показываете ценность через конкретные результаты клиентов и цифры. Вы не придумываете факты; используете только контекст. Если нет данных или они не релевантны — говорите общими преимуществами платформы.
|
||||||
Формат ответа строго JSON: {"subject": str, "body": str, "short_reasoning": str, "used_chunks": [ids...]}. Без тройных кавычек, без Markdown."""
|
Формат ответа строго JSON: {"subject": str, "body": str, "short_reasoning": str, "used_chunks": [ids...]}. Без тройных кавычек, без Markdown."""
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ USER_PROMPT_TEMPLATE = """[ПРОФИЛЬ ЛИДА]
|
||||||
- Тон: деловой, лаконичный, дружелюбный, с ноткой эмпатии.
|
- Тон: деловой, лаконичный, дружелюбный, с ноткой эмпатии.
|
||||||
- Письмо ≤ 1000 символов, раздели на микро‑абзацы по 1–2 предложения, чтобы текст легко читался.
|
- Письмо ≤ 1000 символов, раздели на микро‑абзацы по 1–2 предложения, чтобы текст легко читался.
|
||||||
- Открывай цепляющим вопросом или заявлением («Как насчёт…?», «Представьте, что…»).
|
- Открывай цепляющим вопросом или заявлением («Как насчёт…?», «Представьте, что…»).
|
||||||
- 1 CTA: предложить 15‑минутный звонок/демо. Не навязчиво: «Если вам интересно -», «Буду рад(а) обсудить» и т.п.
|
- 1 CTA: предложить 15‑минутный звонок/демо. Не навязчиво: «Если вам интересно —», «Буду рад(а) обсудить» и т.п.
|
||||||
- Основная боль ↔ основное решение в первых 2–3 абзацах.
|
- Основная боль ↔ основное решение в первых 2–3 абзацах.
|
||||||
- Добавь конкретные цифры или проценты («до 100 % собираемости документов», «сокращение времени на 80 %»).
|
- Добавь конкретные цифры или проценты («до 100 % собираемости документов», «сокращение времени на 80 %»).
|
||||||
- Вставь мини‑соцдоказательство (название клиента + результат (Конкретизировать кейс цифрой ОБЯЗАТЕЛЬНО)) как отдельный абзац или буллет.
|
- Вставь мини‑соцдоказательство (название клиента + результат (Конкретизировать кейс цифрой ОБЯЗАТЕЛЬНО)) как отдельный абзац или буллет.
|
||||||
|
@ -79,10 +79,10 @@ class PromptBuilder:
|
||||||
hooks = {
|
hooks = {
|
||||||
"marketing_agency": "В маркетинговых агентствах часто сложно быстро подключать десятки подрядчиков для проектов",
|
"marketing_agency": "В маркетинговых агентствах часто сложно быстро подключать десятки подрядчиков для проектов",
|
||||||
"logistics": "В логистических компаниях управление множественными исполнителями требует особого внимания к документообороту",
|
"logistics": "В логистических компаниях управление множественными исполнителями требует особого внимания к документообороту",
|
||||||
"software": "В IT-компаниях работа с фрилансерами и ИП - важная часть команды разработки",
|
"software": "В IT-компаниях работа с фрилансерами и ИП — важная часть команды разработки",
|
||||||
"retail": "В розничной торговле сезонные пики требуют быстрого масштабирования команды исполнителей",
|
"retail": "В розничной торговле сезонные пики требуют быстрого масштабирования команды исполнителей",
|
||||||
"consulting": "В консалтинговых компаниях привлечение экспертов под проекты - критичный процесс",
|
"consulting": "В консалтинговых компаниях привлечение экспертов под проекты — критичный процесс",
|
||||||
"construction": "В строительстве координация подрядчиков и документооборот - ключевые вызовы",
|
"construction": "В строительстве координация подрядчиков и документооборот — ключевые вызовы",
|
||||||
"other": "При работе с внешними исполнителями всегда актуальны вопросы автоматизации процессов",
|
"other": "При работе с внешними исполнителями всегда актуальны вопросы автоматизации процессов",
|
||||||
}
|
}
|
||||||
return hooks.get(industry_tag, hooks["other"])
|
return hooks.get(industry_tag, hooks["other"])
|
||||||
|
|
|
@ -166,39 +166,3 @@ class RetrievalService:
|
||||||
total_tokens=total_tokens,
|
total_tokens=total_tokens,
|
||||||
summary_bullets=summary_bullets,
|
summary_bullets=summary_bullets,
|
||||||
)
|
)
|
||||||
|
|
||||||
def calculate_context_quality_score(self, chunks: List[DocChunk]) -> float:
|
|
||||||
if not chunks:
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
scores = [
|
|
||||||
chunk.similarity_score
|
|
||||||
for chunk in chunks
|
|
||||||
if chunk.similarity_score is not None
|
|
||||||
]
|
|
||||||
|
|
||||||
if not scores:
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
mean_similarity = sum(scores) / len(scores)
|
|
||||||
|
|
||||||
high_quality_ratio = sum(1 for score in scores if score > 0.7) / len(scores)
|
|
||||||
|
|
||||||
if len(scores) > 1:
|
|
||||||
variance = sum((score - mean_similarity) ** 2 for score in scores) / len(
|
|
||||||
scores
|
|
||||||
)
|
|
||||||
consistency_score = max(0, 1 - variance * 2)
|
|
||||||
else:
|
|
||||||
consistency_score = 1.0
|
|
||||||
|
|
||||||
quantity_factor = min(1.0, len(chunks) / 3.0)
|
|
||||||
|
|
||||||
quality_score = (
|
|
||||||
0.40 * mean_similarity
|
|
||||||
+ 0.25 * high_quality_ratio
|
|
||||||
+ 0.20 * consistency_score
|
|
||||||
+ 0.15 * quantity_factor
|
|
||||||
)
|
|
||||||
|
|
||||||
return max(0.0, min(1.0, quality_score))
|
|
||||||
|
|
Loading…
Reference in New Issue