首页
看点啥
插画图片
首页 经济看点 BoxAgnts 工具系统(5)——WASM 工具开发:从 Hello World 到生产部署

BoxAgnts 工具系统(5)——WASM 工具开发:从 Hello World 到生产部署

2026-06-13 0

WASM 沙箱为 BoxAgnts 提供了指令级的安全隔离,工具注册链路则实现了零配置的自动发现。在这两个基础设施之上,开发者只需要关注一件事:编写符合 CLI 惯例的程序。这篇直接上手,从一个 base64 编码工具的完整开发过程开始,到编译、部署、测试,再到一些容易踩坑的地方。

BoxAgnts 工具系统(5)——WASM 工具开发:从 Hello World 到生产部署


为什么选 base64 作为示例

base64 编码/解码是一个理想的示例工具:逻辑足够简单(不会分散注意力),但覆盖了 AI Agent 工具的典型特征——有多个输入参数(模式、输入来源、输出目标)、有错误处理(非法 base64 字符串)、有文件 I/O、有严格的输出格式要求。理解了 base64 工具的开发模式,就理解了所有 WASM 工具的开发模式。

完整的示例代码位于 BoxAgnts 仓库的 examples/tool-sample-base64-component/


Cargo.toml 配置

[package]
name = "tool-sample-base64-component"
version = "1.0.0"
edition = "2021"[[bin]]
name = "base64"
path = "src/main.rs"[dependencies]
clap = { version = "4", features = ["derive", "string"] }
base64 = "0.22"
serde_json = "1"

依赖非常轻:clap 处理 CLI 参数解析,base64 处理编解码逻辑,serde_json 做结构化输出。没有 WASM 特定的依赖——Wasmtime 在宿主侧提供运行环境,WASM 工具本身不需要知道自己跑在沙箱里。

WASM 编译目标需要在 .cargo/config.toml 中指定(或者通过命令行 --target):

[build]
target = "wasm32-wasip2"

核心代码

主函数结构如下(完整代码见仓库):

use clap::{Parser, ValueEnum};
use base64::{engine::general_purpose, Engine as _};
use serde_json::json;#[derive(Copy, Clone, Debug, PartialEq, ValueEnum)]
enum Mode { Encode, Decode }#[derive(Copy, Clone, Debug, PartialEq, ValueEnum)]
enum Alphabet { Standard, UrlSafe }#[derive(Parser, Debug)]
#[command(name = "base64")]
#[command(version)]
#[command(about = "Strict Base64 encode/decode tool")]
struct Args {
    #[arg(long, value_enum, required = true)]
    mode: Mode,    #[arg(long, conflicts_with = "file_path")]
    input: Option<String>,    #[arg(long, conflicts_with = "input")]
    file_path: Option<String>,    #[arg(long)]
    output_file: Option<String>,    #[arg(long, value_enum, default_value = "standard")]
    alphabet: Alphabet,    #[arg(long, default_value_t = false)]
    no_padding: bool,
}fn main() {
    let args = Args::parse();    if let Err(e) = validate_args(&args) {
        eprintln!(r#"{{"error":true,"content":"{}"}}"#, e);
        std::process::exit(1);
    }    let input_bytes = match read_input(&args) {
        Ok(b) => b,
        Err(e) => {
            eprintln!(r#"{{"error":true,"content":"{}"}}"#, e);
            std::process::exit(1);
        }
    };    let engine: &dyn Engine = match (&args.alphabet, args.no_padding) {
        (Alphabet::Standard, false) => &general_purpose::STANDARD,
        (Alphabet::Standard, true) => &general_purpose::STANDARD_NO_PAD,
        (Alphabet::UrlSafe, false) => &general_purpose::URL_SAFE,
        (Alphabet::UrlSafe, true) => &general_purpose::URL_SAFE_NO_PAD,
    };    let result = match args.mode {
        Mode::Encode => engine.encode(&input_bytes),
        Mode::Decode => {
            let input_str = std::str::from_utf8(&input_bytes)
                .unwrap_or_else(|_| "");
            match engine.decode(input_str.trim()) {
                Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
                Err(e) => {
                    eprintln!(r#"{{"error":true,"content":"Invalid base64: {}"}}"#, e);
                    std::process::exit(1);
                }
            }
        }
    };    if let Some(output_file) = &args.output_file {
        std::fs::write(output_file, &result).unwrap_or_else(|e| {
            eprintln!(r#"{{"error":true,"content":"Write failed: {}"}}"#, e);
            std::process::exit(1);
        });
        println!(r#"{{"error":false,"content":"Written to {}"}}"#, output_file);
    } else {
        println!(r#"{{"error":false,"content":"{}"}}"#, result);
    }
}

几个实现细节值得说明。

JSON 输出格式。WASM 工具通过 stdout 返回 JSON 对象,格式约定为 {"error": bool, "content": "..."}。BoxAgnts 的 WasmTool::execute() 会自动解析这个 JSON 并映射到 ToolResult。如果 stdout 不是合法 JSON,整段文本被视为成功结果的 content

参数冲突处理input 和 file_path 互斥——conflicts_with 让 clap 在解析阶段就拒绝同时出现的情况,而不是等到业务代码里再检查。

错误输出到 stderr。WASM 安全失败时应输出到 stderr 而非 stdout。BoxAgnts 分别捕获两个流,stderr 内容用于错误报告,stdout 用于工具结果。


编译与部署

# 编译
cargo build --target wasm32-wasip2 --release# 产物位置
ls target/wasm32-wasip2/release/base64.wasm

编译完成后直接复制到扩展目录:

cp target/wasm32-wasip2/release/base64.wasm 
   app/extensions/tools/base64-component.wasm

文件系统的变化被 notify 事件监听器捕获,触发热加载流程:沙箱执行 --help、解析输出、生成 ToolSpec、注册到全局工具表。从文件复制到工具可用的总延迟通常在 100 毫秒以内,其中主要耗时是 Wasmtime 编译 WASM 为 .cwasm 缓存。


跨语言开发

虽然示例用了 Rust,但 WASM 工具可以用任何支持 wasm32-wasi 的语言。以下是用 Go 写一个简单的 file-read 工具的伪代码对比:

// Go 版本 file-read(使用 TinyGo 编译)
package mainimport (
    "fmt"
    "os"
)func main() {
    if len(os.Args) < 2 {
        fmt.Fprintf(os.Stderr, `{"error":true,"content":"Missing file path"}`)
        os.Exit(1)
    }
    data, err := os.ReadFile(os.Args[1])
    if err != nil {
        fmt.Fprintf(os.Stderr, `{"error":true,"content":"%s"}`, err)
        os.Exit(1)
    }
    fmt.Printf(`{"error":false,"content":"%s"}`, string(data))
}
# 编译
tinygo build -target wasm-wasi -o file-read.wasm main.go

Go 版本和 Rust 版本的 file-read 行为完全一致——它们输出相同格式的 JSON,在相同的沙箱约束下运行,被相同的 WasmTool::execute() 调用。这是 WASM 作为工具分发格式的核心价值:定义一个简单的输出约定,不同语言的实现自动兼容。


常见问题

文件 I/O 的路径

WASM 工具看到的文件系统不是宿主机的完整文件系统。如果 RunOption.work_dir 被设为 /home/user/project,WASM 工具内部用 ./src/main.rs 访问的就是宿主机的 /home/user/project/src/main.rs。如果试图访问 /etc/passwd,会因为不在映射的目录范围内而失败。

stdout 缓冲区

WASM 的 stdout 是行缓冲还是全缓冲取决于 WASI 实现。如果工具在写 JSON 后没有显式 flush 就退出,最后一块输出可能丢失。对于单次输出少量 JSON 的场景通常不会出问题,但如果工具产生大量输出(比如 file-read 读取 100MB 的文件),建议分段输出或使用流式协议。

编码问题

println! 在 WASI 环境下默认输出 UTF-8。如果工具需要输出非 UTF-8 编码的文本(比如读取 GBK 编码文件),需要手动控制编码并在结果的 content 字段中做 Base64 包装。


测试工具

开发过程中可以用 BoxAgnts 的 CLI 直接测试 WASM 工具,不需要通过 AI 对话:

# 模拟工具注册——查看系统解析出的 ToolSpec
boxagnts tool:validate path/to/tool.wasm# 模拟工具执行——传入 JSON 参数
boxagnts tool:execute path/to/tool.wasm '{"mode":"encode","input":"hello"}'

这比通过 AI 对话测试快得多,而且能够直接看到 Wasmtime 层面的错误信息(如果沙箱启动失败)。


工具 vs 技能

WASM 工具适合处理有确定性的计算型任务:编解码、文件操作、数据库查询、正则匹配。但如果一个任务的核心不是"计算"而是"指导 AI 的思维过程"——比如代码审查、架构建议、写作指导——就不适合用 WASM 工具实现。这类场景应该用 Skill(技能),它是纯 Markdown 提示词模板,由系统加载后注入到 AI 的上下文中,AI 据此自主决策和执行操作。


总结

BoxAgnts 的 WASM 工具开发流程在简洁性上做了减法——开发者不需要学习任何 BoxAgnts 特有的 API 或配置格式,只需要遵循两条约定:

  1. --help 输出必须包含标准的 CLI 帮助块Usage:Options:Arguments: 或 Commands:),供系统自动提取 Schema。
  2. stdout 输出 JSON 格式 {"error": bool, "content": "..."},可选的 metadata 字段用于向前端传递结构化渲染信息。

除此之外,工具代码完全是普通的 CLI 程序。这在开发者体验上是一个分水岭——传统的 Agent 框架要求开发者理解框架的 Tool 基类、Schema 声明格式、回调注册方式,BoxAgnts 把这些全部替换为"写好 --help 就行"。

跨语言支持是另一个独有的优势。Rust、Go、Python、C——任何能编译到 wasm32-wasi 的语言都可以用来开发 BoxAgnts 工具。编译后的 .wasm 文件放入扩展目录,热加载机制自动处理剩余的注册和缓存步骤。

参考资源

喜欢(0)

上一篇

VS Code 本地 Agent 完全指南:Agent Plan Ask 三种模式怎么选

VS Code 本地 Agent 完全指南:Agent Plan Ask 三种模式怎么选

下一篇

小米MiMoCode官网颜值高?Codex:拿来吧您嘞!1:1完美复刻~

小米MiMoCode官网颜值高?Codex:拿来吧您嘞!1:1完美复刻~
猜你喜欢