AI 에이전트는 이제 많은 양의 데이터 코드를 작성합니다. 에이전트에게 "매출액 기준으로 상위 지역을 찾아줘"라고 요청하면, 보통 pandas 코드를 작성하고 실행한 다음 숫자를 보고합니다.
문제는 pandas가 노트북에 앉아 있는 사람을 위해 설계되었다는 것입니다. 사람은 출력을 훑어보고 뭔가 이상하다는 것을 알아차려 수정합니다. 하지만 에이전트는 종종 그렇지 못합니다. 그리고 pandas는 아무 말 없이 잘못된 답변을 제공하는 데 매우 능숙합니다.
pandas가 불평 없이 수행하는 세 가지 작업은 다음과 같습니다:
import pandas as pd
pd.DataFrame({"x": [2**62, 2**62]}).x.sum()
...
이 중 어느 것도 오류를 발생시키지 않습니다. 사람이 포착할 수도 있습니다. 하지만 20단계 파이프라인을 실행하는 에이전트는 이를 기반으로 자신 있게 결과를 보고할 것입니다.
저는 이 문제를 해결하기 위해 **pyursa**를 만들었습니다. 이는 에이전트(그리고 그들을 감독하는 사람)가 실제로 작동하는 방식에 맞춰 설계된 오픈 소스 Python 데이터프레임 라이브러리입니다.
pip install "pyursa[all]"
pyursa란 무엇인가
pyursa는 Polars 또는 DuckDB에서 실행되는 지연(lazy)되고 엄격하게 타입 지정된(strictly typed) 데이터프레임 API이며, 그 아래에는 Apache Arrow가 있습니다. 파이프라인을 하나 작성하면, 실행할 때 엔진만 선택하면 됩니다. 게다가 에이전트를 위해 특별히 구축된 계층이 존재합니다: 구조화된 오류 처리, 되돌리기(undo), 계보 추적(lineage), 드라이 런(dry runs), 컨텍스트 크기 요약, LLM 기반 연산자 및 MCP 서버.
import pyursa as pu
from pyursa import col
...
Polars를 알고 있다면 익숙하게 느껴질 것입니다. 차이점은 문제가 발생했을 때와 쿼리 주변의 모든 부분에 있습니다.
1. 에이전트가 스스로 수정할 수 있는 오류
에이전트가 실수를 했을 때, 라이브러리가 할 수 있는 가장 유용한 일은 무엇이 잘못되었는지 그리고 어떻게 고쳐야 하는지를 파싱 가능한 형태로 정확히 알려주는 것입니다. 모든 pyursa 오류는 안정적인 코드, 상세 정보 및 코드를 바로 붙여넣을 수 있는 제안된 수정 사항을 가집니다:
orders.filter(col("quantity") > 5)
[COLUMN_NOT_FOUND] Column 'quantity' not found in the input of this step.
suggestion: Did you mean 'qty'? -> col("qty")
동일한 오류는 에이전트가 실제로 소비하는 JSON 형식으로도 제공됩니다:
{
"error": "COLUMN_NOT_FOUND",
"message": "Column 'quantity' not found in the input of this step.",
...
pandas의 == None 문제를 기억하십니까? pyursa는 이를 거부하고 그 이유를 설명합니다:
[INVALID_OPERATION] Comparing to None with '==' always yields null, so it filters out every row.
suggestion: Use .is_null() to test for missing values. -> col("status").is_null()
숫자를 문자열과 비교하면 TYPE_MISMATCH가 발생하고 수정 방법은 col("qty") > 5입니다. 정수를 오버플로우시키면 음수 대신 INTEGER_OVERFLOW가 발생합니다.
결정적으로, 이러한 오류는 계획(plan)을 구축할 때 발생하며, 실행될 때는 아닙니다. 계획은 단계별로 타입 검사를 수행하므로, 에이전트는 단 하나의 기가바이트를 스캔하기도 전에 밀리초 단위로 실수를 발견합니다.
2. 엄격한 타입: 조용한 손실 없음 (no silent corruption)
pyursa는 데이터를 조용히 잃어버리는 것은 항상 버그라는 입장을 취합니다:
- 정확한 소수점.
Decimal(12, 2) 연산은 모든 엔진에서 정확하게 유지됩니다. qty * price 계산 시 Decimal(5,2) 가격을 사용하면 부동소수점(float)이 아닌 Decimal(38,2) 결과가 나옵니다.
- 엄격한 캐스팅. `
pyursa는 **하나의 의미론적 계약(one semantics contract)**을 정의하고, 모든 엔진에서 모든 연산에 대해 실행되는 일관성 테스트로 이를 강제합니다. 이 테스트들은 결과를 비교합니다:
- null은 가장 마지막에 정렬됩니다; 정렬은 안정적입니다.
- NaN은 누락된 값이 아니라 값(모든 숫자보다 크고, 자기 자신과 같습니다).
- 반올림은 0에서 멀어지는 쪽으로 반(half away from zero)을 합니다.
- 분위수(quantiles)는 선형적입니다;
std와 var는 ddof=1를 사용합니다.
- Int64 오버플로우는 모든 곳에서 오류가 발생합니다.
따라서 collect("polars")와 collect("duckdb")는 동일한 Arrow 테이블을 반환합니다. 유일하게 허용되는 차이는 부동 소수점 합계의 마지막 자릿수뿐인데, 이는 부동 소수점 덧셈이 순서에 의존하기 때문입니다. 사용자는 자신의 노트북에서 Polars를 사용하여 개발하고 프로덕션에서는 DuckDB에서 실행하거나, 또는 한 엔진이 작업 부하에 더 적합할 때 전환할 수 있으며, 이 과정에서 숫자가 변경되지 않습니다.
4. 취소(Undo), 히스토리(History), 계보(Lineage) 및 드라이 런(Dry runs)
에이전트는 탐색합니다. 무언가를 시도해 보고, 그것을 살펴본 다음, 되돌아가서 다른 것을 시도합니다. pyursa 프레임은 불변(immutable)하므로 되돌아가는 것이 자유롭습니다.
**히스토리(History)**는 에이전트의 명시된 의도를 각 단계에 기록하여 제공됩니다:
for step in report.history():
print(step["op"], step["intent"])
# source None
...
계보(Lineage): 이 숫자는 어디에서 왔을까요?
report.lineage("revenue")
{
"expr": "col(\"revenue\").sum()",
"sources": ["data.price", "data.qty"],
...
이해관계자가
데이터셋 data: 6행 × 5열
| column | type | nulls | distinct | min | max | mean | top values |
...
통계는 엔진에 의해 계산되므로 수백만 행에서도 빠르게 유지됩니다. 모든 백엔드에서 동일합니다. 또한 프롬프트 주입(prompt-injection) 텍스트인 "ignore previous instructions"와 같은 텍스트 열의 모든 값을 스캔하여, 표시된 값들 사이에 있더라도 플래그를 지정합니다. 데이터는 신뢰할 수 없는 입력이며, pyursa는 이를 그렇게 취급합니다.
6. 데이터프레임 연산자로서의 LLM
어떤 단계들은 산술 계산이 아닌 판단을 필요로 합니다: "이 티켓은 배송에 관한 것인가?", "이 이메일에서 주문 번호를 추출하라". pyursa는 이러한 타입 지정된(typed) 단계들을 동일한 파이프라인 내에서 만듭니다:
pu.set_llm(pu.llm_client("gemini:gemini-3.8-flash"))
tickets = (
...
프로덕션 환경에서 사용 가능하게 만드는 부분들:
- 타입 지정 출력.
severity는 Int64 열로 반환됩니다. 타입에 맞지 않는 답변은 null이 되어 실패로 계산되며, 단계를 중단시키지 않습니다.
- 비용 통제. 중복 행은 한 번만 전송되고, 답변은 캐시되며, 행들은 배치(batch) 처리되고, 행 및 토큰 예산은 모델을 호출하기 전에 단계가 멈추게 하여, 실수로 인해 천만 개의 행에 대해 청구서가 발생하는 것을 막습니다.
- 개인정보 보호.
redact_pii=True는 이메일, 전화번호, 카드 번호 및 유사한 데이터가 기기를 벗어나기 전에 제거합니다.
- 모든 모델 지원. OpenAI와 호환되는 API를 통해 50개 이상의 제공업체에서 작동합니다: OpenAI, Gemini, Mistral, xAI, DeepSeek, Qwen, Groq, Together, Azure, Vertex, Databricks, Snowflake, OpenRouter, 로컬 Ollama 또는 vLLM 등. 또한 API 키 또는 일반 AWS 액세스 키를 사용하여 Claude와 Amazon Bedrock (Nova 및 AWS의 Claude)을 네이티브로 지원합니다. 모델을 전환하는 것은 하나의 문자열만 변경하면 됩니다.
7. 안전한 MCP 서버 및 도구 계층
pyursa를 에이전트에게 직접 전달하려면, 이를 MCP 서버로 실행합니다:
에이전트는 load_file, describe, transform, preview, explain, undo, lineage와 같은 도구를 얻습니다. 이 설계는 에이전트의 입력은 신뢰할 수 없다고 가정합니다:
- 데이터가 아닌 핸들(Handles)을 다룹니다. 에이전트는
f1과 같은 프레임 ID로 작업하며, 미리보기(최대 50개 행 제한)만 컨텍스트에 다시 들어갑니다.
- 코드 실행 없음.
col("qty") > 0 and col("status") != "cancelled"와 같은 표현식은 화이트리스트 파서에 의해 구문 분석됩니다. eval은 절대 사용되지 않으며, 퍼즈 테스트는 위험한 호출이 도달할 수 없음을 확인합니다.
- 파일 샌드박스. 모든 경로(path), 글롭 매치(glob match) 및 심볼릭 링크 대상은
--root와 비교됩니다.
- 아무것도 예외를 발생시키지 않습니다. 모든 도구는
{"ok": true, ...} 또는 구조화된 오류를 반환하므로, 실수로 에이전트 루프가 충돌하는 일이 없습니다.
- 격리된 세션. HTTP를 통해 각 클라이언트는 자체 프레임, 캐시 및 사용량 보고서를 얻습니다.
동일한 기능은 LangGraph, CrewAI, Pydantic AI 또는 사용자 지정 도구 호출 루프를 위해 일반 Python(pyursa.tools.ToolSession)으로 사용할 수 있습니다.
빠른가요?
pyursa는 각 파이프라인을 엔진용 단일 최적화된 쿼리로 컴파일하므로, 대부분의 작업은 Rust (Polars) 또는 C++ (DuckDB)에서 실행됩니다. 제 컴퓨터에서 종단 간(end-to-end) 테스트 파이프라인(필터링, 조인, 파생 열, 그룹별 집계 및 정렬을 포함하여 2백만 건의 주문 데이터 처리) 결과는 다음과 같습니다:
| time | vs pandas |
|---|
| pandas | 525 ms | 1× |
| ... | | |
<br>이것이 측정하는 바를 명확히 하자면: pyursa는 Polars와 DuckDB 위에서 실행되므로, 이 엔진들을 직접 사용하는 것보다 빠르지 않습니다. 이는 그 속도 위에 에이전트 계층을 추가한 것입니다. python scripts/e2e.py --rows 2000000으로 수치를 재현할 수 있습니다. | | |
테스트 방법
"사일런트 손상(silent corruption)"이 없다고 약속하는 라이브러리는 그것을 증명해야 합니다:
- 500개 이상의 테스트: 두 엔진의 모든 연산에 대한 적합성 테스트(conformance tests)를 포함하며, 결과를 값별로 비교하고 pandas와 결과가 일치하는지 확인하는 테스트도 포함합니다.
- 독립적인 검토 라운드: 엔진 간 불일치, 샌드박스 탈출(sandbox escapes), 손상 버그 등을 찾기 위해 진행되었습니다. 발견된 모든 문제는 수정되어 회귀 테스트(regression test)로 전환되었습니다.
- Linux, macOS 및 Windows에서의 CI: Python 3.10부터 3.14까지의 환경에서, 지원하는 가장 오래된 버전과 최신 프리릴리스 버전을 포함한 의존성 라이브러리들을 대상으로 진행됩니다.
- 사용자나 에이전트가 실제로 사용하는 방식을 시뮬레이션하는 종단 간(end-to-end) 스크립트(
scripts/e2e.py)를 제공하며, 선택한 모든 공급자의 LLM 호출을 포함합니다.
사용해보기 (Try it)
pip install "pyursa[all]" # Polars + DuckDB
pip install "pyursa[mcp]" # + MCP 서버
import pyursa as pu
from pyursa import col
...
pyursa는 새로운 라이브러리입니다. 데이터와 상호작용하는 에이전트를 구축하고 계시다면, 어떤 부분이 깨지는지(breaks), 무엇이 빠져 있는지(missing), 그리고 다음에 어떤 엔진을 지원받고 싶은지 알려주시면 감사하겠습니다. 이슈 및 풀 리퀘스트(pull requests)를 환영하며, GitHub의 ⭐는 다른 사람들이 이 라이브러리를 찾는 데 도움이 됩니다.