🚀 요약
SUMMARY
내가 만든 CLI 도구(Deck)는 작업 절차를 “워크플로우”라는 YAML 파일에 적어두고 실행하는데, 이 YAML을 손으로 쓰기가 번거로워 “자연어로 요청하면 워크플로우 파일을 대신 만들어주는” AI 기능을 붙였다. 그런데 결과물에서 매번 다른 문제가 터져 파이프라인을 세 번 갈아엎었다. 모델이 직접 생성(결과물이 깨짐) → 정합성을 코드로(모델이 정작 결과물에 도달 못 함) → 코드가 울타리 친 채 모델이 실제 파일을 만짐. 이 글은 그 세 번의 시행착오와 매번 무엇이 문제였는지의 기록이다.
“AI가 파일을 만들게 한다”는 게 생각보다 어려웠다. 어려운 건 모델이 아니라 그 둘레를 어떻게 두르느냐였다.
1. 먼저: 요청을 성격별로 갈랐다
“AI에게 맡긴다”를 한 덩어리로 두지 않았다. 요청의 의도를 먼저 분류해 길을 나눴다.
- 질문에 답하기 / 기존 파일 설명하기 / 워크스페이스 검토하기 → 읽기만 하는 경로
- 새 워크플로우 만들기 / 기존 것 고치기 → 파일을 쓰는 경로
- 계획만 세우고 파일은 안 쓰기 → 읽기 전용 계획 아티팩트
읽기 경로와 쓰기 경로를 가른 게 첫 설계였다. 답만 하면 되는 요청이 실수로 파일을 건드리는 일이 없어진다. 그리고 요청이 너무 모호하면 추측해서 지어내지 않고 되묻는다. 아래 세 번의 갈아엎기는 전부 “파일을 쓰는 경로”를 어떻게 굴리느냐의 이야기다.
2. 1세대: 모델이 파일을 통째로 생성하게 했다
무엇을 했나. 제일 단순한 형태. 모델에게 “파일 경로와 YAML 내용을 통째로 내놔”라고 시키고, 코드는 그 결과를 검증해서 통과하면 디스크에 썼다.
프롬프트 → 모델이 (파일경로 + YAML 전체) 생성 → 코드가 검증 → 통과 시 기록결과물에 터진 문제. 스키마가 어긋나거나(있지도 않은 필드, 잘못된 타입), 경로 규칙을 안 지키거나, 존재하지 않는 스텝 종류를 지어내는 일이 잦았다. 검증에서 걸러내긴 하는데, 걸러낸 다음 할 수 있는 게 “될 때까지 다시 시켜보기”밖에 없어서 실패율이 높고 결과가 들쭉날쭉했다.
그래서 왜 바꿨나. 겨눈 과녁(실제 워크플로우 파일)은 옳았다. 하지만 워크플로우의 정확성을 거의 통째로 모델에게 떠넘긴 게 문제였다. 모델이 조금만 헛디디면 결과가 깨졌다. 정확성을 모델 밖으로, 코드 쪽으로 끌어와야겠다고 판단했다.
3. 2세대: 정합성을 코드로 끌어왔다
무엇을 했나. 모델이 YAML을 바로 뱉게 두지 않고, 중간 표현(IR)을 거치게 했다. 계획을 세우고(planning), 그 계획을 검토하고(review), 애매하면 확인하고(clarification), 틀리면 고치는(repair) 단계를 넣었다. 생성은 raw YAML 대신 정해진 빌더 고르기로, 수정은 자유 편집 대신 정해진 변환 후보 고르기로 좁혔다. 그리고 코드가 그 중간 표현을 워크플로우 파일로 컴파일했다.
결과물에 터진 문제. 구조적 통제력은 확실히 좋아졌다. 그런데 실패 지점이 옮겨갔을 뿐이었다. 모델이 좋은 워크플로우에 도달하기도 전에, 그 앞단의 “중간 표현 계약(스키마)“을 못 맞춰서 먼저 엎어졌다. 워크플로우 오류가 중간 계약 오류로 바뀐 것이다. 게다가 단계가 늘어나 파이프라인 자체가 무거워지고, 새 워크플로우 문법을 추가할 때마다 중간 표현·빌더·컴파일러를 다 손봐야 했다.
그래서 왜 바꿨나. 정확성을 코드로 옮긴 방향은 맞았다. 하지만 모델을 정작 해야 할 일(실제 파일 편집)에서 너무 멀리 떼어놓았다. 중간 표현의 우아함을 다듬느라, 나와야 할 결과물과의 거리가 오히려 벌어졌다.
4. 3세대: 코드가 울타리를 친 채 모델이 실제 파일을 만진다
무엇을 했나. 모델을 다시 실제 파일 가까이 붙이되, 이번엔 가드레일을 코드가 강하게 쥐었다. 모델에게는 실제 워크스페이스 위에서 도는 작은 도구 몇 개만 준다 — 파일 찾기·읽기·쓰기·수정·검증. 그 이상은 못 한다.
요청 → 정규화·워크스페이스 점검 → 경로 분기
├─ 질문/설명/검토 → 분석 모드 (읽기 전용)
├─ 계획 → 계획 아티팩트만 저장 (읽기 전용)
└─ 생성/수정 → 저작 모드
→ 제한된 도구 루프 (찾기·읽기·쓰기·수정·검증)
→ 후보 상태에만 반영 → 검증(lint) 통과 → 그때서야 디스크에 기록여기서 결과물 문제를 어떻게 막나. 앞선 두 세대의 교훈이 전부 장치로 들어갔다.
- 쓰기가 즉시 디스크로 가지 않는다. 모델의 쓰기·수정은 세션이 소유한 후보 상태만 바꾼다. 세션이 성공으로 끝나야 코드가 진짜 파일을 쓴다. 중간에 잘못돼도 실제 워크스페이스는 안 더럽혀진다(1세대의 “깨진 파일이 그대로 남는” 문제 차단).
- 완료는 검증이 문을 지킨다. 모델이 “끝났다”고 해도 후보가 lint를 통과 못 하면 그 완료 신호를 거부한다. 정확성 판단을 모델에게 안 맡긴다(1세대 문제 차단).
- 루프에 예산이 있다. 검증 통과로 끝나거나, 진짜 막히면 되묻거나, 턴 예산이 소진되면 멈춘다. 무한히 헤매지 않는다.
- 모델은 실제 파일을 본다. 중간 표현 계약을 넘길 필요 없이 진짜 파일을 읽고 고친다(2세대 문제 차단).
핵심은 “모델은 실제 파일을 만지고, 코드는 경계·검증·기록을 쥔다”는 역할 분담이다. 1세대(모델이 다 함)와 2세대(코드가 중간 표현을 다 통제함) 사이에서, 각자 잘하는 것만 맡긴 셈이다.
5. 세 번 갈아엎어도 안 바뀐 원칙
파이프라인 모양은 세 번 뒤집혔지만, 매번 살아남아 오히려 또렷해진 원칙이 있다. 사실상 이게 결론이다.
- “정답”은 AI 밖에 둔다. 워크플로우 경로·스키마·검증 규칙 같은 진실은 도구 본체가 갖고, AI는 그걸 참조만 한다. AI가 규칙 자체를 지어내게 두지 않는다.
- 범위와 검증은 코드가 쥔다. 어떤 파일을 건드릴 수 있는지, 결과가 유효한지는 모델 판단에 안 맡긴다.
- 진짜 모호하면 추측 말고 되묻는다. 그럴싸하게 지어낸 결과보다 확인 한 번이 낫다.
- 품질은 최종 결과물로 잰다. 중간 단계가 얼마나 정교한지가 아니라, 나온 워크플로우가 실제로 맞는지로 판단한다. 2세대가 준 뼈아픈 교훈이다.
6. 곁가지: 외부 AI를 쓸 때의 제약
폐쇄망을 겨냥한 도구라, AI 파이프라인이라고 외부 API에 다 던질 수 없었다. 제약이 몇 겹 붙는다.
- 프로바이더 추상화. 특정 벤더에 묶이지 않게 갈아끼운다. OpenAI 호환 방식으로 여러 제공자를 받고, 모델·엔드포인트는 설정으로 저장하거나 명령마다 덮어쓴다. 폐쇄망이면 사내 게이트웨이 엔드포인트를 꽂는 식이다.
- 나가기 전에 시크릿을 가린다. 모델이나 로그로 흘러나가는 출력에서 알려진 시크릿 문자열을
***로 마스킹하는 층을 뒀다(문자열이 청크 경계에 걸쳐도 잡도록). AI에 컨텍스트를 주다 토큰이 같이 새는 걸 막는다. - 외부 조회는 선택이고 제한적이다. 도구가 아는 로컬 규칙이 우선이고, 외부 근거(설치 절차·버전별 호환성 같은 신선한 사실)는 필요할 때만 가져온다. 매번 미리 긁어오지 않고 루프 안의 선택적 도구로만 노출한다.
남은 것
“AI 기능을 붙인다”는 건 결국 파이프라인을 어디까지 모델에게 맡기고 어디부터 코드가 쥘지를 정하는 일이었다. 다 맡기면 결과물이 깨지고, 다 통제하면 모델이 결과물에 도달을 못 한다. 세 번 갈아엎고 내린 답은 “모델은 실제 과녁 가까이, 코드는 경계와 검증”이다. 어떤 모델을 쓰냐보다 이 역할 분담이 먼저였다. 다음 숙제는 이 저작 런타임을 워크플로우 생성 말고 다른 작업에도 재사용할 수 있게 떼어내는 것이다.