AI 에이전트 — 모델이 아니라 그래프 · 관측과 평가, 그리고 회귀 판정 · 이론
점수가 같은데 다른 건이 틀리고 있었다
한 줄 요약
에이전트를 고친 뒤 점수 하나로 좋아졌는지 판단하면 안 된다. 점수가 같아도 안에서 하나가 고쳐지고 하나가 깨졌다면 그것은 좋아진 것이 아니라 바뀐 것이다.
왜 이게 필요했나
문의 분류 규칙에 "반품" 을 더했다. 그동안 "반품하고 싶어요" 가 기타로 빠지던 것을 환불로 잡게 하려는 것이었다. 골든 세트로 재 보니 정확도가 그대로였다. 83퍼센트에서 83퍼센트.
"그대로면 최소한 나빠지진 않았네" 하고 내보냈다. 이틀 뒤 "배송비 환불되나요" 문의들이 전부 배송팀으로 가 있었다.
규칙을 고치면서 검사 순서도 바꿨고, 그래서 하나가 고쳐지고 하나가 깨졌다. 여섯 건 중 다섯 건 맞는 것은 똑같은데, 맞는 다섯 건이 달랐다.
점수는 그 사실을 감춘다. 점수는 개수이고, 우리가 알아야 했던 것은 이름이었다.
어떻게 동작하나
평가를 제대로 하려면 세 가지를 나눠서 봐야 한다.
- 품질 — 골든 세트에서 몇 건을 맞혔나. 그리고 어느 건을 틀렸나.
- 비용 — 한 건을 처리하는 데 노드가 몇 번 돌았나.
- 경로 — 같은 입력이 지나는 길이 달라졌나.
셋 다 같은 실행에서 잴 수 있다. 노드를 한 겹 감싸면 된다.
def instrument(name, fn): def wrapped(state): started = time.perf_counter() update = fn(state) row = PROFILE.setdefault(name, {"calls": 0, "written": 0, "ms": 0.0}) row["calls"] += 1 row["written"] += len(update or {}) row["ms"] += (time.perf_counter() - started) * 1000.0 return update return wrapped여기서 중요한 선택이 하나 있다. 시간도 재 두지만 판정에는 쓰지 않는다. 같은 코드·같은 입력이어도 시간은 기계와 그때의 부하에 따라 달라진다. "느려졌다" 를 시간으로 판정하면 회귀 검사가 흔들리고, 흔들리는 검사는 곧 꺼진다. 판정에는 횟수와 크기를 쓴다. 시간은 사람이 들여다볼 때 실마리로 쓴다.
골든 세트는 작아도 된다. 대신 안 바뀌어야 한다
골든 세트는 "손으로 정답을 붙인 입력 묶음" 이다. 여섯 건이어도 쓸모가 있다 — 조건은 매번 같은 것을 잰다는 것이다.
그래서 골든 세트는 코드나 파일에 박아 두고, 고칠 때는 고쳤다는 사실 자체를 기록한다. 실행할 때마다 표본을 새로 뽑으면 어제와 오늘을 비교할 수 없다.
또 하나. 골든 세트에는 틀리는 건을 남겨 둔다. 전부 맞는 골든 세트는 "지금 잘 되고 있다" 만 말하고 아무것도 잡아내지 못한다. 실제로 틀리는 건이 들어 있어야 고쳤을 때 고쳐진 것이 보인다.
회귀는 점수가 아니라 집합으로 본다
두 판을 견줄 때 봐야 하는 것은 이 둘이다.
- 새로 깨진 것(newly broken) — 옛 판에서 맞던 것이 새 판에서 틀린다
- 새로 고쳐진 것(newly fixed) — 옛 판에서 틀리던 것이 새 판에서 맞는다
둘이 같은 수면 점수는 그대로다. 그런데 이 둘의 무게는 보통 같지 않다. 이미 잘 되던 것이 깨지는 쪽이 훨씬 아프다 — 사용자가 이미 그 동작에 기대고 있기 때문이다. 그래서 "새로 깨진 것이 0인가" 를 먼저 묻고, 그다음에 "새로 고쳐진 것이 있는가" 를 묻는 순서가 실무에서 안전하다.
비용과 경로도 회귀한다
품질이 그대로라도 비용이 늘 수 있다. 분기 하나를 더 두면 노드가 한 번 더 돌고, 그 노드가 외부를 부른다면 그만큼 돈이 나간다. 그래서 노드 호출 수도 회귀 지표로 남긴다. 시간과 달리 이 숫자는 같은 입력에 늘 같다.
경로도 마찬가지다. 같은 라벨이 나와도 다른 길로 나왔다면 그건 다른 동작이다. 지금은 답이 같아 보여도 다음 변경에서 다르게 갈라진다. 발자국(trace)을 비교하면 이것이 보인다.
현장에서 만나는 모습
첫째, 점수만 기록한다. 위의 사고다. 어느 건이 틀렸는지를 남기지 않으면 되짚을 수가 없다.
둘째, 골든 세트가 실행마다 바뀐다. 무작위 표본으로 재면 숫자가 매번 달라서 회귀인지 표본 탓인지 가를 수 없다.
셋째, 시간으로 성능 회귀를 판정한다. 검사가 흔들리고, 흔들리는 검사는 무시되다가 결국 꺼진다.
넷째, 관측을 나중에 붙이려 한다. 노드를 감싸는 자리는 그래프를 만들 때가 가장 싸다. 나중에 붙이면 노드마다 고쳐야 하고, 한 곳을 빠뜨리면 그 노드만 장부에 안 남는다.
실무에서 진짜 중요한 것
- 점수와 함께 틀린 건의 이름을 남긴다. 되짚을 수 있는 유일한 기록이다.
- 회귀는 새로 깨진 것과 새로 고쳐진 것으로 가른다. 깨진 쪽을 먼저 본다.
- 판정에는 횟수와 크기를 쓰고 시간은 실마리로만 쓴다.
- 비용과 경로도 회귀 지표다. 답이 같아도 길이 달라졌으면 달라진 것이다.
다음 실습에서 할 것
/root/work/ageval/evalkit.py 를 한 단계씩 키운다. 먼저 문의를 분류하는 그래프를 만들고, 노드를 감싸 호출 수와 쓴 열쇠 수를 장부에 남긴다. 어느 노드가 제일 바쁜지 뽑아 보고, 손으로 정답을 붙인 골든 세트로 정확도와 틀린 건의 목록을 낸다. 그다음 규칙을 고친 두 번째 판을 만들어 두 판을 견주고, 점수는 같은데 어느 건이 깨지고 고쳐졌는지를 이름으로 가른다. 마지막으로 호출 수와 경로의 변화까지 재어 기록으로 남긴다.