채점 루브릭

채점 루브릭 (Grading rubrics)

RubricMiddleware"완료"가 무엇인지를 루브릭으로 선언하고, 에이전트가 그 루브릭을 만족할 때까지(또는 설정한 최대 반복 횟수까지) 스스로 평가하며 반복하도록 해주는 LLM-as-a-judge 채점 방식이에요. 작업 모델만으로는 첫 시도에 맞추기 어려운 기준—올바른 음절 패턴의 하이쿠, 모든 테스트가 통과하는 리팩터링, 필요한 섹션을 빠짐없이 채운 보고서—같은 것에 적합합니다. (RubricMiddlewaredeepagents>=0.6.5 필요, beta.)

출처: 공식문서

동작 방식

Deep agent가 추론을 끝내고 출력을 내면, 전용 grader 모델이 그 결과물을 루브릭에 맞춰 검토하고 판정을 내려요. needs_revision이면 기준별 피드백이 대화에 주입되고 에이전트가 다시 실행됩니다. 루프는 satisfied, max_iterations_reached, failed, grader_error 중 하나로 종료돼요.

graph LR
    Start[User invokes<br/>with rubric] --> Agent[Deep agent]
    Agent --> Grader{Grader<br/>verdict}

    Grader --> |satisfied| Done[Finish execution]
    Grader --> |failed| Done
    Grader --> |grader_error| Done
    Grader --> |needs_revision| Cap{Iterations < <br/> max_iterations?}

    Cap --> |yes| Inject[Re-prompt deep agent with per-criterion feedback]
    Cap --> |no| Done

    Inject --> Agent

미들웨어 설정

create_deep_agent를 호출할 때 middleware 목록에 RubricMiddleware를 추가해요.

from deepagents import RubricMiddleware, create_deep_agent
from langgraph.checkpoint.memory import InMemorySaver

agent = create_deep_agent(
    model="google_genai:gemini-3.6-flash",
    middleware=[
        RubricMiddleware(
            model="anthropic:claude-haiku-4-5",
            max_iterations=3,
        ),
    ],
    checkpointer=InMemorySaver(),
)

모델은 공급자 포맷(openai:gpt-5.5, anthropic:claude-sonnet-4-6, openrouter:z-ai/glm-5.2, fireworks:accounts/fireworks/models/glm-5p2, baseten:zai-org/GLM-5.2, ollama:north-mini-code-1.0 등)에 맞춰 지정하면 됩니다.

설정 인자:

인자 필수 기본 설명
model None LLM-as-a-judge grader sub-agent가 쓰는 채팅 모델. "provider:model-id" 문자열 또는 BaseChatModel 인스턴스. 보통 deep agent의 작업 모델보다 작거나 저렴한 모델 사용
system_prompt 내장 grader 프롬프트 커스텀 채점 지시어. 기본 시스템 프롬프트로 대체됨(판정 형식과 사용 가능한 툴을 가르침)
tools None grader가 판정 전 증거를 모으기 위해 쓸 툴(테스트 실행·토큰 세기·파일 읽기). 없으면 전사(transcript)만으로 추론
max_iterations 3 루브릭 시도당 최대 grader 반복 수(양의 정수). 이 상한에 도달했는데 satisfied가 아니면 max_iterations_reached 상태로 종료
on_evaluation None 매 채점 반복 후 RubricEvaluation으로 호출되는 콜백. 로깅·커스텀 메트릭·eval 데이터셋·UI 업데이트에 유용

호출 시 루브릭 전달

self-evaluation 루프를 시작하려면 호출 상태에 rubric 문자열을 넘겨요. 단일 차단 호출에는 invoke(), grading 이벤트를 실시간으로 받으려면 stream_events(..., version="v3")CustomTransformer를 써요.

from langchain.messages import HumanMessage

config = {"configurable": {"thread_id": "my-rubric-thread"}}
result = agent.invoke(
    {
        "messages": [HumanMessage("Write a haiku about spring.")],
        "rubric": (
            "- The poem has three lines\n"
            "- Lines follow a 5-7-5 syllable pattern\n"
            "- The theme is spring"
        ),
    },
    config=config,
)

stream_events()로는 stream.custom에서 rubric_evaluation_start/rubric_evaluation_end 이벤트를 받을 수 있어요. 시작 이벤트는 grading_run_id(하나의 루브릭 시도 내 모든 이벤트가 공유)와 0-기반 iteration을, 종료 이벤트는 여기에 result(판정), explanation(grader 요약), criteria(기준별 판정)를 담습니다.

루브릭 판정 (verdicts)

상태 의미 루프 백?
satisfied 루브릭의 모든 기준 통과
needs_revision 최소 하나 이상 기준 실패 → grader 피드백 주입 후 다시 실행
max_iterations_reached grader가 여전히 수정을 원하지만 max_iterations 상한 도달
failed grader가 루브릭이 형식에 안 맞거나 transcript로 평가 불가하다고 판단
grader_error LLM-as-a-judge grader sub-agent가 예외 발생 (프로바이더 타임아웃·자격증명 누락·구조화 응답 오류 등)

반복 진행 관찰하기

on_evaluationinvoke()stream_events()든 매 채점 반복 후 grader 판정과 함께 호출되는 콜백이에요. stream.custom에서 루브릭 이벤트를 읽지 않거나 LangSmith tracing을 안 쓴다면, grading 중 무슨 일이 있었는지 확인하는 주요 경로입니다.

from deepagents import RubricMiddleware, create_deep_agent
from deepagents.middleware.rubric import RubricEvaluation
from langchain.messages import HumanMessage
from langgraph.checkpoint.memory import InMemorySaver


def log_evaluation(ev: RubricEvaluation) -> None:
    print(f"iteration {ev['iteration']}: {ev['result']} — {ev['explanation']}")


agent = create_deep_agent(
    model="google_genai:gemini-3.6-flash",
    middleware=[
        RubricMiddleware(
            model="google_genai:gemini-3.6-flash",
            on_evaluation=log_evaluation,
        ),
    ],
    checkpointer=InMemorySaver(),
)

config = {"configurable": {"thread_id": "rubric-eval-session"}}
agent.invoke(
    {
        "messages": [HumanMessage("Write a one-sentence summary of photosynthesis.")],
        "rubric": (
            "- The answer is one sentence\n"
            "- The answer mentions light and chlorophyll"
        ),
    },
    config=config,
)

RubricEvaluation 딕셔너리 필드: grading_run_id(str, 한 시도 내 공유 식별자), iteration(int, 0-기반), result(str, satisfied/needs_revision/failed/grader_error), explanation(str), criteria(list, 각 항목은 {name, passed: true} 또는 {name, passed: false, gap}).

grader가 needs_revision인데 max_iterations에 도달하면 콜백은 여전히 result: "needs_revision"(grader의 판정)을 받아요. 이때 실행의 최종 상태는 max_iterations_reached이고 private 상태 _rubric_status에 있어요. invoke 완료 후 _rubric_status를 검사하거나 _rubric_evaluations의 마지막 항목을 _rubric_iterations와 함께 읽어 상한 소진을 분기하세요. grader 예외는 result: "grader_error"로, 콜백 자체 오류는 로그만 남기고 억제되어 grading 루프는 계속됩니다.

호출 사이 루브릭 유지하기

agent.invoke()/agent.stream_events() 한 번은 루브릭 루프를 끝까지 돌리고 satisfied/failed/max_iterations_reached 종결 판정으로 마칩니다. 후속 호출로 루브릭을 이어가려면 checkpointer를 붙이고 같은 thread_id를 넘겨주세요. 그러면 새 루브릭을 넘길 때까지 같은 루브릭이 이후 invoke()/stream_events() 호출에도 유지돼요. KeyboardInterruptasyncio.CancelledError는 잡히지 않고 밖으로 전파되며, 체크포인트된 thread에서는 같은 루브릭으로 다음 호출 시 진행 중이던 grading run을 재개합니다.

예시: 검증된 Python 코드 생성

find_duplicates 함수를 작성하는 deep agent를 만드는 예시예요. RubricMiddleware를 한 번 정의해 에이전트에 붙이고, 호출 시점에 rubric 문자열을 넘겨요. grader에게 추상적으로 정확성을 추론하게 하는 대신 run_test_suite 툴을 줘서 동작을 직접 검증하게 합니다.

from deepagents import RubricMiddleware, create_deep_agent
from langchain.tools import tool


@tool
def run_test_suite(code: str) -> dict:
    """Run the find_duplicates test suite against Python source code."""
    namespace: dict = {"__builtins__": __builtins__}
    try:
        exec(code, namespace)
    except Exception as exc:
        return {"ok": False, "failures": [f"Failed to execute code: {exc}"]}

    find_duplicates = namespace.get("find_duplicates")
    if find_duplicates is None:
        return {"ok": False, "failures": ["Function find_duplicates is not defined"]}

    tests = [
        ("test_basic", [1, 2, 2, 3, 1], [2, 1]),
        ("test_empty", [], []),
        ("test_no_duplicates", [1, 2, 3], []),
        ("test_unhashable", [[1], [1], 2], [[1]]),
    ]
    failures: list[str] = []
    for name, args, expected in tests:
        try:
            actual = find_duplicates(args)
            if actual != expected:
                failures.append(f"{name}: expected {expected}, got {actual}")
        except Exception as exc:
            failures.append(f"{name}: {exc}")

    return {"ok": not failures, "failures": failures}


rubric_middleware = RubricMiddleware(
    model="google_genai:gemini-3.6-flash",
    system_prompt="You are a code reviewer grading generated code against a rubric.",
    tools=[run_test_suite],
    max_iterations=5,
)

에이전트의 system_prompt는 작업을 어떻게 할지, 루브릭은 grader가 작업을 어떻게 판단할지를 지시해요.

agent = create_deep_agent(
    model="google_genai:gemini-3.6-flash",
    system_prompt=(
        "You are a careful Python engineer. Write correct, readable code. "
        "Follow the user's instructions exactly."
    ),
    middleware=[rubric_middleware],
    checkpointer=InMemorySaver(),
)

호출 시점에 사용자 요청을 messages에, 기준을 rubric에 넣어요. 입력 상태에 rubric이 없으면 미들웨어는 실행되지 않습니다.

from langchain.messages import HumanMessage

result = agent.invoke(
    {
        "messages": [
            HumanMessage(
                content=(
                    "Write a Python function `find_duplicates(lst)` that returns a list of "
                    "all elements that appear more than once in the input list, in the order "
                    "they first appear."
                )
            )
        ],
        "rubric": (
            "- All tests pass in run_test_suite\n"
            "- The function is named `find_duplicates` and accepts a single list argument\n"
        ),
    },
    config={"configurable": {"thread_id": "code-generation-session"}},
)
print(result["messages"][-1].text)

에이전트가 출력을 낸 뒤 grader가 각 기준을 확인(예: test_unhashable이 입력에 비해시블 타입이 있을 때 TypeError로 실패하는지)하고, 문제가 있으면 피드백을 주고 에이전트가 구현을 수정해 다시 제출하는 흐름이에요.

더 알아보기 (Learn more)