Overfitting 환경 속에서 Agent를 개선시켜 본 결과
Overview
이번 테스트의 출발점은 아래와 같은 생각을 통해서 테스트를 해봤다.
LLM의 지능이라고 표현할 수 있는 성능의 지표가 매 모델이 출시될 때 마다 우상향 하고 있다.
이런 상황을 저는 "IQ는 높아지는데 전문성이 혹은 문제 해결 능력이 '아직' 없다" 라고 판단했습니다.
이 시점에서 "나는 학창시절 때 시험 점수를 잘 받기 위해서 어떻게 했지?" 라는 생각으로 이어졌고 방법은 '개념 확립 후 기출 문제 풀이' -> '기출 문제 풀이 후 오답 해설을 보고 학습하기'의 방법을 되살펴 봤습니다.
이 상황에서 LLM의 지능이라고 표현할 수 있는 성능이 올라갔다는 것은 매 모델마다 '개념이 확립'되고 있다고 가정을 했고 이 가정을 기반으로 '기출 문제 풀이 후 오답 해설'의 환경만 만들어 보기로 계획을 세웠습니다.
Expieriment
저는 2가지를 실험해보려 했습니다.
첫 번째는, Active Directory Lateral Movement에 대한 지식이 얕은 상태에서 AD Lateral Movement Agent가 개발이 될 수 있을 것인가.
두 번째는, Smart Contract Audit 분야에서 지식이 얕은 상태에서 AI Smart Contract Audit Agent를 만들 수 있을 것인가.
이 두 가지의 테스트를 해보려 했지만 첫 번째의 경우 Claude 모델의 Cyber 가드레일에 위반이 되는 상황이 많이 발생해 첫 번째는 중단하고 두 번째만 진행하게 되었습니다.
제가 생각한 '지식이 없는 상태에서의 Agent 개발'을 위해서는 문제 해결을 위한 '문제'가 존재하는 Play Ground를 만들어 주는 것 그리고 그 Play Ground에 대한 Write up이 존재한다는 조건이 필요합니다.
예시를 들자면 CTF Pwnable Player Agent를 만들기 위해서 수많은 워게임 문제 혹은 CTF문제 그리고 이에 대응하는 Write up이 존재 해야한다는 가정입니다.
이러한 환경 조건을 저는 제목에서 처럼 write up이라 불리는 '정답지'에 대한 내용만 추구하게 될거라 예상해서 Overfitting과 비슷한 현상의 문제가 발생할 것이라고 추측하고 테스트를 진행하게 되었습니다.
Smart Contract Audit Agent
초반 설계는 Claude의 Plan 모드를 가지고 초반 설계를 진행했습니다.
초반 아키텍처의 구상은 과거 gemini를 가지고 Multi Agent Audit System을 구상했던 것을 기반으로 초반 설계를 진행했습니다.
https://efficacious-collision-7ce.notion.site/Audit-Agent-System-4-Audit-Agent-Architecture-31c78187eb49805d8925e77dc0a6ef9f?source=copy_link
과거의 구조에서 가져간 부분은 오케스트레이터 존재 여부, 오탐을 걸러내기위한 Juge Looping, 그리고 POC를 작성할 때 project에 있는 test code 기반으로 작성할 것, 다양한 관점에서의 취약점 분석이 진행되어야 할 것 이라는 규칙만 가져갔습니다.
이 후 문제 풀이 후 개선 작업을 위한 기록 스키마를 요청했습니다.
- 어떤 취약점을 찾았는가.
- 증거가 무엇인가.
- 판단 과정은 무엇인가.
Agent 개선 과정
Audit 진행 -> 정의된 schema에 맞게 기록하면서 audit 결과 도출 -> 진행한 Audit Contest의 실제 report와 결과 비교 -> 기록한 내용을 기반으로 Agent 구조 개선
이렇게 매 audit 진행마다 위의 loop를 돌게 되어있다.
그리고 최종 plan에서 나온 작업 tree와 audit agent의 분석 과정은 아래와 같이 plan이 되도록 마무리 되었습니다.
auditforge/ # 배포 패키지
├── SKILL.md # 얇은 오케스트레이터 (<300 lines)
├── VERSION
├── references/
│ ├── frozen/ # 학습 편집 금지
│ │ ├── shared-rules.md # 신뢰 모델 · 무효 카탈로그 · 기각 기준
│ │ ├── platforms/
│ │ │ ├── code4rena.md
│ │ │ ├── sherlock.md
│ │ │ ├── codehawks.md
│ │ │ └── cantina.md
│ │ ├── poc-rules.md # 상속 패턴 · 치트코드 정책 · assertion 표
│ │ └── report-format.md
│ └── learned/ # 학습 편집 가능
│ ├── lenses/
│ │ ├── fund-flow.md
│ │ ├── ordering.md
│ │ ├── trust-boundary.md
│ │ ├── extreme-values.md
│ │ └── economic.md
│ ├── rejection-criteria.md # 잘못된 기각 사유 목록
│ ├── interaction-probes.md # 그래프 구조 발동 질문
│ └── vuln-patterns/ # 최후 수단. 카드 상한 있음
│ └── VP-*.yaml
├── schemas/
│ ├── finding.yaml
│ ├── trace.yaml
│ └── evidence.yaml
└── scripts/
├── probe_build.sh
├── validate_poc.py
├── slice.py # Phase 5
└── lint_cards.py
harness/ # 비배포
├── archive/
│ └── <platform>-<name>/
│ ├── repo/ # 타겟 스냅샷 (불변)
│ ├── truth.yaml # 정규화된 정답지
│ ├── meta.yaml # platform, date, rules_version, lang
│ └── baseline.json # 지식 비운 상태 점수
├── graders/
├── gate/
└── runs/<timestamp>/
[0] 스코프 확정 README 파싱 → in-scope 파일 목록
↓
[1] 환경 구축 probe_build.sh → forge build/test 성공 확인
↓ 실패 시 중단. 분석으로 넘어가지 않는다
[2] 문서 이해 DocumentReader → 스펙 요약 + 신뢰 모델 추출
DocumentMatcher → 구현이 스펙과 일치하는지 대조
↓
[3] 분석 단위 분할 v1: 파일/컨트랙트 단위
Phase 5+: 슬라이스 단위
↓
[4] 렌즈 병렬 분석 각 단위 × 5개 렌즈 → 가설 생성
가설 예산 강제, 흔적 기록
↓
[5] 게이트 시퀀스 G1 Invalid-catalog → G2 Trust → G3 Reachability
→ G4 Impact → G5 PoC
↓
[6] PoC 작성/검증 VulnTester → validate_poc.py → PROVEN/REFUTED/INVALID/ARGUED
↓
[7] severity 매핑 증거 필드 → 플랫폼별 등급
↓
[8] 제출 임계값 상위 N개 선별
↓
[9] 리포트 렌더링 플랫폼 포맷으로 출력
Agent 개선 과정
11일 동안 41번의 채점 라운드를 돌렸습니다.
이 절에는 개선 환경이 어떻게 굴러갔고 무엇이 나왔는가만 적었습니다.
각 단계는 앞 단계의 실패를 기반으로 개선한 다음 돌린 결과입니다.
읽기 전에 — 용어 정리
앞으로 반복해서 나올 것들을 먼저 정리했습니다.
| 표기 |
뜻 |
| R1, R2 … R27 |
라운드 번호입니다. 한 라운드는 "과거 대회 하나에 파이프라인을 붙여 감사하고 → 정답지와 대조해 채점하고 → 기록을 보고 구조를 고치는" 한 사이클 전체를 말합니다. 숫자는 실행 순서입니다. |
| C1, C10 |
정정 번호입니다. 제가 냈다가 스스로 무효화한 측정에 붙인 일련번호입니다. 지우지 않고 번호를 붙여 남겼습니다. |
| 부품 |
하는 일 |
| 렌즈 |
하나의 관점입니다. 초기에는 "자금 흐름 관점에서 보라" 같은 산문 지시였고, 나중에는 "이 코드는 안전하다" 는 단언 + 그것을 공격할 지점 목록이 되었습니다. 이 변화가 이 글의 절반입니다. |
| 분석 단위 |
코드베이스를 쪼갠 조각입니다. 진입점 하나와 그 함수가 실제로 닿는 코드를 묶은 덩어리입니다. 에이전트 한 명이 한 번에 붙잡는 작업량의 단위입니다. |
| 검출기 |
소스를 훑어서 위치가 붙은 질문을 발행하는 스크립트 규칙입니다. "결함이다"라고 말하지 않고 "여기를 이렇게 물어보라"고만 합니다. |
| 게이트 |
발견 하나가 제출까지 가려면 통과해야 하는 순차 검사입니다. 어느 게이트에서 떨어졌는지가 나중에 실패 진단의 입력이 됩니다. |
| 지표 |
뜻 |
| 재현율 |
그 대회의 확정 정답 중 우리가 지목한 비율입니다. 5/8 이면 8개 중 5개입니다. |
| 배점 |
그 문제 하나의 값입니다. 대회 공식으로 계산되며, 맞힌 사람이 많을수록 0에 수렴합니다. |
| 순위 |
그 대회에 실제로 참가했던 사람들 사이에 우리를 끼워 넣었을 때의 가상 등수입니다. |
0. 그냥 돌렸을 때 — 처참했습니다
설계한 파이프라인을 과거 대회 세 개에 붙였습니다. 대조군으로 관점 렌즈가 없는
평범한 패스를 같이 돌렸습니다. 렌즈가 값을 하는지 보려는 것이었습니다.
| 라운드 |
평범 패스 |
렌즈 패스 |
순위 |
| R1 |
2/3 |
2/3 |
11 / 239 |
| R2 |
1/4 |
1/4 |
11 / 58 |
| R3 |
0/8 |
0/8 |
83 / 83 |
R3은 참가자 83명 중 83등, 꼴찌였습니다.
그런데 더 나쁜 것이 있었습니다. 세 라운드 연속으로 렌즈의 추가분이 정확히
0이었습니다. "이런 관점으로 보라"는 산문 지시를 붙였는데, 에이전트는 그것을
읽고 "네, 그 관점에서 보겠습니다"라고 한 뒤 원래 보던 것을 봤습니다.
R2 기록에 이렇게 적었습니다 — "원인은 렌즈가 아니라 입력 부재".
1. 첫 번째 깨달음 — 지표 자체가 틀렸습니다
R1을 다시 봤습니다. 3건 중 2건을 맞혔는데 점수가 0.0000이었습니다.
이 대회의 배점 공식은 이렇습니다.
배점 = weight × 0.9^(n-1) / n
n 은 그 문제를 실제 대회에서 맞힌 참가자 수입니다.
많이들 맞힌 문제는 배점이 0에 수렴합니다.
표로 펴면 절벽입니다.
맞힌 참가자 n 상위 등급 배점 하위 등급 배점
1 10.0000 3.0000
2 4.5000 1.3500
5 1.3122 0.3937
8 0.5979 0.1794 ← R1이 놓친 문제
10 0.3874 0.1162
25 0.0319 0.0096
77 0.0000 0.0000 ← R1이 맞힌 문제
207 0.0000 0.0000 ← R1이 맞힌 문제
제가 맞힌 둘은 238명 중 207명과 77명이 같이 맞힌 것이었습니다.
그 대회의 값은 8명만 맞힌 문제 하나에 전부 들어 있었고, 저는 그것을
놓쳤습니다.
"몇 개 맞혔나"는 성능 지표가 아니었습니다.
기출 문제로 비유하면 정답률이 아니라 배점 큰 문제를 맞혔는가를 봐야 합니다.
채점 스크립트를 그 대회의 실제 리더보드를 재구성하도록 다시 짰습니다.
이때부터 재현율과 순위를 항상 같이 봤습니다. 이 결정이 이후 모든 판단의
기준선이 되었습니다.
2. 산문 지시를 검출기로 바꿨습니다
렌즈 추가분이 0이었던 원인 진단이 "산문으로는 질문이 안 간다"였으므로,
산문을 코드로 바꿨습니다. 여기서 나온 것이 검출기입니다.
핵심 설계는 이것입니다.
검출기가 내놓는 것은 발견이 아니라 질문입니다.
배포된 스킬 문서에 그대로 박아 놓았습니다 —
*"A hit is a question, never a finding."
검출기는 "이 모양은 결함이다"라고 말하지 않습니다. 그 모양은 대부분 지극히
평범한 코드입니다. 검출기가 하는 일은 위치를 짚고 질문을 발행하는 것뿐입니다.
"이 값을 나중에 누가 소비하는가", "이 검사를 통과한 뒤에 상태가 바뀌면
어떻게 되는가" 같은 것들입니다.
여기서 처음으로 "오답 해설" 루프가 실제로 돌기 시작했습니다.
라운드 실행 → 채점 → 놓친 정답을 보고
"어떤 질문을 했어야 이것을 찾았나"를 역산 → 검출기로 고정
검출기는 26종까지 늘었습니다. 재현율이 10~30%대로 올라왔습니다.
그런데 거기서 멈췄습니다.
3. "그럼 코드를 다 읽게 하자" — 커버리지를 밀어붙였습니다
다음 가설은 단순했습니다. 못 찾는 이유는 안 읽어서다.
커버리지를 지표로 잡고 몇 라운드에 걸쳐 밀어붙였습니다. 분석 단위를 뽑는
로직을 고치고, 예산 규칙을 바꾸고, 워커가 구조적으로 못 보던 코드 유형을
열거에 편입했습니다.
컨트랙트 커버리지 53.0% ████████████████ → 86.5% ██████████████████████████
함수 수준 커버리지 15.0% ████ → 66.0% ████████████████████
사정권 (최악 라운드) 29.0% ████████ → 93.0% ████████████████████████████
수치는 잘 올라갔습니다. 그리고 다음 라운드에서 이것이 나왔습니다.
4. 커버리지를 올려도 재현율이 안 올랐습니다
커버리지를 변수로 고정한 라운드를 돌렸습니다.
스코프의 95.7% (23개 분석 단위 중 22개)가 사정권
1,577행을 실제로 걸음
정답 26건
커버리지 ███████████████████████████████████████████████ 95.7%
재현율 0.0%
정밀도 0.0% (0/4)
95.7%를 걷고 0%였습니다.
놓친 것들을 하나씩 열어봤습니다. 전부 코드가 읽힌 상태에서 놓친
것들이었습니다.
커버리지는 필요조건이지 충분조건이 아니었습니다.
코드는 읽혔고, 질문이 안 갔을 뿐이었습니다.
이 라운드 전까지 저는 커버리지를 성능 대리 지표로 쓰고 있었고, 거기서
그만뒀습니다. 같은 시기에 커버리지 리포트 자체도 정정했습니다 —
"선정 안 됨"과 "아무도 안 봄"은 다른 사건인데 한 숫자로 세고 있었습니다.
5. 그러면 병목이 무엇인가 — 커버리지가 아니라 '걸은 양'
커버리지가 통제된 덕에 처음으로 실행 범위가 병목으로 분리되었습니다.
같은 타깃, 같은 렌즈, 같은 분석 단위 추출기, 같은 모델에서
동시에 굴리는 에이전트 수만 바꿨습니다.
|
단일 스레드 |
12개 병렬 |
| 걸은 분석 단위 |
20 |
79 |
| 채운 기록 행 |
2,729 |
12,024 |
| 재현율 |
0/10 |
7/10 |
0% → 70%. 방법론은 한 줄도 안 바꿨습니다.
같은 방향의 확인이 세 번 나왔습니다.
| 비교 |
바꾼 것 |
재현율 |
| 타깃 A |
20단위 → 79단위 |
0% → 70% |
| 타깃 B |
20단위 → 전량 |
67% → 87% |
| 타깃 C |
걸은 범위 안/밖 대조 |
안 100% (3/3) / 밖 9% (1/11) |
여기서 규칙이 나왔습니다.
분석 단위를 먼저 쪼개고, 관점은 나중.
여섯 렌즈가 독립적으로 같은 결함에 중복 도달한 반면, 에이전트 수를 늘리자
새 결함이 나왔습니다. 관점을 늘리는 것보다 단위를 늘리는 것이 값이 큽니다.
6. 남는 미스는 형태가 달랐습니다
걷는 양을 늘려도 남는 미스가 있었습니다. 실패 형태를 분류해 보니 대부분
하나였습니다.
"올바른 위치에 도달했는데 잘못된 질문을 했다."
그리고 더 나쁜 변형이 있었습니다 — 찾아놓고 스스로 기각하는 것입니다.
| 라운드 |
무슨 일이 있었나 |
그 문제의 배점 |
| R9 |
정답 위치를 읽고 인용까지 하고 다른 것을 물음 |
1.3500 |
| R10 |
해당 코드를 읽고 "정상"으로 분류 |
0.1162 |
| R24 |
결함을 인정해 놓고 "가능성이 낮다"며 격하, 제출 안 함 |
3.0000 |
| R27 |
네 에이전트가 그 위치를 읽고 아무도 제출하지 않음 |
0.5468 |
| — |
세 분석자가 독립적으로 같은 결함에 도달, 셋 다 기각 |
— |
R24가 특히 중요했습니다. 에이전트가 자기 기록에 이렇게 써 놓았습니다.
real defect but requires [조건]; noted, not asserted (Low)
정답지로는 "놓쳤다"까지만 보입니다.
산출물을 읽어야 "인정하고 스스로 격하했다"가 보입니다.
여기서 나온 규칙이 "가능성은 심각도의 논거이지 제출 여부의 논거가 아니다"
입니다.
7. 반박 패스 — 추가로 걷지 않고 회수하기
파이프라인이 "문제없다"고 결론 낸 항목만 모읍니다.
그리고 그 결론을 본 적 없는 에이전트에게 주장과 위치만 줍니다.
추가로 읽는 코드는 0입니다.
Someone else audited this code and concluded it was fine.
You are being given their CONCLUSION and the file paths —
never their reasoning, and never the finding they wrote.
독립성이 장치입니다. 자기 결론을 다시 읽으면 그 결론이 재생산됩니다.
일곱 타깃에 걸쳐 15회 돌린 집계입니다.
결론이 깨짐 12 / 15 ████████████████████████
결론이 버팀 3 / 15 ██████
그중 확정 정답이 된 것 5건
버틴 3건도 소득이 있었습니다 — 셋 다 '동의'가 아니라
가정을 검증된 근거로 교체했습니다
재현율이 70% → 90% 가 되었습니다. 세 라운드에서 재현되었습니다.
그리고 저는 이 수치를 전부 무효화했습니다. 정정 C1 입니다.
정정: 반박 패스 수치 전부 무효 — 내가 정답을 보고 표적을 골랐다
재공격할 표적을 제가 골랐는데, 그때 저는 이미 정답지를 본 상태였습니다.
악의가 아니라 구조입니다. 채점하려면 정답지를 열어야 하고, 답을 본 사람이
다음 실험을 설계합니다.
이후 규칙이 바뀌었습니다 — 표적 선정을 사람에게서 떼어내고 전량 무필터 투입.
사람이 표적을 고르면 그 패스의 상한은 고른 사람이 이미 의심한 것까지입니다.
깨끗하게 다시 측정한 것은 한 라운드뿐인데, 그 하나가 컸습니다.
블라인드로 돌린 반박 패스가 배점 10.0000 — 대회 전체에서 참가자 한 명만 맞힌
문제를 잡았습니다. 이 실험 전체에서 가장 값이 큰 단일 항목입니다.
재현율로는 1/6 = 17%인데, 성적 값으로는 최고였습니다.
8. 반박을 세 방향으로 늘렸습니다
한 방향이 되니 다른 방향도 열어봤습니다. 각각 다른 실패 형태에서 나왔습니다.
① 기각 재공격 — "문제없다"는 결론을 독립 에이전트가 다시 칩니다.
(위에서 나온 것)
② 다른 피해자 찾기 — 이름이 헷갈릴 수 있어 풀어 쓰면 이렇습니다.
결함 하나를 찾으면, 에이전트는 "그래서 누가 손해를 보는가" 를 한 명 지목하고
그것으로 제출합니다. 그런데 같은 코드 한 줄이 전혀 다른 사람에게도 손해를
입히는 경우가 많고, 심사가 인정한 것은 종종 그 다른 사람 쪽이었습니다.
실제 사례가 이해가 빠릅니다.
찾은 것 어떤 권한 검사가 잘못 걸려서, 관리자가 설정을 바꿀 수 없다
제출한 피해자 관리자 → 심각도: "운영이 불편함"
"또 누가 손해인가?"를 물었더니 —
두 번째 피해자 일반 사용자 → 그 설정이 안 되면 입금 처리가 영구히
실패하고, 이미 들어온 돈이 0주를 받은 채
갇힙니다. 취소 경로도 없습니다.
같은 코드 한 줄인데 "관리자 불편"에서 "사용자 자금 손실"로 바뀝니다.
찾은 것은 처음부터 맞았고, 누가 손해인지를 잘못 지목했을 뿐입니다.
이 형태로 순위가 갈린 라운드가 있었습니다. 어떤 라운드가
"X 경로에서 검사가 돌지 않으므로 X는 영향 없음" 이라고 결론지었는데,
확정 정답은 똑같은 문장에서 반대 결론을 냈습니다 — 검사가 안 도는 경로는
곧 아무 제약 없이 쓸 수 있는 경로라는 것입니다. 같은 사실, 같은 위치,
정반대 결론. 그것이 89등과 6등의 차이였습니다.
그래서 수용된 발견마다 "이 사실이 또 누구를 손해 보게 하는가" 를 강제로
묻게 했습니다.
③ 위치만 주고 다시 읽기 — 앞의 두 패스에는 공통된 약점이 있습니다.
둘 다 에이전트에게 결론을 쥐어준 채로 공격시킵니다.
①은 "누군가 여기를 안전하다고 결론 냈다", ②는 "여기서 이런 결함을 찾았다"
를 먼저 알려줍니다.
산출물을 읽어보니 문제가 보였습니다. 공격하라고 시켜도, 그 결론이 사고의
출발점이 됩니다. 사람에게 "이 문장이 틀렸음을 증명해보라" 고 하면 그 문장을
중심으로 생각하게 되는 것과 같습니다. 코드를 새로 보는 게 아니라, 주어진
문장이 맞는지를 확인하러 갑니다.
그래서 세 번째 패스는 아무 맥락도 주지 않습니다.
주는 것 파일 경로와 줄 번호. 그것뿐입니다.
주지 않는 것 앞에서 무엇을 찾았는지
앞에서 무엇을 기각했는지
그 줄이 왜 인용되었는지
누가 안전하다고 했는지
질문 "이 줄을 무권한 사용자가 어떻게 쓸 수 있는가?"
앞 라운드들이 인용해 놓은 줄 번호 목록만 뽑아서, 그 줄을 처음 보는
에이전트에게 넘깁니다. 그 에이전트는 이 코드에 대해 앞에서 무슨 말이 오갔는지
전혀 모릅니다.
가장 최근 라운드에서 세 패스의 실제 산출입니다.
| 패스 |
입력 |
결과 |
| ① 기각 재공격 |
안전 결론 15건 |
12건이 깨짐 |
| ② 다른 피해자 찾기 |
수용된 결함 29건 |
29건 전부 다른 피해자가 있었음 |
| ③ 위치만 주고 다시 읽기 |
인용된 위치 154개 |
13개가 정반대로 읽힘 |
②는 "다른 피해자 없음"이 0건이었습니다. 제출된 29건 전부가 처음
지목한 사람 말고도 손해를 보는 쪽이 따로 있었습니다.
그리고 ③은 13개 렌즈 전부가 놓친 결함 2건을 잡았습니다.
앞선 라운드들이 전부 "정상"으로 읽고 지나간 줄들이었습니다.
9. 마지막 천장 — 나의 영향력을 배제
처음에는 "취약점을 발견해라" 라는 식의 명령 구문이 존재했습니다.
하지만 매번 라운드마다 자동으로 개선되면서 Agent는 아래와 같이 바꿨습니다.
|
기존 |
변경 |
| 에이전트에게 주는 것 |
"결함을 찾아라" |
"이 코드는 안전하다" 는 단언 |
| 에이전트의 일 |
열린 탐색 |
그 단언의 반증 |
| 산출 |
찾은 것 |
깨짐(결함) 또는 버팀(근거 원장) |
반증할 대상이 명시되면, 안 한 질문이 보입니다.
"뭐라도 찾아와"는 무엇이 없는지 알 수 없지만, "이건 안전하다"는 틀렸음을
보이는 방법이 정해져 있습니다.
이렇게 "이 코드는 안전하다" 라는 단언을 두고 문제 해결을 다음 라운드에 시킨 결과는 정답 25건 중 16건, 참가자 249명 중 1위였습니다.
10. 특이점 — 여기 '취약점 패턴'이 한 장도 없습니다
공개된 다른 Audit Agent나 Skill을 찾아보면 대부분 취약점 패턴 목록 위에
서 있습니다. 이런 모양은 이 결함이고 저런 모양은 저 결함이다 — 그 목록을
프롬프트에 넣고 대조시키는 방식입니다.
제 초기 설계에도 그 자리가 있었습니다. 앞선 트리에서 보이는
references/learned/vuln-patterns/ 가 그것이고, 설계 문서에
"최후 수단. 카드 상한 있음" 이라고 적어두었습니다.
41라운드가 끝난 지금, 그 디렉터리에 카드가 0장입니다.
한 장도 만들지 않았습니다. 그리고 그것은 게을러서가 아니라 측정 결과입니다.
배포된 스킬 문서에 이렇게 박혀 있습니다.
Do not preload `vuln-patterns/`: preloading measurably hurts hits.
패턴을 미리 넣으면 적중률이 떨어졌습니다.
이유는 지금 보면 명확합니다. 패턴을 쥐어주면 에이전트가 그 패턴을 찾으러
갑니다. 목록에 있는 것을 찾고, 목록에 없는 것은 안 봅니다.
그런데 대회에서 값이 큰 문제는 정의상 희소한 것이고, 희소하다는 것은
흔한 패턴 목록에 없다는 뜻입니다.
이 실험에서 값이 가장 컸던 발견 — 배점 10.0000, 대회 전체에서 참가자
한 명만 맞힌 문제 — 는 어떤 패턴 목록에도 없습니다. 있었다면 한 명만
맞히지 않았을 것입니다.
대신 이 시스템이 쓰는 것은 패턴이 아니라 구조입니다.
|
패턴 기반 |
이 시스템 |
| 입력 |
"이런 모양은 결함이다" |
"이 코드는 안전하다" 는 단언 |
| 하는 일 |
목록과 대조 |
그 단언을 반증 |
| 못 찾는 것 |
목록에 없는 것 |
(목록이 없음) |
검출기 26종도 패턴이 아닙니다. 결정적 차이가 있습니다.
검출기는 "이건 결함이다"라고 말하지 않습니다.
"이 결과를 누가 소비하는가"를 묻습니다.
세 검출기는 이미 읽고 통과시킨 위치에서 발화합니다. 그중 하나는 어떤
라운드가 그 지점을 소비하는 모든 경로를 추적하고 "올바른 설계"로 결론 내린
곳이었습니다. 그 라운드는 한쪽 방향만 물었고, 반대 방향은 한 번도 묻지
않았습니다.
패턴은 "무엇이 결함인가"를 알려주고, 이 접근은 "무엇을 안 물었는가"를
알려줍니다. 후자가 6절의 실패 형태 — "올바른 위치에 도달했는데 잘못된 질문을
했다" — 를 정면으로 겨냥합니다.
재현율이 18.2% → 56.9% 로 오르는 동안, 그리고 외부 벤치마크에서 65% 가
나오는 동안, 패턴 카드는 단 한 장도 추가되지 않았습니다.
물론 "패턴이 쓸모없다"는 뜻은 아닙니다. 카드를 넣고 A/B를 돌린 적이 없으므로
****패턴을 추가했을 때의 효과는 측정되지 않았습니다.**
측정된 것은 하나뿐입니다 — 미리 주입하면 적중률이 떨어진다.
그래서 스킬은 카드를 금지하지 않고, 담당자가 질문 때문에 필요할 때 ID로
요청하도록 남겨두었습니다. 41라운드 동안 그 요청이 한 번도 없었을 뿐입니다.
11. 흐름 정리
각 단계가 앞 단계의 실패에서 나왔습니다.
그냥 돌림
→ 꼴찌 (0/8, 83위/83), 관점 추가분 3회 연속 0
↓ "산문 지시로는 질문이 안 간다"
검출기로 전환
→ 재현율 10~30%에서 정체
↓ "못 찾는 건 안 읽어서다"
커버리지 밀어붙이기 (53% → 86.5%, 함수 15% → 66%)
→ 95.7% 커버리지에서 재현율 0%
↓ "코드는 읽혔다. 질문이 안 갔다"
걷는 양 확대 (20단위 → 79단위)
→ 0% → 70%
↓ "남는 미스는 찾아놓고 기각한 것들"
반박 패스 (추가로 읽는 코드 0)
→ 회수됨. 단 수치는 내가 표적을 골라 무효 (C1)
↓ "표적 선정을 사람에게서 뗀다" + 방향 확장
두 번째 패자 · 무맥락 재독
→ 13개 관점 전부가 놓친 결함을 잡음
↓
주장 작성을 스크립트로 — "이 코드는 안전하다"의 반증으로 재구성
→ 25건 중 16건, 249명 중 1위
성능 추이입니다. (같은 플랫폼·같은 언어로 축을 통일했습니다)
| 구간 |
라운드 |
합산 재현율 |
점수 0.0000 라운드 |
| 형성기 (R1~R18) |
15 |
10/55 = 18.2% |
8 / 15 |
| 사전등록 + 배포판 (R19~) |
8 |
70/123 = 56.9% |
0 / 7 |
재현율보다 오른쪽 열이 선명합니다. 형성기는 절반 이상이 0점에 수렴했습니다.
라운드별로 펴면 이렇습니다.
형성기 ──────────────────────────── 사전등록 + 배포판 ──────────────────
R1 ████████████████ 67% (2/3) R19 ██████████ 40% (6/15) 4위/117
R2 ██████ 25% (1/4) R20 ████████████ 48% (11/23) 4위/185
R3 · 0% (0/8) 꼴찌 R21 ████████████████ 64%(9/14) 1위/147
R5 ████████ 33% (1/3) R23 ████████████████ 64%(16/25) 1위/249
R6 ██████ 25% (1/4) 6위/67 R24 ████████████████ 64%(7/11) 2위/166
R9 ██████ 25% (1/4) R26 ██████████ 43% (3/7) 20위/73
R13 · 0% (0/2) 꼴찌 R27 ███████████████ 63%(5/8) 17위/83
R16 · 0% (0/6) 꼴찌 ────────────────────────────────────
R18 · 0% (0/4) 꼴찌 외부 ████████████████ 65%(13/20)
형성기에서 꼴찌가 다섯 번 나왔습니다. 그리고 R23 이후 다섯 라운드는
64 · 64 · 43 · 62 · 65 로 평평합니다 — 계단을 한 번 오르고 평지입니다.
인과를 주장할 수 있는 비교가 하나 있습니다.
R3과 R27은 같은 대회, 같은 정답지, 같은 채점 스크립트입니다.
R3 (3일차) → 0/8 · 83위 / 83 (꼴찌)
R27 (5일차) → 5/8 · 17위 / 83
교란 요인도 적어둡니다. R3 기록에 그때 남긴 것이 있습니다 — R3 타깃이 훨씬
컸는데 컨텍스트 예산이 소진되어 얕게 걸었습니다. 0점의 일부는 방법이
아니라 두께입니다. 이 비교는 "개선분"이 아니라 "개선분 + 실행 여유분"을
함께 잽니다.
정리
처음에는 이런 정답지를 보고 공부를 시키는 이 환경이 overfitting이 일어날 것이라고 생각했고 예측한 개선 방향은 "Vuln Pattern"을 학습할 것이라고 예상했습니다.
41개의 audit 결과를 가지고 개선이 된 결과 커버리지를 늘리려는 시도, 분석의 관점을 수정하는 시도, false positive를 줄이기 위한 Gate를 수정하는 시도 등 Vuln Pattern을 학습 하는 것이 아닌 "어떤 질문을 내가 던져야 취약점을 발견할 수 있는가"로 스스로 발전해 나갔고
현재 까지는 우상향으로 재현율이 올라가게 되었습니다.
하지만 아직 한계가 있다면 찾은 사람이 적은 취약점 즉 점수 배점이 높은 취약점을 발견하는 확률이 아직 높지 않다는 점입니다.
그래도 가끔씩 재현율 100%의 1등을 찍는 경우도 존재해서 여러번 개선 루프가 돌아가면 이 부분도 개선될 것이라고 기대 중입니다.