AI 에이전트 — 모델이 아니라 그래프 · 관측과 평가, 그리고 회귀 판정 · 실습
정확도는 그대로인데 다른 건이 틀린다
목표
노드를 감싸 어디서 무엇을 했는지 장부에 남기고, 골든 세트로 정확도와 틀린 건의 목록을 낸다. 규칙을 고친 두 번째 판과 견주어 새로 깨진 것과 새로 고쳐진 것을 이름으로 가르고, 비용과 경로의 변화까지 잰다.
왜 중요한가
에이전트를 고친 뒤 "좋아졌나" 를 점수 하나로 묻는 것이 흔하다. 그런데 점수가 같아도 안에서 하나가 고쳐지고 하나가 깨졌다면 그것은 좋아진 것이 아니라 바뀐 것이다. 점수는 개수이고, 우리가 알아야 하는 것은 이름이다.
관측은 노드를 한 겹 감싸면 된다. 이때 중요한 선택이 하나 있다 — 시간도 재 두지만 판정에는 쓰지 않는다. 같은 코드·같은 입력이어도 시간은 기계와 부하에 따라 달라지고, 시간으로 판정하는 회귀 검사는 흔들리다가 결국 꺼진다. 판정에는 횟수와 크기를 쓴다.
골든 세트는 작아도 되지만 매번 같은 것이어야 한다. 그리고 지금 틀리는 건을 남겨 두어야 고쳤을 때 고쳐진 것이 보인다. 전부 맞는 골든 세트는 아무것도 잡아내지 못한다.
품질이 그대로여도 비용과 경로는 회귀할 수 있다. 노드 호출 수는 같은 입력에 늘 같은 값이라 회귀 지표로 쓸 수 있고, 발자국이 달라졌다면 답이 같아 보여도 다른 동작이다.
채점기는 여러분이 적어 둔 설명을 믿지 않는다. 여러분의 모듈을 실제로 불러 임의의 문의로 돌려 보고, 골든 세트의 판정과 두 판의 차이를 채점기가 따로 계산한 값과 대조한다.
단계
1. /root/work/ageval/evalkit.py 에 LABELS·ANSWER·classify_v1·State·노드 다섯(normalize·refund·shipping·other·finish)·ROUTE·build_graph(version="v1")·run_one(text, version="v1") 을 만드세요.
2. PROFILE·reset_profile()·instrument(name, fn) 을 더하고 build_graph 가 모든 노드를 감싸서 더하게 하세요.
3. hotspots(texts, version="v1") 을 더해 노드별 호출 수와 가장 바쁜 노드를 내게 하세요.
4. GOLDEN 과 evaluate(version="v1") 을 더해 정확도와 틀린 건의 목록을 내게 하세요.
5. classify_v2 와 compare(old="v1", new="v2") 를 더해 새로 깨진 건과 새로 고쳐진 건을 가르게 하세요.
6. cost_delta(old="v1", new="v2") 를 더해 노드 호출 수의 변화를 재게 하세요.
7. path_changes(old="v1", new="v2") 를 더해 발자국이 달라진 건을 찾게 하세요.
8. /root/work/ageval/eval_report.json 과 /root/work/ageval/eval_report.md 에 잰 것을 기록하세요.
참고
- 실행 계약: 채점기는
/root/work/ageval/evalkit.py를 파이썬 모듈로 불러 위에 적은 이름들을 직접 씁니다. 스크립트로 실행하지 않습니다. classify_v1(text):"환불"이 있으면"환불", 아니면"배송"이 있으면"배송", 아니면"기타".classify_v2(text):"배송"이 있으면"배송", 아니면"환불"이나"반품"이 있으면"환불", 아니면"기타". 검사 순서가 v1 과 다릅니다 — 그것이 이 실습의 재료입니다.- 상태 열쇠:
text·clean·label·answer·trace.trace만 이어 붙이는 리듀서를 씁니다. 노드 이름과 상태 열쇠는 겹치면 안 됩니다 — 겹치면 컴파일할 때ValueError: 'x' is already being used as a state key가 납니다. normalize는text의 공백을 하나로 줄여clean에 담습니다. 분류는clean을 봅니다.refund·shipping·other는label과ANSWER[라벨]을 담고, 셋 다finish로 모입니다. 다섯 노드 모두trace에 자기 이름을 씁니다.ROUTE = {"환불": "refund", "배송": "shipping", "기타": "other"}를 조건부 엣지의 경로 지도로 씁니다.run_one(text, version)의 답:{"label": 문자열, "answer": 문자열, "trace": [...]}.PROFILE은{노드이름: {"calls": 정수, "written": 정수, "ms": 실수}}입니다.written은 그 노드가 돌려준 딕셔너리의 열쇠 수를 더한 값입니다.ms는 재 두기만 하고 판정에는 쓰지 않습니다.hotspots(texts, version)의 답:{"calls": {...}, "total_calls": 정수, "busiest": 문자열, "timed": [...정렬된 노드 이름]}. 부를 때마다 장부를 새로 폅니다.busiest는 호출 수가 가장 많은 노드이고 동점이면 이름 오름차순입니다.GOLDEN은(문의, 정답라벨)여섯 쌍입니다:("환불 절차 알려 주세요", "환불"),("반품하고 싶어요", "환불"),("배송비 환불되나요", "환불"),("배송 언제 오나요", "배송"),("영수증 좀 보내 주세요", "기타"),("반품 배송비는 누가 내나요", "배송").evaluate(version)의 답:{"total": 정수, "correct": 정수, "accuracy": 실수, "wrong": [{"text":…, "gold":…, "got":…}, …]}.wrong은 골든 세트의 순서를 따릅니다.compare(old, new)의 답:{"old_accuracy": 실수, "new_accuracy": 실수, "newly_broken": [...정렬됨], "newly_fixed": [...정렬됨]}.cost_delta(old, new)의 답:{"old_calls": 정수, "new_calls": 정수, "delta": 정수}. 골든 세트 전체를 한 번씩 돌린 뒤의 호출 수입니다.path_changes(old, new)의 답:[{"text":…, "old": [...], "new": [...]}, …]를text오름차순으로. 발자국이 같은 건은 담지 않습니다.- 이 파드에는 인터넷이 없습니다. langgraph 0.2.60 이 이미 들어 있습니다.
- 공식 문서: [Graph API overview](https://docs.langchain.com/oss/python/langgraph/graph-api) · [Use the graph API](https://docs.langchain.com/oss/python/langgraph/use-graph-api) · [Streaming](https://docs.langchain.com/oss/python/langgraph/streaming)
- 흔한 실수: 장부를 부를 때마다 비우지 않기(앞 실행의 숫자가 섞입니다), 시간으로 회귀를 판정하기, 틀린 건의 이름을 안 남기기, 골든 세트를 실행마다 새로 뽑기.
단계 8개
- 문의를 갈래로 보낸다
- 노드를 감싸 장부를 남긴다
- 어느 노드가 제일 바쁜가
- 손으로 정답을 붙인 여섯 건
- 점수는 같은데 다른 건이 틀린다
- 품질이 같아도 비용이 달라진다
- 같은 답이 다른 길로 나왔다
- 무엇을 내보낼지 판단한다