요약

SUMMARY

내가 만든 CLI 도구(Deck)는 작업 절차를 “워크플로우”라는 YAML 파일에 적어두고 실행하는데, 이 YAML을 손으로 작성하기가 번거로워 “자연어로 요청하면 워크플로우 파일을 대신 만들어주는” AI 기능을 추가했다. 그런데 결과물에서 매번 다른 문제가 발생해서 파이프라인을 세 번 재설계했다. 1세대는 모델 직접 생성(결과물이 깨짐), 2세대는 정합성을 코드로 이동(모델이 정작 결과물에 도달하지 못함), 3세대는 코드가 경계를 유지한 상태에서 모델이 실제 파일을 수정하는 방식이다. 이 글은 세 번의 시행착오와 매번 무엇이 문제였는지를 기록한다.

“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에 컨텍스트를 넘기는 과정에서 시크릿 토큰이 함께 유출되는 것을 막는다.
  • 외부 조회는 선택 사항이고 제한적이다. 도구가 이미 아는 로컬 규칙이 우선이며, 외부 근거(설치 절차·버전별 호환성 같은 최신 정보)는 필요할 때만 가져온다. 매번 미리 수집하지 않고 루프 안의 선택적 도구로만 노출한다.

7. 다음 숙제

세 번의 재설계 끝에 내린 답은 “모델은 실제 목표물 가까이, 코드는 경계와 검증을 담당”이라는 역할 분담이다. 어떤 모델을 사용하는가보다 이 역할 분담이 먼저였다. 다음 숙제는 이 저작 런타임을 워크플로우 생성 외에 다른 작업에도 재사용할 수 있도록 분리하는 것이다.

참고