
monty v0.0.20
一个使用 Rust 编写的、最小化且安全的 Python 解释器,供 AI 使用。
Monty
一个用 Rust 编写的、供 AI 使用的最小化、安全的 Python 解释器。
实验性 - 该项目仍处于开发阶段,尚未做好正式发布的准备。
一个用 Rust 编写的、供 AI 使用的最小化、安全的 Python 解释器。
Monty 避免了为运行 LLM 生成的代码而使用基于完整容器的沙箱所带来的成本、延迟、复杂性以及各种麻烦。
相反,它让你安全地运行由嵌入在智能体中的 LLM 编写的 Python 代码,启动时间以个位数微秒计算,而不是数百毫秒。
Monty 可以做什么:
- 运行一个合理的 Python 子集——足以让你的智能体表达它想要做的事情
- 完全阻止对主机环境的访问:文件系统、环境变量和网络访问都通过开发者可以控制的外部函数调用来实现
- 调用主机上的函数——仅限于你授予访问权限的函数
- 运行类型检查——Monty 支持完整的现代 Python 类型提示,并在单个二进制文件中附带 ty 用于运行类型检查
- 在外部函数调用时被快照为字节,这意味着你可以将解释器状态存储在文件或数据库中,稍后恢复
- 启动极快(从代码到执行结果少于 1μs),并且运行时性能与 CPython 相似(通常快 5 倍到慢 5 倍之间)
- 可从 Rust、Python 或 Javascript 调用——因为 Monty 不依赖 cpython,你可以在任何能运行 Rust 的地方使用它
- 控制资源使用——Monty 可以跟踪内存使用、堆栈深度和执行时间,并在超出预设限制时取消执行
- 收集 stdout 和 stderr 并将其返回给调用者
- 通过主机端的异步或同步代码在主机上运行异步或同步代码
- 使用标准库的一小部分:
sys、os、typing、asyncio、re、datetime、json、dataclasses(即将支持)
Monty 不能做什么:
- 使用标准库的其余部分
- 使用第三方库(如 Pydantic),支持外部 Python 库不是目标
- 定义类(支持应很快到来)
- 使用 match 语句(同样,支持应很快到来)
简而言之,Monty 功能极其有限,专为一个用例而设计:
运行由智能体编写的代码。
如果你想了解为什么要这么做,可以参考:
- Codemode 来自 Cloudflare
- 编程式工具调用 来自 Anthropic
- 使用 MCP 执行代码 来自 Anthropic
- Smol Agents 来自 Hugging Face
简单来说,上述所有方案的核心思想是:如果让 LLM 编写 Python(或 Javascript)代码,而不是依赖传统的工具调用,它们可以更快、更便宜、更可靠地工作。Monty 让这一切成为可能,而不必面对沙箱的复杂性,也不必承担直接在主机上运行代码的风险。
注意: Monty 将(很快)被用于在 Pydantic AI 中实现 codemode。
使用
可以从 Python、JavaScript/TypeScript 或 Rust 调用 Monty。
Python
要安装:```bash uv add pydantic-monty
(或者,`pip install pydantic-monty` 是给老古董们用的)
`pydantic-monty` 是一个元包,将 `pydantic-monty-client`(`pydantic_monty` 模块)与 `pydantic-monty-runtime`(`monty` 工作程序二进制文件)配对。如果二进制文件已经来自其他来源,则可以单独安装 `pydantic-monty-client`。
用法:```python
from typing import Any
import pydantic_monty
code = """
async def agent(prompt: str, messages: Messages):
while True:
print(f'messages so far: {messages}')
output = await call_llm(prompt, messages)
if isinstance(output, str):
return output
messages.extend(output)
await agent(prompt, [])
"""
type_definitions = """
from typing import Any
Messages = list[dict[str, Any]]
async def call_llm(prompt: str, messages: Messages) -> str | Messages:
raise NotImplementedError()
prompt: str = ''
"""
Messages = list[dict[str, Any]]
async def call_llm(prompt: str, messages: Messages) -> str | Messages:
if len(messages) < 2:
return [{'role': 'system', 'content': 'example response'}]
else:
return f'example output, message count {len(messages)}'
async def main():
async with pydantic_monty.AsyncMonty() as pool:
async with pool.checkout(
script_name='agent.py',
type_check=True,
type_check_stubs=type_definitions,
) as session:
output = await session.feed_run(
code,
inputs={'prompt': 'testing'},
external_lookup={'call_llm': call_llm},
)
print(output)
#> example output, message count 2
if __name__ == '__main__':
import asyncio
asyncio.run(main())
执行发生在 monty 工作子进程池中,因此即使由对抗性代码
触发的内存错误(栈溢出、分配器中止)也永远不会使你的
进程崩溃——工作子进程会终止,抛出 MontyCrashedError,
并被替换。此外还有一个完全同步的 API:```python
import pydantic_monty
with pydantic_monty.Monty() as pool: with pool.checkout() as session: # session state persists between feed_run calls session.feed_run('x = 21') print(session.feed_run('x * 2')) #> 42
### JavaScript / TypeScript
安装:```bash
npm install @pydantic/monty
The JS package is a native (napi) binding over the same Rust worker pool the
Python package uses — the binding and the monty worker binary ship via
platform-specific npm packages:```ts
import { Monty } from '@pydantic/monty'
await using pool = await Monty.create() await using session = await pool.checkout()
// session state persists between feedRun calls await session.feedRun('x = 21') console.log(await session.feedRun('x * 2')) // 42
// external functions may be async const result = await session.feedRun('await fetch_data()', { externalLookup: { fetch_data: async () => 'data' }, })
对于浏览器(或任何无法使用子进程的环境),同一个包
在 `@pydantic/monty/wasm` 子路径下提供了进程内 WebAssembly 构建
(没有崩溃隔离:那里的沙箱崩溃即是主机崩溃)。
### Rust
对于在 Rust 中运行不受信任的代码,我们推荐使用
[`monty-pool`](https://crates.io/crates/monty-pool) crate,而不是下面提供的进程内 API。
`monty-pool` 仅在 `monty` 工作子进程中运行代码,这提供了额外保护:
由对抗性代码(栈溢出、分配器中止)触发的崩溃只会杀死工作进程 —
池会检测到该进程死亡并替换它 — 并且父端看门狗可以杀死
超过硬超时时间的工作进程。它与上述 Python 和 JavaScript 包所基于的
引擎相同。有关用法,请参阅 [monty-pool README](https://github.com/pydantic/monty/tree/main/crates/monty-pool)
以获取用法。
`monty` crate 本身提供了进程内解释器:```rust
use monty::MontyRun;
use monty_types::{CompileOptions, ResourceTracker, MontyObject, PrintWriter, ResourceLimits};
let code = r#"
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
fib(x)
"#;
let runner = MontyRun::new(code.to_owned(), "fib.py", vec!["x".to_owned()], CompileOptions::default()).unwrap();
let result = runner.run(vec![MontyObject::Int(10)], ResourceTracker::default(), PrintWriter::Stdout).unwrap();
assert_eq!(result, MontyObject::Int(55));
序列化
REPL 会话可以使用 dump() 序列化,并通过 Dump::load() 恢复。转储数据携带会话元数据(脚本名称、类型检查存根)以及解释器状态,并附有加载构建所检查的版本:```rust
use monty::{Dump, MontyRepl, Session, SessionRef, dump};
use monty_types::{CompileOptions, MontyObject, PrintWriter, ResourceTracker};
// Snapshot a session between snippets let mut repl = MontyRepl::new("main.py", ResourceTracker::default(), CompileOptions::default()); repl.feed_run("x = 41", vec![], PrintWriter::Stdout).unwrap(); let bytes = dump("main.py", None, SessionRef::Idle(&repl)).unwrap();
// Later, restore and carry on feeding let Session::Idle(mut restored) = Dump::load(&bytes).unwrap().state else { panic!("dumped an idle session") }; let result = restored.feed_run("x + 1", vec![], PrintWriter::Stdout).unwrap(); assert_eq!(result, MontyObject::Int(42));
`MontyRun` 和 `RunProgress` 本身没有转储格式,但两者都实现了 `serde::Serialize`/`Deserialize`,因此宿主可以使用它已有的任何格式来序列化解析后的代码或暂停中的运行。
## Worker 中的内存限制
会话的 `max_memory` 由 worker 的分配器(allocator)衡量。解释器在超过软限制后会报告一个优雅的 `MemoryError`;如果某次分配在检查点之间跳跃过大,更高的硬限制会终止并替换该 worker。
有关超过限制后如何向宿主呈现的说明,请参阅 [`limitations/resource_limits.md`](https://github.com/pydantic/monty/blob/HEAD/limitations/resource_limits.md);有关子进程和 WebAssembly worker 运行时所使用的分配器,请参阅 `monty-alloc`。
## PydanticAI 集成
Monty 将为 [Pydantic AI](https://github.com/pydantic/pydantic-ai) 中的代码模式(code-mode)提供支持。LLM 不再进行顺序的工具调用,而是编写 Python 代码,以函数形式调用你的工具,并由 Monty 安全地执行。```python test="skip"
import asyncio
import json
import logfire
from httpx import AsyncClient
from pydantic_ai import Agent, RunContext
from pydantic_ai.toolsets.code_mode import CodeModeToolset
from pydantic_ai.toolsets.function import FunctionToolset
from typing_extensions import TypedDict
logfire.configure()
logfire.instrument_pydantic_ai()
class LatLng(TypedDict):
lat: float
lng: float
weather_toolset: FunctionToolset[AsyncClient] = FunctionToolset()
@weather_toolset.tool
async def get_lat_lng(
ctx: RunContext[AsyncClient], location_description: str
) -> LatLng:
"""Get the latitude and longitude of a location."""
# NOTE: the response here will be random, and is not related to the location description.
r = await ctx.deps.get(
'https://demo-endpoints.pydantic.workers.dev/latlng',
params={'location': location_description},
)
r.raise_for_status()
return json.loads(r.content)
@weather_toolset.tool
async def get_temp(ctx: RunContext[AsyncClient], lat: float, lng: float) -> float:
"""Get the temp at a location."""
# NOTE: the responses here will be random, and are not related to the lat and lng.
r = await ctx.deps.get(
'https://demo-endpoints.pydantic.workers.dev/number',
params={'min': 10, 'max': 30},
)
r.raise_for_status()
return float(r.text)
@weather_toolset.tool
async def get_weather_description(
ctx: RunContext[AsyncClient], lat: float, lng: float
) -> str:
"""Get the weather description at a location."""
# NOTE: the responses here will be random, and are not related to the lat and lng.
r = await ctx.deps.get(
'https://demo-endpoints.pydantic.workers.dev/weather',
params={'lat': lat, 'lng': lng},
)
r.raise_for_status()
return r.text
agent = Agent(
'gateway/anthropic:claude-sonnet-4-5',
# toolsets=[weather_toolset],
toolsets=[CodeModeToolset(weather_toolset)],
deps_type=AsyncClient,
)
async def main():
async with AsyncClient() as client:
await agent.run('Compare the weather of London, Paris, and Tokyo.', deps=client)
if __name__ == '__main__':
asyncio.run(main())
社区绑定
替代方案
向人们展示 Monty 时,通常会有两种反应:
- 天哪,这解决了好多问题,我想要。
- 为什么不用 X?
其中 X 是某种替代技术。奇怪的是,这两种反应常常同时出现,这说明人们尚未找到适合自己的替代方案,但又难以置信的是,从头实现一个完整的 Python 竟然真的没有好的替代方案。
我将尝试梳理最显而易见的替代方案,以及为什么它们不适合我们的需求。
注意:所有这些技术都令人印象深刻且应用广泛,这篇关于它们在我们用例中局限性的评论不应被视为批评。这些解决方案大多并非以提供 LLM 沙箱为目标而设计,因此它们在这方面未必表现出色。
| 技术 | 语言完整性 | 安全性 | 启动延迟 | FOSS | 设置复杂度 | 文件挂载 | 快照 |
|---|---|---|---|---|---|---|---|
| Monty | 部分 | 严格 | 0.06ms | 免费 / 开源 | 简单 | 简单 | 简单 |
| Docker | 完整 | 良好 | 195ms | 免费 / 开源 | 中等 | 简单 | 中等 |
| Pyodide | 完整 | 差 | 2800ms | 免费 / 开源 | 中等 | 简单 | 困难 |
| starlark-rust | 非常有限 | 良好 | 1.7ms | 免费 / 开源 | 简单 | 不可用? | 不可能? |
| WASI / Wasmer | 部分,几乎完整 | 严格 | 66ms | 免费 * | 中等 | 简单 | 中等 |
| 沙箱服务 | 完整 | 严格 | 1033ms | 非免费 | 中等 | 困难 | 中等 |
| YOLO Python | 完整 | 不存在 | 0.1ms / 30ms | 免费 / 开源 | 简单 | 简单 / 危险 | 困难 |
参见 ./scripts/startup_performance.py 以获取用于计算启动性能数据的脚本。
以下是每一行的详细信息:
Monty
- 语言完整性:暂无类(class),标准库有限,没有第三方库
- 安全性:显式控制文件系统、网络和环境访问,严格限制执行时间和内存使用
- 启动延迟:微秒级启动
- 设置复杂度:只需
pip install pydantic-monty或npm install @pydantic/monty,下载约 4.5MB - 文件挂载:严格受控,参见 #85
- 快照:Monty 的暂停和恢复功能配合
dump()和load(),使得暂停、恢复和分支执行变得轻而易举
Docker
- 语言完整性:完整的 CPython,可使用任意库
- 安全性:进程和文件系统隔离、网络策略,但存在容器逃逸,内存限制可行
- 启动延迟:容器启动开销(实测约 195ms)
- 设置复杂度:需要 Docker 守护进程、容器镜像、编排,
python:3.14-alpine为 50MB——docker 无法通过 PyPI 安装 - 文件挂载:卷挂载效果良好
- 快照:可以使用 Temporal 等持久执行解决方案,或者对镜像做快照并保存为 Docker 镜像。
Pyodide
- 语言完整性:完整的 CPython 编译为 WASM,几乎所有库都可用
- 安全性:依赖浏览器/WASM 沙箱——并非为服务端隔离而设计,Python 代码可在 JS 运行时中执行任意代码,只有 deno 支持隔离,而通过 deno 实施内存限制很困难/不可能
- 启动延迟:WASM 运行时加载缓慢(冷启动约 2800ms)
- 设置复杂度:需要加载 WASM 运行时、处理异步初始化,pyodide NPM 包约 12MB,deno 约 50MB——仅靠 PyPI 包无法调用 Pyodide
- 文件挂载:通过浏览器 API 提供虚拟文件系统
- 快照:大概可以通过 Temporal 等持久执行解决方案实现,但很困难
starlark-rust
参见 starlark-rust。
- 语言完整性:配置语言,并非 Python——没有类、异常、async(异步)
- 安全性:设计上确定且封闭(hermetic)
- 启动延迟:像 Monty 一样嵌入进程运行,因此启动时间非常出色
- 设置复杂度:可通过 starlark-pyo3 在 python 中使用
- 文件挂载:据我所知,设计上不处理文件?
- 快照:据我所知,不可能?
WASI / Wasmer
通过 Wasmer 在 WebAssembly 中运行 Python。
- 语言完整性:完整的 CPython,纯 Python 外部包可通过挂载工作,带 C 绑定的外部包无法工作
- 安全性:原则上,WebAssembly 应提供强大的沙箱保证。
- 启动延迟:wasmer Python 包已 3 年未更新,我也找不到从 Python 中调用 wasmer 中 Python 的文档,因此我通过 subprocess 调用。启动延迟为 66ms。
- 设置复杂度:wasmer 下载包为 100mb,"python/python" 包为 50mb。
- FOSS:我将其标记为“免费 *”,因为成本为零,但并非所有内容都似乎是开源的。截至 2026-02-10,
python/pythonwasmer 包 没有 readme、没有许可证、没有源代码链接,也没有任何关于其构建方式的说明;最近上传的版本显示大小为“0B”,尽管下载量约为 50MB——Python 二进制的构建过程并不清晰透明。(如果我在这里搞错了,请创建 issue 来纠正我) - 文件挂载:支持
- 快照:通过日志记录支持
沙箱服务
自行用 k8s 搭建沙箱环境也会面临类似的挑战,设置更复杂,但网络延迟更低。
- 语言完整性:完整的 CPython,可使用任意库
- 安全性:专业管理的容器隔离
- 启动延迟:网络往返和容器启动时间。我从伦敦使用 Daytona EU 实测冷启动约 1s,Daytona 宣称低于 90ms 延迟,大概是指已有容器的情况,不清楚是否包含网络延迟
- FOSS:按执行或计算时间付费,部分实现是开源的
- 设置复杂度:API 集成、认证令牌——对初创公司适用,但对企业来说通常行不通
- 文件挂载:通过 API 调用上传/下载
- 快照:可以使用 Temporal 等持久执行解决方案,这些服务也提供一些解决方案,我认为是基于 docker 容器
YOLO Python
通过 exec()(约 0.1ms)或 subprocess(约 30ms)直接运行 Python。
- 语言完整性:完整的 CPython,可使用任意库
- 安全性:无——完整的文件系统、网络、环境变量、系统命令访问权
- 启动延迟:
exec()接近零,subprocess 约 30ms - 设置复杂度:无
- 文件挂载:直接文件系统访问(这正是问题所在)
- 快照:可以使用 Temporal 等持久执行解决方案
Pydantic Stack 的一部分
Pydantic Stack 是交付生产级 AI 智能体所需的一切:
- Pydantic AI - 类型安全的智能体框架
- Pydantic Logfire - AI 优先的全栈可观测性
- Logfire AI Gateway - 统一的 LLM 代理