init
This commit is contained in:
35
6_генерация_эмбеддингов/README.md
Normal file
35
6_генерация_эмбеддингов/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Генерация эмбеддингов (шаг 6)
|
||||
|
||||
Отдельный шаг пайплайна после мержа анализа и тегов (шаг 5). В вектор превращается **валидированный анализ главы** (каркас, инсайты, применение), а не сырой текст — поиск идёт по смыслу, а не по формулировкам.
|
||||
|
||||
## Модель
|
||||
|
||||
- **По умолчанию:** **BAAI/bge-m3** (bge-m3). Мультиязычная модель, хорошая поддержка русского; размерность вектора **1024**, лимит контекста **8192 токенов** — полный анализ главы обычно укладывается без обрезки. Размер ~1.2GB, ~5–15 сек на главу.
|
||||
- **Лимит входа:** 8192 токенов (bge-m3). При превышении — стратегия из `embed_input_spec.txt` (truncation или chunk + агрегация).
|
||||
- **Альтернативы:** nomic-embed-text, evilfreelancer/enbeddrus (для более лёгкого режима). Модель задаётся конфигом; при смене — пересоздание коллекций Qdrant (размерность 1024 для bge-m3).
|
||||
|
||||
## Вход
|
||||
|
||||
Выход шага 5 (мерж): валидированный анализ главы (framework, insights, application) — сериализуется в текст по спецификации `embed_input_spec.txt`. Теги в вектор не входят; они хранятся в payload Qdrant для фильтрации.
|
||||
|
||||
## Выход
|
||||
|
||||
Вектор эмбеддинга фиксированной размерности (**1024** для bge-m3). Сохраняется в Qdrant в коллекции `chapter_analyses` (коллекция создаётся с size=1024) вместе с payload (book_id, chapter_id, validation_score, tags и т.д.).
|
||||
|
||||
## Спецификация входа
|
||||
|
||||
| Файл | Назначение |
|
||||
|-----------------------|----------------------------------------------------------------------------|
|
||||
| embed_input_spec.txt | Рецепт построения текста для эмбеддинга: какие поля, порядок, приоритет при truncation |
|
||||
|
||||
## Подстановки при построении текста
|
||||
|
||||
- `{framework}` — сериализованный блок framework (принципы, terms, chains)
|
||||
- `{insights}` — сериализованный блок insights
|
||||
- `{application}` — сериализованный блок application
|
||||
|
||||
Ограничения и limitations в эмбеддинг по умолчанию не включаются (опционально — в конфиге). Теги — только в payload, не в тексте для эмбеддинга.
|
||||
|
||||
## Использование
|
||||
|
||||
Вызывается после шага 5 (мерж анализа и тегов). Модель задаётся конфигом (env/конфиг); смена модели не меняет формат хранения в Qdrant, при смене — пересчёт эмбеддингов по необходимости.
|
||||
231
6_генерация_эмбеддингов/embed_cli.py
Normal file
231
6_генерация_эмбеддингов/embed_cli.py
Normal file
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CLI для получения эмбеддинга по JSON шага 5 (мерж анализа и тегов).
|
||||
|
||||
На вход — путь к JSON-файлу, сформированному на этапе 5 (merged_with_tags.json),
|
||||
и имя модели. Текст для эмбеддинга собирается из блоков framework, insights, application
|
||||
по спецификации embed_input_spec.txt. На выход — вектор.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _serialize_framework(fw: dict[str, Any]) -> str:
|
||||
"""Собирает текст секции FRAMEWORK из JSON блока framework."""
|
||||
parts: list[str] = []
|
||||
principles = fw.get("principles") or []
|
||||
for p in principles:
|
||||
line = f"Принцип: {p.get('title', '')}. Описание: {p.get('description', '')}."
|
||||
if p.get("example"):
|
||||
line += f" Пример: {p['example']}."
|
||||
parts.append(line)
|
||||
for chain in p.get("chains") or []:
|
||||
c_line = "Причина: {} Механизм: {} Результат: {}.".format(
|
||||
chain.get("cause", ""),
|
||||
chain.get("mechanism", ""),
|
||||
chain.get("result", ""),
|
||||
)
|
||||
parts.append(c_line)
|
||||
terms = fw.get("terms") or {}
|
||||
if terms:
|
||||
terms_str = " ".join(f"{k} — {v}" for k, v in terms.items())
|
||||
parts.append(f"Термины: {terms_str}.")
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def _serialize_insights(insights: list[Any]) -> str:
|
||||
"""Собирает текст секции INSIGHTS."""
|
||||
parts: list[str] = []
|
||||
for i in insights:
|
||||
line = f"Инсайт: {i.get('title', '')}. {i.get('description', '')}."
|
||||
if i.get("example"):
|
||||
line += f" Пример: {i['example']}."
|
||||
parts.append(line)
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def _serialize_application(app: dict[str, Any]) -> str:
|
||||
"""Собирает текст секции APPLICATION из блока application."""
|
||||
parts: list[str] = []
|
||||
techniques = app.get("techniques") or []
|
||||
for t in techniques:
|
||||
line = f"Техника: {t.get('name', '')}. Цель: {t.get('goal', '')}."
|
||||
if t.get("steps"):
|
||||
steps = t["steps"] if isinstance(t["steps"], list) else [t["steps"]]
|
||||
line += f" Шаги: {'; '.join(steps)}."
|
||||
if t.get("context_example"):
|
||||
line += f" Контекст: {t['context_example']}."
|
||||
parts.append(line)
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def merged_json_to_embed_text(merged: dict[str, Any]) -> str:
|
||||
"""
|
||||
Собирает текст для эмбеддинга из JSON шага 5 по embed_input_spec.txt.
|
||||
|
||||
Входят только блоки framework, insights, application. Порядок секций:
|
||||
FRAMEWORK, INSIGHTS, APPLICATION.
|
||||
Limitations и теги не включаются: теги хранятся в payload Qdrant для фильтрации,
|
||||
вектор строится по смыслу (принципы, инсайты, техники), не по тегам.
|
||||
|
||||
Args:
|
||||
merged: Документ из шага 5 (merge.json + выход_valid_tag.json).
|
||||
|
||||
Returns:
|
||||
Нормализованная строка для подачи в модель эмбеддингов.
|
||||
"""
|
||||
fw = merged.get("framework") or {}
|
||||
insights = merged.get("insights") or []
|
||||
app = merged.get("application") or {}
|
||||
|
||||
framework_text = _serialize_framework(fw)
|
||||
insights_text = _serialize_insights(insights)
|
||||
application_text = _serialize_application(app)
|
||||
|
||||
sections = [
|
||||
"FRAMEWORK",
|
||||
framework_text,
|
||||
"INSIGHTS",
|
||||
insights_text,
|
||||
"APPLICATION",
|
||||
application_text,
|
||||
]
|
||||
text = "\n\n".join(sections)
|
||||
# Нормализация: схлопнуть множественные пробелы и переносы, обрезать по краям
|
||||
text = re.sub(r"[ \t]+", " ", text)
|
||||
text = re.sub(r"\n\s*\n", "\n\n", text)
|
||||
return text.strip()
|
||||
|
||||
|
||||
def get_embedding(
|
||||
base_url: str,
|
||||
model: str,
|
||||
text: str,
|
||||
) -> list[float]:
|
||||
"""
|
||||
Запрашивает эмбеддинг текста у OpenAI-совместимого API.
|
||||
|
||||
Args:
|
||||
base_url: Базовый URL API (например http://localhost:1234/v1).
|
||||
model: Имя модели эмбеддингов.
|
||||
text: Текст для эмбеддинга.
|
||||
|
||||
Returns:
|
||||
Вектор эмбеддинга (список float).
|
||||
|
||||
Raises:
|
||||
urllib.error.HTTPError: При ошибке HTTP.
|
||||
ValueError: Если в ответе нет ожидаемой структуры.
|
||||
"""
|
||||
url = f"{base_url.rstrip('/')}/embeddings"
|
||||
payload = {
|
||||
"model": model,
|
||||
"input": text,
|
||||
}
|
||||
body = json.dumps(payload).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=120) as resp:
|
||||
data: dict[str, Any] = json.loads(resp.read().decode("utf-8"))
|
||||
if "data" not in data or not data["data"]:
|
||||
raise ValueError("В ответе API нет поля data с эмбеддингом")
|
||||
embedding = data["data"][0].get("embedding")
|
||||
if not embedding:
|
||||
raise ValueError("В ответе API нет поля embedding")
|
||||
return list(embedding)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Точка входа CLI."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Эмбеддинг по JSON шага 5 (мерж анализа и тегов). На вход — JSON-файл, на выход — вектор.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"input_json",
|
||||
type=Path,
|
||||
help="Путь к JSON-файлу шага 5 (merged_with_tags.json).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"model",
|
||||
help="Имя модели эмбеддингов (например text-embedding-bge-m3).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--base-url",
|
||||
default="http://localhost:1234/v1",
|
||||
help="Базовый URL API (по умолчанию LM Studio).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Путь к файлу для записи вектора (JSON). По умолчанию — stdout.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--format",
|
||||
choices=("json", "compact"),
|
||||
default="json",
|
||||
help="Формат вывода: json — массив, compact — числа через пробел.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.input_json.exists():
|
||||
print(f"Ошибка: файл не найден: {args.input_json}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
with open(args.input_json, encoding="utf-8") as f:
|
||||
merged = json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Ошибка разбора JSON: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
text = merged_json_to_embed_text(merged)
|
||||
if not text:
|
||||
print("Ошибка: текст для эмбеддинга пуст (нет framework/insights/application).", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
vector = get_embedding(args.base_url, args.model, text)
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"Ошибка HTTP {e.code}: {e.reason}", file=sys.stderr)
|
||||
if e.fp:
|
||||
try:
|
||||
body = e.fp.read().decode("utf-8")
|
||||
print(body[:500], file=sys.stderr)
|
||||
except Exception:
|
||||
pass
|
||||
return 1
|
||||
except urllib.error.URLError as e:
|
||||
print(f"Ошибка запроса: {e.reason}", file=sys.stderr)
|
||||
return 1
|
||||
except ValueError as e:
|
||||
print(f"Ошибка: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if args.format == "json":
|
||||
payload = json.dumps(vector, ensure_ascii=False)
|
||||
else:
|
||||
payload = " ".join(str(x) for x in vector)
|
||||
|
||||
if args.output is None:
|
||||
print(payload)
|
||||
else:
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(payload, encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
103
6_генерация_эмбеддингов/embed_input_spec.txt
Normal file
103
6_генерация_эмбеддингов/embed_input_spec.txt
Normal file
@@ -0,0 +1,103 @@
|
||||
# Спецификация входа для генерации эмбеддинга главы
|
||||
|
||||
Текст, который передаётся в модель эмбеддингов (по умолчанию BAAI/bge-m3), строится из **валидированного анализа главы**. Источник полей — JSON анализа по схеме из ARCHITECTURE_SUMMARY.md (блоки framework, insights, application). Цель — сохранить смысл для семантического поиска: по вектору ищут главы по смыслу (принципы, инсайты, техники), а не по сырому тексту.
|
||||
|
||||
---
|
||||
|
||||
## ЧТО ВКЛЮЧАТЬ
|
||||
|
||||
В текст для эмбеддинга входят **только** блоки анализа:
|
||||
|
||||
1. **Framework (каркас):** принципы (title, description, example), terms (термин → краткое пояснение), при необходимости — ключевые цепочки cause–mechanism–result (кратко).
|
||||
|
||||
2. **Insights (инсайты):** title, description (и при необходимости example) по каждому инсайту. Без лишних метаполей.
|
||||
|
||||
3. **Application (применение):** техники — name, goal, steps (кратко или полный список), при необходимости context_example. То, по чему будет искаться «как это применить».
|
||||
|
||||
**Не включать:**
|
||||
- Оригинальный текст главы.
|
||||
- Limitations — по умолчанию не включать (опционально через конфиг).
|
||||
- Теги — хранятся в payload Qdrant, в вектор не входят.
|
||||
|
||||
---
|
||||
|
||||
## ПОРЯДОК И ФОРМАТ СЕРИАЛИЗАЦИИ
|
||||
|
||||
**Язык текста:** тот же, что и язык главы (или основной язык книги). Для мультиязычных коллекций — зафиксировать в конфиге (например, всегда русский / всегда язык главы).
|
||||
|
||||
**Порядок секций** (приоритет при truncation — сверху вниз):
|
||||
|
||||
```
|
||||
FRAMEWORK
|
||||
{framework}
|
||||
|
||||
INSIGHTS
|
||||
{insights}
|
||||
|
||||
APPLICATION
|
||||
{application}
|
||||
```
|
||||
|
||||
**Разделители:**
|
||||
- Между секциями: заголовок секции (FRAMEWORK, INSIGHTS, APPLICATION) + пустая строка.
|
||||
- Внутри секции: элементы разделять переносом строки или маркером «—»; не склеивать в один абзац. Пример: каждый принцип — с новой строки; каждый инсайт — с новой строки; каждая техника — с новой строки.
|
||||
|
||||
**Формат полей:** читаемый текст с подписями («Принцип: …», «Инсайт: …», «Техника: …») или компактный структурированный текст. Избегать сырого JSON без подписей; предпочитать человекочитаемую склейку.
|
||||
|
||||
**Подстановки при сборке:**
|
||||
- `{framework}` — строка из JSON блока framework (принципы, terms; chains при необходимости).
|
||||
- `{insights}` — строка из JSON блока insights.
|
||||
- `{application}` — строка из JSON блока application.
|
||||
|
||||
**Пример сериализации (фрагмент):**
|
||||
|
||||
```
|
||||
FRAMEWORK
|
||||
|
||||
Принцип: Правило двух минут. Описание: Если действие занимает меньше двух минут — сделай его сразу. Пример: ответ на короткий email, вынести мусор.
|
||||
Термины: атомная привычка — сверхмалое действие, повторяемое ежедневно.
|
||||
|
||||
INSIGHTS
|
||||
|
||||
Инсайт: Привычки формируют идентичность. Мы меняемся не через цели, а через системы и повторяющиеся действия.
|
||||
|
||||
APPLICATION
|
||||
|
||||
Техника: Снижение барьера входа. Цель: начать действие без откладывания. Шаги: сформулировать версию привычки «на 2 минуты»; выполнять её в одно и то же время; после закрепления — расширять.
|
||||
```
|
||||
|
||||
**Нормализация перед подачей в модель:** схлопнуть множественные пробелы и переносы в один; обрезать пробелы по краям строк и в начале/конце итогового текста. Это уменьшает разброс из‑за форматирования.
|
||||
|
||||
---
|
||||
|
||||
## ЛИМИТ МОДЕЛИ И СТРАТЕГИЯ ПРЕВЫШЕНИЯ
|
||||
|
||||
- **Лимит (bge-m3):** 8192 токенов на вход. Полный анализ главы обычно укладывается. Для других моделей — лимит указывать в конфиге.
|
||||
|
||||
- **Если текст укладывается в лимит:** передать один сконкатенированный текст (FRAMEWORK + INSIGHTS + APPLICATION) в один вызов модели.
|
||||
|
||||
- **Если текст длиннее лимита** — выбрать одну из стратегий (зафиксировать в конфиге):
|
||||
|
||||
1. **truncate_priority:** обрезать текст до лимита. Сохранять в порядке приоритета: сначала APPLICATION (техники важны для поиска), затем INSIGHTS, затем FRAMEWORK. Отбрасывать с конца не поместившегося блока.
|
||||
|
||||
2. **truncate_per_section:** обрезать до лимита, сохраняя начало каждой секции. Сначала поместить начало FRAMEWORK, затем начало INSIGHTS, затем начало APPLICATION, пока не исчерпается лимит.
|
||||
|
||||
3. **chunk_aggregate:** разбить текст **последовательно** (с начала к концу) на чанки по лимиту; получить вектор для каждого чанка; усреднить векторы (или другой способ агрегации) и сохранить один итоговый вектор. Способ агрегации — в конфиге.
|
||||
|
||||
---
|
||||
|
||||
## ВЫХОД
|
||||
|
||||
- Один вектор фиксированной размерности. Для bge-m3 — **1024**. При смене модели размерность меняется; коллекции Qdrant создавать с соответствующим size.
|
||||
- Вектор сохраняется в Qdrant в коллекции `chapter_analyses`. Payload — по схеме из ARCHITECTURE_SUMMARY.md: bookId, chapterId, chapterNumber, chapterTitle, validationScore, tags и др. Текст анализа в payload при необходимости хранить отдельно или не хранить (по политике системы).
|
||||
|
||||
---
|
||||
|
||||
## КОНФИГУРАЦИЯ
|
||||
|
||||
- Модель эмбеддингов: env/конфиг (по умолчанию EMBED_MODEL=bge-m3).
|
||||
- Лимит токенов модели: конфиг (для bge-m3: EMBED_MAX_TOKENS=8192).
|
||||
- Размерность вектора: для bge-m3 — 1024; при смене модели задавать в конфиге и пересоздавать коллекции Qdrant.
|
||||
- Стратегия при превышении: конфиг (truncate_priority | truncate_per_section | chunk_aggregate).
|
||||
- Включение limitations в текст: опционально, конфиг (по умолчанию false).
|
||||
- Язык сериализации (при мультиязычности): опционально, конфиг (например, chapter_lang | book_lang | fixed:ru).
|
||||
Reference in New Issue
Block a user