AI агенты для анализа данных: pandas-ai, Code Interpreter и генерация SQL


title: "AI агенты для анализа данных: pandas-ai, Code Interpreter и генерация SQL" slug: agents-data-analysis-pandas-code-interpreter-2026-ru date: 2026-02-21 lang: ru tags: [data-analysis, pandas, code-interpreter, sql-generation, llm-agents, e2b] description: "Создание production AI-агентов для анализа данных: запросы к DataFrame в естественном языке с pandas-ai, безопасное выполнение кода с E2B, NL-to-SQL с контекстом схемы и конвейеры генерации графиков."

AI агенты для анализа данных: pandas-ai, Code Interpreter и генерация SQL

Ключевые факты

  • pandas 3.0.1 (выпущена 2025-12, последняя стабильная версия); крупные breaking changes от 2.x включают copy-on-write по умолчанию
  • e2b-code-interpreter 2.4.1 (последняя версия); предоставляет изолированные облачные песочницы для выполнения кода; каждая песочница работает в свежем контейнере
  • langchain-experimental 0.4.1 включает обёртку create_pandas_dataframe_agent(); по умолчанию использует PythonREPLTool (локальное выполнение)
  • matplotlib 3.10.8 (последняя версия) и plotly 6.5.2 для генерации графиков; E2B поддерживает обе через backend matplotlib.use('Agg')
  • LangChain PythonREPLTool выполняет Python код локально используя exec() с ограниченными globals; опасно для сгенерированного LLM кода (может получить доступ к файловой системе, сети, subprocess)
  • E2B sandbox security: изолированные Docker контейнеры с 2GB RAM, 30GB хранилища; не могут получить доступ к host-системе, production базам данных или учётным данным
  • create_pandas_dataframe_agent() сигнатура функции: create_pandas_dataframe_agent(llm, df, agent_type="openai-tools", verbose=False, allow_dangerous_code=False)
  • Установка allow_dangerous_code=True включает локальное выполнение через exec(); требуется для LangChain агентов но небезопасно без sandboxing
  • Overhead токенов для DataFrames: DataFrame с 1000 строк × 10 столбцов с типичными string/numeric данными занимает ~3000-8000 токенов при отправке df.head(5).to_markdown() в LLM; полный df.to_string() может превышать 100K токенов для средних датасетов
  • E2B free tier: 100 часов выполнения sandbox/месяц; каждое invocation sandbox считает время от создания до завершения (типично 5-30 секунд на query)
  • За пределами 100 часов: $0.01 за минуту выполнения (~$0.60/час); биллится ежемесячно с автоматическим shutdown неиспользуемых sandbox после 5 минут
  • Chart output из E2B: графики сохраняются через plt.savefig('output.png') и получаются как base64-закодированные PNG через execution.results[0].png; типичный график 1200×600 это 50-200KB
  • Для отрисовки графиков в ответе: декодировать base64 → записать в файл → встроить в markdown как ![chart](data:image/png;base64,...) или сохранить в static path
  • pandas-agent iteration: LLM генерирует код → E2B выполняет → при ошибке LLM получает traceback → генерирует новый код; типично сходится за 1-3 итерации
  • Schema context optimization: отправлять имена столбцов + dtypes + примеры значений (3-5 строк) вместо полного DataFrame; снижает использование токенов на 90%+
  • SQL validation: использовать библиотеку sqlparse для парсинга сгенерированного SQL и блокировки INSERT/UPDATE/DELETE/DROP до выполнения
  • Common failure mode: код pandas сгенерированный LLM предполагает наличие столбцов без проверки; использовать schema introspection перед генерацией кода чтобы включить доступные столбцы в prompt
  • Modal alternative: Modal.com предоставляет похожий sandboxing с поддержкой GPU; требует написания custom executor class заменяющего E2B API calls

Что такое Data Analysis Agent

Data analysis agent работает на основе loop генерации кода + выполнения: LLM пишет Python или SQL код для ответа на естественно-языковой вопрос пользователя, выполняет его в безопасной среде, наблюдает результат (или ошибку) и итерирует до успеха.

В отличие от прямых LLM ответов (которые должны быть правильными с первой попытки), паттерн code interpreter позволяет агенту:

  1. Написать exploratory код для понимания формы данных
  2. Увидеть ошибки выполнения и исправить их
  3. Уточнить вычисления на основе промежуточных результатов
  4. Генерировать графики и валидировать visual output

Эта multi-step refinement резко улучшает точность для сложных queries включающих multi-table joins, date arithmetic или статистические расчёты.

Chart generation расширяет loop: LLM решает тип графика (line/bar/scatter/heatmap), генерирует matplotlib/seaborn код с правильными labels и стилизацией, выполняет его в sandbox и возвращает PNG bytes. Агент может переделать попытку с разными типами графиков если первая попытка не даёт ясного ответа на вопрос.

Decision Framework

Подход Безопасность Стоимость Latency Use Case
PythonREPLTool (локальный) Опасно; код LLM работает на вашем сервере с доступом к файловой системе/сети Бесплатно ~200ms на выполнение Только разработка; никогда в production
E2B sandbox Изолированный контейнер; нет доступа к production системам $0.01/мин после 100ч/месяц free tier ~2-5s на выполнение (включает startup контейнера) Production default для большинства use cases
Modal Изолированный; поддерживает GPU для ML workloads $0.0003/сек CPU, $0.003/сек GPU ~3-8s cold start, <1s warm Large-scale ML inference или heavy compute

Security tradeoff: локальное выполнение в 10-25× быстрее но позволяет LLM коду потенциально читать environment variables, получить доступ к базам через connection strings или делать outbound HTTP requests. E2B добавляет latency но гарантирует изоляцию.

Cost tradeoff: при 20 queries/day в среднем 10 секунд каждая, E2B стоит ~$1/месяц (хорошо в free tier). При 500 queries/day стоимость растёт до ~$25/месяц.

Recommendation: используйте E2B для production. Только используйте локальное выполнение в разработке с dummy данными, никогда с реальными credentials в environment variables.

Parameters Reference Table

Параметр Значение Примечания
model claude-opus-4-6 Best для сложной pandas/SQL генерации; Claude Sonnet приемлемо для простых queries
max_tokens 2048 Генерация кода; 512 для интерпретации результатов
timeout_seconds (E2B) 60 Timeout выполнения; увеличить до 120 для больших DataFrames (>1M строк)
max_iterations 5 Retry limit генерации кода; 3 достаточно для 90% queries
allow_dangerous_code (LangChain) False Должно быть True чтобы включить agent, но только используйте с E2B sandboxing
cache_results True Кешировать question → answer mapping; invalidate когда DataFrame меняется
df.head() rows 5 Строки для показа как sample в schema context; 3-5 оптимально для token efficiency
result_limit (SQL) 10000 Max строк возвращаемых из сгенерированных queries; предотвращает memory overflow
rate_limit 20 requests/min Per-user limit чтобы предотвратить quota exhaustion

Common Pitfalls

❌ Pitfall 1: Отправка полного DataFrame в LLM context

Impact: DataFrame с 10,000 строк может потребить 50K+ токенов, превышая context limits или вызывая медленные ответы.

# ❌ BAD: Отправляет весь DataFrame representation
messages = [{
    "role": "user",
    "content": f"Answer this question:\n{df.to_string()}\n\nQuestion: {question}"
}]
# ✅ GOOD: Отправляет только schema + sample
schema = {
    "shape": df.shape,
    "columns": [
        {
            "name": col,
            "dtype": str(df[col].dtype),
            "sample": df[col].head(3).tolist()
        }
        for col in df.columns
    ]
}

messages = [{
    "role": "user",
    "content": f"Schema:\n{json.dumps(schema)}\n\nQuestion: {question}"
}]

❌ Pitfall 2: Использование allow_dangerous_code=True без sandboxing

Impact: LLM-сгенерированный код может читать /etc/passwd, получить доступ к AWS credentials из environment или exfiltrate данные.

# ❌ DANGEROUS: Выполняется на host системе
agent = create_pandas_dataframe_agent(
    llm, df, allow_dangerous_code=True
)
agent.run("What is the average revenue?")  # LLM код работает с полным доступом к host
# ✅ SAFE: Выполнить в E2B sandbox
executor = E2BSafeExecutor(api_key=os.environ['E2B_API_KEY'])
code = llm_generate_code(question, schema)
result = executor.execute_with_dataframe(code, df)  # Работает в изолированном контейнере

❌ Pitfall 3: Не валидировать SQL перед выполнением

Impact: LLM может генерировать DROP TABLE или UPDATE statements, корруптируя production данные.

# ❌ BAD: Выполняет сгенерированный SQL вслепую
sql = llm.generate_sql(question)
df = pd.read_sql(sql, db_connection)  # Может быть "DROP TABLE users;"
# ✅ GOOD: Валидировать что SQL read-only
import sqlparse

def validate_sql_safe(sql: str) -> bool:
    parsed = sqlparse.parse(sql)
    for stmt in parsed:
        if stmt.get_type() not in ['SELECT', 'UNKNOWN']:
            return False
    dangerous = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'ALTER', 'TRUNCATE']
    return not any(kw in sql.upper() for kw in dangerous)

sql = llm.generate_sql(question)
if not validate_sql_safe(sql):
    raise SecurityError("Generated SQL contains mutation operations")
df = pd.read_sql(sql, db_connection)

❌ Pitfall 4: Отсутствие проверок существования столбцов в prompts

Impact: LLM генерирует код вроде df['revenue'].sum() когда столбец на самом деле называется total_revenue, вызывая KeyError.

# ❌ BAD: Prompt не включает доступные столбцы
prompt = f"Write pandas code to calculate average revenue for this dataset."
# ✅ GOOD: Включить точные имена столбцов в prompt
columns = df.columns.tolist()
prompt = f"""Write pandas code to calculate average revenue.

Available columns: {columns}

Rules:
- Check column exists before using it
- Use exact column names from the list above
"""

❌ Pitfall 5: Отсутствие retry logic для ошибок выполнения

Impact: Одна синтаксическая ошибка или неправильная ссылка на столбец вызывает полный отказ.

# ❌ BAD: One-shot выполнение
code = llm.generate_code(question, schema)
result = executor.execute(code)
if not result.success:
    return {"error": result.error}  # Сдаёмся сразу
# ✅ GOOD: Retry с error feedback
code = llm.generate_code(question, schema)
result = executor.execute(code)

if not result.success:
    # Отправить ошибку обратно в LLM на исправление
    fixed_code = llm.fix_code(question, code, result.error, schema)
    result = executor.execute(fixed_code)

return result

Pandas Agent Implementation

create_pandas_dataframe_agent из LangChain оборачивает DataFrame с conversational interface. Агент использует PythonREPLTool чтобы выполнять код локально по умолчанию.

from langchain_experimental.agents import create_pandas_dataframe_agent
from langchain_anthropic import ChatAnthropic
import pandas as pd

llm = ChatAnthropic(model="claude-opus-4-6")

df = pd.read_csv("sales_data.csv")

# WARNING: allow_dangerous_code=True выполняет LLM код на host
# Используйте только в разработке или с E2B sandboxing
agent = create_pandas_dataframe_agent(
    llm,
    df,
    agent_type="openai-tools",
    verbose=True,
    allow_dangerous_code=True  # Required but dangerous
)

response = agent.invoke("What is the total revenue by product category?")
print(response)

Prompt tuning для лучшей генерации кода:

prefix = """You are a data analysis expert working with a pandas DataFrame.

Rules for code generation:
1. Always check if columns exist before using them
2. Handle NaN values explicitly (use .dropna() or .fillna())
3. For date columns, parse with pd.to_datetime() first
4. Round numeric results to 2 decimal places
5. Limit output to top 20 rows for lists
6. Store final result in variable named `result`

Available DataFrame: `df`
Columns: {columns}
Shape: {shape}
"""

agent = create_pandas_dataframe_agent(
    llm,
    df,
    agent_type="openai-tools",
    prefix=prefix.format(
        columns=df.columns.tolist(),
        shape=df.shape
    ),
    verbose=True,
    allow_dangerous_code=True
)

Multi-DataFrame analysis:

sales_df = pd.read_csv("sales.csv")
customers_df = pd.read_csv("customers.csv")

# LangChain agent accepts list of DataFrames
agent = create_pandas_dataframe_agent(
    llm,
    [sales_df, customers_df],
    allow_dangerous_code=True
)

# Agent can reference both as df1 and df2
response = agent.invoke(
    "Join sales and customer data to find revenue by customer segment"
)

Агент будет генерировать код вроде:

merged = df1.merge(df2, left_on='customer_id', right_on='id')
result = merged.groupby('segment')['revenue'].sum()

E2B Code Interpreter Integration

E2B предоставляет secure sandboxes для выполнения LLM-сгенерированного кода без риска для вашей production окружения.

from e2b_code_interpreter import Sandbox
import pandas as pd
import base64

class E2BSafeExecutor:
    def __init__(self, api_key: str, timeout: int = 60):
        self.api_key = api_key
        self.timeout = timeout

    def execute_with_dataframe(self, code: str, df: pd.DataFrame) -> dict:
        """Execute code in isolated sandbox with DataFrame pre-loaded."""

        # Serialize DataFrame to parquet (efficient binary format)
        df_bytes = df.to_parquet(index=False)
        df_b64 = base64.b64encode(df_bytes).decode()

        # Wrap user code with DataFrame loading logic
        full_code = f"""
import pandas as pd
import numpy as np
import matplotlib
matplotlib.use('Agg')  # Non-interactive backend
import matplotlib.pyplot as plt
import base64
import io

# Load DataFrame from base64 parquet
df_bytes = base64.b64decode('{df_b64}')
df = pd.read_parquet(io.BytesIO(df_bytes))

# User-generated code
{code}

# Capture result
if 'result' in dir():
    print("RESULT:", repr(result))
"""

        with Sandbox(api_key=self.api_key, timeout=self.timeout) as sandbox:
            execution = sandbox.run_code(full_code)

            # Extract charts if generated
            charts = []
            for output in execution.results:
                if hasattr(output, 'png') and output.png:
                    charts.append(base64.b64decode(output.png))

            # Parse result from stdout
            result = None
            for log in execution.logs.stdout:
                if log.startswith("RESULT:"):
                    result = eval(log[7:].strip())

            if execution.error:
                return {
                    "success": False,
                    "output": None,
                    "error": str(execution.error.value),
                    "charts": []
                }

            return {
                "success": True,
                "output": result,
                "error": None,
                "charts": charts
            }

File upload для CSV анализа:

with Sandbox(api_key=api_key) as sandbox:
    # Upload CSV to sandbox
    sandbox.filesystem.write("data.csv", csv_content)

    code = """
import pandas as pd
df = pd.read_csv('data.csv')
result = df.groupby('category')['sales'].sum().to_dict()
"""

    execution = sandbox.run_code(code)

Chart retrieval и отрисовка:

code = """
import matplotlib.pyplot as plt
import pandas as pd

# Generate chart
df.groupby('month')['revenue'].sum().plot(kind='bar')
plt.title('Revenue by Month')
plt.xlabel('Month')
plt.ylabel('Revenue ($)')
plt.tight_layout()
plt.savefig('output.png', dpi=150, bbox_inches='tight')
result = 'chart generated'
"""

result = executor.execute_with_dataframe(code, df)

if result['charts']:
    chart_bytes = result['charts'][0]

    # Option 1: Save to file
    with open('chart.png', 'wb') as f:
        f.write(chart_bytes)

    # Option 2: Base64 embed in markdown/HTML
    chart_b64 = base64.b64encode(chart_bytes).decode()
    html = f'<img src="data:image/png;base64,{chart_b64}" />'

SQL + Pandas Hybrid

Для больших датасетов хранящихся в базах, гибридный подход сначала запрашивает базу, затем загружает результаты в pandas для сложного анализа.

from anthropic import Anthropic
import pandas as pd
import sqlparse

class SQLPandasAgent:
    def __init__(self, db_connection, e2b_executor):
        self.db = db_connection
        self.executor = e2b_executor
        self.client = Anthropic()

    def analyze(self, question: str, schema_context: dict) -> dict:
        """
        1. Generate SQL to extract relevant data
        2. Load results into pandas DataFrame
        3. Use pandas agent for detailed analysis
        """

        # Step 1: Generate SQL
        sql = self._generate_sql(question, schema_context)

        # Validate SQL is read-only
        if not self._is_safe_sql(sql):
            return {"error": "Generated SQL contains unsafe operations"}

        # Step 2: Execute SQL
        try:
            df = pd.read_sql(sql, self.db)
        except Exception as e:
            # Retry with fixed SQL
            sql = self._fix_sql(question, sql, str(e), schema_context)
            df = pd.read_sql(sql, self.db)

        # Step 3: Pandas analysis on result set
        pandas_code = self._generate_pandas_analysis(question, df)
        result = self.executor.execute_with_dataframe(pandas_code, df)

        return {
            "sql": sql,
            "row_count": len(df),
            "analysis_code": pandas_code,
            "result": result['output'],
            "success": result['success']
        }

    def _generate_sql(self, question: str, schema: dict) -> str:
        response = self.client.messages.create(
            model="claude-opus-4-6",
            max_tokens=2048,
            messages=[{
                "role": "user",
                "content": f"""Generate SQL to extract data for this question.

Database schema:
{json.dumps(schema, indent=2)}

Question: {question}

Rules:
- Use SELECT only (no INSERT/UPDATE/DELETE/DROP)
- Add LIMIT 100000 to prevent memory issues
- Use proper JOINs for related tables
- Include only columns needed for analysis

Return only the SQL query."""
            }]
        )

        return response.content[0].text.strip()

    def _is_safe_sql(self, sql: str) -> bool:
        parsed = sqlparse.parse(sql)
        for stmt in parsed:
            if stmt.get_type() not in ['SELECT', 'UNKNOWN']:
                return False

        dangerous_keywords = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'ALTER', 'TRUNCATE']
        return not any(kw in sql.upper() for kw in dangerous_keywords)

    def _generate_pandas_analysis(self, question: str, df: pd.DataFrame) -> str:
        schema_info = {
            "columns": [
                {"name": col, "dtype": str(df[col].dtype)}
                for col in df.columns
            ],
            "shape": df.shape
        }

        response = self.client.messages.create(
            model="claude-opus-4-6",
            max_tokens=2048,
            messages=[{
                "role": "user",
                "content": f"""Write pandas code to analyze this data.

DataFrame schema (from SQL query results):
{json.dumps(schema_info, indent=2)}

Original question: {question}

The data has already been filtered by SQL. Now perform detailed analysis.
Store final answer in variable `result`.

Return only Python code."""
            }]
        )

        return self._extract_code(response.content[0].text)

    def _extract_code(self, text: str) -> str:
        if '```python' in text:
            return text.split('```python')[1].split('```')[0].strip()
        elif '```' in text:
            return text.split('```')[1].split('```')[0].strip()
        return text.strip()

Usage example:

import psycopg2

db = psycopg2.connect("postgresql://user:pass@localhost/db")
executor = E2BSafeExecutor(api_key=os.environ['E2B_API_KEY'])

agent = SQLPandasAgent(db, executor)

schema_context = {
    "orders": {
        "columns": ["id", "customer_id", "order_date", "total_amount", "status"],
        "description": "Customer orders with amounts and dates"
    },
    "customers": {
        "columns": ["id", "name", "segment", "signup_date"],
        "description": "Customer master data"
    }
}

result = agent.analyze(
    "What is the month-over-month revenue growth for enterprise customers?",
    schema_context
)

print(f"SQL executed: {result['sql']}")
print(f"Rows retrieved: {result['row_count']}")
print(f"Analysis result: {result['result']}")

Агент будет:

  1. Генерировать SQL: SELECT DATE_TRUNC('month', order_date) as month, SUM(total_amount) FROM orders JOIN customers ON orders.customer_id = customers.id WHERE segment = 'enterprise' GROUP BY month
  2. Загружать 12 месяцев агрегированных данных в pandas (маленький DataFrame)
  3. Генерировать pandas код: result = df.sort_values('month').pct_change()['sum'].iloc[-1]
  4. Выполнять в E2B sandbox и возвращать процент роста

Performance & Benchmarks

Примечание: Приведённые ниже цифры являются иллюстративными оценками на основе типичных production-конфигураций, а не измерениями конкретной системы.

Query latency breakdown (E2B sandbox, Claude Opus 4):

  • LLM код генерация: 1.5-3 секунды
  • E2B sandbox startup: 0.8-2 секунды (cold start), 0.2-0.5 секунды (warm)
  • Выполнение кода: 0.1-5 секунд (зависит от размера DataFrame и операций)
  • Chart генерация: 0.5-2 секунды (matplotlib рендеринг)
  • Total end-to-end: 3-12 секунд для типичных queries

Token consumption (Claude Opus 4):

  • Schema context (50-column DataFrame): 800-1500 токенов
  • Сгенерированный pandas код: 200-600 токенов
  • Error traceback + retry: +300-500 токенов за итерацию
  • Result интерпретация: 100-300 токенов
  • Total на query: 1400-2900 токенов (без retries)

Cost estimates (Claude Opus 4 на $15/MTok input, $75/MTok output):

  • Simple query (1 итерация, без графика): $0.03-0.05
  • Complex query (2 итерации, с графиком): $0.08-0.15
  • При 100 queries/день: $3-15/день ($90-450/месяц)

E2B sandbox efficiency:

  • Cold start overhead: 40-60% всего latency для простых queries
  • Warm sandbox reuse: снижает latency на 1-2 секунды
  • Parallel execution: E2B поддерживает до 10 concurrent sandboxes на account
  • Memory limit: 2GB за sandbox (достаточно для DataFrames до ~500K строк с 50 столбцами)

DataFrame size limits:

  • Оптимальная performance: <100MB DataFrame (~500K строк × 20 столбцов смешанных типов)
  • Приемлемая performance: 100-500MB (1-2M строк; требует увеличенного E2B timeout до 120s)
  • Требует chunking: >500MB (использовать SQL фильтрацию чтобы снизить размер датасета перед pandas анализом)

Accuracy benchmarks (качественно):

  • Single-step агрегации (sum, mean, count): высокая точность с ясным schema context
  • Multi-step трансформации (pivot, merge, window functions): требует 1-2 retry итераций для сложных случаев
  • Date arithmetic и timezone handling: частый источник ошибок; включить date format в schema context
  • Chart type selection: LLM выбирает подходящий тип графика для большинства queries; иногда требует явной инструкции ("use a line chart")

Optimization strategies:

  • Кешировать schema introspection результаты (имена столбцов, dtypes, примеры значений) чтобы избежать перевычисления
  • Переиспользовать warm E2B sandboxes для sequential queries на том же датасете
  • Pre-filter большие датасеты с SQL перед загрузкой в pandas
  • Использовать parquet сериализацию вместо CSV для DataFrame трансфера (3-5× меньше, быстрее десериализации)
  • Ограничить df.head() samples до 3-5 строк в schema context (больше 5 строк добавляет токены без улучшения точности)