조회 결과는 모델이 다시 받아 적지 않게 파일 경로로 넘긴다

사용자가 「방금 조회한 결과를 CSV 파일로 받아 줘」라고 하면, 모델은 이미 받은 행을 파일 쓰는 도구의 인자로 다시 써야 합니다. 도구 인자는 모델의 출력이라서 받은 데이터의 셀 하나하나가 출력 토큰으로 한 번 더 나옵니다. 실운영 환경 관측에서 321행 결과를 이렇게 옮기는 데 약 90만 토큰이 들었는데, 그 모델의 한 호출 출력 상한은 128,000 토큰이었습니다. 상한 안에 들어가는 크기여도 느리고, 옮겨 적은 값이 원본과 조금씩 달라집니다.
그래서 데이터가 모델의 출력을 거치지 않게 했습니다. 도구 결과가 1,000자를 넘으면 서버가 모델에 건네기 전에 원본을 샌드박스 파일로 먼저 쓰고, 결과에 그 파일 경로를 붙입니다. 이 동작을 스필(spill)이라고 부릅니다. 파일을 만드는 도구와 다른 도구를 대신 불러 주는 도구는 데이터 대신 이 경로를 받습니다. 이 글은 문턱을 정한 기준, 경로를 받는 도구들, 경로로 할 일과 SQL로 다시 할 일의 경계를 다룹니다. 결과 하나가 입력 상한을 넘을 때 줄이는 장치는 「큰 도구 결과는 프로바이더가 자르기 전에 줄이고, 줄였다고 알린다」에서 다룹니다.
받은 데이터를 다시 적으면 느리고, 값이 바뀌고, 크면 끝까지 못 쓴다
다시 받아 적기는 드문 일이 아닙니다. 실운영 환경에서 두 달 동안 표와 파일을 만드는 도구가 인자로 받은 글자의 83.3%는, 같은 턴에 SQL 실행 도구가 이미 결과를 돌려준 뒤에 쓰였습니다. 턴은 질문 하나를 받아 답을 내기까지의 한 번의 질의응답입니다. 모델이 방금 받은 데이터를 한 글자씩 다시 쓴 분량이 두 달에 469만 자였습니다.
첫째 문제는 시간입니다. 한 플레이북 실행은 외부 도구를 13번 부르면서 이슈 본문과 콜 스택, 댓글 원문을 인자로 실었고, 인자는 합계 89,337자였습니다. 이 단계가 35분 58초짜리 실행 가운데 20분 50초를 썼는데, 외부 서버가 실제로 일한 시간은 8.9초였습니다. 출력 속도는 초당 75~90 토큰으로 내내 일정했으니, 나머지는 모델이 인자를 쓰는 시간이었습니다.
둘째 문제는 값이 바뀐다는 점입니다. 모델은 텍스트를 복사하지 않고 다시 생성합니다. 사용자가 원문 그대로 달라고 한 콜 스택에서 libpthread.so.0이 libpthread.so.6으로 나왔습니다. 원본 파일에는 .so.0이 85번 나오고 .so.6은 한 번도 없었습니다. 같은 답에서 os_thr_id@entry=232155는 @entry=를 잃었습니다. 둘 다 그럴듯해서 읽는 사람이 알아차리기 어렵습니다.
셋째 문제는 크기입니다. 321행에 90만 토큰이 들면 그 모델의 출력 상한 128,000 토큰 안에서는 끝까지 쓸 수 없고, 파일은 중간에서 끊긴 채 만들어집니다.
출력 상한을 늘리거나 여러 번에 나눠 쓰게 하면 셋째 문제는 줄지만 앞의 두 문제가 그대로 남습니다. 「원문 그대로 옮겨라」라고 프롬프트로 지시해도 모델이 다시 생성한다는 사실은 바뀌지 않습니다. 그래서 데이터가 지나가는 길에서 모델의 출력을 뺐습니다.
1,000자를 넘는 결과는 서버가 먼저 파일로 쓴다
도구 실행이 끝나면 서버는 결과를 모델에 싣기 전에 크기를 봅니다. 1,000자를 넘으면 원본을 샌드박스의 결과 폴더에 쓰고, 결과에 파일 경로와 형식, 바이트 수를 붙입니다. 표 결과라면 서버가 실제로 쓴 행 수와 컬럼 이름도 붙습니다. 모델이 「모두 321건」이라고 답할 때 그 수는 서버가 쓴 행 수에서 옵니다.
스필 파일은 아래 규칙으로 씁니다.
| 항목 | 규칙 |
|---|---|
| 표 결과(컬럼과 행) | 헤더 한 줄과 데이터로 된 CSV. 이 파일이 그대로 사용자가 원한 내보내기 파일이 된다 |
| 그 밖의 결과 | 들여 쓴 JSON. 한 줄짜리 JSON은 행 범위로 나눠 읽을 수 없다 |
| MCP 도구 결과 | 응답을 감싼 겉 구조를 벗기고, 안의 데이터를 위 규칙대로 쓴다 |
| 파일 이름 | 도구 호출 ID. 같은 호출은 늘 같은 경로라서 재시도해도 두 번 쓰지 않는다(멱등) |
| 크기 상한 | 50MB를 넘는 결과는 읽기 도구가 거절하므로 쓰지 않는다 |
| 보존 기간 | 기본 7일 뒤 6시간마다 도는 청소 작업이 지운다 |
MCP 결과의 겉 구조를 벗기는 것은 MCP 서버가 데이터를 담는 방식 때문입니다. MCP 서버는 데이터를 JSON 문자열로 바꿔 텍스트 칸에 담아 보내는 경우가 많습니다. 이 응답을 그대로 들여 써서 저장하면 데이터 전체가 이스케이프된 한 줄 문자열이 됩니다. 격리 환경 실측에서 이렇게 만든 파일은 12줄이었고, 모델은 줄 단위로 읽는 도구로 원하는 자리를 찾지 못해 26라운드를 돌았습니다.
실패한 도구 호출의 오류 메시지도 전문을 파일로 씁니다. 모델에게는 짧게 줄인 메시지와 파일 경로가 갑니다. 문서 생성 스크립트가 실패한 26건을 살펴보니 22건은 메시지를 자르면서 예외 줄이 잘려 나갔습니다. 실패를 고칠 재료는 모델이 다음에 쓸 코드뿐이라서, 그 재료를 잃지 않게 했습니다.
문턱은 「다시 받아 적어야 하는가」로 정했다
문턱은 「보존할 만큼 큰 결과인가」가 아니라 「모델이 이 결과를 다른 도구에 넘길 때 다시 받아 적어야 하는가」로 정합니다. 큰 결과만 남기는 기준이라면 문턱은 2만 자쯤이 되고, 결과 건수의 24.6%와 분량의 88.3%를 덮습니다. 파일 수는 적지만, 앞의 플레이북 실행에서 모델이 외부 도구에 써 보낸 원문은 1,489자, 3,454자, 8,534자, 17,278자짜리 조회 결과에서 왔습니다. 2만 자 문턱이라면 넷 모두 파일이 없고, 경로가 없으니 모델은 다시 받아 적을 수밖에 없습니다.
다시 받아 적을 결과를 덮는 문턱은 1,000자입니다. 실운영 환경 30일 동안의 도구 결과 33,128건 가운데 1,000자 이상인 결과는 건수로 67.6%, 분량으로 99.2%였습니다. 문턱을 더 낮추면 파일만 늘고 덮을 분량은 0.8%밖에 남지 않습니다.
판정은 글자 수로 합니다. 도구 실행 직후에 판정하는 자리라서 토크나이저를 한 번 더 부를 이유가 없습니다. 정확한 토큰 상한은 그 뒤에 결과 하나를 기본 5만 토큰으로 묶는 장치가 따로 봅니다. 스필 문턱이 그보다 훨씬 낮아서, 결과를 줄여야 하는 시점에는 원본이 이미 파일로 있습니다(「큰 도구 결과는 프로바이더가 자르기 전에 줄이고, 줄였다고 알린다」). 루프가 도구를 하나라도 싣는 모든 기능에 파일을 다시 읽는 도구를 붙여 두므로, 어느 기능에서든 이 파일을 읽을 수 있습니다(「에이전트 루프는 하나만 두고, 기능별 동작은 훅으로 끼운다」).
파일을 만드는 도구는 데이터 대신 경로를 받는다
CSV와 엑셀 시트를 쓰는 도구는 행 데이터를 인자로 받는 대신 스필 파일 경로를 원본 인자로 받습니다. 서버가 그 파일을 직접 읽어 새 파일을 씁니다. 200바이트짜리 인자가 90만 토큰을 대신하고, 모델이 옮겨 적는 단계가 없으니 옮겨 적다 생기는 오류도 없습니다. 원본 인자와 함께 줄 수 있는 것은 아래 셋입니다.
- 컬럼 선택과 순서, 사람이 읽을 머리글
- 정렬 기준 컬럼과 방향
- 행 범위(시작 위치와 개수)
형식을 바꿀 필요가 없으면 파일 복사 도구로 스필 파일을 그대로 첨부합니다. 사용자는 다운로드 카드로 원본 전체를 받습니다. 첨부한 사본은 대화 산출물로 따로 보관하므로 7일 청소의 대상이 아닙니다.
워드·파워포인트·엑셀 문서를 만드는 스크립트도 같습니다. 행을 코드에 붙여 넣지 않고, 스크립트가 실행될 때 스필 파일을 읽습니다. 긴 스크립트 자체도 매번 다시 쓰지 않고 샌드박스에 둔 파일의 경로로 넘깁니다. 긴 스크립트를 다시 쓰면 고치려 하지 않은 부분이 바뀌고, 그 부분에서 문서가 깨지기 때문입니다.
답 안에 표를 그리는 도구는 한 표에 2,000행까지 받습니다. 그보다 많으면 도구 설명이 필요한 행만 표로 보여 주고 전체는 스필 파일 경로로 CSV를 만들라고 안내합니다.
파일을 읽지 못하는 도구에는 인자 파일을 만들어 넘긴다
MCP 도구는 샌드박스 파일을 읽지 못합니다. 외부 서버에 이슈 본문을 보내려면 그 본문이 인자 안에 있어야 합니다. 이때는 다른 도구를 대신 불러 주는 위임 호출을 씁니다. 모델은 대상 도구 이름과 인자를 담은 JSON 파일의 경로만 쓰고, 서버가 파일에서 인자를 읽어 대상 도구를 부릅니다.
인자를 전부 파일에 둘 수는 없습니다. 앞의 플레이북 실행에서 인자를 칸별로 나눠 보니 65%는 DB에서 온 원문을 옮긴 것이었고, 27%는 판정 근거나 관련 이슈 순위처럼 모델이 판단해서 쓴 내용이었습니다. 판단한 내용은 어디에도 파일로 없습니다. 그래서 위임 호출은 파일을 인자의 본체로 쓰고, 모델이 직접 쓴 값을 그 위에 덮어씁니다.
스필 파일은 대개 대상 도구가 원하는 모양이 아닙니다. 그래서 흔한 순서는 두 단계입니다. 먼저 파이썬 실행 도구로 스필 파일을 읽어 대상 도구의 인자 파일을 만들고, 그 파일로 위임 호출을 합니다. 대상 도구가 파일을 직접 읽으면 모델은 안내가 없어도 스필 경로를 씁니다. 하지만 인자 파일을 만드는 단계가 하나 끼면, 위임 호출 도구와 스필 경로를 모두 갖고 있어도 본문 3,002자를 다시 받아 적은 실측이 있습니다. 모델이 두 단계를 스스로 떠올리지 못하는 것이라서, 도구 설명에 두 단계의 순서를 적어 둡니다.
위임 호출의 치환은 루프가 도구를 실행하기 직전에 합니다. 권한 확인과 사용자 승인은 펼친 실제 인자를 보고 판정하고, 승인 창도 실제로 보낼 내용을 보여 줍니다(「에이전트 루프는 하나만 두고, 기능별 동작은 훅으로 끼운다」). 반대로 대화 맥락에는 모델이 쓴 짧은 호출을 남깁니다. 펼친 인자를 이력에 넣으면 다음 라운드의 입력으로 되돌아와, 아낀 토큰을 그대로 다시 냅니다.
거르기와 묶기는 파일 위에서 하지 않고 SQL로 다시 묻는다
원본 인자로 할 수 있는 일을 컬럼과 정렬, 행 범위까지로 정한 것은 일부러 그은 선입니다. 조건과 묶기까지 넣으면 SQL을 다시 부르지 않고 한 번에 끝나 편해 보입니다. 하지만 그 순간 SQL을 서툴게 다시 만드는 셈이 되고, 원천 DB는 같은 일을 인덱스를 써서 더 빠르고 정확하게 합니다. 그래서 거르기와 집계가 필요하면 모델은 SQL을 다시 씁니다. 받은 행 수를 총계로 읽지 않게 하는 장치는 「RAG로는 건수와 합계를 셀 수 없다, 원천 DB에 SQL로 묻는다」에서 다룹니다.
파이썬 실행 도구도 같은 선 안에 있습니다. 샌드박스에서 스크립트를 돌려 표준 출력을 돌려받는 도구이고, 표준 라이브러리와 오피스 문서 라이브러리만 있습니다. pandas·numpy는 없고 네트워크도 없습니다. 여러 스필 파일을 합치거나 다른 도구의 인자 모양으로 바꾸는 가벼운 일에 씁니다.
설계에서 고른 것
문턱을 1,000자로 두면 파일이 많습니다. 7일 보존 기준으로 늘 남아 있을 스필 파일은 약 5,200개, 86MB로 추산되고, 2만 자 문턱의 8배쯤입니다. 디스크는 6시간마다 도는 청소 작업이 회수하고, 대가로 7일이 지난 결과는 다시 읽을 수 없습니다. 사용자에게 남겨야 할 결과는 첨부로 옮겨 따로 보관합니다.
경로로 하는 작업은 컬럼 선택과 정렬, 행 범위에서 멈춥니다. 파일 위에서 무거운 분석을 하려면 SQL로 돌아가야 하지만, 집계는 원천 DB가 가장 잘하는 일이라 그쪽을 골랐습니다.
스필은 실패해도 도구 호출을 실패시키지 않습니다. 디스크가 차서 쓰지 못하면 경고 로그만 남깁니다. 파일이 없으면 모델은 받은 결과로 일하고, 큰 결과는 다시 받아 적는 길로 돌아갑니다. 보존 장치 때문에 조회가 실패하는 것보다 그 편이 낫다고 판단했습니다.
위임 호출로 펼친 인자는 이력에서 빼지만 실행 기록에는 남고, 표를 그리는 호출이면 화면용 기록에도 다시 실립니다. 그래서 저장량이 끝없이 늘지 않게 인자 파일에는 1MiB 상한을 두었습니다. 위임 호출이 푸는 문제는 모델의 출력 한도이고 받는 서버의 한계는 그대로 남으므로, 상한은 모델이 손으로 쓸 수 없는 크기를 넉넉히 덮는 선에서 정했습니다.
모델이 도구 결과를 다른 도구에 넘기는 에이전트라면, 데이터가 모델의 출력을 거쳐 가는 자리가 있는지부터 찾아봅니다. 그런 자리가 있으면 결과를 줄이는 문턱보다 훨씬 낮은 크기에서 원본을 파일로 남기고, 파일을 만드는 도구와 외부 도구 호출이 그 경로를 받게 하면 됩니다.
어떤 문제를 해결하나요?
정형 데이터와 NL2SQL의 다른 글

엔지니어링 노트 · 정형 데이터와 NL2SQL
테이블을 잘못 고른 SQL도 실행된다, 검색이 후보부터 좁힌다
테이블이 수천 개인 업무 DB에는 주문 테이블 옆에 ORDMSTBAK, ORDMST202312 같은 백업·작업용 사본이 흔히 함께 남아 있습니다.

엔지니어링 노트 · 정형 데이터와 NL2SQL
컬럼의 뜻은 DDL에 없다, 질문 전에 값을 읽어 둔다
주문 테이블의 STATUSCODE CHAR(1)에는 S, D, C가 들어 있습니다. 모델이 C를 완료(complete)로 짐작하면 「지난달 완료 주문 매출」에 취소 주문이 섞여 나옵니다.

엔지니어링 노트 · 정형 데이터와 NL2SQL
부서마다 다른 매출 계산식, 승인된 용어집 정의만 질문 앞에 싣는다
「지난달 매출」을 물으면 영업팀은 수주 금액을, 재무팀은 반품을 뺀 세금계산서 발행 금액을 떠올립니다.
