day3_practice/cycle.py

사이클: 조건을 만족할 때까지 되돌아가기

LangGraph에는 루프 문법이 따로 없습니다. 조건부 엣지의 라우팅 함수가 이미 지나온 노드 이름을 반환하면 그게 사이클이 됩니다. 이 파일은 답변을 생성하고, 길이로 품질을 매긴 뒤, 기준 미달이면 generate로 돌아가 다시 생성합니다.

그래프 구조

route_quality가 END를 반환하면 종료, "generate"를 반환하면 한 바퀴 더 돕니다.

START generate LLM 답변, attempts+1 answer check_quality min(len/200, 1.0) route_ quality score ≥ 0.8 or attempts ≥ 3 END 그 외 → return "generate" 사이클 (한 바퀴 = 2 step)

구성 요소

생성

generate (노드) "200자 이상으로 자세하게" 답하라고 LLM을 호출합니다. answer를 덮어쓰고, attempts를 1 올립니다.

평가

check_quality (노드) len(answer) / 200을 1.0으로 자른 값을 quality_score에 기록합니다. 200자 이상이면 만점, 160자부터 통과입니다.

분기

route_quality (라우팅 함수) 점수 → 시도 횟수 순서로 확인해 END 또는 "generate"를 반환합니다. 노드가 아니므로 State는 바꾸지 않습니다.
def route_quality(state) -> str:
    if state["quality_score"] >= 0.8:
        return END                # 1) 품질 통과 → 종료
    if state["attempts"] >= 3:
        return END                # 2) 안전장치: 시도 한도 → 종료
    return "generate"             # 3) 이전 노드로 → 사이클

conditional.py와 달리 Literal 타입 힌트나 path_map이 없습니다. 그래서 get_graph()로 그리면 실제 갈 수 없는 경로까지 점선으로 표시될 수 있습니다. 실행 동작에는 영향이 없습니다.

종료 장치는 두 겹

장치위치동작
attempts >= 3코드 (route_quality)3번 생성해도 미달이면 정상 종료합니다. 결과는 마지막 답변입니다.
recursion_limitLangGraph 프레임워크노드 실행 횟수(step)가 한도(기본 25)를 넘으면 GraphRecursionError를 던집니다. invoke(..., config={"recursion_limit": N})으로 조정합니다.

한 바퀴는 generate + check_quality로 2 step이라 3회 시도면 6 step입니다. 코드 쪽 한도가 먼저 걸리므로 recursion_limit은 라우팅 로직에 버그가 있을 때의 최후 방어선입니다. 사이클이 있는 그래프에는 코드 쪽 한도를 꼭 둡니다.

실행 결과

실제 실행"LangGraph의 장점을 설명해 주세요"
generate #1 · 541자→score 1.00→END

첫 시도에 541자가 나와 바로 종료됐습니다. 프롬프트가 "200자 이상"을 요구하고 Sonnet이 잘 따르기 때문에, 실제 실행으로는 사이클을 보기 어렵습니다.

사이클 확인용 시뮬레이션

그래프 구성은 그대로 두고 generate만 정해진 길이의 문자열을 돌려주는 가짜 함수로 바꿔 stream_mode="updates"로 돌린 결과입니다. check_quality와 route_quality는 cycle.py의 것을 import해 그대로 썼습니다.

시뮬레이션100자 → 170자
generate #1 · 100자→score 0.50→↺ generate #2 · 170자→score 0.85→END (품질 통과)
시뮬레이션매번 50자
generate #1 · 50자→score 0.25→↺ generate #2 · 50자→score 0.25→↺ generate #3 · 50자→score 0.25→END (시도 한도)

품질은 끝내 미달이지만 3회에서 멈춥니다. 반환된 State는 quality_score=0.25인 마지막 답변입니다.

이 코드를 실서비스에 쓴다면

문제지금 코드개선 방향
재시도가 같은 요청 반복 generate는 매번 같은 프롬프트를 보냅니다. 이전 답이 왜 떨어졌는지 모델은 모릅니다. 이전 답변과 점수를 State에서 꺼내 "이전 답이 N자였으니 보강하라"처럼 피드백을 프롬프트에 넣습니다.
평가 기준이 길이뿐 길면 통과입니다. 틀린 내용이어도 200자면 만점입니다. LLM 평가자(with_structured_output로 점수와 이유 반환)나 규칙 기반 체크를 섞습니다.
종료 이유를 구분 못 함 통과로 끝났는지 한도로 끝났는지 호출자가 quality_score를 다시 비교해야 압니다. status: "passed" | "exhausted" 같은 필드를 State에 남기거나, 한도 종료 시 별도 노드로 보내 처리합니다.
이전 답변 유실 answer는 리듀서 없이 덮어쓰기라 앞선 시도 결과가 사라집니다. 시도 이력이 필요하면 Annotated[list, operator.add] 같은 리듀서로 누적합니다.

정리