Search

Harness Engineering

프롬프트가 아니라 하네스를 만들었다

AI 코딩 에이전트에 팀의 기준을 넘기기까지, 몇 달간의 기록
팀에 AI 코딩 에이전트를 도입하고 두어 달쯤 지났을 때, 뭔가 이상하다는 걸 느꼈다. 코드는 분명히 빨리 나오는데 내가 리뷰에 쓰는 시간은 줄지 않았다. 오히려 늘어난 것 같았다.
처음엔 적응 기간이려니 했다. 그런데 몇 주가 지나도 그대로였다. 그래서 최근 리뷰들을 다시 훑어봤다.
세 가지가 보였다. 사람마다 프롬프트가 달라서 같은 종류의 작업인데도 산출물의 모양이 제각각이었다. 누구는 API 응답 규격을 지켰고 누구는 안 지켰는데, 그 차이가 항상 리뷰 단계에 와서야 드러났다. AI가 쓴 API 문서가 실제 구현과 어긋나는 일도 주 1~2회씩 있었다. 문서만 보고 붙인 프론트가 통합 단계에서 깨지는 식이었다. 그리고 이 둘을 잡느라 리뷰어가 "이게 우리 컨벤션이 맞나"를 매번 처음부터 다시 확인하고 있었다.
생성 속도는 올라갔는데 검증 비용이 같이 올라가서, 전체 처리량은 그대로였던 것이다. 병목이 사라진 게 아니라 생성에서 검증으로 옮겨간 것뿐이었다.

프롬프트를 고치던 시기

한동안은 프롬프트를 고쳤다. 더 길게 쓰고, 예시를 넣고, "반드시 ~하라"를 굵게 강조하고, 잘 나온 프롬프트를 노션에 모아뒀다.
효과가 없진 않았다. 그런데 오래가지 않았다. 지금 돌아보면 이유는 하나였던 것 같다. 재현되지 않았다.
프롬프트는 개인의 자산이라 팀에 복제되지 않았다. 잘 쓴 사람의 결과만 좋았다. 그래서 좋은 프롬프트를 공유했는데, 사람들은 그걸 그대로 쓰지 않고 조금씩 고쳐 썼다. 당연한 일이다. 강제되는 게 아니니까. 나부터도 남의 프롬프트를 받으면 손을 댔다.
이쯤에서 내가 뭘 하고 있는 건지 다시 생각했다. 말을 잘 거는 법을 연구하고 있는 건가, 아니면 시스템을 설계하고 있는 건가.
후자라면 이건 처음 보는 문제가 아니다. 모델은 못 고친다. 하지만 모델에 무엇을 넣을지, 무엇을 할 수 있게 할지, 나온 걸 어떻게 판정할지는 전부 내가 정할 수 있다. 이 세 층을 통틀어 하네스라고 부르기로 했다. 원래 테스트 하네스에서 온 말인데, 대상 코드는 손대지 않고 감싸는 껍데기만 바꿔서 검증 강도를 조절한다는 점이 지금 상황과 정확히 겹쳤다.
프롬프트는 이번 대화를 좋게 만들고, 하네스는 모든 대화의 하한선을 올린다. 그 시점부터는 프롬프트 튜닝을 멈추고 하네스를 만들기 시작했다.

컨텍스트를 예산으로 보게 되기까지

첫 번째로 손댄 건 컨텍스트 레이어였다. CLAUDE.md를 썼다.
중요한 건 파일 형식이 아니라 위치라고 생각했다. 팀 컨벤션은 원래도 위키에 있었다. 문제는 그걸 아무도 매번 읽지 않는다는 거였다. 사람도 안 읽고 AI도 못 읽었다. CLAUDE.md는 에이전트가 작업을 시작할 때마다 자동으로 읽는다. 같은 내용을 "찾아봐야 하는 문서"에서 "항상 읽히는 문서"로 옮기기만 해도 준수율이 달라질 거라고 봤고, 실제로 달라졌다.
넣은 건 PR 형식, API 공통 응답 래퍼 규격, 에러 코드 체계, 디렉토리 구조와 레이어 간 의존 방향, 문서 템플릿의 목차 구조 같은 것들이었다.
그리고 여기서 한 번 크게 헛발질을 했다.
좋다고 생각한 걸 다 넣었다. 도메인 배경 설명, 과거 의사결정 히스토리, 예외 케이스 목록까지 밀어넣었다. 많이 알려주면 더 잘하겠지 싶었다. 그런데 파일이 길어지자 정작 중요한 규칙이 묻히기 시작했다. 응답 규격은 지키는데 디렉토리 규칙은 무시하는, 이상하게 선택적인 결과가 나왔다. 어떤 규칙이 살아남고 어떤 게 무시될지 예측이 안 됐다.
컨텍스트가 무한한 게 아니라 예산이라는 걸 그때 알았다. 아깝지만 절반 넘게 덜어냈다.
기준을 다시 세웠다. 모든 작업에 적용되는 것만 상주시키고, 특정 도메인에만 해당하는 내용은 뺐다. 가끔 필요한 건 본문 대신 "이럴 땐 이 문서를 읽어라"는 포인터만 남겼다. 그리고 설명체 문장을 다 지웠다. "우리 팀은 일관성을 중요하게 생각합니다" 같은 문장 말이다.
마지막 게 제일 도움이 됐다. 그 뒤로 지시문을 쓸 때마다 스스로에게 묻는 질문이 하나 생겼다. 이 문장을 어겼는지 아닌지를 기계가 판정할 수 있나. 판정할 수 없으면 그건 규칙이 아니라 정서 표현이고, 예산만 잡아먹는다.

"추측하지 마"라고 쓰던 걸 그만둔 이유

컨텍스트를 정리하고 나서도 스펙 불일치는 남았다. 줄긴 했는데 없어지진 않았다.
한동안은 지시를 더 강하게 썼다. 추측하지 마라, 확실하지 않으면 물어봐라, 코드를 확인해라. 대부분은 지켜졌다. 가끔 안 지켜졌다. 그 가끔이 주 1~2회였다.
어느 순간 이게 방향이 틀렸다는 생각이 들었다. 확률적으로 동작하는 시스템에 확률적으로 동작하는 지시를 하나 더 얹고 있었던 거다. 지시는 통제가 아니었다. 통제는 틀린 산출물이 다음 단계로 못 넘어가게 막는 절차였다.
그래서 문서 생성 작업을 커스텀 스킬로 감싸고, 그 안에 검증을 세 겹 넣었다.
템플릿을 먼저 고정했다. 문서의 목차와 필수 섹션을 스킬이 강제한다. 자유 형식으로 잘 쓰게 하는 것보다 빈칸을 채우게 하는 쪽이 훨씬 안정적이었다. 형식이 고정되니 빠진 항목을 기계적으로 검출할 수 있다는 게 예상 못 한 이득이었다.
그다음이 소스코드 대조 검증인데, 결과적으로 제일 효과가 컸다. 문서에 적힌 엔드포인트·파라미터·응답 필드를 실제 소스코드와 대조하게 했다. 문서엔 있는데 코드엔 없는 필드, 코드엔 있는데 문서엔 없는 필드가 나오면 그 자리에서 실패시킨다. 이걸 넣고 나서 주 1~2회씩 나오던 불일치가 사실상 사라졌다.
마지막은 내부 정보 누출 스캔이다. 외부 공개용 문서에 내부 서버 주소나 키 형태의 문자열이 섞여 나가지 않는지 본다. 사람이 보면 놓치기 쉽고 기계가 보면 확실한 종류의 검사다.
만들면서 한 가지는 지키려고 했다. 검증은 가능한 한 모델이 아니라 결정론적인 코드가 하게 하는 것. AI에게 AI 결과물을 검토시키는 방식이 처음엔 매력적으로 보였는데, 같은 입력에 같은 판정이 안 나오면 CI에 붙일 수가 없다. 판정이 확률적이면 그건 검증이 아니라 두 번째 의견일 뿐이다.
이걸 다 붙이고 나서 리뷰의 성격이 바뀐 걸 느꼈다. 형식이 맞나 스펙이 맞나를 보던 층을 규칙이 먼저 걸러주니까, 사람이 볼 게 "이 설계가 도메인에 맞나"만 남았다. 리뷰 시간이 줄었다기보다 리뷰가 쓸 만한 데 쓰이게 됐다는 쪽이 정확한 것 같다.

틀린 게 아니라 못 봤던 것

세 번째로 붙인 건 도구였다. MCP로 노션을 연결했다.
계기는 좀 우연이었다. AI가 잘못된 스펙을 쓴 케이스를 모아놓고 원인별로 분류해보다가, 대부분이 몰라서 틀린 게 아니라는 걸 알았다. 확인할 방법이 없어서 지어낸 거였다. 기획 문서는 노션에 있는데 에이전트는 그걸 못 본다. 그러면 주변 코드와 일반적인 관례로 그럴듯한 답을 만든다. 자료 접근이 막힌 사람도 비슷하게 행동한다.
노션을 도구로 붙이고 나니 지어내던 자리에서 찾아보기 시작했다. 사양을 조회하고, 확정된 정책을 인용하고, 작성한 문서를 다시 반영하는 흐름이 도구 호출로 이어졌다.
지금 생각하면 순서를 반대로 밟느라 시간을 꽤 버렸다. 에이전트가 자주 틀리는 영역이 있으면 지시를 강화하기 전에 그 영역의 사실에 접근할 수 있는지부터 봤어야 했다. 접근 경로가 없으면 어떤 프롬프트를 써도 추측이 나온다. 반대로 경로가 생기면 프롬프트를 거의 안 고쳐도 정확도가 올라간다.

결함을 앞으로 당기기

층을 다 깔고 나서는 개발 흐름 자체를 손봤다.
정책 문서 → 백엔드 설계 문서 → API 명세로 3단을 잡고, 앞 단계가 확정돼야 다음으로 넘어가게 했다. 40개쯤 되는 도메인, 120여 개 문서가 같은 구조로 정리됐다. 여기까지는 흔한 문서화 이야기다.
내가 실제로 효과를 본 건 그다음이다. 구현 전에 설계 문서를 적대적으로 리뷰시켰다. 잘 썼는지 봐달라고 하는 게 아니라, 이 설계가 실패할 시나리오를 찾아라, 동시 요청이 들어오면 어디가 깨지나, 이 정책과 저 정책이 충돌하는 케이스는 없나 하는 식으로 깨뜨리는 걸 목표로 시켰다.
처음엔 그냥 검토를 부탁했었다. 그랬더니 칭찬을 했다. 반박하라고 시켜야 반박한다. 프롬프트 요령 같지만 실은 검증 설계의 문제에 가깝다고 생각한다. 검증자는 생성자와 목표가 달라야 한다.
같은 결함이라도 문서 단계에서 잡으면 문단 하나 고치면 되고, 구현 후에 잡으면 코드와 테스트와 이미 붙은 프론트를 같이 고쳐야 한다. 결함 개수를 줄인 게 아니라 발견 지점을 앞으로 당긴 것뿐인데, 체감은 그쪽이 훨씬 컸다.

숫자와, 잘 안 된 것들

정량적으로는 문서 작성 시간이 70% 이상 줄었고, 주 1~2회씩 나오던 문서·실제 스펙 불일치가 사실상 0건이 됐다. 40개 도메인·120여 개 문서가 같은 기준을 통과하게 됐다.
그런데 이 숫자만 남기면 정직하지 않은 것 같다.
하네스도 유지보수 대상이다. 컨벤션이 바뀌면 CLAUDE.md도 바뀌어야 하고, 응답 규격이 바뀌면 검증 스킬도 같이 고쳐야 한다. 한동안 이걸 놓쳤더니 하네스가 낡은 기준을 강제하는 장치가 돼 있었다. 사람 리뷰어라면 "아 이건 예전 규칙이죠" 하고 넘어갈 걸 기계는 끝까지 막는다. 도움이 되던 게 방해가 되는 전환점이 분명히 있었다.
만드는 비용도 작지 않다. 프롬프트를 다듬는 건 몇 분이면 되는데 검증 스킬을 만드는 건 그렇지 않다. 한 번 하고 마는 작업이었다면 이걸 만들 이유가 없었다. 같은 종류의 작업이 여러 사람에 의해 반복될 때만 손익분기점을 넘는 것 같다. 우리 팀은 문서 작성이 딱 거기 해당했고, 그래서 됐다고 본다.
그리고 기계가 판정할 수 있는 것과 없는 것의 경계는 생각보다 빨리 나온다. 형식이 맞는지, 스펙이 코드와 일치하는지는 기계가 본다. 그런데 이 설계가 우리 도메인에 맞는 선택인지, 이 트레이드오프를 지금 감수하는 게 맞는지는 여전히 내 몫이다. 하네스는 판단을 대신해주지 않는다. 판단해야 할 것만 남겨준다.

결국 익숙한 문제였다

다 만들고 나서 든 생각은, 이게 새로운 일이 아니었다는 것이다.
백엔드를 하면서 늘 다루던 문제가 있다. 통제할 수 없는 외부 시스템에 의존해야 할 때 어떻게 할 것인가. 외부 PG는 가끔 타임아웃을 낸다. 외부 기기는 예고 없이 죽는다. 그럴 때 우리는 그 시스템이 완벽해지길 기다리지 않는다. 재시도를 넣고, 실패를 격리하고, 응답을 검증하고, 유실된 걸 회수할 경로를 만든다.
LLM도 그런 의존성이었다. 확률적으로 동작하고, 내부를 못 고치고, 대체로 맞지만 가끔 틀린다. 그렇다면 이미 아는 방법을 쓰면 되는 거였다. 입력을 통제하고, 사실에 접근할 경로를 주고, 출력을 결정론적으로 검증하고, 실패한 건 다음 단계로 못 넘어가게 막는 것.
"AI를 잘 쓴다"는 말을 프롬프트를 잘 쓴다는 뜻으로 이해하던 시기가 있었다. 지금은 불확실한 구성요소를 신뢰할 수 있는 시스템 안에 배치할 줄 안다는 뜻으로 읽고 있다. 그건 원래 하던 일이다.