LangGraph에는 루프 문법이 따로 없습니다. 조건부 엣지의 라우팅 함수가 이미 지나온 노드 이름을 반환하면 그게 사이클이 됩니다.
이 파일은 답변을 생성하고, 길이로 품질을 매긴 뒤, 기준 미달이면 generate로 돌아가 다시 생성합니다.
route_quality가 END를 반환하면 종료, "generate"를 반환하면 한 바퀴 더 돕니다.
answer를 덮어쓰고, attempts를 1 올립니다.
len(answer) / 200을 1.0으로 자른 값을 quality_score에 기록합니다. 200자 이상이면 만점, 160자부터 통과입니다.
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_limit | LangGraph 프레임워크 | 노드 실행 횟수(step)가 한도(기본 25)를 넘으면 GraphRecursionError를 던집니다. invoke(..., config={"recursion_limit": N})으로 조정합니다. |
한 바퀴는 generate + check_quality로 2 step이라 3회 시도면 6 step입니다. 코드 쪽 한도가 먼저 걸리므로 recursion_limit은 라우팅 로직에 버그가 있을 때의 최후 방어선입니다. 사이클이 있는 그래프에는 코드 쪽 한도를 꼭 둡니다.
첫 시도에 541자가 나와 바로 종료됐습니다. 프롬프트가 "200자 이상"을 요구하고 Sonnet이 잘 따르기 때문에, 실제 실행으로는 사이클을 보기 어렵습니다.
그래프 구성은 그대로 두고 generate만 정해진 길이의 문자열을 돌려주는 가짜 함수로 바꿔 stream_mode="updates"로 돌린 결과입니다. check_quality와 route_quality는 cycle.py의 것을 import해 그대로 썼습니다.
품질은 끝내 미달이지만 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] 같은 리듀서로 누적합니다. |
END를 반환하는 것뿐입니다. 품질 조건과 별개로 시도 한도를 반드시 둡니다.recursion_limit(기본 25 step)이 무한 루프를 막습니다.