
모델이 코드로 엑셀 파일을 만들면 실행은 종료 코드 0으로 끝나고 모델은 「완성했습니다」라고 답하는데, 정작 Excel은 그 파일을 열지 못할 수 있습니다. LET 수식의 매개변수 이름 앞에 붙어야 할 _xlpm. 접두사가 빠지면 생성 라이브러리는 오류 없이 저장하지만 Excel은 파일 열기를 거부합니다. 파일이 열려도 문제는 남습니다. 다섯째 슬라이드의 제목이 상자를 넘쳐도 파일 구조에는 아무 잘못이 없습니다. 모델은 자기 파일을 열어 보지 못해 어느 쪽도 알아차리지 못합니다.
두 결함은 보이는 곳이 다릅니다. 열리는지는 파일의 XML을 읽어야 알 수 있고, 모양이 맞는지는 페이지를 그려 봐야 알 수 있습니다. 그래서 확인도 둘로 나눴습니다. 검증기가 파일 구조를 읽어 열리지 않을 결함이 있으면 사용자에게 내보내지 않습니다. 통과한 파일은 페이지 이미지로 그려 모델의 다음 입력에 붙이고, 모델이 그림을 보고 고칠지 정합니다. 이 글은 검증기를 포맷마다 어떻게 짰는지, 그림 확인을 모델이 부르는 도구로 두지 않은 이유가 무엇인지를 다룹니다.
열리지 않는 결함은 화면에 보이지 않고, 넘친 글자는 검증기가 보지 못한다
워드·파워포인트·엑셀 파일은 zip 안에 XML 파트와 파트 사이의 관계 정의를 묶은 것입니다. 앱이 파일을 거부하는 결함은 대부분 이 XML 안에 있습니다.
- DB나 PDF에서 가져온 문자열에 세로 탭 같은 제어 문자가 섞이면, 그 파트는 well-formed XML이 아니게 되고 앱은 파일을 열지 못합니다. 일부 생성 라이브러리는 XML 특수 문자 다섯 개만 바꾸고 제어 문자는 그대로 씁니다. 그래서 도구가 검증 전에 이 문자를 걷습니다(아래 3단계).
- 슬라이드 파트의 콘텐츠 타입 선언이 빠지면, 패키지의 기본값이 그 자리를 메워 타입이
application/xml로 바뀝니다. 공개 스키마 검증기와 python-pptx 모두 이 파일을 통과시켰습니다. - 엑셀의
_xlpm.,_xlfn.같은 수식 접두사는 Excel이 화면에서 지워 보여 주므로, 파일을 열어 눈으로 봐도 빠졌는지 알 수 없습니다._xlfn.이 빠진 셀은 열리기는 하지만#NAME?을 보입니다.
반대로 글자 넘침, 도형 겹침, 빈 슬라이드는 XML이 완벽해도 생깁니다. 한쪽 확인만으로는 다른 쪽 결함을 잡을 수 없습니다.
흔한 접근은 셋입니다. 모델이 파일 내용을 토큰으로 직접 쓰게 하면, XML 한 글자 실수가 곧 열리지 않는 파일입니다. 범용 OOXML 검증기 하나를 통과 조건으로 삼으면, 포맷에 따라 아무것도 잡지 못하거나 정상 파일을 막습니다(뒤에서 봅니다). 모델에게 「다 만들었으면 확인 도구를 불러라」라고 안내하면, 모델은 그 단계를 건너뜁니다. 작업 가이드를 먼저 불러오라는 안내가 거의 지켜지지 않았다는 측정은 「양식 복사·가이드 읽기는 도구가 하고, 모델에는 인자로 묻는다」에서 다룹니다.
모델은 코드를 쓰고, 파일이 실제로 생겼는지는 서버가 본다
모델은 파일 내용 대신 문서 라이브러리를 쓰는 코드를 씁니다. 새로 만들 때는 파워포인트가 pptxgenjs, 워드가 docx-js, 엑셀이 openpyxl, 한글이 python-hwpx입니다. 코드가 파일이 되기까지는 다음 단계를 거칩니다.
- 실행. 코드는 격리된 실행 사이드카에서 돌고, 결과 파일을 샌드박스 볼륨에 바로 씁니다.
- 파일 확인. 종료 코드 0은 성공의 근거가 되지 못합니다. 모델이 오류를
catch로 받아 출력만 하면 실패해도 0으로 끝나고, 파일 저장을 기다리지 않으면 저장 전에 프로세스가 끝납니다. 그래서 파일이 실제로 생겼는지를 봅니다. - 문자 정리. 만든 파일의 XML 파트에서 XML이 실을 수 없는 문자(탭·개행을 뺀 제어 문자, U+FFFE 같은 비문자)를 걷습니다. 생성 라이브러리가 이 문자를 그대로 써도 파일은 열립니다. 걷은 문자는 본문에서 사라지므로, 작업 가이드는 글자 배치가 중요한 텍스트를 스크립트가 넣기 전에 직접 정리하라고 안내합니다. 엑셀은 openpyxl이 셀에 그 문자를 넣는 순간 거절해 스크립트가 멈추므로, 가이드가 넣기 전에 걸러 내는 코드를 함께 줍니다.
- 정규화. 파워포인트에만 적용합니다. 생성 라이브러리가 매번 같은 자리에 남기는 결함(차트 축 개수, 요소 순서 같은 것)을 고칩니다.
- 검증. 포맷별 검증기가 문제를 등급과 함께 찾습니다. 치명 등급이 하나라도 있으면 그 파일은 사용자에게 가지 않습니다.
치명 결함이 나오면 도구는 실패를 돌려주면서 문제마다 위치와 고칠 방향을 함께 싣습니다. XML이 깨졌다면 어느 파트의 몇째 줄 몇째 글자인지까지 알려 줍니다. 모델은 이 결과를 읽고 코드를 고쳐 다시 만듭니다. 같은 이름의 파일이 이미 그 자리에 있었다면 원래 파일을 되돌려 놓습니다. 앞서 사용자에게 건넨 파일이 그 경로를 가리키고 있어서, 거부된 결과를 남기면 사용자는 거부된 파일을 내려받게 됩니다.
범용 검증기의 결과는 포맷마다 다르게 쓴다
모든 포맷에서 XML 파싱과 패키지 검사(콘텐츠 타입, 관계가 가리키는 파트가 실제로 있는지)를 먼저 합니다. 파싱은 깨진 위치를 알려 주는 유일한 검사입니다. 공개 스키마 검증기(Open XML SDK의 검증 로직을 파이썬으로 옮긴 openxml-audit)도 깨진 사실은 알아채지만, 어느 줄인지는 알려 주지 않습니다. 그 뒤로는 포맷마다 다릅니다.
| 포맷 | 스키마 검증기 | 자체 검사의 예 |
|---|---|---|
| 파워포인트 | 씀. 정해진 2건만 뺀다 | 차트 축 참조, 내장 통합 문서 |
| 워드 | 씀 | 변경 내용 추적의 작성자 누락, 문단 속성 순서 |
| 엑셀 | 쓰지 않음 | 수식 접두사, 공유 문자열 번호, 동적 배열 수식, 수동 계산 모드 |
| 한글(hwpx) | 전용 검증기 | 컨테이너 규약, 본문 파트 도달 여부 |
파워포인트는 정규화로 스키마 오류를 줄입니다. 세 장짜리 덱에서 스키마 오류 7건이 나왔고 정규화 뒤 2건이 남았습니다. 남은 2건은 프레젠테이션 파트의 자식 요소 순서입니다. 이 순서를 ECMA 표준대로 바꾸면 검증기 오류는 0건이 되지만, PowerPoint 16.0은 그 파일을 열지 못했습니다. 원래 순서의 파일은 정상으로 열립니다. 그래서 이 2건은 고치지 않고 검증 결과에서 뺍니다. 단, 두 건이 정확히 그 모양으로 함께 나올 때만 뺍니다. 같은 요소 이름의 오류는 다른 순서 사고에서도 나오기 때문에, 하나라도 다르면 전부 보고합니다.
엑셀에는 스키마 검증기를 쓰지 않습니다. 대조해 보니 이 검증기는 실제로 영향이 있는 결함 9종 가운데 하나도 잡지 못했습니다. Excel이 열기를 거부하는 파일(_xlpm. 누락)과 정상 파일에 똑같은 진단을 냈고, openpyxl로 만든 파일 전부에 스타일 파트의 요소 순서로 오류를 냈습니다. 정상 파일과 깨진 파일을 가르지 못하는 검사는 통과 조건이 될 수 없어서, 엑셀은 자체 검사만으로 판정합니다. 동적 배열 수식이 그 예입니다. openpyxl에는 스필 메타데이터를 쓰는 API가 없어, 결과가 왼쪽 위 셀 하나에만 채워지고 오류는 0건입니다. 자체 검사는 이것을 잡습니다.
한글 문서는 hwpx로 만듭니다. hwpx는 OOXML이 아니라 ODF 계열의 OCF 컨테이너라 콘텐츠 타입 파트도 관계 파일도 없습니다. 그래서 검증기를 따로 두고, 결과의 모양과 「치명이면 막는다」는 규칙만 함께 씁니다. 스키마 경고는 막지 않습니다. 공개된 OWPML 스키마가 한컴 오피스의 실제 동작과 갈리는 자리가 있어서, 라이브러리도 스키마 위반을 경고로 냅니다. 실제 hwpx 4종(정부 서식 2, 사내 보고서 2)과 새로 만든 문서에서 스키마 검사 결과는 모두 0건이었습니다.
막는 것은 열지 못하는 파일뿐이고, 나머지는 모델에게 알린다
검증기가 찾은 문제는 네 등급으로 나눕니다.
| 등급 | 뜻 | 처리 |
|---|---|---|
| 치명 | 앱이 파일을 열지 못하거나 복구 창을 띄운다 | 내보내지 않는다 |
| 다른 앱 위험 | 주 앱은 열지만 LibreOffice나 Keynote 같은 다른 앱에서 깨진다 | 내보내고 모델에게 알린다 |
| 조용한 오류 | 열리고 오류도 없지만 내용이 의도와 다르다(#NAME?, 왼쪽 위만 찬 배열 수식) |
내보내고 모델에게 알린다 |
| 허용 | 규격 위반이지만 확인한 앱이 모두 연다 | 기록만 남긴다 |
막는 기준은 「주 앱이 이 파일을 여는가」 하나입니다. 치명이 아닌 문제는 파일이 열리므로 막지 않고, 문제마다 등급·위치·고칠 방향을 도구 결과에 실어 모델에게 돌려줍니다. 수동 계산 모드가 남은 통합 문서라면 「Excel이 다시 계산하지 않아 옛 값을 보인다」는 사실과 그 설정을 지우라는 방향이 함께 갑니다. 고칠지는 그 결과를 읽은 모델이 정합니다.
편집은 원본 사본과 견줘 새로 생긴 문제와 줄어든 구조를 본다
남이 준 파일을 편집할 때는 문제의 개수로 판정하지 않습니다. 사내 양식 12개를 검사하자 9개(계약서 6개, 프레젠테이션 템플릿 3개)가 손대기 전부터 치명 결함을 갖고 있었고, 한 파일은 411건이었습니다. 개수로 막으면 편집이 완벽해도 늘 되돌려집니다. 그래서 편집 전에 원본 사본을 떠 같은 검증기에 넣고, 편집 뒤에 새로 생긴 문제만 셉니다. 원래 있던 문제는 막지 않되 몇 건인지 한 줄로 모델에게 알립니다. 모르면 모델은 그 문제를 자기 편집 탓으로 알고 엉뚱한 곳을 고칩니다. 목록 대신 요약으로 싣는 것은 수백 건이 컨텍스트를 채우지 않게 하려는 것입니다.
검증기가 보지 못하는 손실도 있습니다. 편집 코드가 원본의 구조를 지워도 결과 XML은 유효할 수 있습니다. python-docx에서 셀의 텍스트를 바꾸는 속성은 셀 안의 문단뿐 아니라 표까지 지웁니다. 이 방식으로 값 칸 안의 중첩 표 두 개(선택지 17개)가 사라진 문서도 검증 결과는 0건이었습니다. 스키마는 무엇이 있어야 하는지만 말하고, 무엇이 있었는데 사라졌는지는 말하지 못합니다. 그래서 같은 원본 사본과 구조 노드 수를 견줍니다. 워드는 표·행·문단을, 파워포인트는 슬라이드·문단·텍스트 런을 셉니다. 양식을 채우면 글자는 늘기 마련이라 글자 수는 세지 않습니다. 엑셀도 세지 않습니다. openpyxl은 저장할 때 통합 문서를 통째로 다시 쓰므로, 정상 편집에서도 구조가 줄어든 것처럼 보입니다. 줄어든 구조는 막지 않고 전후 수치만 모델에게 알립니다. 「이 표를 지워라」는 정당한 지시일 수 있고, 지시를 아는 것은 모델입니다.
렌더 이미지는 모델이 부르지 않아도 다음 라운드 입력에 붙는다
검증을 통과한 파일도 글자가 넘치거나 도형이 겹친 채 나갈 수 있습니다. 그래서 오피스 파일을 쓰는 데 성공한 라운드(모델을 한 번 부르고 그 응답의 도구 호출을 실행하기까지)가 끝나면, 렌더 사이드카가 그 파일을 페이지 이미지로 그리고 그 이미지를 도구 결과 뒤에 붙여 다음 호출의 입력으로 넣습니다. 모델은 이미 그림을 본 상태에서 고칠지만 판단합니다. 이 단계는 이미지를 읽을 수 있는 모델을 쓰면 늘 돕니다. 렌더 방식은 「막대를 모델이 그리면 숫자와 어긋나도 모른다, 차트는 화면이 그린다」에서 다룹니다.
보여 주는 쪽수는 두 가지로 정해 둡니다.
| 대상 | 기본 쪽수 | 설정 키 |
|---|---|---|
| 모델이 이번 턴에 새로 만든 파일 | 30쪽 | officeReview.createdMaxPages |
| 사용자가 준 파일을 편집하거나 읽을 때 | 3쪽 | officeReview.maxPages |
새로 만든 파일을 앞 3장만 보여 주면, 모델은 4장 이후의 넘침과 겹침을 보지 못한 채 완성했다고 답합니다. 자기가 방금 만든 파일이고 고칠 코드도 손에 있으니, 보여 주지 않은 쪽의 결함은 끝내 잡히지 않습니다. 사용자가 준 계약서는 수십 쪽일 수 있고, 다 그리면 컨텍스트가 그림으로 찹니다. 편집에서는 편집 전과 픽셀로 대조해 바뀐 쪽만 보여 주고, 앞쪽 몇 장은 전후를 나란히 보여 줍니다.
같은 파일을 다시 쓰면 그때마다 다시 보여 줍니다. 실측에서 모델은 파일 하나를 평균 3.22번 고쳐 썼는데, 파일마다 한 번만 보여 주면 대개 마지막 결과가 확인을 받지 못합니다. 렌더와 수정이 끝없이 이어질 걱정은 한 턴의 최대 반복 횟수와 시간 예산이 이미 막습니다.
설계에서 고른 것
파워포인트에는 스키마 오류 2건을 남겨 둡니다. 검증기 0건을 목표로 삼으면 PowerPoint가 거부하는 파일이 나오기 때문입니다. 대가로 「검증기 0건」을 통과 조건으로 쓸 수 없고, 빼는 2건을 정확한 모양으로만 인정해야 합니다.
엑셀에서 스키마 검증기를 뺀 대가로 판정은 자체 검사가 혼자 맡습니다. 자체 검사에 없는 종류의 결함은 잡지 못하므로, 새 결함을 발견하면 검사를 하나씩 더해야 합니다. 정상 파일과 깨진 파일에 같은 답을 내는 검사를 두는 것보다 이쪽이 낫다고 판단했습니다.
막는 기준을 「주 앱이 여는가」 하나로 두었습니다. 대가로 다른 앱에서 깨질 파일, 일부 셀이 #NAME?인 파일도 모델이 고치지 않으면 그대로 나갑니다. 한글 문서의 스키마 경고를 막지 않은 것도 같은 기준입니다. 공개 스키마와 한컴 오피스가 갈리는 자리에서 막으면, OwlAI가 만들지 않은 규격 차이 때문에 한글에서 잘 열리는 문서가 막힙니다.
편집 판정은 원본에 기준을 둡니다. 원래 결함이 있던 양식은 편집을 통과하고, 그 결함은 산출물에 그대로 남습니다. 구조가 줄어든 편집을 내보낼지도 모델의 판단에 맡겼습니다.
그림 확인을 늘 붙이는 대가는 컨텍스트입니다. 페이지 이미지가 라운드마다 입력을 키우므로, 고칠 수단이 있는 새 파일에 쪽수를 쓰고 남이 준 파일은 3쪽으로 줄였습니다.
검증기가 답하는 것은 파일이 열리는가까지입니다. 표에 들어간 숫자가 맞는지는 그 숫자를 조회한 SQL이 정합니다. 모델이 코드로 파일을 만드는 에이전트라면, 파일이 생겼는지와 열리는지는 서버가 확인하고, 화면에 어떻게 보이는지는 그림으로 모델에게 돌려주는 것부터 시작할 수 있습니다.
어떤 문제를 해결하나요?
도구와 실행의 다른 글

엔지니어링 노트 · 도구와 실행
대화를 다시 돌리면 실수까지 반복된다, 단계와 성공한 SQL만 남긴다
지난주 매출을 DB에서 뽑아 부서별로 묶고 엑셀로 만드는 일이 채팅에서 한 번 잘 풀렸다고 해 보겠습니다. 그 대화에는 결과만 있지 않습니다.

엔지니어링 노트 · 도구와 실행
스크립트 안에서 부른 도구도 호출마다 권한과 승인을 확인한다
「지난주에 열린 이슈마다 고객사 계약을 조회해서, 계약이 끝난 고객사의 이슈에만 담당자에게 메일을 보내 줘」라는 일은 앞 결과로 다음 인자를 만드는 호출이 사슬로 이어집니다.

엔지니어링 노트 · 도구와 실행
양식 복사·가이드 읽기는 도구가 하고, 모델에는 인자로 묻는다
사내 양식으로 견적서를 만드는 일은 두 단계입니다. 양식을 새 경로로 복사하고, 그 사본에 값을 채웁니다. 복사를 모델이 따로 부르는 도구로 두면 모델은 이 단계를 자주 건너뜁니다.
