두 AI 팀에게 똑같은 질문("반도체 업계 동향 2026")을 던졌습니다. 한 팀(base)은 인터넷 검색 없이 자기 기억만으로 썼고, 다른 팀(개선, improved)은 웹 검색(web_search) 도구로 실제 자료를 찾아보고 출처(URL)를 달아 썼습니다. 페이지는 그 두 리포트 본문을 보여주고(§2), 무엇이 어떻게 달라졌는지 품질 해석을 붙이고, 마지막으로 "이걸 어떻게 객관적으로 쟀는가"라는 측정 방법론까지 설명합니다.
비유하자면, 같은 시험 문제를 오픈북 없이 푼 학생과 자료를 찾아보고 출처를 적은 학생의 답안을 나란히 채점표와 함께 펼쳐 놓은 것입니다.
등장하는 모델은 z.ai의 GLM(GLM-4.7) 하나이고, 두 팀 모두 같은 모델·같은 설정으로 돌려 조건을 똑같이 맞췄습니다.
공통점부터 보면, 두 팀 다 4명의 AI 일꾼(planner→analyst→synthesizer→critic)이 정해진 순서대로 돌아가며(RoundRobin이라고 부릅니다 — 회의에서 자리 순서대로 발언하는 것과 같습니다) 협업합니다. 모델도 z.ai GLM-4.7로 같고, 무작위성을 통제하는 값(seed)과 답변의 들쭉날쭉함을 조절하는 값(temperature)도 고정했습니다.
다른 점은 개선 팀의 analyst가 web_search 도구를 쓸 수 있다는 것입니다. 구체적으로 묶음 하나가 추가됐습니다.
가장 큰 문제는 이렇습니다. AI에게 최신 사건을 물으면, 검색 없이도 아주 그럴듯한 답을 술술 내놓습니다. 문제는 그 답이 학습 시점 이후의 사실은 틀릴 수 있다는 것입니다. 그런데 글이 매끄럽다 보니 사람이 보기엔 진짜인지 가짜인지 구분이 안 됩니다.
이 프로젝트는 그 차이를 숫자로 드러냅니다. 핵심은 근거성(grounding) — 리포트의 사실들이 실제 검색 자료에 "뿌리내리고" 있느냐입니다. base 팀은 출처가 0개라 모든 문장이 기억에 의존하고, 개선 팀은 출처 URL을 달아 "이 수치는 여기서 왔다"를 검증 가능하게 만듭니다.
여기서 "에이전트(agent)"는 스스로 도구를 쓰고 여러 단계를 거쳐 일하는 AI를 말합니다. 단순히 한 번 묻고 답하는 챗봇이 아니라, 검색하고 → 분석하고 → 글을 쓰고 → 스스로 검토하는 식의 작업자입니다. 그런 여럿이 모이면 "팀"입니다.
평가 과정은 이렇게 흐릅니다.
네 일꾼의 역할은 이렇습니다.
이들은 정해진 순서대로 발언을 돌아가며(RoundRobin) 진행하고, critic이 승인하거나("APPROVE") 메시지가 너무 많아지면(10개 한도) 멈춥니다.
시중의 검색형 리서치 기능은 훌륭하지만, 우리 목적에는 빠진 게 있습니다. 직접 만든 이유는 다음과 같습니다(다른 제품의 성능을 깎아내리려는 게 아니라, 용도가 다릅니다).
그래서 작업 방식이 과학 실험을 닮았습니다. 각 변경 시도를 실험(experiment)으로 기록하고, 그 결과로 채택(promoted) 또는 기각(rejected) 판정을 내립니다. 예를 들면,
각 라더(ladder)는 base에서 출발해 한 변수씩 바꿔가며 점점 나아지는 "개선 사다리"입니다. 그 과정 자체가 산출물입니다.
이 도구가 준 구체적 가치는 다음과 같습니다.
| 얻은 것 | 구체적 근거(실측) |
|---|---|
| 근거성 도약 | 출처 0→6개, web 검색 0→5회, grounded 0→100%(호출기준), 검증불가→검증가능 |
| 비용·속도까지 동시 개선 | 토큰 29.4k→21.3k(−28%), 지연 280s→177s(−37%) — 더 좋으면서 더 쌈 |
| 나쁜 시도를 거르는 안전망 | tokens_per_success가 기각 케이스를 정확히 적발(EXP-60 119k, EXP-63 55k, EXP-65 97k) |
또한 "웹의 가치가 만능은 아니다"라는 경계도 배웠습니다. 5개 주제 실측(§9)에서, 웹은 최신 사실이 중요한 주제(신제품 경쟁 등)에선 명확히 이겼지만, 개념 설명처럼 시점이 무관한 주제에선 무승부였습니다. 심지어 검색이 빈약하면 개선 팀은 정직하게 "데이터 부족, 작성 불가"라고 거부한 반면, base는 출처 0으로 통계를 환각했습니다 — 거부가 환각보다 낫다는 교훈입니다.
먼저 둘이 왜 다르게 생겼는지부터. 리서치 라더(research ladder)는 정답이 딱 하나로 정해진 사실 문제(멀티홉 사실 QA — 여러 자료를 거쳐야 답이 나오는 문제)를 풉니다. 사내 문서 더미(doc corpus)에서 답을 찾고, 답이 정답과 글자 그대로 맞는지(exact-match)로 채점합니다. 그래서 점수가 pass@1(여러 번 시도 중 한 번이라도 맞힌 비율)로 깔끔하게 떨어집니다. 베이스라인이 이미 0.967(거의 만점)이라, 여기서 승부는 '더 망가뜨리지 않기'입니다.
분석 라더(analysis ladder)는 정답이 없는 개방형 장문 리포트("업계 동향" 같은)를 씁니다. 형식만 갖추면 거의 항상 '완성'되므로 success=1.00은 변별력이 없습니다. 진짜 승부처는 근거성(grounding) — 리포트의 사실이 실제 자료에서 왔는가, 아니면 모델 머릿속에서 지어낸 것인가입니다. 베이스라인(웹 없음)은 grounding 0%, 개선(웹 검색 도입)은 0→100%로 점프합니다.
두 라더를 한 페이지에 두는 이유는 n=1 결론을 피하기 위해서입니다. 한 영역에서만 보면 "이 도메인 우연 아니냐"는 반박이 가능합니다. 채점법도 과제도 정반대인 두 곳에서 같은 신호가 나오면, 그건 도메인 특성이 아니라 더 일반적인 무언가입니다.
메타패턴(meta-pattern, 여러 실험을 가로질러 반복되는 큰 규칙)을 데이터로 보면 이렇습니다. 채택(promoted)된 개입은 전부 도구·입력 레벨이고(분석: EXP-65·66·68, 리서치: EXP-64), 기각(rejected)된 개입은 전부 프롬프트·구조 추가입니다(분석: EXP-67, 리서치: EXP-57·63·60).
| 실험 | 라더 | 레버 종류 | 판정 |
|---|---|---|---|
| EXP-65/66/68 | 분석 | 도구(web_search)·정제 | promoted |
| EXP-64 | 리서치 | 도구(decoy 출처 강등) | promoted |
| EXP-67 | 분석 | 프롬프트(인용 강제) | rejected |
| EXP-57/63/60 | 리서치 | 프롬프트·토폴로지 교체 | rejected |
특히 같은 목표를 두 방식으로 시도한 짝이 결정적 증거입니다. 리서치 라더에서 '엉뚱한 출처를 덜 인용하게 하라'는 목표를, EXP-63은 "더 신중히 하라"는 프롬프트 한 줄로 시도했다가 pass@1 0.80→0.27로 붕괴했고, EXP-64는 도구 단에서 가짜 후보 출처를 검색 결과에서 강등시켜 pass@1·인용 정확도·지연·토큰을 동시에 개선했습니다(유일한 dominant 케이스). 목표가 같은데 레버만 달랐고, 결과는 정반대였습니다.
이 결론이 성립한 조건을 솔직히 짚어야 합니다. 두 라더 모두 베이스라인이 이미 잘 작동합니다(리서치 pass@1 0.967, 분석은 형식상 success 1.00). 모델 자체의 능력은 충분한데, 빠진 건 '능력'이 아니라 '재료'(최신 사실, 실제 출처)였던 상황입니다. 모델은 시계가 없어서 도구가 없으면 학습 시점(컷오프) 지식을 현재처럼 출력합니다 — 이게 분석 라더 베이스라인의 grounding 0%의 정체입니다. 이런 국면에서 부족한 재료를 채우는 가장 강한 수단은 '더 잘 시키는 말'이 아니라 '재료를 가져올 도구'입니다.
그래서 진단별로 레버를 골라야 합니다.
| 증상 | 맞는 레버 |
|---|---|
| 필요한 사실·근거가 없다 | 도구·검색(grounding) |
| 능력은 있는데 협업 흐름이 깨진다 | 토폴로지(누가 언제 발화하나) |
| 도구·사실은 충분한데 형식·톤이 문제 | 이때는 프롬프트가 지배적일 수 있음 |
프롬프트가 약하다는 게 아니라 프롬프트는 비단조적(non-monotonic)·취약하다는 것입니다 — 좋은 의도의 지시 한 줄이 잘 돌던 흐름(working flow)을 깰 수 있습니다(EXP-63이 그 증거). 반면 토폴로지는 load-bearing(구조를 떠받치는 핵심)이라, 발화 순서 한 줄만 round_robin으로 바꿔도 리서치 라더가 0.967→0.067로 무너졌습니다(EXP-60). 즉 토폴로지는 강력하지만 그만큼 위험한 레버이기도 합니다.
핵심 구분: 우리는 리서치의 0.967과 분석의 1.00을 더하거나 평균 내지 않습니다. 그건 정말로 사과+오렌지일 겁니다. 대신 각 라더 안에서 "어떤 종류의 개입이 채택되고 어떤 게 기각됐나"라는 방향(부호)만 뽑아, 두 방향이 같은지를 봅니다. 채점 단위가 달라도 방향은 단위가 없으므로 비교가 정당합니다.
오히려 채점법이 다른 게 장점입니다. 만약 두 라더가 채점법까지 똑같았다면, 패턴이 채점법의 부산물일 수 있다는 의심이 남습니다. exact-match로 채점하는 리서치 라더와 grounding으로 채점하는 분석 라더가 둘 다 '도구는 채택, 프롬프트 추가는 기각'을 가리킨다면, 그 패턴은 특정 채점법의 인공물(artifact)일 수 없습니다. 서로 다른 두 측정기가 같은 눈금을 가리킨 셈입니다.
이 안전장치를 더 든든하게 하는 가드레일도 있습니다.
일반 챗봇은 질문 하나에 답 하나를 내놓고 끝납니다. 자기가 아는 것(학습한 지식)만으로 한 번에 대답하죠. 마치 '닫힌 책으로 보는 시험'과 같습니다 — 책을 다시 펴거나 검색할 수 없고, 기억에만 의존합니다.
AI 에이전트(agent)는 여기에 두 가지가 더 붙습니다.
이 프로젝트에서 그 차이가 숫자로 드러납니다. 도구가 없는 base 팀(v2-analyst-deep)은 기억만으로 리포트를 쓰고 인용 출처가 0개입니다. 도구가 있는 개선 팀(v6-analyst-web)은 웹 검색을 5회 하고 출처 URL을 6개 붙입니다. 같은 모델·같은 4명·같은 순서인데 '검색이라는 도구 하나'를 줬느냐 아니냐만 다릅니다 — 그게 에이전트와 챗봇의 본질적 차이를 보여줍니다.
한 개의 AI에게 '조사도 하고, 글도 쓰고, 동시에 틀린 데 없나 검사도 해라'라고 시키면 산만해지기 쉽습니다. 사람 회사로 비유하면, 기획·실무·정리·검수를 한 사람이 동시에 하는 셈이죠. MAS는 이걸 여러 전문가로 나눕니다.
이 프로젝트의 두 팀은 모두 4명으로 구성됩니다 — planner → analyst → synthesizer → critic (기획자 → 분석가 → 정리자 → 검수자). 각자 한 가지 일에만 집중합니다.
critic이 '이 글이 기준을 통과했나'를 따져서 통과(APPROVE)시키거나 한 번 되돌립니다.0.967 → 0.067로 붕괴했습니다. 여러 명을 어떻게 '엮느냐'가 그 자체로 성능을 좌우하는(load-bearing) 부품이라는 뜻입니다. 즉 MAS는 강력하지만, 구성하기에 따라 깨지기도 쉽습니다.여러 에이전트를 직접 손으로 엮으려면 '누가 먼저 말하나, 도구 결과를 어떻게 넘기나, 언제 멈추나' 같은 배관(plumbing) 작업이 잔뜩 필요합니다. AutoGen은 이 배관을 이미 만들어 둔 '레고 부품 상자'라고 보면 됩니다. 부품을 조립하면 팀이 됩니다.
이 프로젝트가 쓰는 주요 AutoGen 부품(1급 객체)은:
OpenAIChatCompletionClient — 어떤 LLM을 어떤 설정(seed·temperature)으로 부를지 정하는 모델 연결부AssistantAgent — 지시문(system_message)을 가진 에이전트 한 명FunctionTool — 평범한 파이썬 함수를 도구로 자동 변환(함수의 독스트링이 곧 '도구 설명'이 됨)RoundRobinGroupChat / SelectorGroupChat — 여러 에이전트를 한 팀으로 묶는 두 가지 협업 방식*Termination — 대화를 언제 끝낼지 정하는 종료 조건AutoGen의 또 다른 장점은 만든 팀을 dump_component()로 '실행 가능한 JSON'으로 통째로 내보낼 수 있다는 점입니다. 이 JSON에는 모델 설정·도구 소스코드·플래그가 들어 있어 다른 환경(예: AutoGen Studio)으로 그대로 불러올(load) 수 있습니다.
두 팀(base·개선) 모두 이 4명을 같은 순서로 씁니다. 차이는 오직 analyst가 검색 도구를 갖느냐뿐입니다.
| 역할 | 하는 일 |
|---|---|
| planner (기획자) | 큰 주제를 5~6개 소주제(시장·기술·경쟁·공급망·리스크·전망)로 쪼갭니다. 개선 팀에서는 여기에 오늘 날짜(2026-06-14)를 끼워넣어 '학습 시점이 아니라 지금 기준으로 보라'고 알려줍니다. |
| analyst (분석가) | 각 소주제를 구체적 사실·수치로 채웁니다. base는 기억(학습지식)만으로, 개선은 web_search 도구로 실제 검색해 날짜·수치를 확인하고 각 사실에 출처 URL을 답니다. |
| synthesizer (정리자) | 흩어진 분석을 하나의 마크다운 리포트(FINAL ANSWER)로 묶습니다. 개선 팀은 중복 출처를 정리해 핵심 5~8개만 '## 출처'로 인용하고 '기준 시점'을 명시합니다. |
| critic (검수자) | 완성된 글이 기준(구체성·균형·구조, 개선 팀은 '출처 URL ≥3개')을 통과하는지 점검해 통과(APPROVE)시키거나 한 번 되돌립니다. |
핵심은 도구를 가진 건 analyst 한 명뿐이라는 점입니다. planner·synthesizer·critic은 두 팀 모두 도구가 없습니다. 그래서 '검색 능력이 결과를 얼마나 바꾸나'를 깨끗하게 측정할 수 있습니다.
흔한 오해부터 풀자면, LLM은 글자를 생성하는 모델이라 스스로 웹에 접속하거나 파일을 열 수 없습니다. 대신 이렇게 협력합니다 — 식당에서 손님(LLM)이 메뉴(도구 목록)를 보고 주문서(호출 요청)를 쓰면, 주방(프레임워크)이 요리(실행)해서 내오는 것과 같습니다.
web_search(query) 같은 함수를 쓸 수 있다'고 미리 알려줍니다. AutoGen에서는 FunctionTool이 파이썬 함수를 자동으로 감싸고, 그 함수의 독스트링이 곧 도구 설명이 됩니다.web_search(query="...") 같은 구조화된 호출(JSON)을 내놓습니다.이게 가능하려면 모델 설정에 function_calling=True가 켜져 있어야 합니다 — 이 프로젝트가 도구 실험을 할 수 있는 전제 조건입니다.
(검색 결과 없음))가 에러가 아니라 정상 결과(tool_result) 문자열로 저장됐습니다. 그래서 '호출만 세면' 근거 없는 리포트도 100% 근거 있는 것처럼 보일 수 있습니다(자세한 건 grounding 관련 항목 참고).LLM은 글자 그대로의 단어가 아니라 '토큰'이라는 잘게 쪼갠 조각으로 읽고 씁니다. 영어는 대략 단어 비슷한 단위, 한국어는 그보다 더 잘게 쪼개집니다. 모델이 처리하는 모든 입력 + 출력 글자가 토큰으로 환산됩니다.
토큰이 비용인 이유는 단순합니다 — 모델은 토큰을 하나씩 처리하므로, 많이 읽고 많이 쓸수록 계산량·시간·돈이 늘어납니다. 그래서 '같은 품질이면 토큰이 적은 쪽'이 더 좋은 팀입니다.
이 프로젝트에서 토큰은 핵심 비용 지표입니다. 흥미롭게도 개선 팀이 도구를 더 쓰는데도 토큰은 줄었습니다:
v2-analyst-deep: 29.4k 토큰/run (정확히 29,409)v6-analyst-web: 21.3k 토큰/run (정확히 21,285, −28%)또 하나 중요한 파생 지표가 tokens_per_success(성공 1건당 토큰)입니다. 이건 망가진 실험을 정확히 집어냅니다 — 정답이 적어 분모가 줄고 헛도느라 토큰이 늘면 폭증하죠. 기각된 실험들이 EXP-60 119k, EXP-63 55k, EXP-65 97k로 정확히 잡혔습니다. 즉 토큰은 단순 비용을 넘어 '팀이 헛돌고 있나'를 보여주는 건강 신호이기도 합니다.
temperature는 모델이 다음 토큰을 고를 때 얼마나 모험을 하느냐를 정합니다. 높으면 답이 더 다양·창의적이지만 들쭉날쭉하고, 낮을수록(0에 가까울수록) 매번 거의 같은 안정적 답을 냅니다. 비유하면 '주사위를 얼마나 흔드느냐'입니다.
seed는 그 주사위의 시작점을 고정하는 값입니다. 같은 seed면 같은 무작위 경로를 밟아 결과가 재현됩니다.
이 둘을 고정하는 이유는 공정한 비교 때문입니다. base와 개선 팀을 비교할 때, 만약 매번 답이 무작위로 달라진다면 '개선이 좋아진 건지, 그냥 운이 좋았던 건지' 구분할 수 없습니다. 이 프로젝트는 두 팀 모두 seed 42 · temperature 0.2로 똑같이 묶어(낮은 고정값), 바뀐 변수를 '웹 검색 하나'로만 한정합니다(이른바 1변수 통제). temperature를 정확히 0이 아니라 0.2로 둔 건, 0에 가깝게 낮춰 거의 결정적으로 만들되 모델이 자연스럽게 동작하도록 한 선택입니다.
pass^k(k회 전부 성공)도 봅니다. 실제로 EXP-69 실험에서 pass@1은 0.889인데 pass^k는 0.667로 갈렸습니다 — temperature를 고정해도 남는 '비결정성'이 드러난 자리입니다. 그래서 이 프로젝트의 규칙 중 하나가 '단발(N=1)의 극적인 수치는 노이즈로 보고 믿지 않는다'입니다.이 프로젝트에서 모델 자체는 실험 변수가 아닙니다. 오히려 의도적으로 고정한 '상수'입니다. 두 팀이 똑같이 GLM-4.7을 쓰고, 똑같은 seed 42 · temperature 0.2 설정을 씁니다. 그래야 '팀 구성(도구·협업 방식)의 차이'만 순수하게 측정할 수 있기 때문입니다.
GLM-4.7은 function_calling(도구 호출)을 지원합니다. 모델 설정의 model_info.function_calling=True가 바로 이것을 켜는 부분이고, 이게 켜져 있어야 analyst가 web_search 같은 도구를 부를 수 있습니다. 도구 호출을 지원하지 않는 모델이었다면 이 프로젝트의 핵심 실험(웹 검색 도구 추가) 자체가 불가능했을 겁니다.
AutoGen에서는 이 모델을 OpenAIChatCompletionClient라는 연결부로 부릅니다 — 이름에 'OpenAI'가 들어가지만 이건 'OpenAI 호환 API 규격'을 따른다는 뜻일 뿐, 실제로는 GLM 엔드포인트에 연결됩니다. 모델을 바꾸지 않고 팀만 바꿔서 측정하는 게 이 프로젝트의 설계 철학입니다.
이 프로젝트는 모든 실행 과정을 PostgreSQL DB에 기록합니다. 그 기록을 이해하려면 다섯 단위만 알면 됩니다.
| 단위 | 뜻 | 비유 |
|---|---|---|
| batch | 여러 run을 묶은 한 번의 실험 단위(예: 3개 주제 × k회). | '시험 한 회차 전체' |
| run | 작업(task) 하나를 한 번 실행한 결과. 성공 여부·토큰·지연 등 결과/조건이 여기 기록됩니다. | '학생 한 명의 답안지 한 장' |
| turn | 팀 안에서 에이전트가 한 번 발언하는 것. 예: base는 약 5턴, 개선도 약 5턴. | '회의에서 한 사람이 한 번 말하는 것' |
| events | 한 run 안에서 일어난 모든 사건(발언·도구 호출·도구 결과)의 로그. | '회의록의 모든 줄' |
| seq | events에 붙는 순번. 사건의 순서를 보존합니다. | '회의록 줄 번호' |
DB 구조로 말하면 runs 테이블 = 실행 1회의 결과/조건, events 테이블 = 그 run 안의 모든 사건(seq 순)입니다. 이 분리 덕분에 '결과가 왜 그렇게 나왔나'를 사건 단위로 되짚을 수 있습니다. 특히 근거성(grounding)을 LLM의 자기보고가 아니라 events의 실제 도구 호출 기록으로 세는 것이 이 프로젝트의 원칙입니다.
여러 에이전트가 한 팀이 되면 '다음 차례를 누구에게 줄까'를 정해야 합니다. 그 규칙이 토폴로지이고, AutoGen에는 두 방식이 있습니다.
| RoundRobin | Selector | |
|---|---|---|
| 방식 | 정해진 순서로 차례차례(planner→analyst→synthesizer→critic→다시 처음) | 매 턴 LLM이 '지금은 누가 말할 차례'인지 판단해 지목 |
| 비유 | 자리 순서대로 돌아가며 발표 | 사회자가 그때그때 적임자를 지명 |
| 이 프로젝트 | 분석 라더(리포트 작성)가 사용 | 리서치 라더(멀티홉 사실 QA)가 사용 |
RoundRobin은 단순하고 예측 가능합니다. Selector는 더 유연해서, 단계마다 흐름이 달라지는 작업(여러 단계를 거쳐 답을 추적하는 멀티홉 QA)에 강합니다.
pass@1 0.967 → 0.067로 붕괴시켰습니다. 고정 순환이 멀티홉 흐름을 깨서, critic이 답이 채 나오기도 전에 조기 APPROVE하고 환각이 통과돼 버린 겁니다. 교훈: selector의 '매번 누구를 부를까' 판단은 낭비가 아니라 성능을 떠받치는(load-bearing) 부품이었습니다. 작업 성격에 맞는 토폴로지를 골라야 합니다.에이전트 팀은 스스로 여러 턴을 돕니다. 그러면 '언제 끝났다고 볼 것인가'를 정해줘야 합니다. 안 그러면 영원히 돌거나, 반대로 너무 일찍 멈출 수 있습니다. AutoGen은 두 종류의 종료조건을 함께 씁니다(둘 중 먼저 걸리는 쪽이 발동):
critic 게이트는 양날의 칼입니다. 너무 빡빡하면 팀이 통과 못 해 헛돌고(토큰·시간 낭비), 너무 헐겁거나 흐름이 깨지면 critic이 답이 채 나오기도 전에 조기 APPROVE해 환각이 통과됩니다 — EXP-60 붕괴가 정확히 이 경우였습니다.
reflect_on_tool_use)을 False로 둔 것도 중요한 안정장치입니다. True면 '검색 결과 없음'에 빈 응답을 만들다 RuntimeError로 배치 전체가 죽을 수 있어서, 도구를 쓰는 개선 팀에선 이 설정이 없으면 안 되는(load-bearing) 부품입니다.먼저 용어부터 쉽게 풀겠습니다. LLM(Large Language Model, 거대 언어모델)은 글을 읽고 쓰는 AI의 '두뇌'입니다. 여기서는 z.ai의 GLM-4.7을 씁니다. 프레임워크(framework)는 그 두뇌 여러 개를 한 팀으로 묶어 순서대로 일 시키는 '뼈대 도구'인데, 여기서는 AutoGen(마이크로소프트가 만든 멀티에이전트 라이브러리)을 씁니다. 비유하자면 LLM은 '직원', 프레임워크는 '회의 진행 규칙'입니다.
고른 이유는 세 가지입니다.
function_calling=True 능력이 전제입니다.dump_component()). 덕분에 '프롬프트 한 글자만 바뀌어도 새 팀으로 강제'하는 재현성 규칙을 코드로 박을 수 있었습니다.이 페이지가 발견한 두 메타패턴(meta-pattern, 여러 실험을 가로지르는 큰 규칙)을 먼저 정리하면:
web_search 도구 하나가 만들었습니다.왜 방향은 옮겨갈 가능성이 높은가: 이 패턴들은 GLM-4.7의 특정 버릇이라기보다, 'LLM에게 더 시키는 것(프롬프트)은 비단조적·취약하고, 할 수 있는 것을 늘려주는 것(도구)이 더 신뢰할 레버다'라는 일반적 성질에 가깝습니다. 다른 모델이라도 빈손으로는 최신 사실을 못 만들어내므로, '근거를 주면 환각이 준다'는 방향은 비슷하게 나올 공산이 큽니다.
왜 숫자는 못 옮기는가: 이 페이지 스스로 단서를 답니다 — "이건 이 regime(체제)의 결론"이라고요. 여기서 regime이란 이미 베이스라인이 매우 높고(예: 리서치 pass@1 0.967), 정답을 좌우하는 게 근거성인 상황을 말합니다. 베이스라인이 낮은 모델이나 다른 과제라면, 프롬프트 개입이 오히려 큰 도움이 될 수도 있습니다. 토폴로지 취약성도 마찬가지로, AutoGen의 selector(매 턴 LLM이 다음 발언자를 고름)와 round_robin(고정 순환)의 차이에서 나온 현상이라 다른 프레임워크에선 다른 형태로 나타날 수 있습니다.
여기서 run은 '한 번의 실행'이고, team은 '에이전트 팀의 설계도'입니다. 이 둘을 일부러 분리해 놓은 게 측정을 지키는 비결입니다.
runs에 적힙니다. 그래서 "모델을 바꿔 다시 돌렸다"는 사실이 데이터에 그대로 남고, 비교가 가능해집니다. seed·temperature가 안 적힌 실행은 아예 비교 불능으로 취급합니다.prompt_bundle_hash를 계산합니다. 프롬프트가 한 글자라도 바뀌면 해시가 달라지고, 같은 team_id인데 다른 해시가 들어오면 시스템이 즉시 에러로 멈춥니다(재현성 가드, AC9). '몰래 두 가지를 동시에 바꾸는' 사고를 원천 차단합니다.teams/*.py 한 파일에만 격리되고, 바꾸지 않는 부분(러너·로거·채점기·DB)은 베이스라인에서 그대로 가져다 씁니다. 그래서 '팀만 바꿔 측정'이 성립합니다.비유하자면, 어떤 친구가 "올해 그 산업이 슈퍼사이클이래"라고 말할 때 두 종류가 있습니다. (1) 예전에 어디서 들은 기억으로 말하는 친구, (2) 지금 막 자료를 찾아보고 출처까지 짚어주는 친구. 근거성은 두 번째 친구처럼 작동하는 정도를 말합니다.
이게 중요한 이유는 언어모델(LLM)의 구조 때문입니다. 모델은 학습이 끝난 시점(컷오프) 이후의 일을 모릅니다. 그런데도 마치 현재를 아는 것처럼 술술 대답합니다 — 시계가 없는 사람이 자신 있게 시간을 말하는 것과 같습니다. 그래서 "최신 사실"을 묻는 질문일수록, 모델이 정말 자료를 찾아봤는지(=근거성)가 답의 신뢰도를 좌우합니다.
이 프로젝트에서는 근거성을 grounded%(근거율)라는 숫자로 잽니다. 핵심은 모델이 "검색했어요"라고 말한 걸 믿지 않는다는 것입니다. 대신 실행 기록(events)에 검색 도구를 진짜 호출한 흔적이 남았는지로 독립 계산합니다.
환각이 위험한 이유는 틀린 답이 맞는 답과 똑같이 매끄럽고 자신 있게 나온다는 점입니다. 모델은 "모르겠다"보다 "그럴듯한 무언가"를 만들도록 학습되어 있어서, 근거가 없으면 빈자리를 추측으로 메웁니다.
이 페이지에는 특히 교훈적인 일화가 있습니다(흔히 '57조 환각'이라 부릅니다). 페이지 초안을 쓰던 AI가, 검색 기반 개선 리포트에 적힌 "A사 1분기 영업이익 57조"라는 수치를 보고 자기 학습지식("예전엔 약 17조였다")에 비춰 "이건 틀렸다"고 적었습니다.
그런데 틀린 건 AI였습니다. 2026년 슈퍼사이클로 57조가 실제로 맞았고, 도메인 출처로 재확인되었습니다. 즉 출처 있는 웹 리포트가 옳았고, AI의 '기억에 기반한 검증'이 오히려 환각이었던 것입니다.
RAG(Retrieval-Augmented Generation, 검색증강생성)란, 모델이 답하기 전에 외부 자료를 먼저 '검색해서 가져온 뒤' 그 자료를 근거로 답을 만드는 기법입니다. "기억으로만 답하지 말고, 자료를 찾아 보고 답해라"는 발상이 핵심으로, 이 프로젝트의 근거성 개선과 같은 정신입니다.
이게 이 페이지가 가장 강조하는 함정입니다. 두 정의를 표로 비교하면:
| 구분 | ① 호출 기준 | ② 성공 기준 |
|---|---|---|
| 무엇을 보나 | 검색 도구를 1번이라도 불렀나 | 검색이 실제 결과를 돌려줬나 |
| 출처 | 원장(EXP 기록) | v_analysis_grounded 뷰 (마이그레이션 010) |
| v3·v4·v5 결과 | 100% (v5는 67%) | 0% |
| v6-dedup 결과 | 100% | 100% |
비유하자면 전화를 걸었느냐(①)와 상대가 받아서 정보를 줬느냐(②)의 차이입니다. v3·v4는 검색을 분명히 호출했지만(전화는 걸었지만), 결과가 빈손이라 근거를 한 톨도 못 얻었습니다(상대가 안 받음). 호출만 세면 100%, 실제 결과를 세면 0%인 것입니다.
핵심은 두 가지를 한꺼번에 봐야 한다는 것입니다.
그래서 페이지는 부풀리는 대신 그 함정을 본문(§11 caveat)과 010 뷰의 존재 이유로 드러냅니다. "검색을 했다 ≠ 근거를 얻었다"는 문장을 헤드라인 옆에 붙여 둔 것이 그 증거입니다.
무슨 일이 있었는지 단계로 풀면:
즉 '가짜'였던 부분은 호출만 세는 측정이 만든 착시이지, 개선 시도 자체가 사기였던 건 아닙니다. 오히려 이 프로젝트의 가치는 그 착시를 스스로 적발해, 측정 정의를 ②성공 기준으로 한 단계 올리고(010 뷰), 백엔드를 키 기반 실검색(Firecrawl)으로 교체하는 운영 결론까지 낸 점입니다.
이 프로젝트의 검색 도구(web_tool.py)는 런타임에 두 갈래로 갈립니다:
EVAL_FIRECRAWL_API_KEY로 정식 검색 API(/v1/search)를 호출합니다. 안정적으로 실제 결과를 돌려줍니다.왜 이게 결과를 좌우하느냐 — 근거성의 성공 여부는 결국 검색이 실제 텍스트를 돌려줬는가에 달려 있기 때문입니다. 백엔드가 빈손이면 모델은 받은 근거가 없어, 다시 기억으로 답하거나(환각 위험), critic이 "출처 부족"으로 반려해 재검색 루프가 돌면서 토큰·지연만 폭증합니다(v3는 토큰 3.3배).
핵심 위험은 모델의 자기보고입니다. 모델은 시계가 없어, 도구가 없어도 학습 컷오프(2024) 지식을 현재처럼 출력하고, 심지어 그럴듯한 URL을 텍스트에 박아 넣을 수도 있습니다. 그래서 "리포트에 출처 줄이 몇 개냐"로 세면 — 검색 안 하고 지어낸 출처도 근거로 잡힐 수 있습니다.
반면 events는 시스템이 자동으로 남기는 실행 로그입니다. analyst가 web_search를 실제로 호출하면 그 호출이 seq 순서대로 events에 기록됩니다. 모델이 거짓말을 해도 events는 바뀌지 않습니다 — 이게 견고한 신호(robust signal)입니다.
비유가 가장 빠릅니다. 도서관에 갔다(검색을 했다)고 해서 필요한 책을 찾았다(근거를 얻었다)는 보장은 없습니다. 문이 잠겨 있었거나(차단), 책이 대출 중이었거나(빈손) 하면, 출입 기록은 남지만 손에 든 자료는 없습니다.
이 프로젝트에서 정확히 그 일이 있었습니다:
그래서 평가 정의를 둘로 나눴습니다(앞의 '두 개의 grounded%'). 만약 이 둘을 구분하지 않고 "검색했으니 근거 있다"고 처리하면, 실제로는 기억으로 채운 환각 리포트가 grounded=100%로 둔갑합니다. 가장 위험한 종류의 측정 오류입니다.
구분이 핵심입니다:
검색 도구 입장에서 "검색했는데 일치 항목이 없음"은 비정상이 아니라 정상적인 빈 결과입니다(그래서 예외를 던지지 않고 문자열을 리턴). 결과적으로 통계에서 도구 에러율(err_rate)은 0%로 깨끗해 보입니다.
왜 함정인가: 에러율 0%만 보면 "검색이 잘 됐다"고 오해합니다. 그러나 그 0% 뒤에 빈손 run이 잔뜩 숨어 있을 수 있습니다. 빈손은 에러가 아니라 정상결과로 분류되니까요. 그래서 err_rate 0%인데 grounding 성공 기준은 0%인 상황이 멀쩡히 성립합니다.
v_analysis_grounded 뷰가 존재합니다. 이 뷰는 단순히 "에러 없었나"가 아니라, events를 실시간으로 뒤져 실제 결과를 반환한 검색이 1건이라도 있었나(n_search_ok ≥ 1)를 따로 셉니다. 즉 '에러 없음'에 속지 않으려고 성공 기준을 별도 축으로 만든 것입니다.먼저 그 일화를 정확히 짚겠습니다. 이 페이지 초안에서 작성자(AI)가 개선 리포트의 "A사 1분기 영업이익 57조"라는 수치를, 자기 학습지식(2025년 기준 약 17조)에 비춰 "틀렸다"고 적었습니다. 그런데 2026년 업황 호조(슈퍼사이클)로 57조가 실제로 맞았고, 도메인 출처로 재확인됐습니다. 즉 틀린 건 출처가 아니라 AI의 기억이었습니다.
여기서 핵심은 우리가 도출한 결론의 정확한 범위입니다. 우리는 "출처가 붙었으니 무조건 참"이라고 결론낸 게 아닙니다. 두 종류의 주장이 충돌했을 때 어느 쪽을 더 신뢰할지를 정한 것입니다.
| 주장의 종류 | 검증 가능성 | 이 사건에서 |
|---|---|---|
| AI 기억(파라미터) | 불가능 — 출처도, 시점도 없음. "왜 그렇게 아는가"를 물을 수 없다 | 학습 컷오프 시점의 옛 일반론(17조) |
| 웹 출처(URL) | 가능 — 클릭해서 원문·날짜를 사람이 직접 대조 | 최신 실측(57조), 재확인됨 |
둘 다 틀릴 수 있다는 점에선 같습니다. 하지만 결정적 차이는 틀렸는지 확인할 방법이 있느냐입니다. 기억 기반 주장은 "내가 그렇게 기억한다"가 끝이라 반박도 검증도 봉쇄됩니다. 출처 기반 주장은 적어도 원문을 열어 날짜와 숫자를 사람이 대조할 수 있습니다. 그래서 우리는 출처를 "정답"으로 모신 게 아니라, 검증을 시작할 수 있는 자리로 삼은 것입니다. 일상 비유로, 누가 "내 기억엔 그래"라고만 하는 것과 "여기 영수증 있어"라고 하는 것은 둘 다 위조 가능하지만, 후자만 대조라는 절차를 허용합니다.
v2-analyst-deep)의 실패 모드입니다 — 출처 0개로 한국 통계 같은 수치를 기억만으로 만들어내(환각), 아무도 대조할 수 없습니다.맞습니다. 출처를 권위로 착각하는 건 실재하는 위험이며, 우리는 이걸 두 가지 서로 다른 환각으로 구분합니다.
이 페이지는 정직하게 아직 2세대를 다 풀지 못했다고 적고 있습니다. 실제로 §6의 EXP-69(inconclusive) 측정에서, 9개 run 중 8/9가 검색을 실제로 했지만 작성자(synthesizer)가 그 URL을 최종 리포트 '## 출처'로 옮긴 건 3/9뿐이었습니다. 즉 "검색했다", "출처를 달았다", "그 출처가 문장을 뒷받침한다"는 세 개의 다른 단계이고, 현재 챔피언(v6)도 뒤쪽 두 단계가 불안정합니다.
그래서 우리 설계에서 '출처 부착'은 신뢰의 종점이 아니라 다음 검증을 가능케 하는 입구입니다. §4에 명시된 파이프라인이 이 점을 분명히 합니다:
비유하면 학원에서 학생들 실력을 비교하려고 같은 모의고사를 보게 하는 것과 같다. 문제가 제각각이면 누가 더 똑똑한지 알 수 없으니, '동일한 시험지'가 공정 비교의 전제가 된다. 우리 시스템은 에이전트 팀(여러 AI가 협업하는 한 묶음)에게 같은 과제를 던지고, 답이 맞았는지(success)와 토큰·지연 같은 비용까지 모두 기록한다.
GAIA는 이런 시험지 중 하나로, '에이전트가 여러 단계를 거쳐 추론하고 도구를 써야만 풀 수 있는 실전형 문제 모음'이다. 단순 상식 퀴즈가 아니라, 자료를 찾고 계산하고 종합해야 답이 나오는 문제들이다. 정답이 사람 손으로 미리 정해져 있어 exact-match(정답 문자열과 정확히 일치하는지)로 자동 채점할 수 있다.
AI는 같은 문제를 같은 조건으로 줘도 매번 답이 조금씩 다를 수 있다. 그래서 한 번만 풀려보고 판단하면 운에 속는다. 그래서 한 과제를 여러 번(k번) 반복시킨 뒤 세 가지로 본다.
k=1(한 번만 돌린 배치)이면 셋이 모두 같은 값이다. 차이는 여러 번 돌렸을 때만 드러난다.
이 페이지의 헤드라인 숫자(토큰/run 29.4k→21.3k, 지연/run 280s→177s 등)는 반도체·전기차배터리·AI가속기GPU 세 가지 서로 다른 주제로 각각 측정한 값을 평균한 것이다(N=3). 본문 리포트(§2)는 그중 반도체 하나만 보여주지만, 메트릭은 세 주제의 평균이다.
왜 한 번이 아닌가? AI 결과는 출렁인다. 한 주제만 보면 그 주제가 마침 쉬웠거나 운이 좋았을 수 있다. 동전을 한 번 던져 앞면이 나왔다고 '이 동전은 항상 앞면'이라 할 수 없는 것과 같다. 여러 번·여러 과제로 평균을 내면 이런 우연이 상쇄돼 '진짜 실력'에 가까워진다.
비유하자면, 두 학생의 실력을 비교할 때 같은 시험지를 둘 다 풀게 해야 공정하다. A는 쉬운 시험, B는 어려운 시험을 봤다면 점수 차이가 실력 때문인지 시험 난이도 때문인지 구분이 안 된다. paired는 '같은 문제를 둘 다 풀게 한 뒤, 문제별로 A와 B를 1:1로 비교'하는 방식이다.
그냥 두 팀의 raw 팀 평균(각자 아무 과제나 풀어 낸 평균)을 비교하면, 한 팀이 우연히 더 쉬운 과제를 더 많이 받았을 때 그게 실력 우위로 둔갑한다. 이걸 난이도 교란(confounding)이라 한다 — 진짜 원인(팀 차이)이 가짜 원인(과제 난이도)에 섞여 흐려지는 것이다.
요리에 비유하면, 소금과 설탕을 동시에 늘리고 맛이 좋아졌다면 둘 중 뭐가 효과였는지 알 수 없다. 한 번에 소금만 바꿔봐야 '소금이 답이었다'고 말할 수 있다. 실험에서 이걸 변수 격리(isolation)라 부른다.
이 프로젝트에서 base(v2-analyst-deep, 웹 없음)와 개선(v6-analyst-web) 팀의 유일한 차이는 '웹 근거 하나'(web_search 도구 + 기준날짜 주입 + 출처 인용)다. 4-에이전트 구성(planner→analyst→synthesizer→critic)도, 토폴로지(고정순환 RoundRobin)도, 모델(z.ai GLM-4.7)도, seed·temperature도 전부 똑같다. 그래서 grounded 0→100% 같은 차이를 '웹 근거 효과'로 깨끗이 귀속할 수 있다.
import한다. 프롬프트가 한 글자라도 바뀌면 prompt_bundle_hash가 달라져 새 team_id가 강제된다. 가드레일 2번: 한 번에 1변수·1파일(나머지는 import).배경: 대시보드가 인용 정확도(cite F1)를 ~0.58(58%)로, 출처 누락을 13건으로 가리켰다. 그래서 '근거가 부실하다'는 가설을 세우고, planner에 'SOURCES 줄 항상 출력', critic에 'SOURCES 없으면 반려' 게이트를 추가했다(EXP-57, v2-research-grounded).
그런데 메트릭 자체가 버그였다. 채점기가 모든 에이전트 이벤트에서 SOURCES를 긁었고, 특히 critic의 추론 텍스트 속에 등장한 'SOURCES' 단어까지 인용으로 오인해 planner가 낸 진짜 인용을 가려버렸다. 채점 범위를 planner의 FINAL ANSWER로 한정하자 베이스라인이 이미 F1 0.97(97%), 누락 1건이었다. 즉 부실하지 않았다. 개입 효과는 0이었고, 덤으로 정답 1건이 회귀했다 → 기각.
메트릭은 '눈금이 잘못된 체중계'일 수 있다. 체중계가 늘 5kg 더 나온다면, 그걸 믿고 굶으면 건강만 해친다. 먼저 '이 저울이 맞나'부터 확인해야 한다. AI 평가에서도 마찬가지로, 숫자가 가리키는 방향으로 달려가기 전에 '이 숫자가 정말 내가 재려던 걸 재고 있나'를 점검해야 한다.
EXP-57이 정확히 이 함정이었다 — 채점기 버그가 만든 가짜 0.58(58%) 점수를 진짜로 믿고 베이스라인을 '고치려다' 효과 0 + 정답 1건 회귀를 얻었다. 진짜 점수는 0.97(97%)이었다. 메트릭을 의심하지 않았다면, 멀쩡한 팀에 불필요한 제약을 영구히 붙일 뻔했다.
temperature(온도)는 모델이 답을 고를 때의 '무작위성 다이얼'이다. 0이면 매번 가장 그럴듯한 토큰을 고르므로 '결정적'일 것 같지만, 현실에서는 다음 이유로 흔들린다.
회계 장부가 '돈이 언제 왜 들고 났나'를 빠짐없이 적듯, 실험 원장(experiments 테이블)은 각 실험(EXP-xx)마다 ① 가설(무엇이 좋아질 거라 봤나) ② 개입(정확히 어디를 한 곳 바꿨나) ③ 측정 변화(DB로 재계산한 숫자) ④ 판정(promoted/rejected/inconclusive)을 한 행으로 남긴다.
왜 DB에 적나? 기억이나 채팅 로그로는 '예전에 그거 효과 있었던 것 같은데'로 흐려진다. 장부에 박아두면 같은 실수를 반복하지 않고, 'EXP-63은 프롬프트로 시도했다 붕괴, EXP-64는 도구로 같은 목표를 달성' 같은 대조 학습이 가능해진다. 이게 채택은 전부 도구레벨·기각은 전부 프롬프트레벨이라는 메타 패턴을 드러내준 토대다.
runs(실행 1회의 조건·결과), events(run 안 모든 이벤트, seq 순), experiments(개선 원장)가 분리돼 있고 불변식은 CHECK 제약으로 강제된다. 개선 루프는 '특이점 발견 → 가설 1개 → 딱 한 곳 개입 → paired 측정 → 판정 → 원장 기록 → promoted면 새 베이스라인'을 반복한다. 핵심은 도구가 아니라 이 규율이다.prompt_bundle_hash는 팀을 이루는 모든 프롬프트를 한 줄 지문(fingerprint)으로 압축한 값이다. 글자 하나만 달라져도 지문이 완전히 달라진다. team_id는 그 팀의 고유 이름표인데, 지문이 바뀌면 새 이름표가 강제로 발급된다.
이게 왜 중요한가? 비교 실험의 생명은 '내가 비교한 두 팀이 정말 내가 의도한 그 두 팀이 맞나'다. 만약 프롬프트를 슬쩍 고치고 같은 team_id로 측정하면, 두 실험 결과를 같은 팀의 것으로 착각해 잘못된 결론을 낸다. 가드는 이 실수를 코드 레벨에서 차단한다 — '같은 team_id + 다른 해시' 조합이 발견되면 즉시 에러로 중단한다(AC9).
먼저 비유부터. 키가 제각각인 세 사람에게 같은 신발을 신겨보고 '평균 키'를 내면 의미가 없다. 하지만 각자에게 깔창을 넣기 전과 후를 재서 '늘어난 키'를 평균 내면, 키가 달라도 '깔창 효과'는 깨끗하게 보인다. 우리가 하는 게 바로 후자다.
이걸 matched A/B(짝지은 비교)라고 부른다. 반도체 주제는 base도 반도체로, 개선도 반도체로 돌린다. 배터리도, GPU도 마찬가지. 그래서 한 주제 안에서 base와 개선은 난이도가 같다(같은 주제니까). 둘의 차이만 빼면 '주제 난이도'라는 방해 요인(교란변수, confounder)이 상쇄된다. 평균 내는 대상은 '각 주제의 점수'가 아니라 '각 주제에서의 개선폭'이다.
표본이 작을 때의 함정은 '작은 차이를 큰 발견으로 착각'하는 것이다. 그래서 이 페이지의 가드레일 7계명 중 ⑤가 'N≥3, 그리고 단발 dramatic은 노이즈로 의심하라'이다. 작은 N에서는 효과 크기(effect size)가 '눈에 띄게 크냐'가 핵심이다.
pass@1 0.889인데 pass^k 0.667이 나왔다. pass@1은 '여러 번 중 한 번이라도 성공', pass^k는 'k번 전부 성공'이다. 둘이 벌어졌다는 건 같은 입력에도 결과가 흔들린다는 증거다. 그래서 EXP-69의 판정은 'promoted'가 아니라 inconclusive(결론 보류)였다. 좋은 평가 시스템은 자기가 'promoted'라 적은 숫자조차 다시 의심한다 — N=3은 출발선이지 결승선이 아니다.용어 먼저. RoundRobin(라운드로빈)은 '정해진 순서로 돌아가며 발언'(planner→analyst→synthesizer→critic→다시 planner...)이다. Selector(셀렉터)는 '매 턴 LLM이 다음 발언자를 직접 고르는' 방식이다. 비유하면, RoundRobin은 고정 좌석 회의(시계방향으로 발언권), Selector는 사회자가 그때그때 발언자를 지명하는 회의다.
왜 한정인가. 이 프로젝트의 과제는 ① 베이스라인이 이미 꽤 잘하고(research pass@1 0.967) ② 승부가 '최신 사실을 가졌나(grounding)'에서 갈렸다. 이런 곳에서는 '없던 정보를 가져다주는 도구'가 가장 큰 레버다. 정보가 없어서 못 푸는 문제에 말투를 다듬는 프롬프트는 효과가 작다.
그래서 결론은 '레버 = 병목'이라는 형태로 일반화해야 안전하다:
| 병목(무엇이 문제인가) | 가장 높은 레버 |
|---|---|
| 필요한 정보·능력이 없다 | 도구·검색·grounding |
| 정보는 있는데 잘못 쓴다(형식·톤·추론) | 프롬프트·역할 설계 |
| 협업이 깨진다 | 토폴로지·종료조건·라우팅 |
| 비용이 문제다 | 제약 제거·dedup·상한 |
먼저 왜 사후 끼워맞춤이 아닌지. tokens_per_success는 '성공 1건당 몇 토큰 썼나' = 총 토큰 ÷ 성공 건수다. 정의상 두 가지에 동시에 반응한다 — 토큰을 많이 쓰면(분자↑) 또는 성공이 적으면(분모↓) 폭증한다. 그래서 '낭비가 큰데 결과는 시원찮은' 개입을 한 숫자로 잡는다. 이건 결과를 보고 만든 게 아니라 비용효율의 자연스러운 정의다.
| 실험 | tokens_per_success | 판정 |
|---|---|---|
| EXP-60(토폴로지 교체) | 119,429 | rejected |
| EXP-65(과검색 v3) | 97,089* | 비용 폭증 |
| EXP-63(disambig 프롬프트) | 54,628 | rejected |
| v1-research(base) | 6,528 | 기준선 |
*EXP-65는 tokens/run 97k(성공은 유지). 기준선 base가 6.5k인 점이 비교 닻이다.
먼저 비유로 풀어보겠습니다. AI 모델은 시계도 인터넷도 없는 방에 갇힌 사람과 같습니다. 아무리 똑똑해도, 그 방에 갇힌 시점(학습 컷오프, 대략 2024년)까지 외운 지식만으로 말합니다. "오늘 무슨 일이 있었냐"고 물으면, 모르면서도 아는 것처럼 그럴듯하게 지어냅니다. 이것을 환각(hallucination)이라고 합니다.
base 팀(웹 없음)은 검색 도구가 없어서 web_search 호출이 0회, 따라서 리포트에 출처(인용 URL)도 0개입니다. 즉 모든 문장이 "기억"에서 나왔고, 그게 맞는지 틀린지 아무도 확인할 수 없습니다.
개선 팀(v6)은 검색 도구를 손에 쥐어줬더니 검색 5회, 출처 6개를 실제로 가져옵니다(헤드라인 KPI 기준). 그래서 헤드라인이 검증불가 → 검증가능으로 바뀝니다.
먼저 흔한 오해부터 짚겠습니다. "도구를 붙이면 단계가 늘어나니 당연히 느리고 비쌀 것"이라 생각하기 쉽습니다. 실제로 처음(EXP-65)엔 그랬습니다 — 토큰이 29.4k에서 97k(3.3배)로 폭증했죠. 검색을 너무 많이 시키고(run당 평균 17.3회), critic이 계속 "다시 찾아봐"라며 재검색 루프를 돌았기 때문입니다.
그런데 최종 챔피언 v6은 base보다도 쌉니다(토큰 29.4k→21.3k, -28% / 지연 280s→177s, -37%). 두 가지 힘이 겹친 결과입니다:
web_search_deduped). 쓸데없는 반복이 사라져 비용이 base 밑으로 떨어집니다.래더(ladder, 사다리)란 한 번에 한 칸씩 변수 하나만 바꿔 "무엇이 효과를 냈는지"를 깨끗이 분리하는 방식입니다. 순서대로 보면(메트릭은 서로 다른 3개 주제의 N=3 평균):
| 단계 | 무엇을 바꿨나 | 결과 | 판정 |
|---|---|---|---|
| EXP-65 | analyst에 web_search 도구 + 오늘 날짜 주입 | grounded 0→100%(호출 기준), 출처 0→4.7. 단 토큰 29.4k→97k(3.3배), 지연 280→602s | 채택 |
| EXP-66 | 과검색·재검색 루프 제거(검색 1라운드) | 토큰 97k→17.8k(-82%), 지연 -65%, 턴 10.3→5.0. grounded 100% 유지 | 채택 |
| EXP-67 | "검색 URL 전부 나열" 강제 | 출처 2.3→15.7(한 task는 37개 덤프), 토큰 +61%, grounded 100→67% | 기각 |
| EXP-68 | 도메인 중복제거 도구 + "핵심 5~8개만" 상한 | 출처 4.7 회복, grounded 100%, 지연 177s(최저) | 채택 |
EXP-65에서 한 일은 단 하나, analyst에게 검색 도구와 "오늘 날짜"를 준 것입니다. 그 효과로 grounded(호출 기준)가 0→100%, 검색 0→17.3회로 뛰었습니다. 그런데 토큰이 29.4k에서 97k(3.3배), 지연이 280s에서 602s로 같이 폭증했죠. 원인은 두 가지입니다:
그래도 왜 채택(promoted)일까요? 래더에서 promoted의 의미는 "완성형"이 아니라 "이전 베이스라인을 넘어선 새 기준점"입니다. EXP-65는 0이었던 grounding을 (호출 기준) 100%로 만든 능력의 첫 획득이라, 비용이 비싸도 "앞으로 다듬을 가치가 있는 새 토대"로 채택됩니다.
동기는 선했습니다. EXP-66에서 출처가 4.7→2.3개로 줄어든 게 아쉬워서, synthesizer(종합 담당)에게 "검색해서 나온 URL을 전부 적어"라고 시킨 거죠. 결과는:
왜 기각(rejected)인가: 래더에서 rejected는 "개입이 순개선이 아니다"라는 뜻입니다. 출처 숫자 하나만 올랐을 뿐, 더 중요한 지표(근거율·비용)가 같이 나빠졌으니 부작용이 이득을 넘었습니다.
먼저 두 용어를 풀겠습니다. 같은 문제를 k번 반복해서 풀게 한 뒤:
pass@1 = 작업당 k회 중 1회 이상 성공한 비율 (운 좋게 한 번이라도 맞으면 OK)pass^k = k회 전부 성공한 비율 (항상 일관되게 맞아야 OK)EXP-69(v6, k=3, n=9)에서 pass@1 = 0.889인데 pass^k = 0.667입니다. 둘의 간격이 신호입니다: 어떤 task는 "3번 중 일부만" 성공했다는 뜻 — 즉 같은 입력·같은 설정(seed·temperature 고정)인데도 답이 흔들리는 비결정성(non-determinism)이 드러난 자리입니다.
여기에 더해 출처 인용은 9번 중 단 3번(cited 3/9)만 리포트에 박혔습니다. 검색은 했는데(grounding 행위는 견고) 그 결과를 본문 인용으로 옮기는 건 비결정적이었던 거죠.
한 주제(반도체)에서 좋았다고 해서 다 좋은 건 아닙니다. 그래서 5개 주제 각각에 base vs 개선을 돌려본 결과입니다(두 숫자는 본문 grounded 횟수 / 출처 인용 횟수입니다).
| 주제 유형 | base → 개선 | 판정 |
|---|---|---|
| 최신 LLM 신제품(컷오프 이후) | 0/0 → 17/6 | 명확한 우위 — 본문·출처 모두 성공 |
| 시점 민감 실적 | 0/0 → 5/0 | 애매 — 본문은 근거 있으나 인용 누락(flaky) |
| 최신 규제 동향 | 0/0 → 6/0 | 애매(flaky) |
| 지역 업황(로컬 출처) | 0/0 → 6/0 | 안전성 우위 — 개선은 정직하게 거부, base는 환각 |
| 시간 안 타는 개념(대조군) | 0/0 → 5/0 | 무승부 — 검색이 무용 |
flaky는 "같은 조건인데 결과가 안정적이지 않다"는 뜻의 일상어입니다. 여기서 핵심은 두 가지가 분리된다는 점입니다:
실측이 이를 적나라하게 보여줍니다. EXP-69(k=3, n=9)에서 검색 도구는 9번 중 8번 호출됐지만, 리포트 인용은 9번 중 3번(3/9)만 박혔습니다(이 배치는 DDG 폴백이라 실제 검색 성공 자체도 약 33%로 낮았습니다 — 호출과 성공은 또 다른 문제입니다). 5주제 일반화에서도 5개 주제 중 출처 인용에 성공한 건 최신 LLM 주제 단 하나뿐이었습니다.
상황은 이렇습니다. 지역 업황 주제는 로컬 출처가 빈약해서 검색이 잘 안 됐습니다. 두 팀의 반응이 갈렸습니다:
왜 거부가 더 나은가: 의사결정에 쓰일 리포트라면, "틀린 답을 자신 있게 주는 것"이 "모른다고 인정하는 것"보다 훨씬 위험합니다. 틀린 숫자를 믿고 결정을 내리면 피해가 크지만, "데이터 부족"이라고 하면 사람이 직접 더 찾아보면 됩니다.
AI 모델은 문장을 자연스럽게 만드는 데 매우 뛰어납니다. 그래서 base 리포트도 전문가가 쓴 것처럼 유창합니다. 함정이 바로 여기 있습니다 — 유창함은 정확성의 증거가 아닙니다. 모델은 사실을 모를 때도 똑같이 자신 있고 매끄럽게 씁니다.
이 페이지에 실제로 그 위험을 보여주는 일화가 있습니다. 이 글 초안에서 작성자(AI)가 개선 리포트의 "A사 1분기 영업이익 57조"를 자기 학습 기억(2025년 약 17조)에 비춰 "틀렸다"고 적었습니다. 그런데 틀린 건 AI 쪽이었습니다 — 2026년 업황 슈퍼사이클로 57조가 실제로 맞았고, 도메인 출처로 재확인됐습니다. 즉 출처 있는 웹 리포트가 옳았고, 기억에 기댄 "검증"이 도리어 환각이었습니다.
이 직관은 매우 흔합니다 — "AI를 더 잘 시키려면 지시문(프롬프트)을 잘 쓰면 되지". 하지만 실측 메타패턴은 다릅니다:
| 레버 종류 | 실험 | 결과 |
|---|---|---|
| 도구/입력 레벨 | EXP-64·65·66·68 | 전부 채택(promoted) |
| 프롬프트/구조 추가 | EXP-57·63·67 | 전부 기각(rejected) |
가장 큰 단일 도약(grounded 0→100%)은 프롬프트 한 줄이 아니라 검색 도구(web_search) 하나가 만들었습니다. 반대로 프롬프트를 만진 시도들은 자주 무너졌습니다:
두 가지를 분리해서 보면 오해가 풀립니다. 하나는 '사람이 직접 읽고 질을 느끼는 예시'이고, 다른 하나는 '그 차이가 통계적으로 믿을 만한지 재는 숫자'입니다. 한 화면에 긴 리포트 3개를 나란히 붙이면 아무도 안 읽습니다. 그래서 읽기용으로는 대표 1개(반도체)만 펼쳤습니다.
반대로 숫자는 1개 주제(N=1)로만 내면 '운'일 수 있습니다. 그래서 성격이 다른 세 주제 — 반도체 + 전기차 배터리 + AI 가속기(GPU) — 를 각각 돌려 평균을 냈습니다. 이것이 N=3의 뜻입니다. 비유하면, 동전 한 번 던져 앞면이 나왔다고 '이 동전은 앞면이 잘 나온다'고 말하면 우연이지만, 세 번 던져 평균을 보면 더 믿을 만한 것과 같습니다.
이 페이지에서 가장 큰 함정이 바로 이 두 개의 grounded%입니다.
왜 갈릴까요. 키 없는 폴백 검색이 차단·rate-limit에 걸리면 '호출은 됐지만 빈손'(검색 결과 없음)을 돌려줍니다. 호출은 셌으니 호출기준 100%, 그러나 내용이 없으니 성공기준은 떨어집니다. '검색을 했다'와 '근거를 얻었다'는 다릅니다.
상태=active/inactive 메타데이터만 보고 비활성 것을 순위에서 내립니다. 정답 키를 본 적이 없으므로 과적합이 아니라 일반적인 검색 위생(hygiene)입니다.실험 환경부터: hard 코퍼스는 문서 60개 중 30개가 decoy — 같은 이름을 쓰는 '비활성 동명 엔티티'(예: 폐업했거나 더는 쓰이지 않는 동명의 항목)입니다. 멀티홉 질문에서 검색이 이 비활성 항목을 윗줄에 올리면 planner가 헷갈려 답을 망칩니다. base(v1)가 hard에서 pass@1 0.80에 머문 원인이 이것입니다.
EXP-64의 처방은 demote_inactive=True 한 줄입니다. 검색 도구가 결과를 정렬할 때 활성 여부 플래그를 보고 비활성 항목의 순위를 내립니다. 핵심은 — 이 규칙이 보는 것은 '이 항목이 살아있는 엔티티인가'이지 '이게 정답인가'가 아니라는 점입니다. 정답이 무엇인지 코드는 모릅니다.
과적합이 아닌 이유를 한 줄로: 정답에 맞춰 검색을 조작하면 '활성·정답인 decoy'까지 강등돼 점수가 떨어졌을 겁니다. 실제로는 '죽은 항목은 안 보여준다'는 도메인 무관한 규칙이라, 다른 코퍼스에서도 같은 위생이 통합니다.
두 구간을 나눠 보세요.
v6은 도구에 도메인 중복제거(web_search_deduped)를 넣고 synthesizer에 '핵심 5~8개만'이라는 상한을 둬 출처를 4.7개로 회복시켰습니다. 같은 '근거 있는 리포트'라는 목표를 더 싸고(토큰 21k) 더 빠르게(지연 177s, 최저) 달성했습니다.
궤적을 보면 v6은 우연히 떨어진 게 아니라 매 단계가 직전 단계의 측정에서 나온 가설입니다.
| 단계 | 무슨 일 | 판정 |
|---|---|---|
| base(v2) | 웹 없음. turns 5·토큰 29k·grounded 0%·출처 0 | 기준 |
| v3(EXP-65) | web_search 첫 투입 → grounded 0→100%. 하지만 과검색으로 토큰 97k·turns 10.3 | 채택(능력) |
| v4(EXP-66) | 'grounding은 도구를 한 번 쓰면 달성, 검색 횟수는 비용만'이라는 측정 → 검색 17→5.7로 줄임. 토큰 97k→17.8k(−82%), grounded 100% 유지 | 채택(비용) |
| v5(EXP-67) | 출처를 전부 나열 강제 → 15.7개 덤프, grounded 100→67% | 기각 |
| v6(EXP-68) | 도구 dedup + '핵심 5~8개' 상한 → 출처 4.7 회복, 토큰 21k, 지연 177s(최저) | 채택(질) |
v3의 '토큰 3배'는 막다른 길이 아니라 진단 데이터였습니다. '왜 비쌌나'를 events 로그로 본 결과 '과검색·재검색 루프'가 범인이라는 게 나왔고, v4는 그걸 정확히 제거했습니다. 즉 v3→v4의 −82%는 운이 아니라 측정이 가리킨 곳을 고친 것입니다.
폐쇄망(air-gapped)이란 인터넷과 물리적으로 끊긴 망을 말합니다. 이 프로젝트는 운영 환경이 폐쇄망이라는 전제로 만들어졌습니다. 그래서 런타임 코드는 외부에 직접 손을 뻗지 않습니다. 손을 뻗는 곳은 딱 두 군데뿐이고, 둘 다 환경변수로 갈아끼울 수 있습니다.
EVAL_LLM_BASE_URL)로 정해집니다. 이 비교 페이지는 외부 GLM-4.7을 썼지만, 폐쇄망에서는 이 값을 사내 vLLM 같은 내부 모델 서버 주소로 바꾸면 됩니다. 코드를 고칠 필요가 없습니다.web_search 한 함수에만 외부 의존이 모입니다. 이걸 사내 검색/RAG로 바꾸는 방법은 별도 질문에서 자세히 다룹니다.요약하면, 폐쇄망은 이 시스템에 '특수 케이스'가 아니라 '기본 가정'입니다. 외부망에서만 해야 하는 일은 검색 도구의 백엔드 교체뿐이고, 그조차 함수 하나로 격리돼 있습니다.
config는 '팀을 어떻게 조립할지'를 적은 설계도입니다. 이걸 그대로 로드하면 에이전트 구성, 프롬프트, 토폴로지(고정순환 RoundRobin), seed=42, temperature=0.2까지 동일하게 재현됩니다. 그래서 구성의 재현성은 보장됩니다.
하지만 측정 숫자는 다음 세 가지에 묶여 있어서 환경이 다르면 달라집니다.
같은 팀을 두 가지 포맷으로 저장합니다. 비유하면, 네이티브 JSON은 '조립 가능한 레고 설명서'이고 config_full은 '완성품 사진과 부품 목록'입니다.
| 구분 | 네이티브 (dump_component) | 스냅샷 (config_full) |
|---|---|---|
| 용도 | 실행 가능 — 다시 로드/임포트 | 관측용 — 사람이 읽기 |
| 모델 설정(model_client) | 인라인 포함(주소·seed·temperature) | 없음 → 그래서 로드 불가 |
| 도구 코드 | 함수 소스코드까지 통째로 | 도구의 impl_sha(지문)만 |
| 담은 것 | 실행에 필요한 모든 것 | 프롬프트·도구·종료조건·토폴로지 |
네이티브 JSON은 한 명령으로 만들어지고, 저장 직전에 api_key를 마스킹(***)하고 프롬프트에 박힌 날짜를 동적화합니다. 이 파일은 그대로 다시 로드하거나 AutoGen Studio로 임포트해 돌릴 수 있습니다.
AutoGen Studio는 멀티에이전트 팀을 GUI에서 불러와 실행해 보는 도구입니다. 우리 네이티브 JSON은 처음부터 Studio 임포트를 염두에 두고 만들어졌습니다. 그 안에는 모델 클라이언트 설정, 각 도구의 파이썬 소스코드, 종료조건, 토폴로지가 모두 들어 있어서 자체완결적(self-contained)입니다.
핵심 설계 포인트는 검색 도구입니다. 도구 함수가 표준 라이브러리(urllib)만 쓰도록 만들어, 함수 소스코드가 통째로 JSON에 직렬화돼도 깨지지 않습니다. 외부 SDK에 의존했다면 Studio로 옮겼을 때 임포트가 실패했을 겁니다.
요컨대 '임포트해서 돌리기'는 의도된 기능이고, 막히는 지점은 거의 키와 망 정책뿐입니다.
이 시스템은 이미 검색 백엔드를 '런타임 분기'로 다룹니다. 키가 있으면 키 기반 실검색(Firecrawl)을, 없으면 무료 폴백(DuckDuckGo)을 씁니다. 사내 검색/RAG를 추가하는 건 이 분기에 한 갈래를 더 얹는 일입니다.
지켜야 할 계약(contract)은 단순합니다.
이 출력 형식만 맞추면, 도메인 중복제거 래퍼(web_search_deduped)나 채점 로직을 건드릴 필요가 없습니다. 사내 RAG라면 'URL' 자리에 사내 문서 ID나 사내 위키 링크를 넣으면 됩니다.
이 페이지의 사례는 반도체였지만, 시스템은 도메인 중립적으로 설계됐습니다. 개선/실험은 팀 정의 파일(teams/*.py) 한 곳에 격리되고, 그 바깥(러너·로거·채점·DB)은 팀과 무관하게 불변입니다. 그래서 도메인을 바꿔도 측정 인프라는 재사용됩니다.
이 시스템은 모든 토큰을 events 테이블에 누적해 기록하므로 run마다 정확한 토큰 수를 알 수 있습니다. 분석 라더의 run당 토큰은 다음과 같습니다.
| 팀 | 토큰/run | 지연/run | 비고 |
|---|---|---|---|
| base (v2-analyst-deep) | 29,409 | 279.7s | 웹 없음 |
| 개선 챔피언 (v6-analyst-web) | 21,285 | 177.1s | 검색+dedupe |
| 실패한 변형 (v3) | 97,089 | 601.6s | 검색 과다(run당 17.3회) |
눈여겨볼 점은, 검색 도구를 '제대로' 붙인 개선판이 base보다 오히려 토큰이 적다는 것입니다. 도구 레벨에서 중복 검색을 차단하고 검색 횟수를 줄였기 때문입니다(v6은 run당 4.0회). 반대로 프롬프트로 검색을 과하게 밀어붙인 변형(v3은 run당 17.3회)은 토큰이 97k까지 치솟았습니다.
최소 셋업은 사내 PostgreSQL(trace DB), 내부 모델 엔드포인트, 검색/RAG 백엔드, 그리고 분석 주제 N>=3입니다. 절차는 다음과 같습니다.
이 프로젝트의 한 사이클(가설→딱 한 곳 수정→재실행→판정)은 대략 이렇게 구성됩니다. 비유하자면 요리 레시피를 한 줄만 바꿔보고 같은 손님 3명에게 다시 내보는 것과 같습니다.
teams/*.py 한 파일에 격리돼 보통 수십 분~몇 시간입니다.events·runs에 쌓이고, 사람은 experiments 원장에 판정 한 줄(promoted/rejected + 한 줄 교훈)만 남깁니다.'그냥 손보기'의 함정은 무엇이 효과였는지 영원히 모른다는 점입니다. 여러 곳을 한꺼번에 바꾸면 좋아져도 왜 좋아졌는지, 나빠져도 어디가 범인인지 알 수 없습니다. 이 프로젝트에서 그 위험이 실제로 드러났습니다.
N≥3가 왜 필수냐: 단발 결과는 노이즈입니다. v6를 k=3로 돌렸더니 pass@1 0.889인데 pass^k(3번 다 성공)는 0.667 — 같은 팀도 비결정적이라는 게 드러났습니다. N=1이었다면 우연한 한 번을 '개선'으로 착각했을 자리입니다.
가장 비싼 실수는 '나빠지는 것'이 아니라 나아진 척하는 가짜 신호를 믿는 것입니다. 가짜 방향으로 며칠을 더 달리게 만들기 때문입니다. 이 프로젝트가 그걸 두 번 잡아냈습니다.
tokens_per_success라는 파생 지표는 기각감을 정확히 때렸습니다(붕괴한 실험들에서 55k·97k·119k로 폭증). 즉 원장은 '느낌'이 아니라 숫자로 손절선을 그어줍니다 — 이게 번거로움의 값을 합니다.
모델·seed·temperature를 전부 고정한 채 웹 검색 도구 하나만 더했더니, 근거 없이 쓰던 리포트가 출처를 인용하는 리포트로 바뀌었습니다(grounded 0→100%, 인용 출처 0→6개, 기준시점 없음→2026-06-14). 같은 모델로요. 즉 답을 좌우한 건 '무엇을 시킬까'가 아니라 '무엇을 할 수 있게 할까'였습니다.
다른 LLM 제품들의 정확한 성능을 단정할 수는 없습니다(그건 우리가 통제·측정한 게 아니니까요). 직접 만든 이유는 그들과의 우열 경쟁이 아니라, 네 가지 요구 때문입니다.
v_analysis_grounded)가 잡아냈습니다.요약하면, 외부 제품은 '좋은 답'을 줄 수 있어도 '우리 변경이 왜 효과였는지'는 알려주지 않습니다. 이 시스템의 산출물은 리포트가 아니라 되돌릴 수 있고 비교 가능한 측정입니다.
이 시스템은 멀티에이전트 팀(여러 AI가 협업하는 한 묶음)을 실제로 한 번 돌릴 때마다, 그 과정에서 벌어진 일을 빠짐없이 기록한다. "trace"는 비행기의 블랙박스처럼 나중에 무슨 일이 있었는지 되짚어 볼 수 있는 흔적 기록이라고 생각하면 된다.
흐름은 한 방향으로 흐른다:
cli.py(명령 입력 창, typer) 가 실행을 시작하면 → runner(배치 실행기, run_batch)가 한 묶음의 작업을 돌린다.registry(팀 명부, build_team)가 어떤 팀을 쓸지 골라 조립하고 → teams/*(실제 AI 팀)가 작업을 수행한다.team.run_stream()) → tracing/logger(기록기)가 그걸 받아 events 테이블에 한 줄씩 INSERT(삽입)한다. 토큰·턴·지연 시간도 이때 누적된다.scorer)가 정답 여부를 매기면 → runs 테이블의 success 칸이 UPDATE(갱신)된다.정리하면 데이터의 출처는 "AI가 자기 입으로 보고한 자랑"이 아니라 실제 일어난 사건 그 자체다. 그래서 나중에 "이 팀이 진짜 검색을 했나?" 같은 질문에 추측 없이 답할 수 있다. 이것이 이 프로젝트의 핵심 자산이다.
세 테이블은 시간 단위가 다르다. 비유하자면 runs는 "한 경기의 스코어보드", events는 "그 경기의 분초별 중계 로그", experiments는 "시즌 전체에 걸친 전술 변경 기록부"다.
| 테이블 | 담는 것 | 예시 칸 |
|---|---|---|
| runs | 실행 1회의 조건과 결과 | 성공 여부(success), 모델, seed, temperature, 토큰 합계, 지연(latency) |
| events | 한 run 안의 모든 사건을 순번(seq) 순서로 | 누가 말했나, 어떤 도구를 호출했나(tool_call), 결과(tool_result), 토큰 |
| experiments | 개선 원장(ledger) — 무엇을 바꿔서 어떤 결론이 났나 | 가설(hypothesis), 개입(intervention), 판정(verdict: promoted/rejected/inconclusive), 교훈(lesson) |
왜 이렇게 나눴나. runs만 보면 "이 팀이 성공했다"는 알지만 "왜"는 모른다. 그래서 events로 내려가 "검색을 4번 했고 그중 4번 결과를 받았다" 같은 속사정을 본다(v6-analyst-web의 실측: search/run 4.0, search_ok 4.0). 그리고 "검색 도구를 하나 추가했더니 grounding이 0→100%로 뛰었다"는 식의 변경의 효과는 experiments에 한 줄로 남긴다. 이 장부 덕분에 채택(promoted)은 전부 도구·입력 레벨, 기각(rejected)은 전부 프롬프트·구조 추가였다는 메타패턴까지 데이터로 읽힌다.
관측성(observability)은 어려운 말 같지만 뜻은 단순하다: 겉으로 안 보이는 내부 상태를, 밖에 남은 기록만으로 알아낼 수 있는 정도를 말한다. 자동차 계기판이 엔진 속을 숫자로 보여주는 것과 같다.
이게 왜 결정적인지는 이 프로젝트의 가장 큰 함정이 잘 보여준다. "근거성(grounding)", 즉 리포트가 실제 웹 근거에 발을 딛고 있는지를 두 방식으로 잴 수 있다:
(검색 결과 없음))으로 돌아와도 "grounded 100%"로 둔갑한다. 환각 리포트가 합격 도장을 받는다.v_analysis_grounded, 마이그레이션 010).실제로 분석 라더에서 v3·v4·v5는 호출기준 grounded는 100%였지만 성공기준 grounded는 0%였다(v3=100/0, v4=100/0, v5=67/0). 호출만 셌다면 셋 다 통과시켰을 것이다. v6만 호출·성공 둘 다 100/100이었다. 빠짐없는 로깅이 있어야 이 차이가 드러난다.
꼬리(기록)가 몸통(평가)을 흔들면 안 된다는 원칙이다. 비유하자면 경기 중계 카메라 하나가 고장 나도 경기는 계속 진행되어야 하는 것과 같다. 만약 이벤트 한 줄을 DB에 넣다 실패했다고 배치 전체를 중단시키면, 비싸게 돌린 수십 개의 실행 결과를 통째로 날리게 된다.
코드에서도 그대로 구현돼 있다. logger.py의 _safe_insert가 INSERT(삽입)를 try/except로 감싸고, 실패하면 예외를 위로 던지는 대신 경고 로그를 남긴 뒤 1(누락 카운트)을 반환하고 진행한다. 호출 루프는 이 반환값을 missed_inserts(놓친 삽입 수)에 더해 나간다. 실제로 누적 변수 missed_inserts: int = 0이 logger에 정의돼 있다.
그렇다면 무엇이 배치를 멈출 자격이 있나. 오직 DB 연결 자체의 소실뿐이다. 개별 줄 하나가 안 들어가는 것과, 데이터베이스 자체에 말을 걸 수 없는 것은 차원이 다른 사고이기 때문이다.
logger.py)에서 민감한 키 값을 모두 ***로 가린다.마스킹(masking)은 비밀 값을 가려서 안 보이게 하는 것이다. 영수증에서 카드번호 가운데를 별표로 지우는 것과 같다. 위험은 이렇다: AI가 도구를 호출할 때 넘긴 인자(tool_args) 안에 비밀번호나 API 키가 섞여 있을 수 있는데, 그게 그대로 DB나 로그에 남으면 유출 사고가 된다.
이 시스템의 안전장치는 단일 지점(single point) 설계다. logger.py 안의 _mask_secrets 함수 하나가, 저장 직전에 인자 객체(dict·list)를 재귀적으로 훑으면서 키 이름이 다음 중 하나에 걸리면 그 값을 ***로 바꾼다:
password · token · secret · api_key · authorization (대소문자 무관)실제 코드에 정규식 (password|token|secret|api_key|authorization)이 있고, 딕셔너리·리스트를 끝까지 파고들어 마스킹한다(tool_args JSONB 저장 전 반드시 이 함수를 통과한다). 왜 한 곳에만 두나? 마스킹 로직이 여러 군데 흩어지면 어느 한 곳을 빠뜨려 구멍이 생기기 쉽다. 한 문(門)으로만 데이터가 나가게 하면 그 문 하나만 지키면 된다.
같은 절단·축약 원칙은 본문·도구결과에도 적용된다 — content는 8KB(8192자), tool_result는 2KB(2048자)로 잘라 전체 sha256 hash와 함께 저장해 과도한 적재를 막는다.
teams/*.py 한 파일에만 들어가고, runner·logger·scorer·DB는 팀과 무관하게 그대로 둔다.이것이 "팀만 바꿔서 측정한다"가 성립하는 비결이다. 과학 실험에서 한 번에 변수 하나만 바꾸는 것과 똑같다. 다른 모든 조건이 고정돼 있어야, 결과 차이를 "바꾼 그 하나" 탓으로 돌릴 수 있다.
코드를 직접 확인해 보면 구조가 이를 강제한다:
registry.py)는 각 팀의 조립 함수를 그 팀 자신의 파일에서만 가져온다. 예: from agent_eval.teams.analyst_web_dedup import build_team. 새 팀은 새 파일에 build_team()을 쓰고 한 줄 등록하면 끝이다.analyst_deep.py)와 개선(analyst_web_dedup.py)은 서로 다른 파일이고, 개선 파일은 공통 부품(프롬프트·종료조건 등)을 import로 재사용한다 — 베끼지 않는다(예: analyst_web_dedup.py가 PLANNER_PROMPT·CRITIC_PROMPT를 다른 모듈에서 import). 그래서 "바뀐 것은 검색 도구 하나"가 코드상으로도 명백하다.runner(실행기)·logger(기록기)·scorer(채점기)·DB 스키마는 어떤 팀을 돌리든 건드리지 않는다. 측정 잣대가 실험마다 흔들리면 비교가 무의미해지기 때문이다.다운로드 가능한 의미 단위 diff(차이 비교)가 이를 시각적으로 증명한다 — 예컨대 v4→v6는 "analyst 도구를 web_search → web_search_deduped로, 프롬프트 일부 변경"만 빨갛게 뜨고 나머지 전부가 동일하다. 이게 "1변수 원칙"의 눈에 보이는 증거다.
메트릭(measure, 측정값)마다 "누가 어디서 길어 올리는가"가 다른데, 이걸 구분하는 게 중요하다. 같은 숫자라도 출처가 다르면 의미가 다르기 때문이다.
| 메트릭 | 출처(누가 만드나) | 저장 위치 |
|---|---|---|
| 토큰 / 턴 / 지연 | logger — run_stream()이 흘리는 이벤트의 사용량(models_usage)을 누적 | events 줄별 + runs 합계 |
| 성공 여부(success) | scorer — 정답과 비교(리스트는 순서 보존) | runs.success UPDATE |
| 근거성(grounding) | 인용 채점은 별도 패스(research/grounding→research_grounding), 성공기준 grounding은 뷰 v_analysis_grounded가 events를 실시간 쿼리(저장 아님) | 별도 테이블 / 뷰(010) |
핵심은 grounding이 두 갈래라는 점이다. ① 호출기준(검색을 1번이라도 했나 — EXP 원장 기록)과 ② 성공기준(검색이 실제 결과를 반환했나 — 뷰 010)은 다른 숫자다. v6만 둘 다 100%였고, v3·v4는 호출 100/성공 0, v5는 호출 67/성공 0이었다.
tool_result에 (검색 결과 없음)이라는 문자열로 정상 저장된다. 그래서 도구에러율은 0%인데 성공기준 grounding은 0%가 될 수 있다 — 두 메트릭의 출처가 다르기 때문이다.하나씩 풀어 본다.
migrations/*.sql 파일이 "진실은 여기다"의 유일한 기준이다. 그래서 ORM(코드로 테이블을 흉내 내는 도구)을 금지하고, 스키마 변경은 오직 번호가 늘어나는 새 마이그레이션 파일 추가로만 한다(기존 파일 수정 금지). 코드와 DB가 따로 놀며 어긋나는 사고를 원천 차단한다.먼저 용어부터 쉽게 풀자. reflect_on_tool_use(도구 사용 후 자기 되짚기)는 AutoGen이라는 멀티에이전트 프레임워크의 옵션이다. 켜두면(=True), 에이전트가 도구(여기선 web_search, 즉 웹검색)를 한 번 부른 직후 그 결과를 LLM에게 다시 한 번 먹여 '내가 방금 받은 걸 어떻게 해석하지?'를 한 번 더 생성하게 한다. 마치 자료를 찾은 뒤 곧바로 짧은 소감문을 한 줄 쓰는 셈이다.
문제는 이 모델(z.ai GLM-4.7)이 빈손 검색 결과를 받았을 때다. 키 없는 폴백 검색이 차단/속도제한(rate-limit)에 걸려 (검색 결과 없음)이라는 빈 문자열만 돌아오는 일이 잦은데, 이걸 되짚게 하면 모델이 쓸 말이 없어 빈 응답을 내고, AutoGen은 빈 응답을 받아들이지 못해 RuntimeError로 런 전체를 크래시시킨다. 한 번의 빈 검색이 4-에이전트가 30분 가까이 돌린 작업을 통째로 날리는 것이다.
analyst 한 명. analyst는 검색 결과를 '해석'하지 않고 출처 URL째로 다음 사람에게 넘기기만 한다. 해석·작성은 synthesizer가, '출처가 부족하다/오래됐다'는 검수는 critic이 맡는다. 즉 '검색이 빈손이었다'는 보정은 한 에이전트의 reflection이 아니라 critic의 반려 → 재검색 루프로 팀 차원에서 일어난다.그래서 실제 품질은 손해가 아니다. 채택된 v6-analyst-web 구성은 reflect를 끈 상태로 grounded(근거 확보) 100/100, 출처 4.7개, 토큰 21.3k(-28%), 지연 177s(-37%)를 동시에 달성했다. 되짚기를 끈 대가는 '빈 응답으로 안 죽는 안정성'이고, 보정 능력은 토폴로지가 대신 지고 있다. 빈손이 곧 정직하게 드러나는 것도 장점인데, 한 주제에서 검색이 빈약하자 개선 팀이 환각 대신 '데이터 부족 — 작성 불가'로 정직하게 거부한 사례가 이를 보여준다.
이 구분이 이 프로젝트의 가장 미묘한 함정 중 하나다. 검색 도구가 차단당해 결과가 없을 때, 도구는 예외를 던지지 않고 (검색 결과 없음)이라는 문자열을 정상 반환한다. 그래서 trace DB(모든 이벤트를 기록하는 PostgreSQL 원장)에는 tool_error가 아니라 tool_result로 남는다. 도구 에러율은 전 구간 0%인데도 근거 확보는 실패하는 역설이 여기서 생긴다.
'검색을 했다 ≠ 근거를 얻었다'가 핵심이다. 호출만 세면 빈손 위에 지어진 환각 리포트가 grounded=100%로 둔갑한다. 그래서 reflect를 켜서 빈손을 '되짚게' 만들면, 모델이 할 말이 없어 빈 응답 → 크래시로 이어지는 것이지, 자동 재시도로 이어지지 않는다. 재검색은 reflection이 아니라 critic이 '출처 부족'을 반려해 루프를 한 바퀴 더 돌게 만드는 방식으로 처리한다.
web_tool.py가 이미 런타임 분기라 키가 있으면 실검색, 없으면 폴백으로 자동 전환된다. reflect=False는 '빈손이 나도 안 죽는다'는 안전판이고, 백엔드 교체는 '빈손을 애초에 줄인다'는 근본 처방이다.이 팀의 4단계 역할 분담을 일상 비유로 보면 이렇다.
| 역할 | 하는 일 | 비유 |
|---|---|---|
| planner | 무엇을 검색할지 분해 | 취재 계획서 작성 |
| analyst | web_search로 자료 수집, 출처 URL째 전달 | 도서관에서 자료만 복사해 옴 |
| synthesizer | 모은 자료로 구조화된 최종 리포트 작성 | 기자가 기사 집필 |
| critic | 출처 수·최신성·구조 검수, 부족하면 반려 | 데스크 데스킹·반려 |
코드상 도구(tools=[web_search])는 오직 analyst에게만 붙어 있고, analyst의 시스템 프롬프트는 '검색 결과를 해석하지 말고 출처와 함께 그대로 넘겨라'다. 그래서 analyst 단계에서 굳이 reflection(자기 해석)을 돌릴 이유가 없다 — 해석은 다음 차례인 synthesizer의 일이기 때문이다.
왜 중요한가: reflection은 한 에이전트가 도구를 부른 직후 같은 머릿속에서 즉시 해석하는 단계인데, 이 토폴로지는 그 '즉시 해석'을 통째로 다음 에이전트로 옮겼다. 빈손이면 critic이 '출처 부족'으로 반려해 재검색 루프가 돌고, 알맹이가 있으면 synthesizer가 출처째 인용해 작성한다. 즉 보정·해석 능력을 잃은 게 아니라, 한 에이전트의 불안정한 즉석 reflection을 여러 에이전트의 안정적인 단계별 분업으로 대체한 설계다. 이 토폴로지 분업이 load-bearing(구조를 떠받치는 핵심)이라는 건 다른 실험에서도 드러났는데, 협업 방식 한 줄(selector→round_robin)을 바꾼 것만으로 pass@1이 0.967→0.067로 붕괴한 사례가 있다.