Orbi의 하네스 찾기, 파트 1: 리뷰어가 놓친 것들
요약
본 기사는 Orbi라는 에이전트 시스템의 안정성을 검증하기 위해 '하네스(harness)' 구축 방법을 다룹니다. 단순히 모델을 교체하거나 이전/이후 비교만으로는 충분하지 않으며, 회귀 테스트는 다양한 유형의 버그와 숨겨진 평가자를 통해 반복적으로 실행해야 함을 강조합니다.
핵심 포인트
- 단순한 '이전-이후' 비교로는 부족하며, 전체 클래스에 걸친 입력 검증이 필요하다.
- 수정된 내용으로 작성된 오라클(oracle)은 신뢰할 수 없으며, 여러 참조값을 교차 확인해야 한다.
- 하네스 자체의 텍스트나 예제도 버그가 될 수 있으므로 주의해야 한다.
Orbi는 GitHub 이슈를 받아 검토되고 병합된 풀 리퀘스트(pull request)로 돌려줍니다. 한 에이전트는 수정 사항을 작성하고, 다른 에이전트가 이를 검토하며, 이 루프는 검토가 통과될 때까지 반복됩니다. 모델 주변에는 하네스(harness)가 있습니다: 각 역할에 할당되는 프롬프트(prompt), 그 옆에 로드된 스킬(skill), 러너의 PATH에 있는 도구(tool), 그리고 리뷰가 언제 통과할 수 있는지에 대한 규칙들입니다.
배포 과정이 잘못되면, 한 번의 실행만으로는 모델 탓인지 하네스 탓인지 알 수 없습니다. 그래서 우리는 추측을 멈추고 밤새도록 다음 날 아침까지 Orbi의 전체 루프를 반복적으로 실행했습니다: 먼저 Orbi가 회귀(regression)를 일으킨 세 개의 오픈 소스 이슈에, 그다음에는 Orbi가 한 번도 본 적 없는 일반적인 버그 아홉 개에, 그리고 실제 세계에서 첫 수정이 무언가를 망가뜨린 세 개의 이슈에 대해 말입니다. 우리는 프롬프트, 스킬, 도구, 구현 모델(implementation model) 및 리뷰 모델(review model)을 교체하고, Orbi가 본 적 없는 숨겨진 평가자(hidden grader)로 모든 병합된 결과를 평가했습니다.
이것은 파트 1입니다: 우리가 실패 사례를 발견하고 이를 해결하는 하네스를 구축한 방법입니다. 파트 2에서는 그 하네스가 비용을 지불할 가치가 있는지에 대해 다룹니다. 결과와 스크립트는 벤치마크 페이지와 orbi-build/orbi-bench에서 확인할 수 있습니다. 여기 짧은 요약본이 있습니다.
10가지 시사점
10가지 시사점
- 우리가 조정했던 이슈에 대해서는, 하네스(harness)가 우리가 시도한 어떤 모델 교체보다 높은 수치를 보여주었습니다.
deepseek-flash를 두 역할 모두에 사용했을 때, 원래의 하네스는 세 가지 튜닝 이슈 중 15회 실행 중 6회를 통과했습니다. v13에서 v15로 올라가면서는 30회 중 28회를 통과했습니다. 이것은 학습 점수입니다. 시사점 7은 우리가 조정하지 않은 이슈에 관한 것입니다. - '이전과 이후 비교'만으로는 충분하지 않습니다. 에이전트에게 회귀(regression)를 확인하도록 요청하면, 이미 믿고 있는 예제들을 검사합니다. 한 실행에서는 U+0301을 테스트했는데, 이는 결코 깨지지 않았던 것이었고, U+3099는 깨졌음에도 불구하고 건너뛰었습니다. 입력은 프로그램에 의해 전체 클래스의 값들에 걸쳐 생성되어야 합니다.
- 수정된 내용으로 작성된 오라클(oracle)은 아무것도 증명하지 못합니다.
gpt-5.6-sol실행은 1,200개의 표를 생성하고, 방금 작성한 규칙을 구현한 '참조값(reference)'과 비교했으며,head wrong: 0을 보고하며 회귀를 배포했습니다. - 참조 라이브러리도 틀릴 수 있습니다.
wcwidth0.2.14는 👍🏽를 네 개의 열로 계산합니다. 이것을 신뢰하면 당신은 회귀가 옳다고 증명하게 될 것입니다. 두 가지 참조값을 교차 확인하고 어느 것을 믿을지 말해야 합니다. - 당신이 제거하는 문자들 자체가 데이터입니다. 비밀번호 유출에는 그 자체로
으로 끝나는 비밀(secret)이 필요했습니다. 에이전트들은 항상 백슬래시를 비밀 뒤에 붙이고, 절대 안에 넣지 않습니다. 이 입력 클래스 이름을 명명함으로써 유출을 막았습니다. - 당신 자신의 하네스 텍스트가 버그일 수 있습니다. 우리가 작성한 예제(
에이전트는 사람이 만든 회귀(regression)를 반복하지 않습니다. 첫 번째 상위(upstream) 수정에서 무언가를 망가뜨린 세 가지 버그(numpy, OpenTelemetry Collector, aiohttp)의 경우, 원래 하네스(harness)는 4개 중 4개의 점수화된 실행을 통과했고 v15에서는 3개 중 3개를 통과했습니다 (numpy 실행은 완료되지 않았음). 이 평가는 버그 자체와 회귀에 대해 모두 이루어졌습니다. 방어해야 할 회귀는 코드가 깨지는 일반적인 목록이라기보다는 에이전트 자신의 습관입니다.
9. 판단(judgement)이 중요한 곳에서는 리뷰 모델이 중요합니다. v13–v14의 두 가지 chainloop 실패 사례 중 하나는 deepseek-flash가 이 데이터인지 이스케이프 문자(escape)인지 잘못 판단한 것이었고, 다른 하나는 v15에서 수정된 충돌이었습니다. sol 리뷰어는 4개 중 4개를 통과했고, v15에서도 deepseek-flash가 그랬으므로 이를 중요한 단서로 간주하십시오.
10. 벤치마크는 소프트웨어이며, 저희의 것은 버그를 포함했습니다. 우리의 평가자(grader) 세 명은 어느 시점에 잘못되었고, 하나의 스냅샷이 추적되는 파일을 조용히 누락시켰으며, 리뷰어 한 명은 실제 상위 수정 내용을 찾아보기도 했습니다. 이 모든 것이 자신감 넘치지만 틀린 숫자를 만들어냈을 것입니다.
나머지 게시물에서는 각 오류가 어디서 발생했는지, 실수들을 포함하여 보여줍니다.
시작하게 된 이유
9월 28일 Orbi는 오픈 소스 프로젝트의 포크(fork)에서 세 가지 문제를 수정했습니다: pyinfra, chainloop, 그리고 fedify. 세 가지 모두 테스트 스위트와 Orbi 자체의 검토를 통과했습니다. 저희는 상위에 전송하기 전에 수동으로 확인했습니다. 두 개가 잘못되었습니다.
chainloop 수정은 비밀 제거기(secret redactor)에 있었습니다. 해당 문제는 이스케이프된 따옴표 앞에 있는 JWT가 백슬래시와 함께 제거되었다고 말했습니다. Orbi의 수정은 각 발견 사항에서 후행 백슬래시를 잘라냈는데, 이는 JWT를 수정했지만 다른 두 가지 것도 망가뜨렸습니다:
input: DB_PASSWORD=Zq8mLp2Vx9rT\n next
output: DB_PASSWORD=[REDACTED:generic-password]\n next
...
(플레이스홀더가 두 번째 예시에서 축약됨.) 첫 번째 경우에는 탐지기가 비밀번호를 Zq8mLp2Vx9rT 로 보고하며, 이는 리터럴 백슬래시와 n으로 끝납니다. 수정 사항은 이 두 문자를 발견 내용에서 잘라내어 로그에 일반 텍스트로 남기게 합니다. 두 번째 경우에는 한 글자 비밀이 리다크터가 텍스트의 모든 's'를 대체하게 만들었습니다.
pyinfra 수정 사항은 CJK(중국어, 일본어, 한국어) 텍스트가 결과 테이블을 잘못 정렬하는 문제에 관한 것이었습니다. 이 수정 사항은 넓은 문자(wide characters)를 두 개의 열로 계산했습니다. 그 결과 문제가 발생한 예시가 올바르게 정렬되었고, 👍🏽와 분해된 が가 이전보다 더 나쁜 결과를 낳았습니다.
두 경우 모두 Orbi의 리뷰는 포크(fork) 이슈에 게시되어 있으며 전체적으로 review: pass, no findings로 읽힙니다. 이 리뷰는 테스트를 실행하고 문제의 예시를 확인했으며, 이는 통과했습니다. 회귀(regressions)는 아무도 시도하지 않은 입력값에 머물렀습니다.
우리는 상위 저장소 세 곳 중 한 곳에 기여했습니다. pyinfra 수정 사항은 Claude Code를 사용하여 수동으로 재작업되었고 pyinfra-dev/pyinfra#1977로 열렸으며, AI의 개입이 공개되었습니다. chainloop는 신뢰할 수 있는 수정 사항을 기다립니다. fedify의 코드는 괜찮았지만, fedify는 기여자들에게 PR(Pull Request)를 열기 전에 이슈가 할당되도록 요청했고, 유지 관리자가 이 문제를 맡아 닫았습니다.
측정 방법
그레이더는 어떤 변형이 실행되기 전에 보정됩니다. 나쁜 포크 PR을 측정하는 그레이더는 아무것도 측정하지 못합니다.
우리가 푸시한 각 이슈에 대해 Orbi가 시작했던 커밋의 사본 스냅샷(private snapshot)을 만들고, 같은 이슈를 ai-ready 레이블과 함께 열었으며, 실제 Orbi 러너를 가리켰습니다. 이 러너는 프로덕션에서 하는 일을 했습니다: 주장하고, 구현하고, PR을 열고, 독립적인 리뷰를 실행하고, 수정하고, 병합했습니다. 그런 다음 숨겨진 그레이더가 저장소 자체의 린트(lint) 및 타입 검사(type checks)와 전체 테스트 스위트와 함께 병합된 코드를 점수화했습니다.
어떤 변형(variant)이 실행되기 전에, 각 그레이더는 원본 코드를 실패시키고, Orbi가 병합한 포크 PR을 실패시키며, 우리가 수동으로 검증한 수정 사항을 통과시켜야 했습니다. chainloop의 경우, 포크 PR에서 실패하는 두 가지 검사는 위에서 언급된 두 출력과 정확히 일치합니다. pyinfra의 경우 👍🏽와 が입니다.
변형이란 하네스(harness)와 모델(models)의 한 조합을 의미합니다. 우리는 프롬프트, 스킬, PATH에 있는 도구, 구현 모델, 리뷰 모델 또는 이들의 혼합을 변경했습니다. 사용된 모델들은 Pi를 통해 deepseek-flash, gpt-5.6-luna, gpt-5.6-sol이었고, Claude Code bridge를 통해 Opus 5.5였으며, zcode bridge를 통해 GLM 5.3 flash였습니다. 두 개의 브릿지에서는 동일한 모델이 구현과 리뷰를 모두 수행했으며, 에이전트 루프(agent loop)는 해당 브릿지 자체의 것이었습니다.
변형들은 한 대의 기계에서 세 개 또는 네 개씩 동시에 실행되었습니다. 각 변형은 자신만의 비공개 저장소(private repository)를 가지고 있었기 때문에, 어떤 실행도 다른 것의 브랜치(branch)를 볼 수 없었습니다.
그리드 (The grid)
행은 하네스 버전이고, 열은 구현자/리뷰어입니다. ds = deepseek-flash. 셀들은 세 개의 저장소에 걸쳐 통과(passes)/실행 횟수(runs)를 나타냅니다.
이 그리드는 의도적으로 희소합니다. 매 단계마다 우리는 우리 앞에 놓인 질문에 답할 수 있는 가장 저렴한 실행을 선택했으며, 완전 인자 조합(full factorial)을 채우려는 목표는 없었습니다. 깊은지식(deepseek) 모델이 아닌 셀들은 대부분 두 개에서 네 개의 실행 횟수를 가지고 있습니다. 다른 열들은 단서로 간주하고 ds/ds 열을 결과로 취급하십시오.
하네스 변경 사항의 영향 (What the harness changes did)
전반에 걸쳐 동일한 모델을 사용했습니다. 연한 막대는 5회 미만의 실행 횟수를 나타냅니다.
[
pyinfra와 fedify가 가장 많이 움직였습니다. chainloop는 원래의 harness에서 대부분의 실행을 통과했습니다. 회귀(regression)는 드물게 나타나며, 그것이 위험한 이유입니다.
아래의 모든 규칙은 실패한 실행의 스크립트를 읽고 작성되었습니다. 각 버전은 이전 버전을 기반으로 구축되었습니다.
v1: 비교 전/후를 요청하기
첫 번째 시도는 명백했습니다. 두 역할 모두 수정 전과 후의 동작을 비교하고 무엇을 확인했는지 적도록 지시하는 것이었습니다. pyinfra는 0/5에서 2/3로 향상되었습니다.
실패 사례들은 면밀히 살펴볼 가치가 있습니다. 에이전트는 비교를 했지만, 스스로 선택한 예제에 한해서였습니다. 한 실행에서는 마크(marks)와 U+0301 (라틴 성구 표기, 구 버전 코드에서 이미 처리하던 것)을 결합하는 것을 테스트했지만, U+3099 (일본어 유성음 표기, 수정 사항으로 인해 깨진 부분)는 전혀 시도하지 않았습니다. 에이전트에게 회귀를 찾도록 요청하면, 에이전트가 이미 예상하는 것들만 얻게 됩니다.
v4: 별도의 스킬로 동일 규칙 적용하기
v1 규칙을 프롬프트 대신 독립적인 스킬로 이동시켰습니다. 이 버전은 1/3의 점수를 받았습니다. 스크립트를 보면 해당 스킬이 읽혔다는 것을 알 수 있습니다. 세 번의 실행으로는 패키징 때문에 문제가 생겼다고 말하기는 어렵지만, 도움이 되지 않았다는 것은 분명합니다. 규칙은 여전히 에이전트가 스스로 샘플을 선택하도록 허용했습니다.
v5: 입력값 생성하기
v5에서는 프로그램이 전체 클래스에 걸쳐(텍스트의 경우: ASCII, CJK 및 전각 문자, 결합 표기, 이모지 수정자, 너비 0 시퀀스, ANSI 코드; 구문의 경우: 이스케이프, 짝수/홀수 백슬래시 연속, 따옴표, 구분자) 입력값을 생성하도록 요구했으며, 검토자가 확인할 수 있도록 그 개수를 .orbi/regression.md 파일에 작성했습니다. 그 이후부터는 실행마다 천 개 이상의 입력값이 생성되었습니다. v5는 리다크터(redactor)를 위해 2,756개의 입력값을 생성했고, 나중에 본 Opus 실행에서 나온 테이블의 경우 최대 코퍼스 크기는 119만 개의 문자 단위였습니다. v5는 4/4로 통과했습니다.
v9 및 v11: 오라클은 변경 사항이 되어서는 안 된다
그러자 강력한 모델이 입력값 생성기가 스스로 고칠 수 없는 것이 무엇인지 보여주었습니다. 이것은 pyinfra에 대한 gpt-5.6-sol 실행의 자체 회귀 보고서에서 가져온 것입니다:
비교는 해당 규칙을 구현하는 독립적인 참조 포매터를 사용합니다
...
- 입력값 (inputs): 1,200
...
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기

