Files
2026-06-04 16:16:02 +00:00

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()