上限を上げてもその案件は終わらない
한국어 원문으로 표시합니다.
목표
고리가 있는 그래프를 직접 만들고, 재귀 한도가 무엇을 세는지 자기 손으로 재고, 상한에 닿은 것과 할 일을 마친 것을 다른 결과로 남긴다. 마지막으로 끝내는 조건에 닿지 못하는 입력을 가려내는 진단을 만든다.
왜 중요한가
LangGraph 에서 고리는 특별한 문법이 아니라 그냥 뒤로 가는 엣지다. 그래서 만들기 쉽고, 끝내는 것이 어렵다. 끝내는 조건이 있어도 그 조건에 닿을 수 없는 입력이 하나 섞이면 그 건은 영원히 돈다 — 이 실습의 에이전트가 딱 그렇다. 점수를 깎는 이유가 둘(금칙어·길이)인데 고치는 노드는 하나만 고친다.
재귀 한도는 이럴 때 마지막 방어선이지만, 그것이 세는 단위를 모르면 진단을 잘못한다. 한도가 세는 것은 노드 실행 횟수가 아니라 수퍼스텝이고, 나란히 둔 노드는 몇 개든 한 수퍼스텝이다. 이 실습에서 직접 재어 확인합니다.
그리고 한도에 닿아 예외가 난 것과 그래프가 스스로 끝낸 것은 완전히 다른 사건이다. 같은 칸에 "실패" 로 적어 두면 나중에 무엇이 문제였는지 가를 수 없다. 상한을 두 겹(상태의 rounds 와 recursion_limit)으로 두고, 안쪽 상한은 결과를 남기게 만든다.
채점기는 여러분이 적어 둔 설명을 믿지 않는다. 여러분의 모듈을 실제로 불러 규칙 함수를 임의의 문장으로 두드려 보고, 그래프를 임의의 한도로 돌려 나온 결과를 채점기가 따로 계산한 값과 대조한다.
단계
- /root/work/agroute/route.py 에
BANNED·MIN_LEN·PASS·make_draft·score_of·revise_text·State·노드 넷(compose·review·revise·ship)·after_review·build_graph()를 만드세요.review에서 갈리는 조건부 엣지에는 경로 지도를 주고, 이 단계에서는revise다음이 END 입니다. revise에서review로 돌아오는 엣지를 더해 고리를 만드세요. 금칙어가 여러 개인 문의가 여러 바퀴를 돌아 통과해야 합니다.build_unbounded()와run_unbounded(question, limit)를 더하세요. 끝내는 조건을 일부러 뺀 판을 주어진 한도로 돌리고, 한도에 닿으면{"error": "GraphRecursionError", "limit": 한도, "message": 예외 메시지}를 돌려줍니다.MAX_ROUNDS = 5와escalate노드를 더해build_graph()가 어떤 입력에도 예외 없이 끝나게 하세요. 상한에 닿으면outcome이escalate입니다.chain_graph(n)·fan_graph(n)·min_limit(app, state)를 더하세요.min_limit은 그 그래프를 끝까지 돌리는 데 필요한 가장 작은recursion_limit을 찾습니다.run_guarded(question, limit)를 더해 마친 것과 상한에 닿은 것을 다른status로 남기세요.diagnose(question)을 더해 이 입력이 규칙을 만족할 수 있는지, 없다면 왜인지(too_short또는banned) 가려내게 하세요.- /root/work/agroute/route_report.json 과 /root/work/agroute/route_report.md 에 재어 본 숫자와 판단을 기록하세요.
참고
- 실행 계약: 채점기는
/root/work/agroute/route.py를 파이썬 모듈로 불러 위에 적은 이름들을 직접 씁니다. 스크립트로 실행하지 않습니다. make_draft(question)은question.strip() + " 라고 안내드립니다."입니다.score_of(text)는 100 에서 시작해 금칙어가 나올 때마다 30 을, 길이가MIN_LEN미만이면 40 을 깎고 0 밑으로는 내려가지 않습니다.revise_text(text)는 금칙어를 하나만 지웁니다.- 상수:
BANNED = ("무조건", "절대로", "보장합니다"),MIN_LEN = 30,PASS = 70,MAX_ROUNDS = 5. - 상태 열쇠:
question·draft·score·rounds·outcome·trace.trace는 이어 붙이는 리듀서를 씁니다. - 노드 이름과 상태 열쇠는 겹치면 안 됩니다. LangGraph 는 둘을 같은 이름 공간에 두어서, 초안을 쓰는 노드를
draft라고 부르면 컴파일할 때ValueError: 'draft' is already being used as a state key가 납니다. 그래서 이 실습의 첫 노드 이름은compose입니다. after_review는 갈래 이름을 돌려줍니다(ship·revise·escalate). 어느 갈래가 어느 노드로 가는지는add_conditional_edges의 세 번째 인자가 정합니다.chain_graph(n)·fan_graph(n)은 노드 n 개짜리 그래프를 돌려줍니다. 노드 이름은 자유이고, 각 노드는 자기 이름을 이어 붙이는 열쇠에 하나 씁니다.min_limit(app, state)는 1 부터 올려 가며invoke를 시도해 처음 성공하는 한도를 돌려줍니다. 실패는langgraph.errors.GraphRecursionError로 옵니다.run_guarded의 답: 마쳤으면{"status": "done", "rounds": 정수, "outcome": "ship"|"escalate", "score": 정수}, 한도에 닿았으면{"status": "limit", "rounds": -1, "outcome": "", "score": -1}.diagnose의 답:{"fixable": 참거짓, "reason": ""|"too_short"|"banned", "best_score": 정수, "rounds_needed": 정수}.best_score는 금칙어를 더 지울 수 없을 때까지 지운 뒤의 점수입니다.- 이 파드에는 인터넷이 없습니다. langgraph 0.2.60 이 이미 들어 있습니다.
- 공식 문서: Use the graph API · GRAPH_RECURSION_LIMIT · Graph API overview
- 흔한 실수: 조건 함수가 갈래 이름 대신 노드 이름을 돌려주게 두고 경로 지도를 생략하기, 끝내는 판단을 노드와 조건 함수 두 곳에 두기, 한도를 올려 증상만 넘기기, 나란히 둔 노드가 한도를 먹는다고 믿기.
갈래 이름과 경로 지도를 나눈다
/root/work/agroute/route.py 에 규칙 함수 셋과 노드 넷, after_review, build_graph() 를 만드세요. review 의 조건부 엣지에는 경로 지도 {"ship": "ship", "revise": "revise"} 를 주고, 이 단계에서는 revise 다음이 END 입니다.
조건 함수는 상태를 받아 갈래 이름을 돌려줍니다. 노드 이름을 직접 돌려주게 만들면 그래프 모양을 바꿀 때마다 판단 코드를 고쳐야 합니다. score_of 가 어떻게 점수를 깎는지가 뒤 단계의 재료이니 지시문의 규칙을 정확히 지키세요.
고친 것을 다시 재러 보낸다
revise 에서 review 로 돌아오는 엣지를 더해 고리를 만드세요. 금칙어가 셋인 문의는 세 바퀴를 돌아 ship 으로 가야 합니다.
고리는 특별한 문법이 아니라 그냥 뒤로 가는 엣지입니다. revise 가 금칙어를 한 번에 하나만 지운다는 점이 바퀴 수를 정합니다. 바퀴 수를 세는 rounds 는 revise 가 올립니다 — 그래야 나중에 안쪽 상한을 걸 자리가 생깁니다.
한도에 닿는 것을 직접 본다
build_unbounded() 와 run_unbounded(question, limit) 를 더하세요. 끝내는 조건을 일부러 뺀 판을 주어진 한도로 돌리고, 한도에 닿으면 {"error": "GraphRecursionError", "limit": 한도, "message": 예외 메시지} 를, 끝났으면 error 를 빈 문자열로 돌려줍니다.
예외는 langgraph.errors.GraphRecursionError 입니다. 메시지에 한도 숫자가 그대로 들어 있으니 지어내지 말고 str(예외) 를 그대로 담으세요. build_unbounded() 는 build_graph() 와 다른 그래프여야 합니다 — 뒤 단계에서 build_graph() 에 상한을 넣어도 이 단계가 계속 유효해야 하기 때문입니다.
결과를 남기는 안쪽 상한
MAX_ROUNDS = 5 와 escalate 노드를 더하고 after_review 가 상한에서 escalate 갈래를 돌려주게 하세요. 경로 지도에도 그 갈래를 더합니다. 이제 build_graph() 는 어떤 입력에도 예외 없이 끝나야 합니다.
바깥 상한(recursion_limit)에 닿으면 예외라 결과가 사라집니다. 안쪽 상한은 결과를 남깁니다 — 그것이 이 단계의 요점입니다. 판단은 after_review 한 곳에만 두세요. 노드 안에서도 끝낼지 보면 두 곳이 어긋나는 날이 옵니다.
한도가 세는 것은 수퍼스텝이다
chain_graph(n)(줄줄이)·fan_graph(n)(나란히)·min_limit(app, state) 를 더하세요. min_limit 은 그 그래프를 끝까지 돌리는 데 필요한 가장 작은 recursion_limit 을 찾아 돌려줍니다.
1 부터 올려 가며 invoke 를 해 보고 처음 성공하는 값을 돌려주면 됩니다. 두 그래프의 답이 어떻게 다른지가 이 단계의 핵심입니다 — 나란히 둔 노드는 몇 개든 같은 수퍼스텝에서 돕니다. 상태는 리듀서가 붙은 열쇠 하나면 충분합니다.
마친 것과 닿은 것을 가른다
run_guarded(question, limit=25) 를 더하세요. 그래프가 스스로 끝났으면 {"status": "done", "rounds": 정수, "outcome": "ship"|"escalate", "score": 정수}, 한도에 닿았으면 {"status": "limit", "rounds": -1, "outcome": "", "score": -1} 입니다.
두 사건을 같은 칸에 "실패" 로 적으면 나중에 가를 수 없습니다. 예외를 잡아 다른 이름을 붙이는 것이 전부이지만, 그 한 줄이 운영에서 크게 갈립니다. 한도를 작게 주면 정상 입력도 limit 이 나옵니다 — 채점기가 그것을 확인합니다.
고쳐도 나아지지 않는 입력을 가려낸다
diagnose(question) 을 더해 {"fixable": 참거짓, "reason": ""|"too_short"|"banned", "best_score": 정수, "rounds_needed": 정수} 를 돌려주게 하세요. best_score 는 금칙어를 더 지울 수 없을 때까지 지운 뒤의 점수입니다.
그래프를 돌리지 않고도 알 수 있습니다 — revise_text 를 더 바뀌지 않을 때까지 적용해 보고 그때의 점수를 재면 됩니다. 그 점수가 PASS 에 못 미치면 이 입력은 아무리 돌려도 통과하지 못합니다. 진단이 한도를 올리는 것보다 값싼 이유가 여기 있습니다.
재어 본 숫자로 보고한다
/root/work/agroute/route_report.json 에 default_recursion_limit·chain_min_limit·fan_min_limit·clean·dirty·stuck·stuck_reason 을, /root/work/agroute/route_report.md 에 ## 고리를 어디에 두었나 ## 재귀 한도는 무엇을 세는가 ## 상한에 닿은 것과 마친 것 ## 고쳐도 나아지지 않는 입력 네 절로 쓰세요.
default_recursion_limit 은 config 를 주지 않았을 때의 기본값입니다 — 지어내지 말고 한도를 올려 가며 재어 확인하세요. chain_min_limit·fan_min_limit 은 노드 수를 문자열 열쇠로 둔 객체입니다(예: {"1": ..., "2": ..., "3": ...}). clean·dirty·stuck 은 각각 run_guarded 의 답 그대로 담습니다.