엔지니어링 노트도구와 실행

대화를 다시 돌리면 실수까지 반복된다, 단계와 성공한 SQL만 남긴다

지난주 매출을 DB에서 뽑아 부서별로 묶고 엑셀로 만드는 일이 채팅에서 한 번 잘 풀렸다고 해 보겠습니다. 그 대화에는 결과만 있지 않습니다. 매출이 아닌 견적 테이블을 먼저 조회했다가 고친 흔적, 사용자가 중간에 바꾼 부서 기준, 한 번 실패한 도구 호출이 함께 남아 있습니다. 다음 주에 이 대화를 그대로 다시 돌리면 모델은 같은 시행착오를 다시 겪거나, 맥락에 남은 지난주 숫자를 이번 주 답에 섞습니다. 반대로 마지막 답만 요약해 남기면 「어느 테이블에서 어떤 조건으로」라는 실행 지식이 사라집니다.

OwlAI는 이런 대화를 플레이북으로 바꿉니다. 플레이북은 정해 둔 단계를 예약한 시각에 사람 없이 돌리는 절차입니다. 변환은 대화에서 두 가지만 남깁니다. 다시 할 일의 단계 목록과, 그 대화에서 실제로 성공한 SELECT 문장입니다. 날짜, 데이터, 실행 시각, 오늘 처리할 것이 있는지처럼 실행마다 달라지는 것은 실행할 때 다시 정합니다. 이 글은 무엇을 남기고 무엇을 실행 때 정하는지, 그 경계를 어떻게 그었는지를 다룹니다.

변환 때 남기는 것과 실행할 때마다 다시 정하는 것
그림 1. 변환 때 남기는 것과 실행할 때마다 다시 정하는 것

대화를 다시 돌리는 방법도, SQL을 굳히는 방법도 깨진다

반복 작업을 자동화하는 흔한 방법은 셋입니다. 대화 전체를 프롬프트로 다시 넣는 방법은 만들기 쉽지만, 앞에서 본 것처럼 틀린 조회와 바뀐 요구까지 다시 읽게 합니다. 대화를 요약해 지시문 하나로 만드는 방법은 시행착오를 걷어 내지만, 요약하면서 테이블 이름과 조건이 빠지기 쉽고 첫 실행이 스키마 탐색부터 다시 합니다.

마지막 방법은 성공한 SQL을 그대로 굳혀 두고 매번 실행하는 것입니다. 비용이 들지 않고 결과도 매번 같습니다. 그런데 컬럼 이름이 바뀌거나 날짜 조건이 문장 안에 박혀 있으면, 굳힌 SQL은 오류 없이 틀린 결과를 냅니다. 「지난주」가 늘 같은 주를 가리키는 보고서가 매주 성공으로 쌓입니다.

플레이북은 단계와 SQL을 남기되, SQL을 정답이 아니라 다음 실행의 제안으로 둡니다.

변환은 단계 목록과 대화에서 성공한 SELECT만 남긴다

변환은 채팅 입력창 위의 Playbook 만들기에서 시작합니다. 도구를 한 번도 쓰지 않은 대화에는 옮길 실행 흔적이 없어서 버튼이 비활성입니다. 모바일 화면에는 이 버튼이 없고, 채팅에서 반복 작업을 요청하면 모델이 플레이북을 만드는 도구를 부릅니다.

변환 모델은 단계 목록을 냅니다. 단계 종류는 정해져 있습니다. 문서 읽기, DB 조회, 내용 정리, 문서 만들기, 외부 서비스 작업(Jira 코멘트 같은 도구 호출), 승인 등이고, 종류마다 반드시 채울 입력이 있습니다. 단계는 위에서 아래로 한 줄로 돌고 갈림길이 없습니다. DB 조회 단계에는 SQL이 아니라 「지난주 부서별 매출」 같은 자연어 질문과 대상 DB가 들어갑니다. 모델 출력은 구조 검증을 거친 뒤 저장되고, 검증 오류를 고치는 재시도는 「고친 질문에 옛 답이 섞이지 않게 대화를 트리로 저장하기」에서 다룹니다.

SQL은 따로 챙깁니다. 코드가 이 대화에서 실행된 읽기 전용 조회를 대화 기록에서 모으는데, 결과가 성공으로 돌아온 문장만 모읍니다(최대 20개). 모델은 이 목록에서 DB 단계마다 맞는 문장을 하나 골라 붙입니다. 모델이 붙인 문장이 목록에 없으면 코드가 버립니다. 모델이 문장을 고쳐 쓰거나 지어내면 확인된 적 없는 SQL이 저장되기 때문입니다. 남은 문장은 읽기 전용인지 한 번 더 검사한 뒤 그 단계의 저장 SQL이 됩니다. 이 과정에서 SQL을 다시 실행하지는 않습니다.

그래서 대화에서 바꾼 플레이북은 첫 실행부터 스키마 탐색과 SQL 작성을 처음부터 하지 않습니다.

저장 SQL은 다음 실행에 제안으로 실린다

DB 단계가 실행되면 저장 SQL이 그 단계의 지시와 함께 모델에게 실립니다. 언제 저장했고 몇 번 다시 썼는지도 함께 붙습니다. 모델은 그 문장을 그대로 돌릴 수도, 고칠 수도, 새로 쓸 수도 있습니다. 실행은 늘 그 시점의 데이터로 하고, 권한도 실행하는 사람 기준으로 매번 다시 확인합니다.

단계를 마칠 때 모델은 다음 실행에 쓸 SQL을 함께 냅니다. 저장 SQL을 그대로 썼으면 「그대로 썼다」고만 알리고, 바꿨으면 새 문장을 냅니다. 새 문장에서는 날짜처럼 실행마다 달라지는 값을 {{name}} 꼴의 자리표시자(placeholder)로 바꾸고, 자리표시자마다 타입과 예시값을 적습니다. 타입은 정수, 숫자, 문자열, 날짜, 참·거짓 다섯 가지입니다. 이 타입들에만 값을 안전한 SQL 리터럴로 바꾸는 규칙이 분명하게 있기 때문입니다.

엔진은 제출된 SQL을 실행하지 않고 다음 세 가지만 확인합니다.

  • 자리표시자를 따옴표로 감싸지 않았는가
  • 모든 자리표시자에 설명이 붙었는가
  • 예시값을 넣어 만든 문장이 읽기 전용 SELECT 하나인가

자리표시자를 따옴표로 감싸면 '{{start}}'가 ''2026-09-01''이 되어 문법 오류가 납니다. 이 오류는 다음 실행에서야 드러나므로 제출할 때 막습니다.

편집 화면의 SQL 고정을 끄면 저장 SQL을 싣지도, 새로 저장하지도 않습니다. 그 단계는 매번 SQL을 새로 씁니다.

예약은 사용자가 고른 시간대의 벽시계 규칙으로 저장한다

실행 시각도 실행 때마다 다시 계산합니다. 「매주 월요일 9시」를 정확한 시각으로 바꾸는 일은 모델에게 맡기지 않습니다. 변환 모델은 반복 종류, 시각, 요일만 내고, 첫 실행 시각은 코드가 예약 시간대에서 계산합니다. 반복은 간격과 단위(시간·일·주·개월·년)로 정하고, 주 단위면 요일을 고릅니다.

반복 규칙을 UTC cron 하나로 바꿔 큐에 등록하면 일광 절약 시간제(DST)가 있는 지역에서 깨집니다. 뉴욕 기준 매일 9시로 등록한 작업은 시계가 한 시간 뒤로 돌아간 뒤 현지 8시에 돕니다. 계절마다 UTC와의 차이가 바뀌는 현지 시간을 cron 하나로 나타낼 수 없기 때문입니다. 시각만 UTC로 바꾸고 요일은 사용자가 고른 그대로 두어도 틀립니다. 한국 시간 평일 08:00은 UTC로 전날 23:00이라서, 이 방식으로 등록하면 작업이 화요일부터 토요일까지 돕니다.

그래서 반복 규칙은 사용자가 고른 시간대의 벽시계 규칙(그 지역 시계에 보이는 날짜와 시각)으로 저장하고, 다음 실행 시각도 그 시간대에서 계산합니다. 실행 여부는 1분마다 도는 작업 하나가 정합니다.

  1. 다음 실행 시각이 지난 예약을 찾는다.
  2. 예약 행을 잠그고, 잠근 상태에서 다음 실행 시각을 다시 확인한다.
  3. 직전 실행 시각과 비교해 같은 회차가 이미 돌았으면 멈춘다.
  4. 실행 행을 만들고, 다음 실행 시각을 벽시계 규칙으로 다시 계산한다.

2번과 3번 덕분에 워커가 여럿이어도, 같은 작업이 재시도돼도 한 회차는 한 번만 돕니다. 워커가 멈춰 회차가 10분 넘게 밀리면 그 회차는 돌리지 않습니다. 반복 예약은 다음 회차로 넘어가고, 한 번만 도는 예약은 그대로 끝납니다. 사용자가 고른 시각에서 한참 지나 도는 보고서는 사용자가 요청한 일이 아닙니다. 같은 이유로 시작 시각이 지난 뒤에 켜면, 반복 예약은 다음 회차부터 돌고 한 번만 도는 예약은 켜지지 않습니다.

오늘 할 일이 없는 날은 실패가 아니다

실행마다 달라지는 마지막 값은 「오늘 처리할 것이 있는가」입니다. 모델은 단계를 마칠 때 결과를 셋 가운데 하나로 알립니다. 일을 했으면 결과가 0건이어도 「완료」이고 다음 단계로 갑니다. 남은 단계가 무의미하면 「중단」으로 실행을 끝내고, 이것은 실패로 세지 않습니다. 해야 할 일을 하지 못했을 때만 「실패」입니다. 「중단」은 편집 화면에서 여기서 중단 가능을 켠 단계에서만 쓸 수 있습니다. 어디서 끝낼 수 있는지를 모델이 아니라 플레이북을 만든 사람이 정하게 하려는 것입니다.

DB 단계에서 성공한 조회가 하나도 없는데 「완료」라고 하면 한 번은 되묻습니다. 대개 모델은 이때 조회를 실제로 돌립니다. 두 번째에도 같으면 「처리 0건」 결과로 받고 실행을 이어 갑니다. 앞 단계가 0건이라 조회할 것이 없었던 단계까지 실패로 만들면 실행 전체가 실패로 끝나기 때문입니다.

실행이 끝나면 실행한 사람(예약 실행이면 예약의 주인)에게 푸시 알림이 갑니다. 알림도 완료, 중단, 실패 세 가지입니다. 중단된 실행을 「완료」로 알리면 주인은 보고서가 만들어졌다고 믿습니다. 실패 알림에는 모델이 마지막으로 남긴 진행 문장을 함께 실어서, 아무도 보지 않던 예약 실행이 어디서 멈췄는지 알 수 있게 합니다.

예약 시각에 실행 전 검사에서 막힌 경우도 실패로 알립니다. 이 알림이 없으면 주인은 실행 기록을 열어 보기 전까지 매일 막히는 예약을 모릅니다. 알림은 예약 처리의 트랜잭션이 커밋된 뒤에 보냅니다. 롤백돼 존재하지 않는 실행을 가리키는 알림이 나가지 않게 하려는 것입니다. 사용 한도를 넘었거나 필수 입력이 빠진 정의라면 실행을 만들지 않고 플레이북을 자동으로 끄고, 편집 화면에 그 이유를 보여 줍니다.

같은 이유로 켜기·예약·실행 경로는 저장보다 엄하게 검사합니다. 편집 도중에는 필수 입력이 빈 단계도 저장할 수 있지만, 필수 입력이 비었거나 단계가 하나도 없는 플레이북은 켤 수 없습니다. 빈 플레이북이 켜지면 실행은 아무것도 하지 않고 성공으로 끝나고, 주인은 일이 돌고 있다고 믿습니다. 켜 둔 플레이북은 편집 화면이 읽기 전용이 됩니다. 켠 채로 고칠 수 있으면 켤 때 받은 검사를 거치지 않은 정의가 예약에 남습니다.

설계에서 고른 것

실행 순서는 목록 순서 하나로 고정했습니다. 갈림길이 있는 그래프는 조건마다 다른 경로를 나타낼 수 있지만, 사람이 간선과 조건식을 편집하고 검증해야 합니다. 조건에 따른 판단은 단계 안에서 모델이 하고, 일찍 끝낼 자리는 「여기서 중단 가능」으로 사람이 정합니다.

대화의 SQL은 모델을 거치지 않고 글자 그대로 저장합니다. 사용자가 대화에서 결과를 확인한 문장을 바꾸지 않기 위해서입니다. 대가로, 채팅 SQL에 날짜가 리터럴로 들어 있으면 첫 실행은 그 날짜가 든 문장을 제안으로 받습니다. 모델이 제안을 보고 날짜를 고쳐 실행하고 자리표시자를 쓴 새 문장을 내면, 다음 실행부터는 그 문장이 실립니다. 또 코드는 「이 대화에서 성공한 문장인가」만 확인합니다. 성공했지만 틀린 테이블을 본 조회도 목록에 있고, 어느 단계에 어느 문장을 붙일지는 모델이 고릅니다. 이 판단까지 코드 규칙으로 만들면 모양이 다른 대화마다 규칙이 틀립니다. 대신 저장 SQL은 언제나 제안이라, 다음 실행의 모델이 질문과 결과를 보고 고칠 수 있습니다.

제출된 SQL은 실행하지 않고 모양만 검사합니다. 제출할 때 한 번 더 실행해 보면 의미 오류를 더 일찍 잡겠지만, 단계마다 DB에 조회를 한 번 더 보냅니다. 의미가 맞는지는 다음 실행에서 모델이 결과를 보고 판단합니다.

반복 주기는 정확히 나타낼 수 있는 값만 받습니다. 시간 간격은 24의 약수(1·2·3·4·6·8·12·24시간)만 받고, 5시간 간격은 거절합니다. 하루 경계에서 반드시 어긋나는 주기를 가까운 값으로 바꿔 저장하면, 사용자가 고른 것과 다른 주기로 오류 없이 돕니다.

결과를 다른 사람에게 보내는 일은 플레이북의 한 단계로 둡니다. 관리자가 연결한 메일이나 메신저 도구를 모델이 부르고, 그 호출은 다른 외부 쓰기 도구와 같은 승인을 거칩니다. 제품이 수신자 목록을 받아 결과를 따로 보내는 기능 대신 이 방식을 골랐습니다. 그런 기능이 있으면 데이터가 밖으로 나가는 길이 도구 호출 말고 하나 더 생기고, 승인과 개인정보 가림을 두 곳에서 지켜야 합니다. 대가는 보내는 일을 단계로 적어 두어야 하고, 승인이 필요한 도구면 처음 켤 때와 정의가 바뀔 때마다 확인을 거친다는 점입니다.

채팅에서 한 번 풀린 일을 반복 작업으로 옮기는 기능이라면, 대화를 통째로 남기기보다 성공한 실행 흔적만 골라 남기고, 그 흔적을 다음 실행이 고칠 수 있는 제안으로 두는 쪽이 맞습니다. 데이터와 스키마가 실행 사이에 바뀌는 환경일수록 그렇습니다.

어떤 문제를 해결하나요?

도구와 실행의 다른 글