콘텐츠로 건너뛰기
Codex

AI 초안을 실제로 실행해서 검증하는 파이프라인 만들기

AI 초안을 실제로 실행해서 검증하는 파이프라인 만들기

TEMPLATE · AUTOMATION OPS

AI가 쓴 초안 속 명령어를 실제로 실행해 검증하고, 검증 이후 원고가 바뀌면 업로드를 막는 구조를 정리했습니다.

실행해 본 초안만 올리게 막기

AI 초안을 바로 업로드하지 않으려고, 초안 안의 shell code block을 실제로 실행하는 게이트를 하나 뒀다. command_verifier.py가 마크다운에서 bash, sh, shell, zsh 블록만 골라 실행하고, 각 명령의 exit code와 출력 꼬리를 저장한다. 그리고 업로드 직전에는 검증 당시의 draft SHA와 지금 파일의 SHA를 다시 비교한다. 초안을 한 글자라도 고쳤으면, 예전에 통과한 검증으로는 못 올라가게 막는 구조다.

처음엔 “명령어는 사람이 눈으로 보면 되지 않나” 싶었다. 그런데 Codex CLI 글을 쓰다 보니 본문에 들어가는 명령이 생각보다 많았다. --help 옵션이 바뀌었는지, 내가 적은 서브커맨드가 진짜 있는지, 작업 폴더 기준이 맞는지 같은 게 글 안에서는 잘 안 보인다. 읽을 때는 그럴듯한데, 막상 터미널에 붙이면 바로 깨지는 종류다.

그래서 글 검수 흐름을 이렇게 잡았다.

단계 하는 일
초안 생성 AI가 마크다운 초안을 만든다
명령 검증 shell code block을 실제 셸에서 실행한다
결과 저장 exit code, stdout/stderr 꼬리, 검증 시각, draft SHA를 남긴다
사람 검수 텔레그램이나 로컬에서 글을 읽고 승인한다
업로드 직전 게이트 검증 통과 여부와 현재 draft SHA를 다시 본다
WordPress 초안 업로드 둘 다 맞을 때만 올라간다

여기서 제일 크게 바뀐 건 “검수했다”의 기준이다. 예전에는 내가 읽고 괜찮으면 검수였는데, 이제는 적어도 본문 명령어가 한 번은 실제로 돌아간 상태여야 한다.

command_verifier.py가 보는 건 shell code block이다

검증기는 글 전체를 이해하려고 하지 않는다. 마크다운을 훑다가 fenced code block을 찾고, 언어 이름이 shell 계열이면 실행 대상으로 본다. bash, sh, shell, zsh 정도만 잡는다.

이게 은근 중요했다. 블로그 글에는 설정 파일 예시나 출력 예시도 들어간다. 그런 것까지 전부 실행하려고 들면 오히려 파이프라인이 이상해진다. 내 경우에는 “실제로 독자가 터미널에 칠 명령”만 shell code block으로 두고, 출력이나 구조 설명은 표나 일반 문장으로 빼는 쪽이 덜 헷갈렸다.

검증 명령이 CLI에 붙어 있는지는 먼저 도움말로 확인했다. 이건 상태를 바꾸지 않아서 본문에 넣어도 부담이 적다.

venv/bin/python manage.py verify-commands --help

이 명령은 실제 초안을 검증하는 명령은 아니고, verify-commands가 어떤 인자를 받는지 보는 용도다. 출력상으로는 --id가 필수다. 실제 파이프라인에서는 글 번호가 붙은 토픽에 대해 verify-commands를 돌린다.

토픽 상태도 같이 본다. 초안이 아직 queued 상태면 검증할 draft 파일이 없을 수 있으니까, 먼저 목록에서 상태를 확인하는 편이 낫다.

venv/bin/python manage.py list

여기서 drafted, reviewed, uploaded 상태인 글만 명령 검증 대상이 된다. 아직 queued면 글이 만들어지지 않은 상태라서 검증할 게 없다. 처음엔 이걸 모르고 “왜 검증이 안 되지” 쪽으로 봤는데, 그냥 상태가 안 맞는 경우가 있었다.

exit code만 남기는 게 아니라 출력 꼬리도 남긴다

검증 결과에는 단순히 성공/실패만 들어가지 않는다. 각 command block마다 이런 정보가 붙는다.

필드 의미
index 글 안에서 몇 번째 shell block인지
command 실제 실행한 명령
exit_code 셸 실행 결과 코드
stdout_tail 표준 출력의 마지막 일부
stderr_tail 표준 에러의 마지막 일부
skipped 실행하지 않고 건너뛴 명령인지
skip_reason 왜 건너뛰었는지

출력 전체를 다 저장하지 않고 꼬리만 남긴 것도 현실적인 선택이었다. --help 같은 명령은 출력이 길다. 전부 저장하면 topics 파일이 금방 지저분해진다. 실패를 볼 때는 보통 마지막 몇 줄이면 충분했다.

실패한 명령은 exit_code가 0이 아니게 남는다. 실행 시간이 너무 길어지면 타임아웃으로 보고 124를 남긴다. 기본 타임아웃은 30초다. 블로그 본문에 들어갈 명령이 30초 안에 끝나지 않는다면, 그건 애초에 독자에게 바로 실행하라고 줄 명령인지 다시 봐야 했다.

위험한 명령은 실행하지 않고 기록만 한다

검증기를 만들 때 제일 애매했던 부분이 이거였다. “본문 명령은 다 실행한다”고 해놓고 rm, git reset --hard, tmux kill-session 같은 걸 실제로 돌리면 검증기가 아니라 사고 장치가 된다.

그래서 실행하면 상태를 바꾸거나 끝나지 않을 가능성이 큰 명령은 건너뛰게 했다. 예를 들면 이런 종류다.

명령 패턴 건너뛰는 이유
tail -f 계속 따라가며 멈추지 않는다
watch, top, less, vim 대화형이거나 끝나지 않는다
tmux new, tmux kill-session, tmux attach 세션을 만들거나 없앤다
launchctl load, launchctl unload 서비스를 올리거나 내린다
rm -... 파일을 지운다
mv, cp 파일을 옮기거나 덮어쓴다
git push, git reset --hard, git clean, git checkout 저장소 상태를 바꾼다
source, . 현재 셸 환경을 바꾼다
shutdown, reboot, killall, pkill 프로세스나 시스템을 종료한다

여기서 헷갈리기 쉬운 게 있다. 건너뛴 명령은 실패로 치지 않는다. 글에 tmux 명령이 나오는 것 자체가 잘못은 아니기 때문이다. 다만 자동 검증 셸에서 실제로 돌리기엔 위험하니, skipped와 이유를 남기고 사람이 보게 둔다.

처음엔 전부 실패 처리해야 더 엄격하지 않나 싶었다. 그런데 그러면 tmux나 launchd 글은 매번 검증을 통과할 수 없다. 자동으로 확인할 수 있는 것과 사람이 판단해야 하는 것을 나누는 편이 더 오래 간다.

draft SHA가 없으면 검증은 금방 낡는다

명령을 한 번 실행해서 통과했다고 끝이 아니다. 글은 검수 중에 계속 바뀐다. 문장 하나 고치다가 code block 안의 옵션도 같이 바뀔 수 있고, 반대로 명령은 그대로인데 앞뒤 설명만 바뀔 수도 있다.

내 파이프라인은 검증 결과에 초안 본문의 SHA256을 같이 저장한다. 업로드 직전에는 현재 draft 파일을 다시 읽어서 SHA를 계산한다. 둘이 다르면 업로드를 막는다.

조건은 단순하다.

조건 결과
command_verification.statuspassed 다음 조건을 본다
검증 당시 draft_sha256과 현재 draft SHA가 같음 업로드 가능
검증 결과가 없거나 실패 업로드 차단
검증 후 초안이 바뀜 업로드 차단

이 구조가 없으면 이런 일이 생긴다. 오전에 명령 검증을 통과했다. 오후에 글을 다듬다가 code block을 살짝 바꿨다. 그리고 사람은 “아까 검증했으니까 됐겠지” 하고 올린다. 이게 제일 싫었다. AI 초안은 그럴듯함이 문제라서, 검증도 그럴듯한 상태로 남아 있으면 안 된다.

업로드 경로마다 같은 게이트를 둔다

검증 게이트는 한 군데에만 있으면 금방 새어 나간다. 내 경우에는 WordPress로 올라가는 길이 둘이었다. CLI에서 직접 올릴 수도 있고, 텔레그램에서 승인해서 올라갈 수도 있었다. 이미 올라간 WordPress 초안을 로컬 draft로 갱신하는 경로도 있다.

그래서 업로드 쪽에서는 같은 조건을 반복해서 본다.

경로 막는 기준
upload reviewed 상태인지, 검증 통과인지, draft SHA가 같은지 본다
텔레그램 승인 drafted 상태인지, 검증 통과인지, draft SHA가 같은지 본다
update-draft uploaded 상태인지, 검증 통과인지, draft SHA가 같은지 본다

텔레그램 승인에서 막히면 메시지도 명확하게 나온다. 명령 검증이 안 됐으면 검증부터 다시 하라고 하고, 검증 이후 초안이 바뀌었으면 다시 검증하라고 한다. 여기서 상태를 애매하게 넘기지 않는 게 중요했다. “승인은 했는데 업로드는 안 됨” 같은 중간 상태가 생기면, 나중에 어느 글이 어디까지 갔는지 다시 뒤져야 한다.

본문 작성 규칙도 검증기에 맞춰 바뀐다

이 파이프라인을 붙이고 나니 글 쓰는 습관도 조금 바뀌었다. 예전에는 설명 중간에 명령 비슷한 걸 편하게 넣었다. 그런데 이제 shell code block에 들어간 건 실제 실행 대상이다. 그러니 code block에 넣는 순간 기준이 생긴다.

내가 쓰는 기준은 대충 이렇다.

본문에 넣을 것 처리 방식
독자가 그대로 실행해도 되는 명령 shell code block
환경마다 값이 달라지는 명령 문장으로 설명
경로, 키, 토큰 같은 placeholder가 필요한 예시 code block에 넣지 않음
출력 예시 shell code block이 아닌 일반 설명이나 표
실행하면 상태가 바뀌는 명령 정말 필요한 경우만 넣고, 검증기에서는 skipped로 남김

특히 placeholder가 문제였다. /path/to/projectYOUR_KEY 같은 걸 code block에 넣으면 보기에는 친절해 보인다. 그런데 자동 검증 관점에서는 실행 불가능한 명령이다. 결국 글의 규칙과 파이프라인의 규칙이 싸운다.

그래서 지금은 placeholder가 필요한 내용은 문장으로 푼다. 예를 들어 “--id 뒤에는 검증할 토픽 번호를 붙인다”처럼 쓴다. 코드 블록에는 실제로 돌아가는 명령만 둔다.

이게 글 품질을 자동으로 보장하진 않는다

명령 검증을 붙였다고 글이 좋은 글이 되는 건 아니다. 검증기는 명령이 실행되는지만 본다. 설명이 틀렸는지, 독자가 따라가기 쉬운지, 지금 이 명령을 이 위치에 넣는 게 맞는지는 사람이 봐야 한다.

그래도 한 가지는 확실히 줄었다. “명령은 맞겠지” 하고 넘어가는 일이 줄었다. 특히 AI가 만든 초안에서는 이 차이가 크다. AI는 없는 옵션을 꽤 자연스럽게 쓴다. 사람도 읽을 때는 놓친다. 그런데 셸은 그런 걸 봐주지 않는다. 실행하면 바로 실패한다.

내 경우에는 이 정도만 해도 업로드 전 불안감이 꽤 줄었다. 글 전체를 완벽하게 자동 검수하는 게 아니라, 깨지면 창피한 부분부터 먼저 막은 셈이다.

같이 나오는 질문

shell block이 하나도 없으면 어떻게 되나

검증할 명령이 없으면 command count가 0으로 남고, 실행 실패가 없으니 통과로 본다. 명령어가 없는 글까지 억지로 실패시킬 필요는 없었다.

건너뛴 명령이 많으면 통과라고 봐도 되나

자동 검증 기준으로는 통과지만, 사람 검수에서는 따로 봐야 한다. skipped가 많다는 건 “실행하지 못한 위험한 명령이 많다”는 뜻이라서, 그 글은 눈으로 더 조심해서 읽는다.

초안을 조금만 고쳐도 다시 검증해야 하나

그렇다. SHA는 글 전체 기준이라 문장 하나만 바뀌어도 달라진다. 귀찮긴 한데, 어디까지가 명령에 영향을 주는 수정인지 사람이 매번 판단하는 것보다 다시 돌리는 쪽이 덜 헷갈렸다.

다음에는 이 검증 결과를 글마다 더 읽기 좋게 보여주는 쪽을 봐야 한다. 지금은 topics.json 안에 충분한 근거가 남지만, 실패한 명령만 따로 모아 보는 화면이 있으면 검수 속도가 더 빨라질 것 같다.

이 블로그 더 보기

여기 나온 템플릿과 체크리스트는 그대로 가져가서 쓰셔도 됩니다.

  • 새 글은 RSS로 받아볼 수 있습니다.
  • 시리즈 전체는 여기에 정리해 두었습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다