# AI 에이전트

에이전트 성공률, pass@k 없이 한 번 돌려서 말해도 될까?

@Hong2026-10-04

이 글에 담긴 코드와 결과는 직접 실행해 측정한 값을 바탕으로 작성했습니다. 실측은 Hong과 팀원들이 진행했고, 그 결과를 바탕으로 AI가 글을 쓰고 Hong이 검토합니다.

실습 코드 내려받기

질문
에이전트를 한 번 돌려서 나온 성공률을 그 에이전트의 성공률로 말해도 될까?
답
안 된다. temperature 0.7에서 qwen3:1.7b의 회차별 성공률은 28.9%~33.3%로 흔들렸고, 두 회차 사이에 성공 여부가 다른 과제가 많게는 7개였다.
조건
Ollama 0.34.4, Docker Desktop CPU, qwen3:1.7b와 qwen2.5:1.5b(Q4_K_M), 과제 45개, temperature 0과 0.7, seed 1~8, 1,440번 실행

환경

실행
macOS(Apple Silicon)의 Docker Desktop, 두 컨테이너 합쳐 메모리 3GB·CPU 2.5개 상한, GPU 없이 CPU 추론
Ollama
0.34.4(메모리 2,700MB, CPU 2.25), num_thread 2, num_ctx 4096
모델
qwen3:1.7b(Q4_K_M, 1.4GB, think: false), qwen2.5:1.5b(Q4_K_M, 986MB)
데이터
가짜 사이트 DB(SQLite, 글 60개), 도구 다섯 개, 과제 45개
반복
temperature 0과 0.7, 조건마다 seed 1~8, 모두 1,440번 실행
목차
  1. pass@1, pass@k, pass^k
  2. 실습 준비
  3. 실습 1: temperature 0.7에서 8번
  4. 실습 2: 반복 간 차이와 모델 간 차이
  5. 실습 3: 최종 답 채점과 궤적 채점
  6. 정리

이번 시간에는 같은 과제 세트를 seed만 바꿔 과제마다 8번씩 돌려 보면서, 에이전트의 성공률이 반복마다 얼마나 흔들리는지를 Ollama의 로컬 모델 두 개와 pass@k, pass^k로 측정하며 배워가는 시간입니다.

예를 들어서 에이전트에게 과제 45개를 한 번씩 풀게 했더니 14개를 맞혔다고 해볼게요. 성공률 31%예요.

이 숫자를 보고 "이 에이전트는 열 번 중 세 번 정도 성공한다"고 말해도 될까요? 같은 과제를 다시 돌리면 같은 14개가 다시 맞을까요? 다른 모델이 29%를 냈다면 이 모델이 낫다고 말할 수 있을까요?

이 질문들에 답하려고 로컬 모델 두 개(qwen3:1.7b, qwen2.5:1.5b)를 temperature 0과 0.7 두 조건에서 각각 360번씩, 모두 1,440번 실행했어요. 같은 실행 기록을 최종 답만 보는 채점기와 도구 호출 순서까지 보는 채점기로 각각 채점해서, 결론이 바뀌는지도 확인합니다.

pass@1, pass@k, pass^k

과제마다 n번 시도해서 c번 성공했다고 해볼게요.

  • pass@1: 시도 한 번이 성공할 확률이에요. 과제마다 c/n을 구해 과제 평균을 냅니다.
  • pass@k: k번 시도해서 한 번이라도 성공할 확률이에요. 사람이 결과를 보고 고를 수 있거나, 성공 여부를 기계가 확인할 수 있어서 여러 번 시도해 하나만 건지면 되는 상황에 맞는 값이에요.
  • pass^k: k번 시도가 모두 성공할 확률이에요. τ-bench 논문(2024-06-17)이 "여러 시도에 걸친 에이전트 동작의 신뢰도"를 재려고 제안한 지표입니다. 사용자가 매번 같은 일을 맡기는 서비스에서는 이 값이 실제 체감에 가까워요.

n번 중 c번 성공한 기록에서 k개를 뽑는다고 보고, 다음처럼 계산합니다. C(a, b)는 a개에서 b개를 고르는 조합의 수예요.

pass@k = 과제 평균( 1 - C(n-c, k) / C(n, k) )
pass^k = 과제 평균( C(c, k) / C(n, k) )

두 식 모두 k가 n보다 클 수 없어서, 이 글에서는 n = 8로 두고 k = 1, 2, 4, 8을 계산할게요.

예를 들어 어떤 과제를 8번 시도해 5번 성공했다면, 8번 중 2개를 고르는 28가지 가운데 실패 3번에서 2개를 고르는 3가지만 둘 다 실패이니까 pass@2는 25/28로 약 0.89입니다. 성공 5번에서 2개를 고르는 10가지만 둘 다 성공이니까 pass^2는 10/28로 약 0.36이에요.

과제가 늘 성공하면(c = n) 둘 다 1이고, 늘 실패하면(c = 0) 둘 다 0입니다. 둘이 벌어지는 건 가끔 성공하는 과제뿐이에요.

실습 준비

이 글의 명령은 모두 실습 코드의 agent-eval-pass-k/ 디렉터리에서 실행합니다.

  • compose.yaml: Ollama 0.34.4 컨테이너와 Python 3.12 클라이언트 컨테이너 정의
  • requirements.txt: 클라이언트 컨테이너가 설치하는 패키지 두 개
  • agentlab/: 가짜 사이트 DB 도구 다섯 개, 에이전트 루프, Ollama 호출 패키지(파일 11개)
  • site.db: 가짜 사이트 DB(SQLite, 글 60개)
  • tasks.jsonl: 과제 45개와 정답, 정답 경로(gold_calls)
  • eval_run.py: 모델·조건·seed를 받아 과제를 돌리고 결과를 runs.jsonl에 붙이는 실행기
  • graders.py: 최종 답 채점기와 궤적 채점기
  • metrics.py: pass@k·pass^k 집계
  • run_all.sh: 두 모델 × 두 조건 × seed 1~8 일괄 실행
  • wait_res.sh: 메모리·디스크 여유 대기
  • report.py: 실습 1~3의 집계
  • show.py: 실행 기록 한 건 출력
  • runs.jsonl: 이 글에서 기록한 실행 결과(1,530줄). eval_run.py는 기록된 조합을 건너뛰므로 직접 돌리려면 이 파일을 먼저 지웁니다

compose.yaml은 컨테이너 두 개를 띄우고, ollama에 메모리 2,700MB·CPU 2.25, client에 메모리 300MB·CPU 0.25 상한을 걸어요. Ollama는 호스트의 127.0.0.1:7580으로 엽니다. Docker Desktop 안에서는 Mac의 GPU를 쓰지 못하니까 모든 추론은 CPU로 합니다.

compose 파일에 name: aeval이 있어서 프로젝트 이름은 따로 주지 않습니다. 아래 블록은 파일로 만들지 말고 실습 디렉터리에서 연 터미널에 그대로 붙여 넣어 주세요. 이후 명령은 모두 이 터미널에서 실행하고, 터미널을 새로 열면 다시 붙여 넣으시면 됩니다.

mkdir -p out
docker compose up -d
docker compose exec ollama ollama pull qwen3:1.7b
docker compose exec ollama ollama pull qwen2.5:1.5b
lab() { docker compose exec -T client python "$@"; }
docker compose exec ollama ollama list
NAME            ID              SIZE      MODIFIED       
qwen2.5:1.5b    65ec06548149    986 MB    25 minutes ago    
qwen3:1.7b      8f68893c685c    1.4 GB    37 minutes ago    

qwen3:1.7b가 주 모델이고, thinking 겸용 모델이라 모든 호출에 think: false를 줬어요. qwen2.5:1.5b는 비교 모델입니다.

과제 세트는 가짜 사이트 DB와 도구 다섯 개(search_articles, get_article, get_like_count, get_course, get_author), 과제 45개로 이뤄져 있어요. 과제는 정답에 이르기까지 도구를 몇 번 이어서 불러야 하는지로 나뉩니다.

  • 1번 호출 10개(T1-01~T1-10): "slug terraform-22 글의 좋아요 수는?" get_like_count 한 번
  • 2번 호출 10개(T2): "제목이 「…」인 글의 좋아요 수는?" 검색으로 slug를 찾고 좋아요 조회
  • 3번 호출 10개(T3): "「…」 글에 연결된 강의의 수강생 수는?" 검색, 글 조회, 연결된 콘텐츠 조회
  • 4번 이상 10개(T4): "태그 Redis가 붙은 글 가운데 좋아요가 가장 많은 글에 연결된 강의의 가격은?" 태그 검색, 후보 두 글의 좋아요 조회, 글 조회, 콘텐츠 조회
  • 형태 밖 5개(T0-01~T0-05): 두 글의 좋아요 차이, 저자 이름 같은 질문

과제마다 정답(정수, slug, 이름)과, 정답에 이르는 가장 짧은 도구 호출 목록(gold_calls)이 들어 있어요. 루프는 모델이 도구를 그만 부를 때까지 돌고, 최대 10단계에서 멈춰요. 시스템 프롬프트는 마지막 줄에 답: <값>을 쓰라고 요구해요.

eval_run.py는 실행 한 번마다 결과를 runs.jsonl에 한 줄씩 붙입니다. Ollama /api/chat 요청의 options에는 다음 값을 넣어요.

  • temperature: 조건 T0은 0, 조건 T7은 0.7이에요. 조건 이름 T0은 형태 밖 과제의 번호(T0-01 등)와는 관계가 없습니다.
  • seed: 시도 번호와 같게 1부터 8까지 바꿔요. temperature가 0보다 클 때 seed가 다르면 다른 토큰이 뽑힐 수 있어요.
  • num_ctx: 4096, num_predict: 512: 컨텍스트 길이와 한 번에 생성할 최대 토큰 수예요.
  • num_thread: 2: 컨테이너 CPU 상한(2.25)에 맞춘 값이에요.

같은 실행 기록을 두 채점기로 채점합니다(graders.py).

  • 최종 답 채점: 최종 답의 마지막 답: <값> 줄에서 값을 꺼내요. 그 줄이 없으면 본문에서 정답 종류에 맞는 마지막 숫자나 slug를 정규식으로 꺼냅니다.
  • 궤적 채점: 최종 답이 맞고, 아래 세 조건을 모두 지켜야 성공이에요.
    • 빠진 호출 없음(missing): gold_calls의 호출을 모두 같은 핵심 인자로 불렀습니다.
    • 순서(order): 앞 단계의 호출을 모두 마친 뒤에 다음 단계의 호출이 처음 나왔습니다.
    • 필요 없는 호출(extra): gold_calls에 맞춰지지 않은 호출(엉뚱한 호출, 같은 호출의 반복)이 1개 이하입니다.

두 모델 × 두 조건의 네 묶음은 run_all.sh로 이어서 돌려요. 묶음마다 eval_run.py를 seed 1~8로 부르고, 로그는 out/에 남깁니다. 앞에 붙은 wait_res.sh는 Docker의 남은 메모리가 1.5GB, 디스크가 8GB 이상이 될 때까지 5분씩 기다리는 스크립트예요. 실제 실행 시간의 합은 139분이었습니다.

./run_all.sh

실습 1: temperature 0.7에서 8번

report.py는 runs.jsonl을 다시 채점해서 숫자를 내는 스크립트예요. dist는 과제마다 8번 중 몇 번 성공했는지(c)를 세고, 그 분포와 pass@k, pass^k, 회차별 성공률을 출력합니다.

lab report.py dist T7
== qwen3:1.7b T7 grader=final: 45 tasks x 8 runs (seeds 1-8)
  successes c :   0   1   2   3   4   5   6   7   8
  tasks       :  23   6   3   0   0   1   1   0  11
  tasks with 0 < c < 8: 11 ['T0-03', 'T2-01', 'T2-02', 'T2-06', 'T2-09', 'T2-10', 'T4-03', 'T4-07', 'T4-08', 'T4-09', 'T4-10']
    1 call    c = [8, 8, 8, 8, 8, 8, 8, 8, 8, 8]
    2 calls   c = [2, 1, 0, 0, 0, 2, 0, 0, 1, 1]
    3 calls   c = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    4+ calls  c = [0, 0, 5, 0, 0, 0, 6, 2, 1, 1]
    outside   c = [0, 8, 1, 0, 0]
  pass@k : k=1 0.308  k=2 0.350  k=4 0.408  k=8 0.489
  pass^k : k=1 0.308  k=2 0.267  k=4 0.251  k=8 0.244
  per-run success: 31% 31% 29% 33% 29% 29% 33% 31%  (min 28.9%, max 33.3%)
== qwen2.5:1.5b T7 grader=final: 45 tasks x 8 runs (seeds 1-8)
  successes c :   0   1   2   3   4   5   6   7   8
  tasks       :  33   1   1   0   0   0   0   0  10
  tasks with 0 < c < 8: 2 ['T0-02', 'T3-03']
    1 call    c = [8, 8, 8, 8, 8, 8, 8, 8, 8, 8]
    2 calls   c = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    3 calls   c = [0, 0, 2, 0, 0, 0, 0, 0, 0, 0]
    4+ calls  c = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    outside   c = [0, 1, 0, 0, 0]
  pass@k : k=1 0.231  k=2 0.238  k=4 0.251  k=8 0.267
  pass^k : k=1 0.231  k=2 0.223  k=4 0.222  k=8 0.222
  per-run success: 22% 22% 27% 22% 22% 22% 22% 24%  (min 22.2%, max 26.7%)
  • successes c와 tasks: 8번 중 c번 성공한 과제가 몇 개인지예요.
  • 과제 유형별 줄의 c = [...]: 과제마다의 성공 횟수입니다.
  • per-run success: seed 하나(8번 중 한 회)의 45개 과제 성공률이에요.

qwen3:1.7b의 회차별 성공률은 28.9%~33.3%로 움직였어요. 8번 중 한 번 이상 성공하고 한 번 이상 실패한 "가끔 되는 과제"가 11개이고, 그중 9개는 8번 중 1~2번만 성공했습니다.

k를 늘리면 두 값이 벌어져요.

  • pass@k: 0.308, 0.350, 0.408, 0.489. 8번 중 한 번이라도 성공한 과제는 45개 중 22개입니다.
  • pass^k: 0.308, 0.267, 0.251, 0.244. 8번 모두 성공한 과제는 11개예요.

pass@8과 pass^8의 간격은 0.244로, 과제 11개 분량이에요. 정확히 가끔 되는 과제 수와 같습니다.

같은 실행 기록을 보고 "8번 시도하면 절반 가까이 푼다"고 말할 수도 있고, "8번 연속으로 믿고 맡길 수 있는 과제는 4분의 1"이라고 말할 수도 있어요. 결과를 사람이 고르거나 검증할 수 있으면 pass@k가, 사용자가 매번 결과를 그대로 받으면 pass^k가 맞는 질문입니다.

qwen2.5:1.5b는 가끔 되는 과제가 2개뿐이라, pass@8과 pass^8의 간격도 0.044(과제 2개)에 그쳤어요.

실습 2: 반복 간 차이와 모델 간 차이

이번에는 두 모델의 성공률 차이를 반복 간 차이와 나란히 놓아 볼게요. 같은 seed의 회차끼리 짝을 지어, 같은 과제에서 한쪽만 성공한 과제 수도 셉니다.

lab report.py versus T7
qwen3:1.7b T7: per-run 28.9% ~ 33.3% (range 4.4%p), mean 30.8%
qwen2.5:1.5b T7: per-run 22.2% ~ 26.7% (range 4.4%p), mean 23.1%
model gap (mean qwen3:1.7b - mean qwen2.5:1.5b): +7.8%p
  seed 1: both 10, only qwen3:1.7b 4, only qwen2.5:1.5b 0, neither 31
  seed 2: both 10, only qwen3:1.7b 4, only qwen2.5:1.5b 0, neither 31
  seed 3: both 11, only qwen3:1.7b 2, only qwen2.5:1.5b 1, neither 31
  seed 4: both 10, only qwen3:1.7b 5, only qwen2.5:1.5b 0, neither 30
  seed 5: both 10, only qwen3:1.7b 3, only qwen2.5:1.5b 0, neither 32
  seed 6: both 10, only qwen3:1.7b 3, only qwen2.5:1.5b 0, neither 32
  seed 7: both 10, only qwen3:1.7b 5, only qwen2.5:1.5b 0, neither 30
  seed 8: both 10, only qwen3:1.7b 4, only qwen2.5:1.5b 1, neither 30
  qwen3:1.7b: tasks that differ between two runs of the same model: max 7, mean 3.8 over 28 pairs
  qwen2.5:1.5b: tasks that differ between two runs of the same model: max 2, mean 0.7 over 28 pairs
  • 한 모델의 회차별 성공률은 T7에서 최대 4.4%포인트(과제 2개) 움직였어요. 두 모델의 평균 차이는 7.8%포인트로 이 범위 밖에 있습니다.
  • 그래도 한 번씩만 돌려 비교했다면 운에 따라 결론의 크기가 달라졌을 거예요. seed 3끼리 비교하면 qwen3:1.7b만 맞힌 과제가 2개, qwen2.5:1.5b만 맞힌 과제가 1개로 차이가 과제 1개(2.2%포인트)예요. seed 4나 7끼리 비교하면 5 대 0입니다.
  • 같은 모델의 두 회차를 비교해도 성공 여부가 다른 과제가 많게는 7개(qwen3:1.7b)였어요. 회차별 성공률은 28.9%~33.3%로 비슷했지만, 맞힌 과제의 구성은 회차마다 달랐습니다.

실습 3: 최종 답 채점과 궤적 채점

같은 1,440번의 실행 기록을 궤적 채점기로 다시 채점했어요. 모델을 다시 부르지 않고 runs.jsonl만 읽습니다.

lab report.py graders
T0 final: qwen3:1.7b pass@1 0.311 pass^8 0.311 pass@8 0.311 | qwen2.5:1.5b pass@1 0.222 pass^8 0.222 pass@8 0.222
T0 traj : qwen3:1.7b pass@1 0.267 pass^8 0.267 pass@8 0.267 | qwen2.5:1.5b pass@1 0.222 pass^8 0.222 pass@8 0.222
T7 final: qwen3:1.7b pass@1 0.308 pass^8 0.244 pass@8 0.489 | qwen2.5:1.5b pass@1 0.231 pass^8 0.222 pass@8 0.267
T7 traj : qwen3:1.7b pass@1 0.258 pass^8 0.244 pass@8 0.333 | qwen2.5:1.5b pass@1 0.222 pass^8 0.222 pass@8 0.222
qwen3:1.7b T0: final ok 112, traj ok 96, dropped 16: {'missing': 16} by type {'4+ calls': 16}
qwen3:1.7b T7: final ok 111, traj ok 93, dropped 18: {'extra': 3, 'missing': 11, 'missing,extra': 4} by type {'2 calls': 3, '4+ calls': 15}
qwen2.5:1.5b T0: final ok 80, traj ok 80, dropped 0: {} by type {}
qwen2.5:1.5b T7: final ok 83, traj ok 80, dropped 3: {'missing': 3} by type {'3 calls': 2, 'outside': 1}
  • 두 모델의 순서는 네 경우 모두 같았어요. 어느 채점기로 보든 qwen3:1.7b가 높습니다.
  • qwen3:1.7b에서 최종 답은 맞았지만 궤적 채점에서 떨어진 실행이 T0에서 16번, T7에서 18번이었어요. 4번 이상 과제가 31번으로 대부분이고, 사유는 대부분 필요한 호출이 빠진 것(missing)이었습니다.
  • T7에서 최종 답 기준 pass@8이 0.489였던 것이 궤적 기준으로는 0.333이에요. 8번 시도해 한 번이라도 푼 과제 22개 가운데 7개는, 제대로 된 경로로는 한 번도 풀지 못했다는 뜻입니다.

show.py는 기록 하나를 골라 정답 경로(gold), 단계별 도구 호출과 결과 앞부분, 두 채점기의 판정을 출력해요. 4번 이상 과제 하나를 볼게요.

lab show.py qwen3:1.7b T0 1 T4-03
[T4-03] qwen3:1.7b T0 seed 1: 태그 `Zookeeper`가 붙은 글 가운데 좋아요가 가장 많은 글에 연결된 강의의 가격은?
gold: search_articles({"query": "", "tag": "Zookeeper"}) -> get_like_count({"slug": "zookeeper-55"}) -> get_like_count({"slug": "zookeeper-56"}) -> get_article({"slug": "zookeeper-55"}) -> get_course({"course_slug": "crs-08"})
step 1: search_articles({"query": "", "tag": "Zookeeper"}) -> {"count": 2, "results": [{"slug": "zookeeper-55", "title": "Zookeeper 모니터링 지표를 고르는 기준"}, {
step 1: get_course({"course_slug": "crs-00"}) -> [error:not_found]
step 2: get_article({"slug": "zookeeper-55"}) -> {"slug": "zookeeper-55", "title": "Zookeeper 모니터링 지표를 고르는 기준", "tags": ["Zookeeper", "데이터베
step 3: get_course({"course_slug": "crs-08"}) -> {"course_slug": "crs-08", "title": "인프라 코드로 관리하기", "price": 51000, "student_count": 6604}
step 4: text: '답: 51000'
answer 51000 | final grader: OK (value 51000) | trajectory grader: NG ['missing']

모델은 좋아요를 한 번도 조회하지 않고 검색 결과의 첫 글을 골랐어요. 이 과제에서는 첫 글이 마침 좋아요가 더 많은 글이었고요.

같은 방식으로 실행 기록을 읽어 보면(명령은 싣지 않았어요), qwen3:1.7b가 4번 이상 과제에서 최종 답을 맞힌 31번은 모두 좋아요 조회에 한 번도 성공하지 않은 실행이었고, 그중 30번은 검색 결과의 첫 글을 고른 경우였어요.

궤적 채점의 규칙(핵심 인자, 순서, 허용할 필요 없는 호출 수)은 채점하는 사람이 정한 것이라, 다른 올바른 경로를 실패로 판정할 위험이 있어요. 그래서 여기서는 두 채점을 함께 돌리고, 판정이 갈린 실행은 궤적을 직접 읽어 봤습니다.

정리

범위: CPU에서 돌린 qwen3:1.7b와 qwen2.5:1.5b(Q4_K_M), 과제 45개, temperature 0과 0.7, 조건마다 seed 1~8, 모두 1,440번 실행.

  • temperature 0.7에서 qwen3:1.7b의 회차별 성공률은 28.9%~33.3%였고, 같은 모델의 두 회차 사이에 성공 여부가 다른 과제가 많게는 7개였습니다. 한 번 돌린 성공률을 그 에이전트의 성공률로 쓰지 마세요.
  • 같은 기록에서 pass@8은 0.489, pass^8은 0.244였고 그 간격은 가끔 되는 과제 11개와 같았습니다. 결과를 골라 쓸 수 있으면 pass@k를, 사용자가 매번 그대로 받으면 pass^k를 보세요.
  • 이 글에 싣지 않은 추가 측정에서는 temperature 0에서 seed 1~8이 두 모델 모두 성공 여부를 하나도 바꾸지 않았지만, 같은 seed라도 CPU 스레드 수를 2에서 1로 바꾸자 45개 중 9개 과제의 출력이 달라졌고 원인은 확인하지 못했습니다. 흔들림을 재려면 실제로 쓰는 temperature로 돌려 주시고, temperature 0 결과가 다른 실행 환경에서도 같다고 가정하지 마세요.
  • 두 모델의 평균 차이 7.8%포인트는 반복 간 범위(최대 4.4%포인트) 밖이었지만, seed 하나씩만 짝지어 비교하면 한쪽만 맞힌 과제 차이가 1개에서 5개까지 달라졌습니다. 반복 간 범위보다 작은 차이는 결론으로 쓰지 마세요.
  • 궤적 채점으로 다시 세자 T7 pass@8은 0.489에서 0.333으로 내려갔습니다. 최종 답 채점과 궤적 채점을 함께 돌리고, 판정이 갈린 실행은 궤적을 직접 읽어 보세요.
내 링크 만들기