From 7e7d9dcc4e46b54eeb6150bd153cda1111728d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=B4=D0=B5=D0=BB=D0=B8=D0=BD=D0=B0=20=D0=A1=D0=B0?= =?UTF-8?q?=D1=82=D1=82=D0=B0=D1=80=D0=BE=D0=B2=D0=B0?= Date: Thu, 4 Jun 2026 16:41:23 +0000 Subject: [PATCH] add --- nodes.py | 190 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 104 insertions(+), 86 deletions(-) diff --git a/nodes.py b/nodes.py index b861be6..e6dbca0 100644 --- a/nodes.py +++ b/nodes.py @@ -1,102 +1,120 @@ from langchain_openai import ChatOpenAI -from langchain_core.messages import HumanMessage, SystemMessage +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) -# Промпты для узлов -DRAFT_PROMPT = """Ты — эксперт, который дает краткие, но содержательные ответы. -Ответь на вопрос пользователя в 5-10 предложений. -Будь конкретным, избегай общих фраз и "воды". +# 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" -Вопрос: {question}""" - -REFLECT_PROMPT = """Ты — строгий критик, оценивающий качество ответа. -Критерии оценки: +# Промпт для генерации с инструкцией по качеству +ANSWER_PROMPT = """Ты — эксперт, который дает качественные ответы. +Требования к ответу: 1. Полнота: ответ покрывает все аспекты вопроса 2. Конкретика: есть примеры, детали, точные формулировки -3. Отсутствие воды: нет общих фраз, повторений, лишних слов +3. Краткость: 5-10 предложений, без воды -Ответ для оценки: {draft} - -Твой ответ должен быть в формате: -VERDICT: [ok/needs_revision] -CRITIQUE: [2-3 конкретных пункта замечаний или похвала] - -Примеры: -VERDICT: ok -CRITIQUE: Ответ полный, есть примеры, нет воды. - -VERDICT: needs_revision -CRITIQUE: 1. Нет конкретных примеров. 2. Слишком много общих фраз. 3. Не раскрыта разница в контексте MCP.""" - -REWRITE_PROMPT = """Улучши ответ на вопрос, учитывая замечания критика. Вопрос: {question} -Предыдущая версия: {draft} -Замечания критика: {critique} -Дай исправленную версию (5-10 предложений), которая: -- Учитывает все замечания -- Становится более полной и конкретной -- Убирает воду -- Сохраняет краткость (5-10 предложений)""" +После ответа добавь метаданные в формате JSON: +{{ + "is_complete": true/false, + "has_examples": true/false, + "is_concise": true/false +}} -def draft_answer(state: ReflectState) -> ReflectState: - """Узел: первичная генерация ответа""" - print(f"\n📝 [Раунд {state['round']}] Генерация черновика...") - - prompt = DRAFT_PROMPT.format(question=state["question"]) - response = llm.invoke([HumanMessage(content=prompt)]) - - new_state = state.copy() - new_state["draft"] = response.content - return new_state +Сам ответ должен быть в markdown формате. +""" -def reflect(state: ReflectState) -> ReflectState: - """Узел: критическая оценка ответа""" - print(f"🔍 [Раунд {state['round']}] Рефлексия критика...") +def generate_answer(state: ReflectState) -> ReflectState: + """ + Узел генерации ответа с try/except и валидацией + При ошибке заполняет поле error + """ + print(f"\n📝 [Попытка {state['round']}/{state['max_rounds']}] Генерация ответа...") - prompt = REFLECT_PROMPT.format(draft=state["draft"]) - response = llm.invoke([HumanMessage(content=prompt)]) + 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 + } - # Парсим ответ критика - lines = response.content.strip().split('\n') - verdict = "needs_revision" # по умолчанию - critique = response.content - - for line in lines: - if line.startswith("VERDICT:"): - verdict = line.replace("VERDICT:", "").strip().lower() - elif line.startswith("CRITIQUE:"): - critique = line.replace("CRITIQUE:", "").strip() - - # Нормализация вердикта - if verdict not in ["ok", "needs_revision"]: - verdict = "needs_revision" - - new_state = state.copy() - new_state["verdict"] = verdict - new_state["critique"] = critique - - print(f" Вердикт: {verdict.upper()}") - if verdict == "needs_revision": - print(f" Замечания: {critique[:100]}...") - - return new_state - -def rewrite(state: ReflectState) -> ReflectState: - """Узел: переписывание ответа с учетом замечаний""" - print(f"✏️ [Раунд {state['round']}] Переписывание с учетом критики...") - - prompt = REWRITE_PROMPT.format( - question=state["question"], - draft=state["draft"], - critique=state["critique"] - ) - response = llm.invoke([HumanMessage(content=prompt)]) - - new_state = state.copy() - new_state["draft"] = response.content - new_state["round"] = state["round"] + 1 - return new_state \ No newline at end of file + except Exception as e: + # Другие ошибки (сеть, API и т.д.) + print(f" ❌ Критическая ошибка: {str(e)}") + return { + **state, + "draft": "", + "error": f"Критическая ошибка: {str(e)}" + } \ No newline at end of file