到目前為止,Agent 大部分的行為都是透過我們事先定義好的 tools。
接下來要進入更重要的一步:
讓 Agent 產生 analysis code,然後真的執行它。
這裡第一個問題就是:
Agent 產生的 code 應該在哪裡執行?
最直接的做法,是直接在 backend 跑:
Agent
↓
Generated Python
↓
Backend
↓
Execute
但這樣其實風險很高,如果 Agent 產生了有危害性的 code,它可能直接影響你的電腦環境,或存取不該碰到的 resource。
所以我們希望 generated code 不要直接跑在 backend,而是在一個 isolated environment 裡執行。
Agent
↓
Generated Code
↓
Sandbox
↓
Execute
↓
Result
這就是 sandbox 在這個 system 裡最主要的角色:
把 Agent-generated code 跟 application environment 隔離開來。
Sandbox 不只是提供一個「可以跑 Python 的地方」。
更重要的是建立一個清楚的 execution boundary:
Backend
│
│ 只送 execution 真正需要的東西
▼
Sandbox
│
│ 執行 generated code
▼
Result / Files / Charts
│
▼
Backend
Agent 可以產生 code,但 code 不會直接跑在 backend process 裡。
Backend 負責控制:
什麼可以進 Sandbox
什麼可以在裡面執行
什麼可以從 Sandbox 出來
當然也可以自己從 Docker 開始做。
但如果從頭自己處理,就要開始管理:
create container
install dependencies
upload files
execute commands
collect outputs
handle failures
cleanup
這些其實都是 execution infrastructure,不是這個 project 最核心的問題。
所以目前我先用 Daytona 當 sandbox provider。
Daytona 官網可以直接申請帳號,接著從 Dashboard 建立 API key。官方 SDK 也支援直接從 DAYTONA_API_KEY environment variable 讀取。:chatgpt-content-reference{index="0"}
Daytona 採用 pay-as-you-go,目前官方 pricing 是按照 sandbox 保留的 vCPU、memory 和 storage 計費,而且 billing 以秒計算。一般方案目前包含 $200 free compute,free trial 不需要信用卡。
另外 Daytona 也有 startup program。官方 pricing page 目前寫的是 startup 可以申請最高 $50,000 credits;不過他們另外的 Startup Grid 頁面目前已經寫到最高 $100,000 credits,所以這類 promotional credits 之後可能還會調整。
對這個系列來說,我比較在意的是一般開發階段已經有 free compute 可以先測,不需要一開始就自己維護整套 sandbox infrastructure。
Project 自己還是保留一層簡單的 provider:
SandboxProvider
↓
DaytonaProvider
Lifecycle 大概是:
Create
↓
Prepare
↓
Execute
↓
Collect Outputs
↓
Destroy
比較重要的是,sandbox lifecycle 不應該交給 Agent 自己管理。
即使中間 execution fail,最後還是要把 sandbox cleanup 掉。
這件事除了 resource management,也直接跟 cost 有關。Daytona 在 sandbox 處於 started、creating、starting、stopping、pausing 等狀態時,都還會計算 CPU、RAM 和 disk;停止或 pause 後則主要剩 storage cost。
Sandbox 相關設定一樣放在 config.yml:
sandbox:
provider: daytona
packages:
- pyarrow
- polars
data_dir: /home/daytona/data
output_dir: /home/daytona/outputs
export_max_rows: 100000
這樣 Agent workflow 不需要綁死在 Daytona。
今天可以用:
Daytona
之後如果要換:
Modal
Other Sandbox Provider
主要只需要換 provider implementation。
Agent 本身不需要知道底下是哪一套 sandbox。
這裡目前跟比較理想的 architecture 有一點差距。
最直接的設計其實是:
Sandbox
↓
直接連 Database
↓
Query 需要的資料
這樣可以少掉中間的 data transfer,也比較適合 iterative analysis。
但 direct database access 也代表 sandbox 需要:
目前我的 PostgreSQL 還是跑在 local machine。
我不想只是為了讓 sandbox 比較方便,就把 local database 或自己的 machine 暴露給外部 sandbox environment。
所以目前先用比較嚴格的 boundary:
PostgreSQL
↓
Backend 執行 Query
↓
Export 需要的 Data
↓
Parquet
↓
Sandbox
Database 留在 backend 這一側。
Sandbox 只拿這次 analysis 真正需要的資料。
它不會拿到 database credentials,也不需要連回我的 local machine。
這不一定是最後的 architecture。
如果之後 database 搬到受控的 cloud environment,那讓 sandbox 直接連 database 可能會是比較好的做法。
但以目前的 setup 來說,Parquet 是比較安全也比較簡單的 boundary。
前面我們已經有:
query_database()
這個 tool 適合比較小的 query result。
例如:
Which category has the highest median views?
可以直接在 SQL 裡 aggregate,最後只回傳少量 rows 給 Agent。
但有些 analysis 需要更多資料。
這時候如果還是把所有 result 塞進 model context,就不太合理。
所以另外提供:
export_query(sql, filename)
Flow 會變成:
Agent
↓
export_query
↓
Backend 執行 SQL
↓
Write Parquet
↓
Upload to Sandbox
資料不需要先經過 model context。
Agent-generated code 可以直接在 sandbox 裡讀取它。
這裡我選 Parquet,而不是 CSV。
其中一個原因是 data type 保留得比較好。
例如:
date
→ date
timestamp
→ datetime
integer
→ integer
boolean
→ boolean
如果用 CSV,很多 type information 在檔案層就會消失,之後讀取時還要重新 infer。
另外一個原因是,我希望後面的 tabular analysis 主要使用 Polars。
Polars 本身很適合處理 columnar data,而 Parquet 也是 columnar format,所以兩個搭配起來很自然。
Parquet
↓
Polars
↓
filter / group_by / aggregate
↓
analysis result
相比 CSV,Parquet 對這個 project 有幾個好處:
所以我希望後面的責任大概是:
PostgreSQL
→ Query / Filtering
Parquet
→ 跨 Sandbox Boundary 傳資料
Polars
→ Sandbox 裡面的 Analysis
我不是因為 pandas 做不到。
只是這個 project 後面會開始處理比較大的 dataset,所以想從一開始就把 sandbox 裡的 tabular analysis 統一用 Polars。
資料進到 sandbox 之後,Agent 就可以自己產生 Python code。
例如:
import polars as pl
df = pl.read_parquet(
"/home/daytona/data/data.parquet"
)
result = (
df.group_by("category_name")
.agg(
pl.col("views")
.median()
.alias("median_views")
)
.sort(
"median_views",
descending=True,
)
)
print(result.head())
如果需要 chart,也可以讓 Agent 產生:
import matplotlib.pyplot as plt
plot_df = result.head(10).to_pandas()
plot_df.plot(
x="category_name",
y="median_views",
kind="bar",
)
plt.savefig(
"/home/daytona/outputs/category_views.png"
)
所以 sandbox 裡面的 flow 大概會是:
Parquet
↓
Polars
↓
Analysis
↓
Result / Chart
Generated artifacts 統一放在:
/home/daytona/outputs
Execution 結束之後,再由 backend 把這些 outputs 拉回來。
有 sandbox,不代表 system 就自動安全。
真正重要的是:
我們允許什麼東西跨過 sandbox boundary?
目前的設計是:
Database credentials
✗ 不進 Sandbox
Application environment
✗ 不暴露
Required data
✓ 可以進 Sandbox
Generated code
✓ 在 Sandbox 執行
Output artifacts
✓ 可以回 Backend
所以 security 不是單純:
Run code in a container
而是:
Control what goes in
+
Control what can execute
+
Control what comes out
Sandbox 解決的是 isolation。
但真正的安全性還是取決於我們怎麼設計這個 boundary。
加入 sandbox 之後,目前的 execution flow 已經開始接近我們真正想做的 Analysis pipeline:
User Intent
↓
Agent
↓
Query / Export Data
↓
Parquet
↓
Generate Analysis Code
↓
Sandbox
↓
Polars
↓
Execute
↓
Result / Artifact
↓
Agent
Daytona 只是目前選擇的 implementation。
真正重要的是:
Agent 可以產生 code,但這些 code 不應該直接執行在 application environment 裡。
目前 database 還在 local,所以 data 先透過 Parquet 進 sandbox。
而 Parquet + Polars 也剛好讓後面的 analysis path 很清楚:
Database
→ Parquet
→ Polars
→ Analysis
之後如果 infrastructure 改變,例如 database 搬到 cloud,再考慮讓 sandbox 直接 query database。
但 sandbox 這層本身,以及我們建立的 execution boundary,都可以繼續保留下來。