实验性 - 该项目仍处于开发阶段,尚未做好正式发布的准备。
一个用 Rust 编写的、供 AI 使用的最小化、安全的 Python 解释器。
Monty 避免了为运行 LLM 生成的代码而使用基于完整容器的沙箱所带来的成本、延迟、复杂性以及各种麻烦。
相反,它让你安全地运行由嵌入在智能体中的 LLM 编写的 Python 代码,启动时间以个位数微秒计算,而不是数百毫秒。
Monty 可以做什么:
sys、os、typing、asyncio、re、datetime、json、dataclasses(即将支持)Monty 不能做什么:
简而言之,Monty 功能极其有限,专为一个用例而设计:
运行由智能体编写的代码。
如果你想了解为什么要这么做,可以参考:
简单来说,上述所有方案的核心思想是:如果让 LLM 编写 Python(或 Javascript)代码,而不是依赖传统的工具调用,它们可以更快、更便宜、更可靠地工作。Monty 让这一切成为可能,而不必面对沙箱的复杂性,也不必承担直接在主机上运行代码的风险。
注意: Monty 将(很快)被用于在 Pydantic AI 中实现 codemode。
可以从 Python、JavaScript/TypeScript 或 Rust 调用 Monty。
要安装:```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 是某种替代技术。奇怪的是,这两种反应常常同时出现,这说明人们尚未找到适合自己的替代方案,但又难以置信的是,从头实现一个完整的 Python 竟然真的没有好的替代方案。
我将尝试梳理最显而易见的替代方案,以及为什么它们不适合我们的需求。
注意:所有这些技术都令人印象深刻且应用广泛,这篇关于它们在我们用例中局限性的评论不应被视为批评。这些解决方案大多并非以提供 LLM 沙箱为目标而设计,因此它们在这方面未必表现出色。
参见 ./scripts/startup_performance.py 以获取用于计算启动性能数据的脚本。
以下是每一行的详细信息:
pip install pydantic-monty 或 npm install @pydantic/monty,下载约 4.5MBdump() 和 load(),使得暂停、恢复和分支执行变得轻而易举python:3.14-alpine 为 50MB——docker 无法通过 PyPI 安装参见 starlark-rust。
通过 Wasmer 在 WebAssembly 中运行 Python。
python/python wasmer 包 没有 readme、没有许可证、没有源代码链接,也没有任何关于其构建方式的说明;最近上传的版本显示大小为“0B”,尽管下载量约为 50MB——Python 二进制的构建过程并不清晰透明。(如果我在这里搞错了,请创建 issue 来纠正我)自行用 k8s 搭建沙箱环境也会面临类似的挑战,设置更复杂,但网络延迟更低。
通过 exec()(约 0.1ms)或 subprocess(约 30ms)直接运行 Python。
exec() 接近零,subprocess 约 30msPydantic Stack 是交付生产级 AI 智能体所需的一切:
| 技术 | 语言完整性 | 安全性 | 启动延迟 | FOSS | 设置复杂度 | 文件挂载 | 快照 |
|---|
| Monty | 部分 | 严格 | 0.06ms | 免费 / 开源 | 简单 | 简单 | 简单 |
| Docker | 完整 | 良好 | 195ms | 免费 / 开源 | 中等 | 简单 | 中等 |
| Pyodide | 完整 | 差 | 2800ms | 免费 / 开源 | 中等 | 简单 | 困难 |
| starlark-rust | 非常有限 | 良好 | 1.7ms | 免费 / 开源 | 简单 | 不可用? | 不可能? |
| WASI / Wasmer | 部分,几乎完整 | 严格 | 66ms | 免费 * | 中等 | 简单 | 中等 |
| 沙箱服务 | 完整 | 严格 | 1033ms | 非免费 | 中等 | 困难 | 中等 |
| YOLO Python | 完整 | 不存在 | 0.1ms / 30ms | 免费 / 开源 | 简单 | 简单 / 危险 | 困难 |