120 lines
4.8 KiB
Python
120 lines
4.8 KiB
Python
from langchain_openai import ChatOpenAI
|
|
from langchain_core.messages import HumanMessage
|
|
from pydantic import BaseModel, Field, ValidationError
|
|
from state import ReflectState
|
|
|
|
# Инициализация LLM
|
|
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)
|
|
|
|
# Pydantic модель для валидации ответа
|
|
class ValidAnswer(BaseModel):
|
|
"""Модель для валидации качества ответа"""
|
|
content: str = Field(description="Ответ на вопрос")
|
|
is_complete: bool = Field(description="Полнота ответа (должно быть True)")
|
|
has_examples: bool = Field(description="Наличие примеров (должно быть True)")
|
|
is_concise: bool = Field(description="Отсутствие воды (должно быть True)")
|
|
|
|
def validate_quality(self) -> tuple[bool, str]:
|
|
"""Проверяет качество ответа по критериям"""
|
|
issues = []
|
|
|
|
if not self.is_complete:
|
|
issues.append("неполный ответ")
|
|
if not self.has_examples:
|
|
issues.append("нет конкретных примеров")
|
|
if not self.is_concise:
|
|
issues.append("содержит воду/общие фразы")
|
|
|
|
if not self.content or len(self.content.split()) < 50:
|
|
issues.append("слишком короткий ответ (<50 слов)")
|
|
|
|
if len(self.content.split()) > 300:
|
|
issues.append("слишком длинный ответ (>300 слов)")
|
|
|
|
if issues:
|
|
return False, f"Проблемы: {', '.join(issues)}"
|
|
return True, "OK"
|
|
|
|
# Промпт для генерации с инструкцией по качеству
|
|
ANSWER_PROMPT = """Ты — эксперт, который дает качественные ответы.
|
|
Требования к ответу:
|
|
1. Полнота: ответ покрывает все аспекты вопроса
|
|
2. Конкретика: есть примеры, детали, точные формулировки
|
|
3. Краткость: 5-10 предложений, без воды
|
|
|
|
Вопрос: {question}
|
|
|
|
После ответа добавь метаданные в формате JSON:
|
|
{{
|
|
"is_complete": true/false,
|
|
"has_examples": true/false,
|
|
"is_concise": true/false
|
|
}}
|
|
|
|
Сам ответ должен быть в markdown формате.
|
|
"""
|
|
|
|
def generate_answer(state: ReflectState) -> ReflectState:
|
|
"""
|
|
Узел генерации ответа с try/except и валидацией
|
|
При ошибке заполняет поле error
|
|
"""
|
|
print(f"\n📝 [Попытка {state['round']}/{state['max_rounds']}] Генерация ответа...")
|
|
|
|
try:
|
|
# 1. Генерация ответа
|
|
prompt = ANSWER_PROMPT.format(question=state["question"])
|
|
response = llm.invoke([HumanMessage(content=prompt)])
|
|
|
|
# 2. Парсинг JSON-метаданных из ответа
|
|
import json
|
|
import re
|
|
|
|
# Ищем JSON в конце ответа
|
|
json_match = re.search(r'\{[^{}]*"is_complete"[^{}]*\}', response.content)
|
|
if not json_match:
|
|
raise ValueError("Не найдены метаданные JSON в ответе")
|
|
|
|
metadata = json.loads(json_match.group())
|
|
|
|
# 3. Создаем объект ValidAnswer
|
|
answer_obj = ValidAnswer(
|
|
content=response.content,
|
|
is_complete=metadata.get("is_complete", False),
|
|
has_examples=metadata.get("has_examples", False),
|
|
is_concise=metadata.get("is_concise", False)
|
|
)
|
|
|
|
# 4. Валидация качества
|
|
is_valid, error_message = answer_obj.validate_quality()
|
|
|
|
if not is_valid:
|
|
raise ValueError(error_message)
|
|
|
|
# 5. Успех - очищаем ошибку
|
|
print(f" ✅ Ответ прошел валидацию!")
|
|
return {
|
|
**state,
|
|
"draft": answer_obj.content,
|
|
"error": None
|
|
}
|
|
|
|
except (ValidationError, ValueError, json.JSONDecodeError) as e:
|
|
# Перехватываем все ошибки валидации
|
|
error_msg = str(e)
|
|
print(f" ❌ Ошибка валидации: {error_msg}")
|
|
|
|
return {
|
|
**state,
|
|
"draft": "", # Очищаем невалидный ответ
|
|
"error": error_msg
|
|
}
|
|
|
|
except Exception as e:
|
|
# Другие ошибки (сеть, API и т.д.)
|
|
print(f" ❌ Критическая ошибка: {str(e)}")
|
|
return {
|
|
**state,
|
|
"draft": "",
|
|
"error": f"Критическая ошибка: {str(e)}"
|
|
} |