
「지난주에 열린 이슈마다 고객사 계약을 조회해서, 계약이 끝난 고객사의 이슈에만 담당자에게 메일을 보내 줘」라는 일은 앞 결과로 다음 인자를 만드는 호출이 사슬로 이어집니다. 모델이 도구를 하나씩 부르면 사슬의 단계마다 라운드가 하나씩 들고, 이슈 수십 건의 본문과 계약 정보가 모두 컨텍스트에 쌓입니다. 라운드는 모델을 한 번 부르고 그 응답에 든 도구 호출을 실행하기까지입니다. 같은 사슬을 파이썬 스크립트 한 편에 맡기면 라운드는 하나로 줄고, 중간 결과는 스크립트의 변수로만 오갑니다. 업계에서는 이 방식을 프로그래매틱 도구 호출(programmatic tool calling)이라고 부릅니다.
문제는 스크립트 실행이 바깥에서는 도구 호출 한 건으로 보인다는 점입니다. 권한 확인과 사용자 승인, 감사 기록이 「스크립트를 실행했다」 한 건만 보면, 그 안에서 나간 메일 스무 통은 아무 확인도 거치지 않습니다. 그래서 스크립트가 부른 도구 호출을 한 건씩 실행을 맡긴 턴으로 돌려보내, 모델이 직접 부른 호출과 같은 순서로 판정합니다. 이 글은 그 호출이 돌아오는 경로와 판정 순서, 스크립트에서 부를 수 없는 도구, 호출 수 상한을 다룹니다. 큰 데이터를 모델이 다시 받아 적지 않게 파일 경로로 넘기는 설계는 「조회 결과는 모델이 다시 받아 적지 않게 파일 경로로 넘긴다」에서 다룹니다.
스크립트 실행을 통째로 승인하면 안에서 무엇이 나가는지 보이지 않는다
스크립트가 도구를 부르게 하는 쉬운 방법은 두 가지입니다.
첫째는 스크립트에 네트워크와 서비스 자격 증명을 주고 API를 직접 부르게 하는 것입니다. 만들기는 가장 쉽습니다. 하지만 관리자가 도구마다 정한 허용·승인 설정, 사용자 권한에 따른 차단, 감사 기록은 모두 서버가 도구를 실행하는 경로에 있습니다. 스크립트가 API를 직접 부르면 그 경로를 지나지 않습니다. 그래서 OwlAI의 샌드박스에는 네트워크가 없습니다(「스크립트 샌드박스는 실행마다 세우고, 그 서버에서 띄워 보고 판정한다」).
둘째는 스크립트 실행 자체를 승인 대상으로 두는 것입니다. 사용자는 실행 전에 코드를 보고 승인합니다. 그런데 사용자가 보는 것은 반복문과 조건문이고, 그 반복문이 메일을 몇 통, 누구에게 보낼지는 실행해 봐야 압니다. 승인 창에 보낼 내용 대신 코드가 뜨면 사용자는 내용을 확인하지 못한 채 승인하게 됩니다. 관리자가 「메일 보내기는 승인 필요, 이슈 조회는 허용」으로 나눠 둔 설정도 스크립트 한 건 앞에서는 쓸모가 없습니다.
스크립트에서 부를 수 있는 도구 목록을 따로 정의하는 방법도 있습니다. 그러면 목록이 두 벌이 되고, 새 도구가 한쪽에만 들어가는 순간 권한 판정이 어긋납니다.
그래서 스크립트가 부를 수 있는 도구는 그 턴에 모델이 부를 수 있는 도구 목록에서 고르고, 호출 한 건 한 건을 모델의 호출과 같은 실행 경로에 태웁니다.
스크립트의 호출은 실행을 맡긴 턴으로 돌아온다
스크립트는 사이드카 컨테이너 안의 샌드박스에서 돕니다. 스크립트에는 도구 이름과 인자를 받는 호출 함수가 미리 들어 있고, 이 함수는 요청을 샌드박스 밖으로 넘기기만 합니다. 호출 한 건은 다음 경로로 오갑니다.
- 스크립트가 호출 함수를 부르면 요청이 파이프를 따라 샌드박스 밖의 사이드카 프로세스로 갑니다.
- 사이드카는 그 요청을, 백엔드가 실행을 맡기며 연 연결의 응답 스트림에 싣습니다. 사이드카가 백엔드로 새 연결을 열지 않습니다.
- 백엔드에서 그 턴을 돌리는 프로세스가 요청을 받아 판정하고 실행한 뒤, 결과를 사이드카에 답합니다.
- 스크립트는 그 결과를 받아 다음 줄로 넘어갑니다.
연결을 연 쪽이 곧 그 턴의 주인이므로 요청이 다른 턴으로 갈 일이 없고, 사이드카가 백엔드에 자기를 증명할 신원도 필요 없습니다. 연결이 끊기면 사이드카는 스크립트를 끝냅니다. 턴 취소와 실패, 백엔드 프로세스 종료가 모두 연결 끊김으로 이어지므로, 턴이 끝난 뒤에도 혼자 도는 스크립트가 남지 않습니다.
스크립트가 낸 호출은 모델의 호출과 같은 순서로 판정한다
턴에 돌아온 호출은 다음 순서를 지납니다. 앞 단계에서 거절되면 뒤 단계로 가지 않습니다.
| 순서 | 확인하는 것 |
|---|---|
| 1 | 스크립트에서 부를 수 없는 도구인가(아래 절) |
| 2 | 이 스크립트의 호출 수와 턴의 도구 호출 총량이 남았는가 |
| 3 | 실행 전 검사: 같은 인자로 이미 부른 호출인가, 이 단계에서 승인 대상 호출을 더 부를 수 있는가 |
| 4 | 도구 정책: 진짜 도구 이름과 인자로 허용·차단·승인 요구를 정하고, 사용자 권한으로 그 커넥터의 도구를 쓸 수 있는지 확인한다 |
| 5 | 실행 뒤: 결과 크기 상한과 스필을 적용하고 감사 기록을 남긴다 |
3단계는 판정과 기록을 한 자리에서 합니다. 실행 전 검사는 통과한 호출을 기억해 두었다가 같은 호출이 다시 오면 거절하고, 승인 대상 호출 수를 셉니다. 판정만 하고 기록을 빠뜨리면 스크립트는 같은 호출을 몇 번이고 되풀이하고, 승인 대상 호출 상한도 비켜 갑니다. 그래서 모델의 호출과 스크립트의 호출이 판정과 기록을 묶은 같은 절차를 부릅니다.
감사 기록에는 그 호출이 어느 스크립트 실행에서 나왔는지가 함께 남습니다. 표시 이름과 오류 코드는 직접 부른 호출과 같아서, 같은 거절이 경로에 따라 두 이름으로 갈리지 않습니다.
채팅에서는 실행된 호출의 뒤처리도 같습니다. 턴이 취소되거나 실패하면 이미 실행됐을 수 있는 승인 대상 작업을 사용자에게 알리는데, 스크립트가 보낸 메일도 이 목록에 들어갑니다. 이미 실행된 승인 대상 작업이 있으면, 답이 비었을 때 모델을 한 번 더 부르는 복구도 하지 않습니다. 다시 부른 모델이 같은 메일을 또 보낼 수 있기 때문입니다.
정책 거절, 승인 거부, 도구 실패, 취소는 모두 예외 대신 실패를 뜻하는 같은 모양의 결과로 스크립트에 돌아오고, 사유가 함께 옵니다. 모델이 직접 불렀을 때 받았을 결과와 같은 모양이라, 스크립트는 그 값 하나를 보고 다음을 정합니다.
승인이 필요한 도구를 부르면 스크립트는 그 자리에서 기다린다
승인이 필요한 도구도 스크립트에서 부를 수 있습니다. 호출이 승인 대상이면 모델이 직접 불렀을 때와 같은 승인 창이 뜨고, 실제로 보낼 인자가 보입니다(「확인하라고 일러도 모델은 메일을 보낸다, 실행 직전에 승인을 받는다」). 스크립트는 사용자가 결정할 때까지 그 줄에서 멈춥니다. 대기 상한도 직접 호출과 같습니다.
기다리는 동안 스크립트는 CPU를 쓰지 않습니다. 그래서 사이드카는 동시에 돌릴 수 있는 실행 자리 하나를 그동안 내놓았다가 결과가 오면 다시 잡습니다. 승인을 오래 기다리는 스크립트가 다른 사용자의 문서 생성을 막지 않게 하려는 것입니다. 도구 결과를 기다리는 실행 수에도 따로 상한이 있어서, 그 상한이 다 차면 새 호출은 「다음 응답에서 직접 부르라」는 거절로 바로 돌아옵니다.
시간도 나눠 셉니다. 스크립트 실행의 시간 상한은 도구 결과를 기다린 시간을 빼고 셉니다. 긴 조회를 몇 번 부르는 정상 스크립트가 도구의 실행 시간까지 떠안아 잘리지 않게 하려는 것입니다. 도구 호출 한 건은 그 도구의 시간 상한이 끊고, 스크립트 자신의 계산은 CPU 시간 상한이 끊습니다(「스크립트 샌드박스는 실행마다 세우고, 그 서버에서 띄워 보고 판정한다」).
사용자의 답을 받는 도구는 스크립트에서 부를 수 없다
스크립트에서 부를 수 없는 도구는 코드가 정합니다.
- 사용자에게 되묻는 도구. 사용자의 답은 모델이 읽고 다음 행동을 정하라고 받는 것인데, 스크립트가 모델에게 돌려주는 것은 최종 결과뿐이라 스크립트 안에서 받은 답은 모델이 보지 못합니다.
- 사용자 기기에서 도는 도구. 데스크톱 앱이 사용자 PC의 파일을 읽고 쓰는 도구처럼, 실행이 사용자 기기로 왕복하는 도구입니다.
- 스크립트 실행 도구 자신. 스크립트 안에서 스크립트를 다시 돌리는 중첩을 막습니다.
- 도구를 켜는 도구와 다른 도구를 대신 불러 주는 도구. 무언가를 실행하는 호출이 아니라 턴의 상태를 바꾸는 호출입니다.
아직 켜지 않은 도구도 부를 수 없습니다. 도구가 많으면 일부는 이름만 알려 두고 필요할 때 켜는데(「도구 185개 정의는 9만 9천 토큰, 이름만 싣고 고른 도구만 펼친다」), 스크립트가 켜지 않은 도구를 부르면 「스크립트 밖에서 먼저 켜라」는 거절이 돌아옵니다. 반대로 턴 도중에 켠 도구는 바로 부를 수 있습니다. 스크립트가 보는 도구 목록이 턴이 늘려 가는 목록 그대로이기 때문입니다.
호출 수는 스크립트마다 50번에서 끊는다
스크립트 하나가 부를 수 있는 도구 호출은 50번입니다. 실운영 환경의 플레이북 한 실행이 레코드를 하나씩 조회하며 낸 호출이 38번이었고, 50은 이런 사슬을 담는 폭으로 정했습니다. 넘으면 거절이 돌아오고, 그 스크립트의 나머지 호출은 턴까지 오가지 않고 같은 거절을 곧바로 받습니다. 모델은 이 거절을 읽고 범위를 좁히거나 스크립트를 나눕니다.
거절될 호출도 먼저 셉니다. 판정을 통과한 호출만 세면, 없는 도구 이름을 반복문으로 부르는 스크립트가 상한 없이 돌며 거절마다 감사 기록을 남깁니다. 도구 결과를 기다리는 시간은 CPU 시간 상한에 걸리지 않으므로, 이 상한이 없으면 그런 스크립트를 끊을 장치가 없습니다. 상한을 넘긴 뒤의 거절은 감사 기록에 한 번만 남깁니다.
턴 전체의 도구 호출 총량을 두는 실행에서는 스크립트가 부른 호출도 그 총량에 듭니다. 같은 라운드에서 아직 실행 중인 호출까지 더해 세므로 스크립트 한 편이 총량을 비켜 가지 못하고, 넘었을 때의 오류 코드도 직접 호출과 같습니다.
설계에서 고른 것
승인이 필요한 도구를 스크립트에서 막는 쪽이 더 단순합니다. 승인 대기로 멈추는 스크립트가 없고, 실행 자리를 내놓았다 다시 잡는 장치도 필요 없습니다. 하지만 그러면 조회해서 고른 뒤 쓰는 사슬이 쓰기 앞에서 끊기고, 모델이 조회 결과를 받아 쓰기 호출을 다시 내야 합니다. 그래서 승인 도구를 허용하고, 대가로 스크립트가 사용자 결정을 기다리는 동안 멈추는 것을 받아들였습니다. 스크립트의 대기 상한을 직접 호출과 같게 둔 것도 같은 판단입니다. 기다리는 스크립트는 실행 자리를 내놓으므로 다른 실행을 막지 않습니다. 다만 쓰기 호출이 많을수록 승인 창도 그만큼 뜹니다.
스크립트가 본 중간 결과는 모델에게 돌아가지 않습니다. 컨텍스트를 아끼려고 고른 성질이지만, 그 대신 모델은 사슬 중간에 끼어들어 판단하지 못합니다. 각 호출은 권한과 정책을 지켰는지만 확인받고, 스크립트의 조건이 맞았는지는 스크립트가 돌려준 최종 결과를 보고 모델과 사용자가 판단합니다. 사용자에게 되묻는 도구를 스크립트에서 뺀 것도 이 성질 때문입니다.
연결을 백엔드 쪽에서만 열게 한 대가는 실행 상태가 사이드카 프로세스 하나에 있다는 점입니다. 진행 중인 실행과 답을 기다리는 호출을 그 프로세스의 메모리가 들고 있으므로, 사이드카를 여러 벌로 늘리려면 답을 돌려보낼 경로부터 다시 설계해야 합니다. 그 대신 사이드카에 백엔드를 부를 자격을 주지 않아도 되고, 턴이 끝나면 스크립트도 함께 끝납니다.
모델이 코드로 도구를 부르게 하는 에이전트라면, 그 호출이 모델의 호출과 같은 실행 경로를 지나는지부터 확인합니다. 권한 확인과 승인, 감사 기록이 스크립트 실행 한 건만 보고 있다면, 스크립트 안의 호출을 하나씩 그 경로로 돌려보내고 판정과 기록을 한 절차로 묶으면 됩니다.
도구와 실행의 다른 글

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

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

엔지니어링 노트 · 도구와 실행
앱이 못 여는 오피스 파일은 검증기가 막고, 모양은 모델이 그림으로 본다
모델이 코드로 엑셀 파일을 만들면 실행은 종료 코드 0으로 끝나고 모델은 「완성했습니다」라고 답하는데, 정작 Excel은 그 파일을 열지 못할 수 있습니다.