monty-go:Pydantic 的 Monty Python 解释器的纯 Go 封装

Hacker News Top 工具

摘要

monty-go 是一款纯 Go 工具,支持在沙箱化的 WebAssembly 环境中运行 LLM 生成的 Python 代码,实现高效、安全的代码执行,并与 Go 集成。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/08/30 15:46

fugue-labs/monty-go

来源:https://github.com/fugue-labs/monty-go

monty-go

从Go安全运行LLM生成的Python代码——无需容器、CGO或子进程。

一个纯Go语言的Pydantic Monty (https://github.com/pydantic/monty) Python解释器封装器,该解释器被编译为WebAssembly,并通过wazero (https://wazero.io)加载。你的Go代理编写Python代码,monty-go在一个启动时间小于毫秒的沙盒化WASM实例中执行该代码,并在代码调用外部函数时暂停,以便你的Go代码处理该调用。

go get github.com/fugue-labs/monty-go

为什么需要它?

当LLM编写代码而非进行顺序工具调用时,它们工作得更快、更便宜、也更可靠。与其:

代理 -> tool_call("search", {query: "weather london"}) -> 结果
代理 -> tool_call("search", {query: "weather tokyo"})  -> 结果
代理 -> tool_call("compare", {a: result1, b: result2}) -> 结果

不如让LLM编写:

london = search(query="weather london")
tokyo = search(query="weather tokyo")
compare(a=london, b=tokyo)

一次模型调用代替了三次。Python代码调用你的Go函数,Monty在每次调用时暂停,你的Go代码执行调用,然后Monty继续。没有容器。没有沙盒服务。没有 exec()。只是一个嵌入到你Go二进制文件中的2.9MB WASM二进制文件。

关于动机,请参阅:

  • Anthropic的《可编程工具调用》(https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling)
  • Anthropic的《使用MCP执行代码》(https://www.anthropic.com/engineering/code-execution-with-mcp)
  • Cloudflare的《代码模式》(https://blog.cloudflare.com/code-mode/)
  • Hugging Face的《Smol Agents》(https://github.com/huggingface/smolagents)

快速开始

package main

import (
    "context"
    "fmt"
    "log"

    montygo "github.com/fugue-labs/monty-go"
)

func main() {
    runner, err := montygo.New()
    if err != nil {
        log.Fatal(err)
    }
    defer runner.Close()

    result, err := runner.Execute(context.Background(),
        "x * 2 + y",
        map[string]any{"x": 10, "y": 5},
    )
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(result) // 25
}

外部函数(暂停/恢复)

真正的力量在于外部函数调用。每当Python代码调用你声明的函数时,Monty会暂停执行,你的Go回调函数处理该调用,然后Monty使用返回值恢复执行:

result, err := runner.Execute(ctx,
    `
london = get_weather("London")
tokyo = get_weather("Tokyo")
f"{london['city']}: {london['temp']}°C, {tokyo['city']}: {tokyo['temp']}°C"
    `,
    nil,
    montygo.WithExternalFunc(func(ctx context.Context, call *montygo.FunctionCall) (any, error) {
        city, _ := call.Args["city"].(string)
        // 你的实际实现——HTTP调用、数据库查询等等。
        return map[string]any{"city": city, "temp": 22}, nil
    }, montygo.Func("get_weather", "city")),
)
// 结果: "London: 22°C, Tokyo: 22°C"

多个函数的工作方式相同——注册它们所有并按名称分派:

result, err := runner.Execute(ctx, code, nil,
    montygo.WithExternalFunc(func(ctx context.Context, call *montygo.FunctionCall) (any, error) {
        switch call.Name {
        case "search":
            return doSearch(call.Args)
        case "calculate":
            return doCalculate(call.Args)
        case "store":
            return doStore(call.Args)
        default:
            return nil, fmt.Errorf("unknown function: %s", call.Name)
        }
    },
        montygo.Func("search", "query"),
        montygo.Func("calculate", "expression"),
        montygo.Func("store", "key", "value"),
    ),
)

资源限制

使用内存、时间、分配和递归限制来防止代码失控:

result, err := runner.Execute(ctx, code, inputs,
    montygo.WithLimits(montygo.Limits{
        MaxDuration:       5 * time.Second,
        MaxMemoryBytes:    10 * 1024 * 1024, // 10 MB
        MaxAllocations:    100000,
        MaxRecursionDepth: 100,
    }),
)

无限循环、内存炸弹和深度递归都会以 *MontyError 干净地终止。Go的 context.Context 超时也会被遵守——取消上下文,WASM实例就会停止。

输出捕获

捕获Python print() 的输出:

var output strings.Builder
_, err := runner.Execute(ctx, `print("step 1 done")`, nil,
    montygo.WithPrintFunc(func(s string) { output.WriteString(s) }),
)
fmt.Print(output.String()) // "step 1 done\n"

系统调用

Python文件系统和环境访问通过你的Go回调函数进行路由:

result, err := runner.Execute(ctx,
    `
from pathlib import Path
data = Path("/config/settings.json").read_text()
data
    `,
    nil,
    montygo.WithOsCallFunc(func(ctx context.Context, call *montygo.OsCall) (any, error) {
        switch call.Function {
        case "Path.read_text":
            path, _ := call.Args[0].(string)
            return readFromYourStorage(path)
        case "Path.exists":
            path, _ := call.Args[0].(string)
            return existsInYourStorage(path), nil
        default:
            return nil, fmt.Errorf("blocked: %s", call.Function)
        }
    }),
)

除非你的回调函数允许,否则不会发生文件系统访问。

Gollem集成

monty-go旨在为Gollem (https://github.com/fugue-labs/gollem)(Go的生产级代理框架)中的代码模式提供支持。LLM不是进行顺序工具调用,而是编写调用你的工具作为函数的Python代码——Monty安全地执行它,而Gollem编排整个过程。

这是与Gollem结合使用的示例:

import (
    "github.com/fugue-labs/gollem"
    "github.com/fugue-labs/gollem/provider/anthropic"
    montygo "github.com/fugue-labs/monty-go"
)

// 你现有的Gollem工具——search、calculate、store等。
searchTool := gollem.FuncTool[SearchParams]("search", "Search the knowledge base", doSearch)
calcTool := gollem.FuncTool[CalcParams]("calculate", "Run calculations", doCalc)

// 创建一个代码模式工具,用Monty封装你的工具集。
// LLM编写Python代码,Monty执行它,外部函数调用路由到你的Go工具。
codeMode := NewCodeModeTool(runner, searchTool, calcTool)

agent := gollem.NewAgent[Analysis](anthropic.New(),
    gollem.WithTools[Analysis](codeMode),
    gollem.WithSystemPrompt[Analysis](`You have a code execution tool.
Write Python code to call the available functions. Available functions:
- search(query: str) -> dict: Search the knowledge base
- calculate(expression: str) -> float: Evaluate math expressions
Write code that calls these functions and returns the result.`),
)

result, _ := agent.Run(ctx, "Compare Q3 and Q4 revenue and calculate the growth rate")

通过一次模型调用,LLM编写:

q3 = search(query="Q3 revenue")
q4 = search(query="Q4 revenue")
growth = calculate(expression=f"({q4['revenue']} - {q3['revenue']}) / {q3['revenue']} * 100")
{"q3": q3, "q4": q4, "growth_rate": growth}

Monty暂停三次(两次搜索,一次计算),你的Go函数处理每次调用,最终结果通过Gollem的类型化输出管道流回。在一个LLM往返中完成三次工具调用。

为什么选择Gollem + monty-go:

传统工具调用使用monty-go的代码模式
LLM调用每个工具使用一次所有工具一次
延迟N × 模型往返1 × 模型往返 + 微秒级执行
成本N × 输入/输出token1 × 输入/输出token
逻辑LLM逐步推理LLM一次性编写逻辑
控制流无(仅顺序)循环、条件、变量
错误处理LLM必须对每个故障作出反应Python中的try/except
安全性✅(工具是Go函数)✅(WASM沙盒 + 你的回调)

Gollem为你提供编译时类型安全、结构化输出、防护栏、成本跟踪、中间件和多提供商支持。monty-go为你提供安全的嵌入式Python执行。两者结合,让你的代理在每次模型调用中完成更多工作。

github.com/fugue-labs/gollem (https://github.com/fugue-labs/gollem) — Go的生产级代理框架。

工作原理

┌─────────────────────────────────────────────────────────┐
│  你的Go应用程序                                          │
│                                                         │
│  runner, _ := montygo.New()                             │
│  result, _ := runner.Execute(ctx, code, inputs, opts)   │
│       │                                                 │
│       ▼                                                 │
│  ┌──────────────────────────────────┐                   │
│  │  wazero(纯Go WASM运行时)        │                   │
│  │                                  │                   │
│  │  ┌───────────────────────────┐   │                   │
│  │  │  monty.wasm(2.9 MB)      │   │  ◄── go:embed    │
│  │  │  Monty Python解释器        │   │                   │
│  │  │  编译为wasm32-wasi         │   │                   │
│  │  └──────────┬────────────────┘   │                   │
│  │             │                    │                   │
│  │     在外部调用时暂停              │                   │
│  │             │                    │                   │
│  └─────────────┼────────────────────┘                   │
│                │                                        │
│                ▼                                        │
│  ExternalFunc回调 ──► 你的Go代码 ──► 恢复               │
│  OsCallFunc回调   ──► 你的Go代码 ──► 恢复               │
│  PrintFunc回调    ──► 你的Go代码                        │
└─────────────────────────────────────────────────────────┘
  • 无需CGO。 wazero是一个纯Go的WebAssembly运行时。
  • 无子进程。 WASM二进制文件通过 go:embed 嵌入,并在启动时编译一次。
  • 每次调用全新实例。 每次 Execute() 调用都获得一个隔离的WASM实例。调用之间没有状态泄漏。
  • 边界处为JSON。 所有跨越Go↔WASM边界的数据都是JSON。Go类型自然映射:int→float64,string→string,bool→bool,nil→None,[]any→list,map[string]any→dict。

API

// 创建一个可重用的运行器。编译WASM模块一次。
runner, err := montygo.New()
defer runner.Close()

// 使用输入和选项执行Python代码。
result, err := runner.Execute(ctx, code, inputs, opts...)

// 选项:
montygo.WithExternalFunc(fn,                     // 注册可调用函数
    montygo.Func("search", "query", "limit"),    // 带命名参数
    montygo.Func("calculate", "expression"),
)
montygo.WithOsCallFunc(fn)                       // 处理文件系统/环境访问
montygo.WithLimits(montygo.Limits{...})          // 资源限制
montygo.WithPrintFunc(fn)                        // 捕获print输出

// FunctionCall提供命名参数(位置参数按参数名映射):
call.Args["query"].(string)    // 按参数名访问
call.ArgsJSON()                // 预序列化的JSON字符串

类型

PythonGo(结果)Go(输入)
intfloat64int,float64
floatfloat64float64
strstringstring
boolboolbool
Nonenilnil
list,tuple[]any[]any
dictmap[string]anymap[string]any
set[]any—

错误

Python异常变为 *montygo.MontyError:

result, err := runner.Execute(ctx, "1 / 0", nil)
var me *montygo.MontyError
if errors.As(err, &me) {
    fmt.Println(me.Message) // "Traceback... ZeroDivisionError: division by zero"
}

Monty能做什么

跟踪上游Monty v0.0.11 (https://github.com/pydantic/monty/releases/tag/v0.0.11)。

  • 算术、字符串操作、f-字符串、切片
  • 函数、lambda、闭包、生成器
  • for/while 循环,if/elif/else,break/continue
  • try/except/finally/else,raise,异常层次结构
  • 列表/字典/集合推导式,字典/集合视图操作符
  • range,len,sum,min,max,sorted,reversed,enumerate,zip,map,filter,all,any,getattr
  • isinstance,type,int(),float(),str(),bool(),abs()
  • 带有 sep 和 end 关键字参数的 print()
  • PEP 448 泛化解包(调用、字面量等中的 *args,**kwargs)
  • 嵌套和增强下标赋值(a[i][j] = v,a[i] += 1)
  • 元组比较(<,>,<=,>=)
  • 多模块导入(import a, b, c)
  • 标准库模块:math(所有函数),re,datetime,json,以及 sys/typing/asyncio 的子集
  • import os,from pathlib import Path(通过OsCallFunc路由)
  • 数据类 实例 通过外部函数调用传递(参数、返回值和方法调用在 method_call=true 时呈现)
  • 资源限制:时间、内存、分配、递归深度

Monty不能做什么

  • 类定义(只能通过外部I/O创建数据类实例;上游Monty标记类 def 为“即将推出”)
  • match 语句(上游即将推出)
  • 上下文管理器(with ...)
  • 标准库的其余部分和所有第三方库
  • float('inf') / float('nan')(此桥接中的JSON序列化限制)

测试

97个端到端测试,覆盖了Monty核心测试套件中所有可测试场景:

make test

涵盖:基本表达式、打印变体、所有异常类型、数据类型往返、外部函数(参数、关键字参数、混合、复杂类型、链式、循环)、输入处理和作用域、资源限制(超时、递归、内存、分配)、系统调用、内置函数、控制流、lambda/闭包,以及执行隔离。

从源码构建

需要带有 wasm32-wasip1 目标的Rust和Go 1.23+:

rustup target add wasm32-wasip1
make build  # 编译 Rust → WASM,复制到 monty.wasm
make test   # 构建并运行 Go 测试

致谢

monty-go的存在源于Monty (https://github.com/pydantic/monty),由Samuel Colvin (https://github.com/samuelcolvin)和Pydantic (https://github.com/pydantic)团队创建。Monty是一个真正新颖的工程作品——一个从零开始用Rust编写的最小化、安全的Python解释器,专为AI代理构建。LLM应该编写代码而不是进行顺序工具调用的洞见,以及需要一个安全的解释器(而不是容器)来执行它,使得代码模式成为可能。

Samuel和Pydantic团队有着构建整个生态系统所依赖的基础工具的记录——Pydantic (https://github.com/pydantic/pydantic)、Pydantic AI (https://github.com/pydantic/pydantic-ai)、Logfire (https://github.com/pydantic/logfire),现在还有Monty。这个项目是他们工作的Go语言桥梁,我们感谢他们构建了它。

许可证

MIT

相似文章

pydantic-monty 调查

Simon Willison's Blog

对 pydantic-monty 的调查,这是一个用 Rust 编写的用于沙盒执行的最小化 Python 解释器,确认其安全限制(持续时间、内存、分配、递归)按预期工作。

micropython-wasm 0.1a2

Simon Willison's Blog

MicroPython 移植到 WebAssembly,作为在浏览器中沙箱化执行 Python 的工具。

mvm - 一个快速的 Go 虚拟机

Lobsters Hottest

mvm 是一个快速、可移植的 Go 虚拟机,支持直接从源码运行 Go 程序、嵌入 Go 解释器,并包含 REPL、调试器和标准库。

使用MicroPython和WASM在沙盒中运行Python代码

Simon Willison's Blog

Simon Willison介绍了micropython-wasm,一个alpha版本包,它使用编译为WebAssembly的MicroPython在沙盒中运行Python代码,从而在Datasette等Python应用中实现安全的插件执行。