행이 아닌 참조를 전달하기: MCP 상의 에이전트 계산을 위한 데이터 아티팩트
요약
AI 에이전트가 데이터를 처리할 때 단순히 행(row) 전체를 컨텍스트로 전달하는 방식은 보안, 비용, 정확성 측면에서 여러 한계를 가집니다. 대신, 데이터는 서버 측 아티팩트로 저장하고 모델에게는 참조와 코드 작성에 필요한 설명만 제공해야 합니다. 이를 통해 에이전트의 역할은 '데이터 읽기'에서 '코드 생성'으로 전환됩니다.
핵심 포인트
- 데이터를 직접 전달하는 대신 아티팩트로 저장하고 참조만 사용합니다.
- 모델의 역할을 데이터 해석 및 코딩(Code Generation)에 집중시킵니다.
- Producer, Calculation Tool, Consumer 세 가지 유형의 툴이 필요합니다.
- 참조는 랜덤해야 하며 소유자 확인 절차가 필수적입니다.
지난 글에서는 AI 에이전트가 Python을 안전하게 실행할 수 있는 장소를 마련했습니다. 이제, 에이전트가 작성한 코드를 어디서 실행할지에 대한 답은 나왔습니다. 이번에는 그 속편으로서, 똑같이 중요하다고 밝혀진 질문에 답합니다. 모델은 실제로 어떤 데이터를 봐야 하는가?
대부분의 경우, 그 답은 우리가 전달하는 양보다 훨씬 적습니다.
에이전트를 업무 시스템에 연결할 때 가장 먼저 떠오르는 방법은 다음과 같습니다.
- 툴이 시스템에 질의하여 결과 세트(result set)를 JSON으로 반환합니다.
- 이 행(row)이 모델의 컨텍스트에 들어갑니다.
- 모델이 그것을 읽고 답변합니다. '상위 5개는...', '평균은...'.
행이 20개인 데모라면 이렇게 작동합니다. 하지만 실제 데이터에서는 세 가지 지점에서 무너집니다.
보안. 툴이 반환한 행은 모두 대화 기록의 일부가 됩니다. 대화 기록은 저장되고, 로그에 남고, 다음 턴에서 다시 컨텍스트에 들어가 모델 제공업체(model provider)에게 전송되며, 텔레메트리(telemetry)로 수집되고, 때로는 평가용으로 보관됩니다. 시스템 외부로 나갈 필요가 전혀 없었던 데이터가 그 모든 장소에 복사되어 버린 것입니다. -
비용과 용량. 필드가 10개인 행은 대략 40~60 토큰이 됩니다. 5,000행이라면 수십만 토큰입니다. 컨텍스트 창의 대부분, 혹은 전부를 차지하며, 그것을 가지고 다니는 턴마다 비용이 발생합니다. -
정확성. LLM(대규모 언어 모델)은 계산하지 않습니다. 추정합니다. 5,000개 수치의 합계나 전 분기 대비 변화에 따른 그룹 순위 매김 같은 것을 모델에게 의존하면, 자신만만한 태도로 아쉽지만 틀린 답변이 돌아옵니다. 같은 분석은 pandas를 사용하면 단 3행으로 정확한 결과를 얻을 수 있습니다.
결국 모델은 자신이 가장 취약한 부분(대량 데이터의 산술 계산)을, 자신이 가장 적게 가진 자원(컨텍스트)을 사용하여 수행하게 되고, 그 과정에서 데이터를 여기저기 뿌리고 있는 것입니다.
발상을 역전시킵니다. 데이터를 생성하는 툴은 데이터를 반환하지 않습니다. 데이터를 서버 측에 **아티팩트(artifact)**로 저장하고, 참조와 모델이 그것에 대해 코드를 작성하는 데 필요한 설명만 반환합니다.
+--------------------------- MCP server ----------------------------+
| |
| fetch_* ---> [ artifact store ] <---> run_python (sandbox) |
...
모델의 역할은 '데이터를 읽고 답변하는 것'에서 '데이터의 형태를 이해하고, 답을 내놓는 코드를 작성하는 것'으로 바뀝니다. 이것은 모델이 잘하는 일입니다.
스토어를 사용하는 툴은 세 종류가 있습니다.
프로듀서(Producer) (fetch_*)는 호출자의 권한으로 질의를 실행하고, 결과를 저장하며 설명을 반환합니다. -
계산 툴(Calculation Tool) (run_python)은 참조를 입력으로 받습니다. 이 참조는 샌드박스 내에서 DataFrame 형태로 나타납니다. 스크립트가 저장한 DataFrame은 새로운 아티팩트가 되며, 모델에게 반환되는 것은 작은 JSON 결과만입니다. -
소비자(Consumer) (render_chart, export_csv)는 참조를 받아 사람이 볼 수 있는 형태로 변환합니다. 행은 스토어에서 사용자 화면으로, 모델을 거치지 않고 전달됩니다.
지난 글과 마찬가지로 예시에는 FastMCP와 pandas를 사용합니다. 일부러 작게 구성했습니다.
import secrets
import time
from dataclasses import dataclass, field
...
여기서 중요한 점이 두 가지 있습니다. 참조는 연속 번호가 아닌 랜덤이어야 하며, 읽을 때마다 소유자를 확인해야 합니다. 참조가 타인의 대화 기록에 섞여 들어와도 그 사람에게는 도움이 되지 않습니다. 또한, 오류 메시지가 에이전트에게 복구 방법을 알려주고 있다는 점에도 주목해 주세요.
import json
def describe(ref: str, df: pd.DataFrame, sample_rows: int = 5) -> dict:
return {
...
컬럼 이름, 타입, null 개수, 그리고 몇 줄의 샘플. 인간이 groupby를 작성하는 데 필요한 것은 이것뿐입니다. 모델도 마찬가지입니다.
from fastmcp import FastMCP, Context
mcp = FastMCP("data-sandbox")
store = ArtifactStore()
...
샌드박스 내부에서는 짧은 전처리 과정이 in/*.parquet 파일을 각각 글로벌 변수로 로드하고, 빈 outputs 딕셔너리를 준비합니다. 후처리 과정이 outputs를 out/에 쓰기하고, result를 직렬화합니다. 샌드박스는 여전히 네트워크 연결 없이 자신의 실행 디렉터리만 볼 수 있기 때문에 스크립트를 통해 데이터가 외부로 나갈 일은 없습니다. (실행 환경에 pyarrow가 없다면 CSV도 괜찮습니다. 단지 컬럼의 타입이 손실될 뿐입니다.)
user: 전 분기 대비 매출이 가장 많이 하락한 제품 라인 상위 5개는 무엇인가요?
agent -> fetch_sales(since="2025-01-01")
<- {dataRef: "data_x7Kq...", rows: 48210, columns: {...}, sample: [5 rows]}
...
(
사용자의 질문은 '전 분기 대비 매출이 가장 많이 하락한 제품 라인 상위 5개는?'이고, 몇 번의 턴 후에 '모두 그래프로 만들어 주세요'입니다.)
처리 과정에 관여한 행은 48,210행입니다. 대화 기록에 남은 것은 약 2 KB에 불과합니다. 답이 정확한 이유는 모델이 아니라 pandas가 계산했기 때문입니다.
참조(reference)는 짧은 문자열이기 때문에 다른 값들과 마찬가지로 대화 속을 이동할 수 있습니다.
도구 호출의 연쇄. 한 run_python 호출의 outputs가 다음 호출의 inputs가 됩니다. 여러 단계에 걸친 분석(클리닝, 결합, 집계, 순위 지정)은 참조의 연쇄입니다. 중간 테이블들은 컨텍스트를 통과하는 적이 없습니다. -
후속 턴. 2번째 턴에서 받은 참조는 9번째 턴에서도 대화 기록에 남아 있습니다. '그걸 지역별로 나누어 주세요'라는 요청은 저장된 아티팩트를 재사용합니다. 다시 가져오거나 데이터를 붙여넣을 필요가 없습니다. -
만료일도 설계의 일부. TTL(Time-To-Live) 덕분에 스토어의 크기는 일정하게 유지됩니다. 만료된 참조는 에이전트에게 재가져오라는 오류를 반환합니다. 오래된 핸들(handle)은 오래된 데이터를 조용히 반환하는 것이 아니라, 명확하게 실패합니다.
이는 에이전트에 지침으로 작성해야 합니다. 질문에 답하기 위해 원시 행을 요구하지 말 것. 데이터는 아티팩트로 가져오고, run_python에서 계산하며, 그래프는 dataRef로 그릴 것.
데이터의 내용이 아닌 형태를 반환. 스키마, 행 수, null 개수, 몇 줄의 샘플입니다. 샘플 수는 설정 가능하게 하고, 기밀성이 높은 데이터셋에서는 0으로 만듭니다. -
모델에 반환되는 것은 모두 작게 유지하고 이를 강제. result의 크기에 상한을 두고, stdout은 잘라냅니다. 그렇지 않으면, print(df) 하나만으로 테이블 전체가 모델에게 다시 전송됩니다. -
참조는 각각 한 명의 소유자에게 속함. 무작위 ID를 사용하며, 읽어올 때마다 소유자를 확인합니다. 참조를 가지고 있다는 것 자체가 권한은 아닙니다. -
생산자는 원래 시스템의 규칙을 지킴. 아티팩트에 들어가는 것은 호출자가 가져오기를 허가받았던 것만입니다. 승인(authorization)도 행 수 제한도 마찬가지입니다. -
스토어에 제한을 둠. 아티팩트별 바이트 수 상한, TTL, 소유자 단위 검색. 작업용 데이터를 임시로 놓는 곳일 뿐, 데이터 웨어하우스가 아닙니다. -
표시에 관련된 것은 모두 참조를 받을 수 있게 함. 그래프, 표, 내보내기는 스토어에서 읽어옵니다. 이렇게 하면 '보여주세요' 단계에서도 행이 모델을 통과할 일이 없습니다.
이 접근 방식은 데이터의 노출을 줄이지만, 없애는 것은 아닙니다. 샘플 행도 집계값도 데이터입니다. 요소가 1개만 있는 그룹은 그 자체가 1개의 레코드입니다. 게다가, 끈기 있는 프롬프트라면 출력 상한 범위 내에서 행을 표시하도록 에이전트에게 요청할 수도 있습니다. 결과의 상한과 샘플 수는 정책으로 취급하고, 진정한 경계는 원래 시스템의 승인에 계속 두어야 합니다.
또한, 모델은 더 이상 데이터를 직접 볼 수 없습니다. 5행의 샘플은 오해를 불러일으킬 수 있습니다. 이상한 값, 단위 혼재, 90%가 null인 컬럼 등 말입니다. 요청에 따라 요약 통계를 반환하는 저렴한 describe_data(dataRef) 도구가 있다면, 데이터를 통째로 돌려주지 않으면서 이러한 문제의 대부분을 처리할 수 있습니다.
이전의 샌드박스는 에이전트가 코드를 안전하게 실행할 수 있도록 했습니다. 데이터 아티팩트는 그것을 실행할 가치가 있게 만듭니다. 모델은 계획을 세우고 코드를 작성하고, Python이 계산하며, 데이터는 사용자의 시스템에 남아 있습니다. 대화 기록에는 질문과 코드와 답변만 남습니다. 분석의 재현 가능한 기록이지만, 그 분석이 다룬 데이터는 전혀 포함되어 있지 않습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기