Agent Skill 需要记忆时,MoonBit + Wasm 怎么走通这条路

2026-05-27 15 预计阅读时间:1 分钟
来源:oschina.net AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:12 分钟

AI Agent 的 Skill 越写越多,一个老问题浮出水面:Skill 执行完就消失了,下次调用还得从零开始。短期记忆靠对话上下文撑着,长期记忆靠外部数据库兜着,但 Skill 自身运行时的中间状态——比如一次推理中缓存的向量索引、分步计算的部分结果——无处安放。

MoonBit 团队在这条路上做了一个选择:把 Skill 编译成 Wasm 模块,让它在沙箱里跑,同时通过 Wasm 的线性内存和组件模型给 Skill 一条可控的内存通路。这不是理论推演,是已经在真实项目里跑通的方案。

Skill 的记忆困境

一个典型 Agent 架构里,Skill 是被 Orchestrator 按需调度的原子能力。每次调用:

  1. Orchestrator 把任务参数传给 Skill
  2. Skill 执行,返回结果
  3. Skill 进程/线程退出,内存释放

问题出在步骤 3。假设你有一个"代码审查"Skill,它需要:

  • 加载项目的 AST 缓存(几 MB)
  • 维护一份 lint 规则的匹配状态
  • 在多轮审查间保留"已检查文件"的集合

如果每次调用都重新加载,延迟和资源开销会迅速膨胀。传统解法有两种:

  • 进程常驻 + RPC:Skill 作为微服务跑,但部署和隔离成本高
  • 外部存储:每次读写 Redis/SQLite,网络延迟和序列化开销不小

两者都能用,但对"轻量 Skill"来说都偏重。Agent 生态需要的是:一个 Skill 能在隔离沙箱里跑,同时拥有受控的、跨调用可持久化的内存。

MoonBit + Wasm 的切入点

MoonBit 是一门面向 Wasm 优先设计的语言,编译产物天然就是 Wasm 模块。这给了它一个结构性优势:Skill 的代码和运行环境从一开始就是一体的,不需要额外打包或适配层。

具体来说,这条路径依赖三个 Wasm 机制:

机制 作用 Skill 记忆场景
线性内存 Wasm 模块自带一块连续内存 Skill 运行时堆栈、临时缓存
组件模型(Component Model) 模块间通过定义好的接口传数据 Skill 向 Memory Service 读写持久状态
WASI 提供文件系统、时钟等系统访问 Skill 通过虚拟文件系统持久化到磁盘

关键设计:Skill 不直接操作宿主内存,而是通过组件模型暴露的接口与一个专门的 Memory Module 交互。这样:

  • Skill 之间内存隔离,不会互相踩数据
  • Memory Module 可以统一做持久化策略(写磁盘、写远程存储)
  • 宿主(Orchestrator)只调度和传递引用,不碰内部数据

一个可跑的 Skill + Memory 示例

下面用 MoonBit 写一个最简 Skill:它维护一个"已处理文件"集合,跨调用持久化。假设你已经安装了 MoonBit 工具链(moon 命令)。

1. 初始化项目

# 创建 Skill 项目
moon new agent_skill_memory

# 进入项目目录
cd agent_skill_memory

2. Skill 代码:维护处理记录

编辑 src/main.mbt

// Skill: 标记已处理的文件路径,跨调用保持记忆
// 通过 Memory 接口读写持久化数据

///| Skill 入口:处理一个文件路径,返回是否为新文件
fn process_file(path : String) -> String {
  let memory = MemoryService.connect()
  let seen : Array[String] = memory.load("processed_files")
  if seen.contains(path) {
    memory.disconnect()
    "already_processed:" + path
  } else {
    seen.push(path)
    memory.save("processed_files", seen)
    memory.disconnect()
    "new_file:" + path
  }
}

///| 查询当前所有已处理文件
fn list_processed() -> Array[String] {
  let memory = MemoryService.connect()
  let seen : Array[String] = memory.load("processed_files")
  memory.disconnect()
  seen
}

///| Wasm 导出函数,供 Orchestrator 调用
fn export_process_file(path : String) -> String {
  process_file(path)
}

fn export_list_processed() -> String {
  let files = list_processed()
  files.join(",")
}

3. Memory Service 的接口定义

编辑 src/memory.mbt,这里用组件模型的思想定义接口,实际部署时 Memory Module 是独立的 Wasm 组件:

// Memory Service 接口
// 实际运行时由宿主环境注入一个实现了该接口的 Wasm 组件

struct MemoryService {
  priv module_id : String
}

///| 连接到 Memory Module
fn MemoryService::connect() -> MemoryService {
  // 通过 Wasm 组件模型实例化 Memory 组件
  // 实际代码中这里会调用 component instantiation API
  { module_id: "memory-v1" }
}

///|  Memory 加载键值
fn MemoryService::load(self : MemoryService, key : String) -> Array[String] {
  // 调用 Memory 组件的 load 导出函数
  // 返回值从 Wasm 线性内存中读取并反序列化
  WasmComponent.invoke(self.module_id, "load", key)
}

///|  Memory 保存键值
fn MemoryService::save(self : MemoryService, key : String, value : Array[String]) -> Unit {
  WasmComponent.invoke(self.module_id, "save", key, value)
}

///| 释放连接
fn MemoryService::disconnect(self : MemoryService) -> Unit {
  WasmComponent.release(self.module_id)
}

4. 编译为 Wasm 组件

# 编译为 Wasm 组件模型格式
moon build --target wasm-component

# 产物在 target/wasm-component/release/agent_skill_memory.wasm

5. 用 Wasm 运行时加载并测试

以 Wasmtime(支持组件模型的运行时)为例:

# 安装 wasmtime(如果还没装)
curl https://wasmtime.dev/install.sh -sSf | bash

# 运行 Skill 组件,需要同时提供 Memory 组件
wasmtime run \
  --component memory_service.wasm \
  target/wasm-component/release/agent_skill_memory.wasm \
  --invoke export_process_file "src/main.py"
# 输出: new_file:src/main.py

# 第二次调用同一文件
wasmtime run \
  --component memory_service.wasm \
  target/wasm-component/release/agent_skill_memory.wasm \
  --invoke export_process_file "src/main.py"
# 输出: already_processed:src/main.py

注意:上面的 MemoryService 内部实现和 WasmComponent.invoke 是示意性的。当前 MoonBit 的 Wasm 组件模型支持正在快速迭代,实际 API 以官方文档为准。核心思路不变:Skill 组件通过接口调用 Memory 组件,而非直接操作宿主内存。

为什么是 MoonBit 而不是 Rust/C++

同样的 Wasm 目标,Rust 和 C++ 都能编译过去。但 Agent Skill 的开发节奏不一样:

  • Skill 数量多、变更快。一个 Agent 平台可能有几十到上百个 Skill,每个都在频繁调整。MoonBit 的编译速度在 Wasm 目标上比 Rust 快一个数量级,开发反馈循环更短。
  • Skill 代码量小。多数 Skill 的核心逻辑在 200-500 行,不需要 Rust 的零成本抽象体系,反而需要更简洁的表达。MoonBit 的函数式风格和自动类型推导在这个尺度上更顺手。
  • Wasm 优先不是后加的。MoonBit 从语言设计阶段就面向 Wasm,垃圾回收直接对接 Wasm GC 提案,字符串和数组不需要额外序列化层。Rust 编译 Wasm 需要手动处理 wasm-bindgen、内存分配策略,C++ 更是需要自己管 ABI。

一个直观对比:同样的"文件处理 Skill + Memory 接口"功能,Rust 版本大约需要 3 倍的代码量来处理 Wasm 边界的类型转换和内存管理。

落地时的几个实际决策

如果你在考虑这条路径,以下是需要提前想清楚的点:

Memory 的持久化粒度

  • 调用间持久化:Skill 在同一会话的多次调用间共享状态,用 Wasm 线性内存即可,会话结束就释放
  • 跨会话持久化:需要 Memory Module 把数据写到 WASI 虚拟文件系统或外部存储,这涉及 I/O 策略
  • 跨 Agent 持久化:多个 Agent 共享同一个 Memory 组件,需要考虑并发和一致性

建议从第一种开始,够用就不升级复杂度。

Skill 的内存上限

Wasm 线性内存默认有上限(通常 4GB,但实际部署会设更低)。给 Skill 设一个合理的内存配额:

# wasmtime 运行时限制最大内存为 64MB
wasmtime run --max-memory-size 64MB agent_skill_memory.wasm

Agent Skill 不是微服务,不需要 GB 级内存。64MB 对大多数 Skill 足够,超了就该反思数据模型。

冷启动时间

Wasm 模块的冷启动在毫秒级,比容器快两个数量级。但如果你用了组件模型的嵌套实例化(Skill 组件 + Memory 组件 + 其他依赖组件),实例化链会拉长。实测建议:组件嵌套不超过 3 层,否则编译和启动时间都会明显增加。

一份简短检查清单

准备把 Agent Skill 迁移到 MoonBit + Wasm 路径前,过一遍这个清单:

  • [ ] Skill 的核心逻辑是否在 1000 行以内?超过的话考虑拆分
  • [ ] Skill 是否需要跨调用保持状态?不需要的话,纯 Wasm 函数就够了,不必引入 Memory 组件
  • [ ] 你的 Wasm 运行时是否支持组件模型?目前 Wasmtime 和 Jco 支持,其他运行时还在跟进
  • [ ] Memory 的持久化范围是调用间、跨会话、还是跨 Agent?选最小够用的范围
  • [ ] 是否给 Skill 设了内存配额?没设的话,一个有 bug 的 Skill 可以吃掉宿主内存
  • [ ] 编译产物是否验证了 Wasm GC 支持?MoonBit 默认启用 Wasm GC,运行时也需要支持

这条路径不是唯一答案。进程常驻和外部存储在重负载场景下仍然有优势。但对于 Agent 生态里大量轻量、高频、需要隔离的 Skill,MoonBit + Wasm 提供了一个目前最紧凑的工程方案:语言和目标平台天然对齐,内存模型受控可持久化,冷启动快到可以按需调度。


相关推荐