This commit is contained in:
2026-06-04 16:41:23 +00:00
parent ef53537ce7
commit 7e7d9dcc4e
+94 -76
View File
@@ -1,102 +1,120 @@
from langchain_openai import ChatOpenAI 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 from state import ReflectState
# Инициализация LLM # Инициализация LLM
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7) llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)
# Промпты для узлов # Pydantic модель для валидации ответа
DRAFT_PROMPT = """Ты — эксперт, который дает краткие, но содержательные ответы. class ValidAnswer(BaseModel):
Ответь на вопрос пользователя в 5-10 предложений. """Модель для валидации качества ответа"""
Будь конкретным, избегай общих фраз и "воды". content: str = Field(description="Ответ на вопрос")
is_complete: bool = Field(description="Полнота ответа (должно быть True)")
has_examples: bool = Field(description="Наличие примеров (должно быть True)")
is_concise: bool = Field(description="Отсутствие воды (должно быть True)")
Вопрос: {question}""" def validate_quality(self) -> tuple[bool, str]:
"""Проверяет качество ответа по критериям"""
issues = []
REFLECT_PROMPT = """Ты — строгий критик, оценивающий качество ответа. 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. Полнота: ответ покрывает все аспекты вопроса 1. Полнота: ответ покрывает все аспекты вопроса
2. Конкретика: есть примеры, детали, точные формулировки 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} Вопрос: {question}
Предыдущая версия: {draft}
Замечания критика: {critique}
Дай исправленную версию (5-10 предложений), которая: После ответа добавь метаданные в формате JSON:
- Учитывает все замечания {{
- Становится более полной и конкретной "is_complete": true/false,
- Убирает воду "has_examples": true/false,
- Сохраняет краткость (5-10 предложений)""" "is_concise": true/false
}}
def draft_answer(state: ReflectState) -> ReflectState: Сам ответ должен быть в markdown формате.
"""Узел: первичная генерация ответа""" """
print(f"\n📝 [Раунд {state['round']}] Генерация черновика...")
prompt = DRAFT_PROMPT.format(question=state["question"]) def generate_answer(state: ReflectState) -> ReflectState:
response = llm.invoke([HumanMessage(content=prompt)]) """
Узел генерации ответа с try/except и валидацией
При ошибке заполняет поле error
"""
print(f"\n📝 [Попытка {state['round']}/{state['max_rounds']}] Генерация ответа...")
new_state = state.copy() try:
new_state["draft"] = response.content # 1. Генерация ответа
return new_state prompt = ANSWER_PROMPT.format(question=state["question"])
response = llm.invoke([HumanMessage(content=prompt)])
def reflect(state: ReflectState) -> ReflectState: # 2. Парсинг JSON-метаданных из ответа
"""Узел: критическая оценка ответа""" import json
print(f"🔍 [Раунд {state['round']}] Рефлексия критика...") import re
prompt = REFLECT_PROMPT.format(draft=state["draft"]) # Ищем JSON в конце ответа
response = llm.invoke([HumanMessage(content=prompt)]) json_match = re.search(r'\{[^{}]*"is_complete"[^{}]*\}', response.content)
if not json_match:
raise ValueError("Не найдены метаданные JSON в ответе")
# Парсим ответ критика metadata = json.loads(json_match.group())
lines = response.content.strip().split('\n')
verdict = "needs_revision" # по умолчанию
critique = response.content
for line in lines: # 3. Создаем объект ValidAnswer
if line.startswith("VERDICT:"): answer_obj = ValidAnswer(
verdict = line.replace("VERDICT:", "").strip().lower() content=response.content,
elif line.startswith("CRITIQUE:"): is_complete=metadata.get("is_complete", False),
critique = line.replace("CRITIQUE:", "").strip() has_examples=metadata.get("has_examples", False),
is_concise=metadata.get("is_concise", False)
)
# Нормализация вердикта # 4. Валидация качества
if verdict not in ["ok", "needs_revision"]: is_valid, error_message = answer_obj.validate_quality()
verdict = "needs_revision"
new_state = state.copy() if not is_valid:
new_state["verdict"] = verdict raise ValueError(error_message)
new_state["critique"] = critique
print(f" Вердикт: {verdict.upper()}") # 5. Успех - очищаем ошибку
if verdict == "needs_revision": print(f" ✅ Ответ прошел валидацию!")
print(f" Замечания: {critique[:100]}...") return {
**state,
"draft": answer_obj.content,
"error": None
}
return new_state except (ValidationError, ValueError, json.JSONDecodeError) as e:
# Перехватываем все ошибки валидации
error_msg = str(e)
print(f" ❌ Ошибка валидации: {error_msg}")
def rewrite(state: ReflectState) -> ReflectState: return {
"""Узел: переписывание ответа с учетом замечаний""" **state,
print(f"✏️ [Раунд {state['round']}] Переписывание с учетом критики...") "draft": "", # Очищаем невалидный ответ
"error": error_msg
}
prompt = REWRITE_PROMPT.format( except Exception as e:
question=state["question"], # Другие ошибки (сеть, API и т.д.)
draft=state["draft"], print(f" ❌ Критическая ошибка: {str(e)}")
critique=state["critique"] return {
) **state,
response = llm.invoke([HumanMessage(content=prompt)]) "draft": "",
"error": f"Критическая ошибка: {str(e)}"
new_state = state.copy() }
new_state["draft"] = response.content
new_state["round"] = state["round"] + 1
return new_state