🧪📊
"팀을 바꿨는데… 진짜 좋아진 거 맞아?"
멀티에이전트 팀을 벤치마크로 평가하고, 모든 과정을 DB에 적어 데이터로 증명하는 8장의 이야기
한 번의 cli.py run 이 어떻게 태스크를 고르고 · P–E–C 팀을 돌리고 · 모든 메시지를 PostgreSQL에 적고 · 채점하고 · 집계 뷰를 거쳐 Grafana 그래프가 되는지 — 눈대중 대신 숫자로.
🎭 등장인물 — AI 한 명이 다 하는 게 아니라, 역할이 다른 AI들이 한 팀(P–E–C: 계획·실행·검증)을 이뤄 번갈아 일합니다. 뒤 3·4장에서 직접 만나요.
🚦셀렉터진행 사회자 (누가 말할 차례인지 정함)
⚖️채점관success / format_valid
🗄️PG기록 창고 (모든 실행을 저장 · runs · events)
🗺 한눈에 보는 전체 흐름
8장을 시작하기 전에 — 한 번의 cli run이 그래프가 되기까지
%%{init: {"theme":"dark", "themeVariables":{"fontFamily":"Pretendard,Noto Sans KR,Segoe UI,system-ui,sans-serif","fontSize":"14px","primaryColor":"#161b22","primaryTextColor":"#e6edf3","primaryBorderColor":"#30363d","lineColor":"#8b949e","textColor":"#e6edf3","clusterBkg":"#0d1117","clusterBorder":"#30363d","edgeLabelBackground":"#161b22"}}}%%
flowchart TD
U(["🧑🔬 평가자<br/>cli.py run --team --batch"]) --> R["⚙️ runner<br/>태스크 선별 · run 박제"]
R -->|"status=running INSERT"| RUNS[("🗄️ runs<br/>결과 · 조건")]
R --> T
subgraph T ["🚦 P–E–C 팀 · SelectorGroupChat"]
direction LR
SEL(["🚦 셀렉터<br/>다음 발화자"]) --> PL(["🤖 플래너"])
PL --> EX(["🛠️ 실행자<br/>파일·산술 도구"])
EX --> CR(["🧐 비평가<br/>APPROVE"])
CR -.->|"reason_code"| SEL
end
T -->|"AutoGen 이벤트"| LOG["📒 tracing/logger.py<br/>이벤트 분류 · 비밀 마스킹"]
LOG -->|"seq 순 INSERT"| EVENTS[("🗄️ events<br/>모든 메시지")]
T -->|"FINAL ANSWER"| SC["⚖️ gaia/scorer<br/>success · format_valid"]
SC -->|"UPDATE status=completed"| RUNS
RUNS --> MV["🧮 집계 뷰<br/>mv_batch_summary · mv_passk · mv_tool_summary"]
EVENTS --> MV
MV --> GRAF(["📊 Grafana<br/>SQL 패널 · 30초 자동반영"])
EVENTS -.->|"라이브 뷰 v_*"| GRAF
classDef user fill:#1c2333,stroke:#58a6ff,color:#e6edf3
classDef run fill:#21262d,stroke:#58a6ff,color:#e6edf3
classDef team fill:#0d1117,stroke:#d2a8ff,color:#e6edf3
classDef log fill:#0d1117,stroke:#56d4dd,color:#56d4dd
classDef score fill:#0d1117,stroke:#e3b341,color:#e3b341
classDef db fill:#0d1117,stroke:#ff7b72,color:#ff7b72
classDef agg fill:#0d1117,stroke:#3fb950,color:#3fb950
classDef graf fill:#1c2333,stroke:#ff9e64,color:#ff9e64
class U user
class R,SEL,PL,EX,CR run
class LOG log
class SC score
class RUNS,EVENTS db
class MV agg
class GRAF graf
linkStyle default stroke:#8b949e,stroke-width:1.5px
📖 읽는 법 — 각 칸은 起(도입)→承(전개)→轉(전환)→合(결말) 순서
起 의심
🤖💬
평가자
비평가 프롬프트에 "형식 체크리스트" 한 줄 넣었어. 이제 더 똑똑해졌겠지?
근데… 증거는?
承 함정
😎→🍀→🎲
내레이터
"좋아 보인다"는 운빨일 수도, temperature 탓일 수도, 그날 태스크가 쉬웠던 걸 수도.
한 번 돌려보고 판단 = 위험
轉 원칙
🗄️
내레이터
그래서
모든 실행을 PostgreSQL에 적는다. 같은 조건·같은 태스크·여러 번 반복해서
분포로 본다.
# 실행 1회의 결과/조건 → runs
# run 안의 모든 메시지 → events
合 선언
🎯
평가자
"팀 구성 변경의 효과를
데이터로 검증한다." 이게 이 시스템의 전부야.
팀 구성→trace DB→정량 비교
한 줄 요약 — runs(결과·조건)와 events(모든 메시지)로 나눠 적고, 거기서 모든 메트릭을 파생한다.
起 호출
🧑🔬
python cli.py run \
--batch exp_a --team v1-baseline \
--exclude-web --attempts 3
평가자
웹 없는 태스크만, 태스크당 3번 반복으로 돌려줘.
承 선별
🗃️
runner
tasks 테이블에서 조건에 맞는 것만 고른다.
WHERE benchmark='gaia'
AND requires_web=false -- --exclude-web
ORDER BY task_id -- LIMIT 적용
첨부 없는 태스크는 기본 제외
轉 박제
🧊
PG·runs
새 run 행을
status=running 으로 INSERT.
이 순간 조건이 얼어붙는다.
model=glm-4.7 temperature=0.2
seed=… max_messages=20
合 의미
🔁
내레이터
temperature·seed·model을 안 적은 실행은
비교 불능. 그래서 NOT NULL처럼 다룬다.
completed → skip
·
crashed → --retry-crashed?
이미 끝난(run completed) 조합은 건너뛰고, 깨진(crashed) 건 --retry-crashed 로만 되살린다 — 멱등하게.
🚦 run 한 건의 상태 전이
%%{init: {"theme":"dark", "themeVariables":{"fontFamily":"Pretendard,Noto Sans KR,Segoe UI,system-ui,sans-serif","fontSize":"13px","primaryColor":"#161b22","primaryTextColor":"#e6edf3","primaryBorderColor":"#30363d","lineColor":"#8b949e","textColor":"#e6edf3"}}}%%
stateDiagram-v2
direction LR
[*] --> pending: 큐 적재
pending --> running: 🧊 조건 박제 INSERT
running --> completed: ⚖️ 채점 끝 (success 기록)
running --> crashed: 💥 런타임 teardown 경합
running --> error: ⚠️ 예외
running --> timeout: ⏱️ max 초과
completed --> [*]: 멱등 skip (재실행 제외)
crashed --> running: 🔁 --retry-crashed
error --> running: 🔁 --retry-crashed
timeout --> running: 🔁 --retry-crashed
note right of completed
success NULL 금지 —
completed면 판결 필수
end note
起 지휘
🚦
{"next":"planner",
"reason_code":"NEEDS_PLANNING"}
承 계획·실행
🤖→🛠️
플래너
3단계로 풀자. 2번은 첨부파일 숫자 합산이 필요해.
실행자
read_attached_file ·
python_calc 로 처리. (eval 금지, AST allowlist)
合 종료
🏁
내레이터
비평가가 "APPROVE" 하거나, max_messages에 닿으면 종료.
TextMention("APPROVE", critic)
| MaxMessage(20)
팀은 SelectorGroupChat — 셀렉터가 reason_code(READY_TO_FINAL 등)로 다음 발화자를 고른다. allow_repeated_speaker=false.
起 받아쓰기
📒
트레이서
팀이 내뱉는 AutoGen 이벤트마다 종류를 분류해서 events에 한 줄씩 적는다.
ToolCallRequest → tool_call
ToolCallExecution → tool_result/error
SelectorEvent → selector_decision
承 순서
①→②→③→④
트레이서
seq 는 0부터 +1. 도구 호출엔 짝(result/error)이 반드시 따라붙어야 한다 — DB가 CHECK로 강제.
轉 비밀 가리기
🕶️
트레이서
tool_args 저장 전, 민감 키는 무조건 가린다.
# password|token|secret|api_key|authorization
{"api_key":"sk-…"} → {"api_key":"***"}
단일 지점 마스킹
合 철칙
🛟
내레이터
"로깅은 평가를 죽이지 않는다." INSERT가 실패해도 경고만 남기고 평가는 계속된다.
missed_inserts++ → 계속 멈추는 건 DB 연결 소실뿐
긴 본문은 잘라 저장(content 8192자 · tool_result 2048자) + 원본 해시. 추적성과 안정성을 둘 다 지킨다.
起 추출
⚖️
채점관
답에서
FINAL ANSWER: 패턴을 뽑는다(마지막 매치 — 자기수정 허용).
뽑히면 format_valid=true
承 정규화
🔢
채점관
통화기호·콤마·% 떼고 숫자화. 숫자는
|a-b|<1e-6 허용오차.
"$1,234.0" → 1234
"The Cat." → "cat"
轉 리스트 함정
📋
채점관
리스트 답은
순서 보존! 정렬하면 틀린다.
合 기록
✅
PG·runs
판결을 run 행에 UPDATE.
UPDATE runs SET status='completed',
success=true, format_valid=true,
tokens=…, latency_ms=…, ended_at=now()
도메인마다 자(尺)가 다르다 — SQL은 실행결과 멀티셋, 리서치는 인용 F1, 분석은 "실제 검색 결과가 있었나"(grounding).
起 유혹
✏️
평가자
v1-baseline 프롬프트만 살짝 고치고 그대로 v1-baseline으로 돌려야지~
承 해시
🧬
registry
팀 프롬프트 전부를 이어 sha256. 이게
prompt_bundle_hash.
sha256(planner+executor
+critic+selector)
轉 차단
🚫
PG·teams
같은 team_id인데 해시가 다르다고?!
즉시 중단.
raise TeamConfigMismatchError(
"구성이 바뀌면 새 team_id를 쓰세요")
合 이유
🧷
내레이터
이걸 막지 않으면 "v1이 좋아졌다"가 사실은 서로 다른 두 팀의 비교가 된다 — 비교가 오염된다.
config_full(JSONB)에 전체 구성 스냅샷
DB 레벨의 문지기 — 구성이 바뀌면 새 이름. 덕분에 ⑥ 팀구성 대시보드가 git 없이도 팀의 진화를 추적한다.
起 집계
🧮
runner
배치가 끝나면 원천 테이블을 미리 계산해 둔다.
refresh_views() →
mv_batch_summary · mv_passk
· mv_tool_summary
承 지표
📐
mv_batch_summary
pass@1 · loop_rate · format_invalid_rate · tokens_per_success · p95_latency …
REFRESH 잊으면 stale!
轉 연결
📊→🗄️
그라파나
postgres 데이터소스로 PG에 직접 붙는다(같은 도커 내부망).
uid=eval_ph1_pg → evaldb
searchPath=eval_ph1
合 시각화
📈
그라파나
대시보드 JSON은 30초마다 자동 반영(재시작 불필요). 코드 배포 없이 데이터가 그래프가 된다.
① 개요 ② 메트릭 ③④ grounding ⑥ 팀구성
흐름: runs+events → mv_* → Grafana SQL 패널. 도메인 grounding은 라이브 뷰(v_*)로 즉시 반영.
起 가설
💡
평가자
이번엔
딱 한 가지만 바꾼다 — 비평가에 형식 체크리스트. (새 team_id로!)
experiments(title, hypothesis,
intervention, team_before/after)
承 A/B
🅰️vs🅱️
compare
두 배치를 level별로 나란히. pass@1 · p95 비교.
轉 판정
⚖️
판정
improved 7 · regressed 0 →
promoted. (아니면 rejected / inconclusive)
key_evidence: {"pass_at_1":[.42,.51],
"improved":7}
合 복리
🔁
내레이터
판정은 Grafana 시계열에 마커로 박히고, lesson은 다음 가설의 씨앗이 된다. 이게 복리(compound)다.
측정 → 개선 → 재측정 → 기록
"좋아 보인다"가 아니라 improved 7, regressed 0 — 한 번에 한 변수만, 증거와 함께. 그게 이 파이프라인의 끝이자 시작.
🔁 복리(compound) 루프
한 변수만 바꾸고 · 증거로 판정하고 · lesson을 다음 가설로
%%{init: {"theme":"dark", "themeVariables":{"fontFamily":"Pretendard,Noto Sans KR,Segoe UI,system-ui,sans-serif","fontSize":"14px","primaryColor":"#161b22","primaryTextColor":"#e6edf3","primaryBorderColor":"#30363d","lineColor":"#8b949e","textColor":"#e6edf3","clusterBkg":"#0d1117","clusterBorder":"#30363d","edgeLabelBackground":"#161b22"}}}%%
flowchart LR
H(["💡 가설<br/>딱 한 변수만 바꾼다"]) -->|"새 team_id"| AB["🅰️🅱️ A/B 측정<br/>before vs after 배치"]
AB --> CMP["📐 compare<br/>level별 pass@1 · p95"]
CMP --> J{"⚖️ 판정<br/>improved vs regressed"}
J -->|"improved 7 · regressed 0"| P(["✅ promoted"])
J -->|"악화"| RJ(["❌ rejected"])
J -->|"애매·표본부족"| INC(["🤷 inconclusive"])
P --> REC["📒 experiments 기록<br/>key_evidence · lesson"]
RJ --> REC
INC --> REC
REC -->|"Grafana 시계열 마커"| M(["📈 마커로 박제"])
REC ==>|"lesson = 다음 씨앗"| H
classDef hyp fill:#1c2333,stroke:#58a6ff,color:#e6edf3
classDef step fill:#21262d,stroke:#8b949e,color:#e6edf3
classDef judge fill:#0d1117,stroke:#e3b341,color:#e3b341
classDef good fill:#0d1117,stroke:#3fb950,color:#3fb950
classDef bad fill:#0d1117,stroke:#f85149,color:#f85149
classDef neu fill:#0d1117,stroke:#8b949e,color:#8b949e
classDef rec fill:#0d1117,stroke:#56d4dd,color:#56d4dd
classDef graf fill:#1c2333,stroke:#ff9e64,color:#ff9e64
class H hyp
class AB,CMP step
class J judge
class P good
class RJ bad
class INC neu
class REC rec
class M graf
linkStyle default stroke:#8b949e,stroke-width:1.5px
linkStyle 10 stroke:#3fb950,stroke-width:2.5px
🛠️▶️
Part 2 · 직접 돌리기
코드를 폐쇄망 머신으로 옮겨, 빈손에서 첫 pass@1까지 — 6장의 실전
Part 1이 "한 번의 run이 그래프가 되는 생애"였다면, Part 2는 당신 손으로 그 run을 처음 일으키는 절차다. 각 칸은 명령 한 줄 + "왜 이게 필요한가" + 다음 칸과의 연결.
🗺 직접 돌리기 — 한눈에
빈 머신 → 환경 → 스키마 → 문제 → LLM 점검 → 스모크 → 본 평가
%%{init: {"theme":"dark", "themeVariables":{"fontFamily":"Pretendard,Noto Sans KR,Segoe UI,system-ui,sans-serif","fontSize":"14px","primaryColor":"#161b22","primaryTextColor":"#e6edf3","primaryBorderColor":"#30363d","lineColor":"#8b949e","textColor":"#e6edf3","clusterBkg":"#0d1117","clusterBorder":"#30363d","edgeLabelBackground":"#161b22"}}}%%
flowchart TD
V(["🐍 venv + 폐쇄망 설치<br/>pip install -e .[dev]"]) --> ENV["📝 .env<br/>DSN · vLLM URL"]
ENV --> DB["🗄️ cli.py init-db<br/>마이그레이션 멱등"]
DB --> LOAD["📥 cli.py load-gaia<br/>tasks 적재"]
LOAD --> SM{"🔌 smoke_llm.py<br/>tool-calling 왕복?"}
SM -->|"실패"| FIX["🔧 vLLM 옵션<br/>--enable-auto-tool-choice"]
FIX -.->|"재설정"| SM
SM -->|"성공"| RUN["▶️ cli.py run --limit 2<br/>dev_smoke 스모크"]
RUN --> REP["📊 cli.py report<br/>pass@1 · 분포"]
REP -->|"OK면 스케일업"| FULL(["🚀 eval_all_teams.py<br/>전체 · 재개 · scorecard"])
classDef setup fill:#1c2333,stroke:#58a6ff,color:#e6edf3
classDef db fill:#0d1117,stroke:#ff7b72,color:#ff7b72
classDef gate fill:#0d1117,stroke:#e3b341,color:#e3b341
classDef fix fill:#0d1117,stroke:#f85149,color:#f85149
classDef run fill:#21262d,stroke:#3fb950,color:#3fb950
classDef full fill:#1c2333,stroke:#ff9e64,color:#ff9e64
class V,ENV setup
class DB,LOAD db
class SM gate
class FIX fix
class RUN,REP run
class FULL full
linkStyle default stroke:#8b949e,stroke-width:1.5px
起 발견
🖥️❓
내레이터
새 머신엔
.venv가 없다. 심지어
python조차 안 잡힌다.
$ python cli.py init-db
bash: python: command not found
承 생성
🐍
평가자
가상환경을 만들고 켠다. 이제부터
python은 이 안의 것.
python3 -m venv .venv
source .venv/bin/activate # (.venv) 프롬프트
轉 폐쇄망
🔒📦
내레이터
운영은
폐쇄망 — 외부 PyPI·huggingface 금지. 의존성은
사내 미러로.
pip install -e ".[dev]" \
--index-url http://사내-pypi/simple
合 확인
✅
평가자
import 되면 도구 준비 끝. 다음 단계로.
python -c "import agent_eval" # 에러 0 = OK
빈 머신→venv·의존성→cli.py 가동
매 세션 첫 줄은 source .venv/bin/activate — 이게 0번 단계다. 안 하면 python: command not found.
起 설정
📝
평가자
먼저 접속 정보를 적는다 — DB와 LLM 주소.
cp .env.example .env # 편집
EVAL_PG_DSN=postgresql://…
EVAL_LLM_BASE_URL=http://vllm:8000/v1
承 규칙
🔑
내레이터
하드코딩 금지 — 설정은
env로만 읽는다(pydantic-settings). 비밀은
.env에만,
커밋 금지.
DSN·토큰을 로그·커밋에 노출 금지
轉 창고
🗄️
PG
init-db가 마이그레이션을
멱등으로 적용. 2번 돌려도 성공한다.
python cli.py init-db # 001+ 적용, 멱등(AC1)
合 주의
⚠️
내레이터
단,
최초 1회 계정·스키마를 만드는
000_provision.sql은 DBA가 수동.
init-db는 그 위에서만 동작한다.
001+ 멱등 자동
·
000_provision 수동(DBA)
설정은 env로 박제, 스키마는 마이그레이션 파일로만 — 코드 내 DDL 금지(스키마가 SSOT).
起 빈 테이블
🫙
runner
tasks가 비어 있으면 선별할 게 없다 → run 0개. 문제부터 넣어야 한다.
承 적재
📥
python cli.py load-gaia # metadata.jsonl → tasks
轉 벤치마크별
내레이터
벤치마크마다 로더가 다르다 —
돌릴 것만 적재.
research → cli.py load-research
cppx → cli.py load-cppx
analysis→ scripts/load_analysis.py
sql → scripts/load_sql.py
extract → scripts/load_extract.py
合 제외
📎🚫
내레이터
첨부가 빠진 태스크는
attachment_missing=true로 표시 →
기본 필터에서 제외(첨부 없이는 못 푸는 문제).
runner 기본 제외
돌릴 벤치마크의 로더만 실행하면 된다 — gaia는 cli.py load-gaia 한 줄.
承 두드림
🔌
내레이터
그래서 먼저
한 번만 왕복시켜 본다 — function calling이 진짜 도는지.
python scripts/smoke_llm.py # tool-call 1회 왕복
轉 실패진단
🔧
비평가
도구호출 실패?
vLLM 기동 옵션부터 의심하라. 가장 흔한 함정이다.
--enable-auto-tool-choice
--tool-call-parser <glm parser>
合 게이트
🚦
내레이터
초록불이 떠야 run으로 넘어간다. 빨간불이면 절대 본 평가로 가지 않는다.
왕복 OK → run
·
실패 → vLLM 재설정
smoke_llm.py는 실측 직전의 필수 게이트 — 통과 못 하면 다음 칸으로 못 간다.
起 작게
🔬
평가자
전체 말고 2문제만 — 파이프가 끝까지 도는지 본다.
python cli.py run --batch dev_smoke \
--team v1-baseline --limit 2 --exclude-web
承 박제·재개
🧊
PG·runs
조건(model·temp·seed) INSERT(running) → 채점 → completed. 같은 batch 재실행 시
completed run은 멱등 skip.
끊겨도 같은 명령 = 이어서
轉 읽기
📊
채점관
눈대중이 아니라 숫자로 — pass@1·레벨별 p50·도구 사용.
python cli.py report --batch dev_smoke
合 디깅
🔎
python cli.py failures --batch dev_smoke
run→report→failures
작게 먼저(--limit 2) 돌려 숫자를 읽은 뒤에 스케일업 — 이게 Part 1의 "분포로 본다"를 직접 실천하는 단계.
起 전체
📈
평가자
스모크가 깨끗하면
--limit을 빼고 팀 하나를 벤치마크 전체로.
python cli.py run --batch perf_v1-baseline \
--team v1-baseline --concurrency 1 --retry-crashed
承 자동
🤖
내레이터
52팀 전부는 드라이버 하나로 — 팀↔벤치마크 매핑·재개·크래시 재시도(3회)까지.
python scripts/eval_all_teams.py # 또는 gaia,research
轉 누적
🗂️
스코어카드
결과는
data/personal/eval/scorecard.json에
실시간 pass@1로 쌓인다 — 어디서든 읽기.
멱등 재개 · 진행현황 즉시 확인
合 경고
🚨
비평가
z.ai는
직렬. 동시에 두 평가를 돌리면 서로 간섭한다.
--concurrency 1, 같은 batch로만 재개.
동시 평가 금지 · z.ai 한 곳만
끊겨도 같은 명령 재실행 = 이어서. 단 z.ai 동시 호출 금지 — 백그라운드 평가가 도는 중이면 그게 끝난 뒤에.
🧾 직접 돌리기 — 전체 순서 한 장
source .venv/bin/activate
python cli.py init-db
python cli.py load-gaia
python scripts/smoke_llm.py
python cli.py run --batch dev_smoke --team v1-baseline --limit 2 --exclude-web
python cli.py report --batch dev_smoke
# OK면 본 평가 (팀 1개 전체)
python cli.py run --batch perf_v1-baseline --team v1-baseline --concurrency 1 --retry-crashed
# 또는 등록된 52팀 전체
python scripts/eval_all_teams.py
🎬 끝 — 더 깊이 보고 싶다면
이 코믹스는 같은 내용을 그림으로 풀었습니다. 정확한 컬럼·플래그·SQL·파일 레퍼런스가 필요하면 코드 총정리 페이지로, 실제 숫자가 궁금하면 Grafana로.