122 lines
4.5 KiB
Python
122 lines
4.5 KiB
Python
"""LangGraph с узлом рефлексии: draft_answer -> reflect -> rewrite (при необходимости)."""
|
|
import os
|
|
from typing import Literal
|
|
from typing_extensions import TypedDict
|
|
from langchain_openai import ChatOpenAI
|
|
from langchain_core.messages import HumanMessage, SystemMessage
|
|
from langgraph.graph import StateGraph, START, END
|
|
|
|
llm = ChatOpenAI(
|
|
model="openai/gpt-oss-20b:free",
|
|
base_url="https://platform.brojs.ru/jrnl-bh/api/inference/v1",
|
|
api_key=os.getenv("JOURNAL_MCP_PAT"),
|
|
temperature=0.5,
|
|
)
|
|
|
|
class ReflectState(TypedDict):
|
|
"""Состояние графа рефлексии."""
|
|
question: str
|
|
draft: str
|
|
critique: str
|
|
verdict: str # ok | needs_revision
|
|
round: int
|
|
max_rounds: int
|
|
|
|
|
|
def draft_answer(state: ReflectState) -> dict:
|
|
"""Узел 1: генерирует первичный ответ на вопрос (5-10 предложений)."""
|
|
response = llm.invoke([
|
|
SystemMessage(content=(
|
|
"Ты эксперт. Напиши развёрнутый ответ на вопрос в 5-10 предложений. "
|
|
"Будь конкретен, приводи примеры, избегай воды."
|
|
)),
|
|
HumanMessage(content=state["question"]),
|
|
])
|
|
print(f"\n[draft_answer] Черновик:\n{response.content}\n")
|
|
return {"draft": response.content, "round": 0}
|
|
|
|
|
|
def reflect(state: ReflectState) -> dict:
|
|
"""Узел 2: LLM-критик оценивает качество ответа и выставляет вердикт."""
|
|
prompt = (
|
|
f'''Оцени следующий ответ на вопрос: "{state["question"]}"
|
|
|
|
Ответ:
|
|
{state["draft"]}
|
|
|
|
Оцени по трём критериям:
|
|
1. Полнота — охвачены ли все аспекты вопроса?
|
|
2. Конкретика — есть ли конкретные примеры?
|
|
3. Отсутствие воды — нет ли лишних слов?
|
|
|
|
Выведи строго в формате:
|
|
VERDICT: ok
|
|
или
|
|
VERDICT: needs_revision
|
|
CRITIQUE: <2-3 конкретных замечания>'''
|
|
)
|
|
response = llm.invoke([HumanMessage(content=prompt)])
|
|
text = response.content
|
|
|
|
verdict = "needs_revision"
|
|
if "VERDICT: ok" in text or "verdict: ok" in text.lower():
|
|
verdict = "ok"
|
|
|
|
critique = ""
|
|
if "CRITIQUE:" in text:
|
|
critique = text.split("CRITIQUE:")[-1].strip()
|
|
|
|
print(f"[reflect] Вердикт: {verdict}")
|
|
if critique:
|
|
print(f"[reflect] Замечания: {critique[:200]}")
|
|
|
|
return {"verdict": verdict, "critique": critique}
|
|
|
|
|
|
def rewrite(state: ReflectState) -> dict:
|
|
"""Узел 3: улучшает черновик с учётом замечаний критика."""
|
|
prompt = (
|
|
f'''Улучши следующий ответ на вопрос: "{state["question"]}"
|
|
|
|
Текущий ответ:
|
|
{state["draft"]}
|
|
|
|
Замечания критика:
|
|
{state["critique"]}
|
|
|
|
Напиши улучшенный ответ в 5-10 предложений, устранив все замечания.'''
|
|
)
|
|
response = llm.invoke([HumanMessage(content=prompt)])
|
|
new_round = state["round"] + 1
|
|
print(f"\n[rewrite] Раунд {new_round}:\n{response.content}\n")
|
|
return {"draft": response.content, "round": new_round}
|
|
|
|
|
|
def route_after_reflect(state: ReflectState) -> Literal["rewrite", "__end__"]:
|
|
"""Роутер после узла reflect: продолжить или завершить."""
|
|
if state["verdict"] == "ok":
|
|
print("[route] Вердикт ok — завершаем.")
|
|
return "__end__"
|
|
if state["round"] >= state["max_rounds"]:
|
|
print(f"[route] Достигнут лимит раундов ({state['max_rounds']}) — завершаем.")
|
|
return "__end__"
|
|
print(f"[route] needs_revision, раунд {state['round']} < {state['max_rounds']} — rewrite.")
|
|
return "rewrite"
|
|
|
|
# ── Сборка графа ──────────────────────────────────────────────────────────────
|
|
builder = StateGraph(ReflectState)
|
|
builder.add_node("draft_answer", draft_answer)
|
|
builder.add_node("reflect", reflect)
|
|
builder.add_node("rewrite", rewrite)
|
|
|
|
builder.add_edge(START, "draft_answer")
|
|
builder.add_edge("draft_answer", "reflect")
|
|
builder.add_conditional_edges(
|
|
"reflect",
|
|
route_after_reflect,
|
|
{"rewrite": "rewrite", "__end__": END},
|
|
)
|
|
builder.add_edge("rewrite", "reflect")
|
|
|
|
graph = builder.compile()
|