미리디 엔진팀 프론트엔드 개발자가 해커톤에서 하루 만에 만든 Prism Lens를 예로, AI 출력의 예측 가능성을 어떻게 확보했는지 정리한 글이다. Prism Lens는 이미지 한 장을 편집 가능한 레이어로 분해하는 도구다. 프롬프트에 쓸 수 있는 도형 다섯 개를 명시하고 목록 밖 도형 금지 문장까지 넣었는데도 모델은 hexagon을 답했다. 저자는 fallback으로 덮는 대신 이미 있던 zod 스키마를 JSON Schema로 바꿔 OpenAI Structured Outputs strict로 넘겨 생성 단계에서 막았다.
핵심 포인트- 프롬프트도 설계 문서도 AGENTS.md도 모두 컨텍스트에 텍스트로 들어가므로 모델이 무시할 수 있고, 문서 스스로는 아무것도 막지 못한다
- 필드에 fallback을 거는 대응은 흐름은 살리지만 모델이 계속 엉뚱한 값을 낸다는 신호까지 덮어 잘못된 의미가 정상 데이터처럼 흘러간다
- 같은 스키마 하나가 사람이 읽는 규약이자 모델 생성을 매는 형식이자 런타임이 닫는 문 역할을 한다
- OpenAI 자체 평가에서 프롬프트 지시만으로는 복잡한 스키마 준수율이 40%를 밑돌지만 strict에서는 100%다
- 검증을 parse() 한 줄에 모으면 그 아래 코드는 자기가 AI 제품의 일부라는 걸 몰라도 된다
- 스키마가 형태를 붙들고 나면 프롬프트에 남는 건 무엇을 무엇으로 볼 것인가 하는 분류 기준뿐이다
상세 정리- 문제 발생: 도형 종류 자리에 스키마에 없는 hexagon이 들어와 로직이 멈추거나 원치 않은 결과물이 나왔다
- 첫 대응의 한계: 그 필드를 fallback인 사각형으로 흘려보내면 장애는 막지만 모델이 어긋난 값을 낸다는 사실 자체가 가려진다
- 두 층 구조: 읽히는 요청과 못 지나가게 막는 강제는 다르며, 코드에서 biome·oxlint가 맡던 자리를 AI 출력에서는 스키마가 맡는다
- 경계선: PrismGptManifestSchema.parse(JSON.parse(content)) 한 줄 위는 AI의 영역이고 아래는 평범한 코드다
- 결과: 그 아래에서 도형을 그리고 이미지를 자르고 배경을 지우고 레이어를 조립하는 코드 어디에도 모델 방어 로직이 없다
- 참고한 설계: manifest를 검증한 뒤 결정론적으로 조립하는 모양은 Vercel Labs의 json-render 접근에서 영향을 받았다
- 전환 경위: 초기에는 형식만 맞으면 통과시키는 JSON mode였고 요구하지 않은 응답을 받고 나서야 strict로 바꿨다
- 역할 분담: 형태는 structured outputs가 막고, 값은 런타임 zod가 받고, 의미는 프롬프트가 잡는다
- 분류 실패 비용: 테스트 이미지 한 장이 도형 41개로 쪼개지는데 초기 프롬프트가 처리 경로를 못 갈라 대부분을 재생성으로 보냈고 이미지 생성 호출이 한 장에 20~40번까지 불어났다
- 첫 원칙: 프롬프트 맨 위에 크기가 역할을 결정한다는 규칙을 두고, 스타일로 짐작하기보다 크기와 레이어 순서 같은 기하학적 사실을 먼저 보게 했다
- crop과 generate 분리: 로고나 사진처럼 정체성이 중요한 조각은 원본에서 그대로 잘라 오고 장식·배경처럼 다시 그려도 되는 것만 재생성에 맡겼다. 둘은 스키마에서 다른 타입이라 보존할 조각이 재생성 규칙으로 검사될 일이 없다
- 호출 감축: 배경 제거와 화질 보정을 브라우저에서 도는 로컬 라이브러리로 옮기고 generate를 코드가 crop으로 바꾸는 변환을 더해, 한 장 분해에 드는 모델 호출을 7~9회에서 1~2회로 줄였다
- 도구 관점: 새 프레임워크를 꺼내지 않았고 structured outputs는 이미 알려진 API 기능이며, 새로 들인 라이브러리도 모델 쪽이 아니라 모델의 일을 가져오는 쪽이었다
- 일반화: 함수와 클래스의 시그니처도 사람이 읽는 설명이자 컴파일러가 강제하는 규약이고 이제는 에이전트가 그 위에서 작업하는 계약이라고 본다
왜 읽나LLM 출력을 코드에 태울 때 문서·프롬프트·검증을 각각 어디까지 맡길지 판단 기준을 얻을 수 있다.