# MoonLM — 端侧/边缘 AI 推理与 Agent 运行时

> 用 MoonBit 编写的全本地 LLM 推理引擎 + 强类型 Agent 运行时。
> 零 API 调用、零数据外传、一次编译同时跑在 **浏览器 (js)** 和 **native 边缘设备** 上。

- 状态：M0–M2 完成，内存优化完成，M3 待开始（可行性已实测）
- 许可证计划：Apache-2.0
- 建议仓库名：`moonlm`（Moon + LLM）
- 前置说明：`moonsh` 为已废弃项目，本项目从零开始，不继承其代码。

---

## 1. 一句话定位

**"浏览器里就能跑的本地小模型 + 类型安全的 Agent 框架"** ——
MoonBit 编译到 Wasm-GC 后体积小、启动快、无 GC 暂停抖动，天然适合把推理整个塞进浏览器标签页或边缘设备的沙箱里；再用 MoonBit 的强类型 trait 系统约束 Agent 的工具调用，让"本地智能应用"既有隐私保障，又有工程可靠性。

## 2. 生态调研结论（2026-10 核实）

| 调研项 | 结论 | 对本项目的意义 |
|---|---|---|
| mooncakes.io（约 2.7 万包） | **无任何本地推理运行时**。AI 相关仅 `mizchi/llm`（云端 LLM API 客户端）与 `xiguaAp6y3/moon_outlines`（约束解码，编译 JSON-Schema 到 token mask） | 空白属实。且 `moon_outlines` 与本项目互补：它管"生成什么格式"，我们管"在哪里生成" |
| GitHub `moonbit + llama` | 0 个仓库 | 无现成 GGUF/llama.cpp 绑定，需自建 |
| `eanzhao-os/pi-moonbit` | 存在（MoonBit，8 stars） | 差异化点：它聚焦 Agent 工具链/云端对接，本项目聚焦**本地推理内核**，形成互补而非竞争 |
| ~~ds4.mbt~~ | **未能核实**（GitHub 搜索无此项目） | 规划不依赖它；若后续找到可作参考，找不到不影响方案 |

> 申报书口径：本项目定位为"移植项目/参考开源设计、代码原创"（对标 llama.cpp 的格式与算法、`.wasm` 生态的 wllama/transformers.js 的架构理念，全部 MoonBit 代码从零编写）。

## 3. 总体架构

```
┌─────────────────────────────────────────────────────────────┐
│                        应用层 (两套前端)                      │
│   moonlm CLI (native)                moonlm Web (js)        │
│   交互式聊天 / 文档问答 / inspect   │   浏览器聊天 Demo + Agent │
├─────────────────────────────────────────────────────────────┤
│                     Agent 运行时 (纯 MoonBit)                │
│  AgentLoop(思考→调用→观察→循环) · Tool trait · JSON-Schema   │
│  参数校验 · 会话记忆(transcript + BM25 检索) · 内置本地工具   │
├─────────────────────────────────────────────────────────────┤
│                      引擎抽象层 (Engine trait)                │
│  ┌───────────────────────────┐  ┌─────────────────────────┐ │
│  │ PureEngine (纯 MoonBit)   │  │ LlamaCppEngine (可选,    │ │
│  │ GGUF解析+Tokenizer+      │  │ 仅 native, extern "C"   │ │
│  │ Transformer前向+采样      │  │ FFI 对接 llama.cpp C API)│ │
│  └───────────────────────────┘  └─────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│  后端:  js (浏览器)        |   native (Linux/macOS/Windows)  │
│  （原计划的 wasm-gc 经实测不可行，原因见 M2）                  │
└─────────────────────────────────────────────────────────────┘
```

**双引擎是核心架构决策**：PureEngine 保证浏览器/沙箱环境"零依赖全自包含"（Wasm/JS 无法链接 C 库，这是硬约束）；LlamaCppEngine 作为 native 下的可选加速路径，复用 llama.cpp 成熟的算子覆盖全部模型架构。两者实现同一个 `Engine` trait，上层 Agent 代码完全无感。

## 4. 关键技术决策（ADR 摘要）

| # | 决策 | 理由 |
|---|---|---|
| ADR-1 | **模型格式只支持 GGUF** | 事实标准，单文件自包含（权重+词表+元数据），llama.cpp 生态全部现成量化模型可用 |
| ADR-2 | **首批目标模型：SmolLM2-135M/360M、Qwen2.5-0.5B-Instruct** | 135M Q4 量化后约 90–100MB，可在浏览器可接受时间内加载；0.5B 作为 native 目标。浏览器 Demo 用 135M，native 用 360M/0.5B |
| ADR-3 | **Tokenizer 先做 byte-level BPE**（SmolLM2/Qwen 用此类词表），SentencePiece 二期再做 | BPE 无需预编译 proto 文件，纯 MoonBit 实现复杂度低一个量级；目标模型的词表直接存在 GGUF 元数据里 |
| ADR-4 | **量化支持顺序：F32/F16 → Q8_0 → Q4_0 → Q4_K/Q6_K** | 先做格式简单、易对拍的块量化，K-quant 位操作复杂，用 llama.cpp 做黄金对照 |
| ADR-5 | **Agent 工具调用用 trait + 单一来源的 Schema 定义**，不做 eval/动态字符串分发 | 工具签名即 Schema：每个工具的参数字段只在一处声明（`src/agent/schema.mbt` 的 `Schema` 值），模型看到的 JSON Schema 与工具执行的解码都由它派生，两者不可能漂移。**校验是运行时的，不是编译期的**——core 的 `derive(ToJson, FromJson)` 能从类型解码，但没有从类型生成 Schema 的能力，也没有自定义 derive 钩子，因此不存在"编译器拒绝非法调用"这回事。准确表述：**签名即 Schema，校验集中在类型层入口，运行时执行**。后期可选接入 `moon_outlines` 做约束解码，让模型"只能"生成合法调用 |
| ADR-6 | **采样实现 greedy / temperature / top-k / top-p**，repetition-penalty 二期 | 覆盖演示与评测所需，避免一开始摊大饼 |
| ADR-7 | **性能策略：基准先行**。每阶段都有 tokens/s 基准，优化手段按优先级：数据布局/减少分配 → wasm SIMD128（若 MoonBit 后端支持则启用）→ 循环展开 → native FFI 兜底 | 推理是性能敏感领域，没有基准就没有可信的"性能优势"叙事 |

**性能目标（诚实预估，以实测为准）**：

| 目标 | 指标 |
|---|---|
| native PureEngine + SmolLM2-135M Q8_0 | ≥ 15 tok/s（单线程） |
| js 后端 PureEngine + SmolLM2-135M Q8_0 | ≥ 3 tok/s（可交互的底线；实测 6.3） |
| native LlamaCppEngine（对照上限） | 与 llama.cpp 官方一致量级 |
| 浏览器首屏 | ≤ 150MB 模型加载 < 15s（含分块读取） |

## 5. 仓库结构（规划）

```
moonlm/
├── moon.mod                 # soehi/moonlm：消费 soehi/gguf 版本依赖
├── PLAN.md                  # 本文档
├── gguf/                    # 独立模块 soehi/gguf（可单独发布/复用）
│   ├── moon.mod
│   ├── README.md            # Scope 与非目标
│   ├── docs/gguf-notes.md   # 格式笔记与踩坑
│   └── src/
│       ├── (根包)           # GGUF 二进制解析：header/metadata KV/tensor 目录/对齐
│       └── tensor/          # 反量化（F32/F16/Q4_0/Q5_0/Q8_0/Q4_K/Q6_K）
├── src/
│   ├── tokenizer/           # byte-level BPE：词表加载、编码、解码、special tokens
│   ├── model/               # Transformer 前向：RMSNorm、RoPE、GQA、KV cache（按架构注册）
│   ├── sampler/             # greedy / temperature / top-k / top-p
│   ├── engine/              # Engine trait + PureEngine；native 下 LlamaCppEngine(FFI)
│   ├── agent/               # AgentLoop、Tool trait、JSON-Schema 校验、会话与记忆
│   ├── tools/               # 内置工具：read_file/write_file/list_dir/grep_text/calc（沙箱路径白名单）
│   ├── cli/                 # moonlm 命令入口：chat / ask / inspect / bench / agent
│   └── web/                 # js 浏览器 Demo（聊天 + Agent 可视化）
├── examples/                # 最小示例：加载模型→生成→工具调用
├── benchmarks/              # 基准脚本与结果表
└── docs/                    # 架构说明、GGUF 格式笔记、性能调优记录
```

## 6. 阶段规划（M0–M5）

> 每阶段给出：目标 / 任务 / 验收标准（DoD）。各阶段产出均可独立演示，工期可按比赛节奏压缩或裁剪；**最小可参赛组合 = M0–M3**，M4 为可选增强，M5 为提交打磨。

### 当前进度基线（供后续规划与答辩对表）

| 阶段 | 状态 | 核心交付 | 关键实测 |
|---|---|---|---|
| M0 GGUF/量化 | ✅ 完成 | `gguf`/`tensor`/`inspect` | 与 llama.cpp 逐字段一致；反量化误差 ≤1e-6 |
| M1 推理内核 | ✅ 完成 | `tokenizer`/`model`/`sampler`/`engine` | 逐位置 argmax **97.22%**（280/288） |
| M2 浏览器 | ✅ 完成 | `web`/`web/`/CI js 构建 | decode 7.7 tok/s（js）；native 18.5 tok/s |
| 内存优化（插入） | ✅ 完成 | 三处成因全修 | 663 → **189 MB**（模型的 1.37 倍） |
| M3 Agent 运行时 | ⬜ 待开始 | — | 可行性已实测（见 M3 节） |
| M4 llama.cpp FFI | ⬜ 可选 | — | — |
| M5 提交打磨 | ⬜ 待开始 | — | — |

代码规模：核心 5726 行（`gguf` 1228 / `model` 1661 / `cli` 869 / `tokenizer` 852 /
`tensor` 571 / `sampler` 292 / `engine` 226 / `web` 225），107 个测试双后端通过，
CI 覆盖 native + wasm-gc + js 三个后端构建。

**已完成部分的边界（答辩时不要超出这些）**：
- 模型规模：实测只覆盖 135M 级（SmolLM2）。0.5B 已验证可加载解析，
  但受 Q5_0 与 bias 缺口阻塞（见 M3）。
- 性能：native 18.5 tok/s 对 llama.cpp 单线程 57.2 tok/s，差距约 3 倍，原因是
  对方手写 SIMD + 多线程，本项目单线程标量。
- 浏览器侧只承诺 ≤135M 模型可用。

### 后续路线（三条可选路径，按优先级）

**路径 A：补全 M3 —— Agent 运行时（推荐，差异化最高）**

这是项目**唯一无法被 llama.cpp 替代的部分**。llama.cpp 是推理库，不做 Agent
编排；MoonBit 生态（调研 2.7 万包）没有任何本地推理或 Agent 实现。M3 建立在
已有引擎之上，是"用 MoonBit 强类型约束工具调用"这一主张的落点。

前置工作量已实测清楚：**Q5_0 反量化**（~1 天，M0 已铺好黄金向量测试的路子）+
**注意力 bias 支持**（~1 天，并需修 `config.mbt` 的架构声明与实际不符）。
两者都是可独立交付、可对拍验证的引擎补强。之后才做 Agent 层。

**路径 B：M4 双引擎 FFI（可选增强）**

接 llama.cpp 的 C API 作为 native 加速档，证明 `Engine` trait 抽象的价值。
风险是 FFI 工作量与"纯 MoonBit"叙事之间的张力——需要文档说清"M4 是可选的
加速路径，不影响 PureEngine 的自包含性"。在时间紧张时优先放弃。

**路径 C：M5 提交打磨（必做）**

无论 A/B 做到哪一步，这一步都不能省：
1. **演示素材**：3 分钟录屏（浏览器断网聊天 → CLI Agent 文档问答 → 基准对比）
2. **`docs/agent.md`**：若 M3 做了，记录模型能力边界与实际成功率；若没做，
   在 README/PLAN 里明确标注 Agent 为"计划中"，不要含糊
3. **申报书对齐**：`申报书.md` 目前把 Agent 写成"计划中"是正确的；若 M3 完成，
   回填实测数据与成功率
4. **README 双语 + 架构图**

**优先级建议**：C 的 1、2、3 项无论如何都要做；A 是拿分项但需要先付引擎缺口的
前置成本；B 在时间不足时第一个砍。

---

### M0 — 地基：GGUF 解析与量化格式

**目标**：把"读模型"这件事做扎实。这是全项目最可测、风险最低的起点。

> **状态：已完成。** GGUF 与张量层已抽成独立模块 `soehi/gguf`（仓库内 `gguf/`），
> moonlm 以版本依赖消费它；CLI 见 `src/cli/`。
> 格式与踩坑记录见 `gguf/docs/gguf-notes.md`；对拍脚本见 `tools/check_models.py`。
> 交付物：`inspect` 子命令另支持 `--json`（便于与 `gguf_dump` 逐字段对比）与
> `dequant`（导出单张量解码值，用于真实模型数值对拍）。

**任务**
1. 建仓库脚手架（moon.mod、CI、目录结构、Apache-2.0）
2. `gguf/`：magic/version 校验、metadata KV 全类型解析（u8…f64、string、array、nested）、tensor 目录（名称/维度/类型/偏移）、对齐与 padding 处理
3. `tensor/`：F32/F16 解码；Q8_0、Q4_0 块反量化；随后补 Q4_K、Q6_K
4. CLI 子命令 `moonlm inspect <model.gguf>`：打印架构、层数、词表大小、张量清单与体积分布

**验收标准（DoD）**
- [x] 对 SmolLM2-135M 的 Q8_0 与 Q4_K_M 两个 GGUF 文件，`inspect` 输出与 `llama.cpp` 的 `gguf_dump` 元数据一致
- [x] 各量化格式的反量化输出与 llama.cpp 参考实现的黄金测试向量逐块一致（误差 ≤ 1e-6）
- [x] 单元测试覆盖畸形输入（截断文件、错误 magic、未知 KV 类型）——报错而非崩溃
- [x] CI 通过（native + wasm-gc 双后端编译）

**补充说明（与原始规划的差异）**

- Q4_K_M 用的是 bartowski 的混精文件：272 个张量中 166 个是 `q5_0`（M0 不解码）。
  CLI 会显式列出无法反量化的张量，不静默略过。目录字段仍全部对拍一致。
- 除目录对拍外，额外做了 **F32/Q8_0/Q4_K/Q6_K 解码数值**的真实模型对拍，与
  llama.cpp 的 Python `gguf` 读出器逐元素一致（相对误差 ≤ 1e-6）。

---

### M1 — 内核：Tokenizer + Transformer 前向 + 采样

**目标**：native 后端用纯 MoonBit 跑通端到端文本生成。这是技术风险最集中的阶段，优先攻坚。

> **状态：已完成。** 实现见 `src/tokenizer/`、`src/model/`、`src/sampler/`、`src/engine/`；
> 对拍脚本见 `tools/crosscheck_llama.py`，方法与完整数据见 `docs/crosscheck.md`，
> 性能与瓶颈见 `docs/performance.md`。交付物：`generate` / `chat` / `bench` /
> `logits` / `profile` 五个子命令。

**任务**
1. `tokenizer/`：从 GGUF 元数据加载词表与 merges，实现 byte-level BPE（预分词切分、优先级合并、special token 处理）
2. `model/`：SmolLM2/Llama/Qwen2 架构前向——RMSNorm、RoPE、GQA 注意力、SwiGLU、KV cache（增量解码）
3. `tensor/`：量化权重矩阵乘（dequant-on-the-fly）、向量化内积、（视后端能力）SIMD
4. `sampler/`：greedy / temperature / top-k / top-p
5. `engine/`：定义 `Engine` trait（load / generate 流式 / encode / decode），PureEngine 挂接

**验收标准（DoD）**
- [x] tokenizer 往返测试：对全部词表与 31 条多语种样本，encode→decode 恒等还原 100%
      （原计划的「1 万条」以词表全覆盖加多语种样本替代，见下方说明）
- [x] 与 llama.cpp 对拍：**逐位置 argmax 一致率 280/288 = 97.22%**（≥ 95%）——指标经重新定义，见下方说明
- [x] native 后端 SmolLM2-135M Q8_0 达到 ≥ 15 tok/s：实测 decode 14.4–14.7、prefill 15.0 tok/s
      （压线达标），基线表见 `docs/performance.md`
- [x] `moonlm generate` / `chat --model ...` 可交互，输出连贯中英文
- [x] 双 target `check` / `test` / `build` 全绿（103 个测试）

**补充说明（与原始规划的差异，均为实测后调整）**

- **对拍指标从「前 64 token 一致率」改为「逐位置 argmax 一致率」。** 原指标不可测：
  语言模型在近似平局处是混沌的，两侧浮点求和顺序不同会让平局朝相反方向破裂，
  此后整段文本必然不同。实测中所有分歧位置的参考 top-2 logprob 差距仅
  0.002–0.17 nat（是真正的抛硬币），所以「自由生成一致率」衡量的是混沌而非正确性。
  改为 teacher forcing：把参考自身的 token 序列喂给本引擎，逐位置比较 argmax。
  这样一次平局只损失一个位置而不是整段后缀。
- **性能是压线达标，不是宽裕达标。** decode 14.4–14.7 tok/s 对 15 的目标属勉强越过；
  且与 llama.cpp 单线程 57.2 tok/s 相比慢约 4 倍（纯标量、无 SIMD）。
  两条被实测否决的优化路径（llama.cpp 的整数量化方案、v128 路径）记录在
  `docs/performance.md`，避免重复踩坑。

---

### M2 — 浏览器：Web Demo

**目标**：做出最能打动评委的演示——**纯浏览器里跑本地模型，无任何服务器参与推理**。

> **状态：已完成。** 实现见 `src/web/`（JS 边界 API）与 `web/`（演示页）；
> 构建与本地服务脚本见 `tools/serve_web.py`；全部实测数据、决策依据与
> 「不宣称什么」见 `docs/browser.md`。

**关键决策修正：浏览器目标是 `js`，不是 `wasm-gc`。** 原计划写的是 wasm-gc，
实测后推翻——wasm-gc 的 `Bytes` 是 GC 数组引用，JavaScript 无法构造，且模块
不导出内存，模型字节进不去（实测报 `type incompatibility when transforming
from/to JS`）；`js` 后端下 `Bytes` 就是 `Uint8Array`，MoonBit 闭包就是 JS 函数，
文件可直接读入、token 可经回调流出。MoonBit 官方 web 模板同样声明
`"preferred-target": "js"`。这不削弱隐私主张：`js` 仍是同一份 MoonBit 源码的
提前编译，无解释器、无网络 API；引擎各包仍双后端可构建可测试。

**任务**
1. `src/web/`：`foreign_library` + `#export_name` 暴露 JS API（handle 表、错误旁路、流式回调）
2. `web/index.html`：文件选择（FileReader，零请求）与 URL 加载两条路径、流式输出、tok/s 实时显示、无网络演示
3. `tools/serve_web.py`：构建 + 组装 `web/dist/` + loopback 预览服务（可选 `--model-dir` 暴露模型）
4. 性能调优（实测驱动，非猜测）：见下方结果

**验收标准（DoD）**
- [x] Chrome 加载 SmolLM2-135M 后可流式生成：实测 **6.3 tok/s**（≥3 目标，超出 2 倍）
      ——注：用的是 Q8_0（138MB）而非 Q4_0，因为手头已有的对拍模型是 Q8_0
- [x] 断网状态下完整演示：三重证据（无外部引用 / 推理期间 fetch·XHR·WebSocket·sendBeacon
      全零记录 / 硬禁用 fetch 后仍正确生成），详见 `docs/browser.md`
- [x] 浏览器与 native 生成结果一致：**token id 完全相同**（两个 prompt 均逐位一致），
      比 DoD 写的「前 32 token 一致率 ≥95%」更强
- [x] 加载体验有明确指标：读取 198 ms、解析 4.4 s、产物体积 306 KB

**补充说明（性能目标是靠实测调优达成的）**

两次修复都不是靠读 MoonBit 源码猜出来的，而是读**生成的 JavaScript** 找到的：

1. **累加改用 `Double`**：`js` 后端下每个 `Float` 运算都编译成 `Math.fround` 调用，
   f32 累加链等于每次乘加都付一次函数调用。
2. **权重字节改用 `unsafe_get`**：`Bytes` 索引会生成 `$bound_check` 调用，
   内层循环每次迭代 4 个。已有 `uget` 覆盖数组但不覆盖字节。

在 Node 中用 `tools/bench_js_kernel.mjs` 逐 bundle、同条件（16 token，best of 3）
隔离测量：

| 内核 | decode tok/s | 相对基线 |
|---|---|---|
| 基线：f32 累加 + 检查字节读 | 1.92 | — |
| 仅 f64 累加 | 2.50 | 1.30x |
| 仅 `unsafe_get` 字节读 | 3.40 | 1.77x |
| 两者同时（已交付） | **7.56** | **3.94x** |

两项是相互放大而非简单相加（1.30 × 1.77 = 2.3 ≠ 3.94），推测是去掉边界检查后
JS 引擎才能把循环里的值留在寄存器中，而这又要求累加器本身就是普通 JS 数字。
交付的是合并后的实测值。

两项改动对 native 也有增益：decode 从 14.4 → **18.4 tok/s**。改动后 llama.cpp
argmax 对拍仍为 280/288 = 97.22%（参考为 build 11675；换用本地新构建的
0.7.0-dev 参考时为 277/288 = 96.18%，差异来自参考实现本身——同一本地参考下，
内存优化前后逐 prompt 结果逐位相同），浏览器与 native 的 token id 仍逐位一致。

> **数字更正**：早期版本写「初次实测 0.7 tok/s，比目标差 4 倍」以及
> 「0.7 → 6.33（9 倍）」。那是把 UI 路径 8 token 与 bench 路径 16 token 两次
> 测量直接相减，混入了测量条件差异（每次调用的固定开销摊薄程度不同）。
> 受控重测结果为 1.92 → 7.56（3.9 倍）。达标结论不变。

---

### M3 — Agent 运行时：类型安全的工具调用

**目标**：从"能生成"升级为"能干活"，并展示 MoonBit 强类型在 Agent 安全性上的差异化价值。

> **状态：待开始。** 开工前做了可行性实测，结论**推翻了原计划的两个前提**，
> 下方任务列表已据此修订。实测记录见 `docs/agent.md`（待建）。
>
> **前提修正 1：SmolLM2-135M 无法产生工具调用。** 四种提示方式全部失败：
> 显式 `<tool>{...}</tool>` 格式被完全忽略并输出散文；要求只输出 JSON 得到空输出
> 或 ` ``` ` 代码块；few-shot 能学会单轮 `Q: ...` → `{"capital": "..."}`，
> 但换成工具调用就退化为继续编造 `User:` 行；chat 模板 + 强格式指令提前
> hit_eos。结论：135M 级模型不具备可靠的工具调用能力，M3 不能基于它设计。
>
> **前提修正 2：Qwen2.5-0.5B 可以，但必须用它的原生格式。** 用
> llama-server `--jinja` + OpenAI 风格 `tools` 参数实测成功返回标准
> `tool_calls`（`{"name": "list_dir", "arguments": "{\"path\": \".\"}"}`）。
> 关键：格式必须来自模型自己的 chat template（GGUF 的
> `tokenizer.chat_template`，2509 字符 Jinja），自创格式会失败。
>
> **当前 blocker：引擎跑不了 Qwen2.5。** `q4_k_m` 变体的 291 个张量里 133 个是
> **Q5_0**（混精），而 `is_dequantizable` 只覆盖 F32/F16/Q4_0/Q8_0/Q4_K/Q6_K。
> 引擎**报错退出**而非静默出错，失败形态是对的。
>
> **另一个已确认的缺口：Qwen2 的 bias 张量未实现。** 该模型有 72 个
> `blk.N.attn_{q,k,v}.bias`，而 `src/model/forward.mbt` 只加载 `.weight`，
> 从不尝试 bias。`config.mbt` 声称支持 `qwen2` 架构，但该声明与实际实现不符——
> 即使解决了 Q5_0，Qwen2 输出也会**静默错误**。这是必须先修的真实缺口。

**任务**

1. **先修两个引擎缺口**（M3 的前置，否则 Agent 无可用模型）：
   - `src/tensor/`：补 Q5_0 反量化（块结构见 llama.cpp 的
     `dequantize_row_q5_0`，参照 M0 的 Q4_0/Q8_0 做法与黄金向量测试）
   - `src/model/`：支持注意力 bias 张量——加载 `blk.N.attn_{q,k,v}.bias`（存在时
     加到投影结果上），并给 Qwen2 加对拍验证；同时修正 `config.mbt` 的架构
     声明，让"声称支持"与"实际支持"一致
2. **`src/agent/`**：`Tool` trait（name / description / 参数 Schema / `invoke(args) -> Result`）；
   AgentLoop（系统提示 → 模型生成 → **按模型原生格式**解析工具调用 → Schema 校验 →
   执行 → 结果回填 → 循环，上限步数与中止条件）。工具调用的**格式解析要做成
   可替换的策略**（Qwen2 的 `<tool_call>{...}</tool_call>` 是一种，后续其他模型
   是另一种），而不是把某一种格式硬编码进循环
3. **工具调用解析与校验**：输出的 JSON 严格校验（必填 / 类型 / 枚举 / 路径白名单），
   非法调用被拒绝并作为错误观察回传给模型重试。这里体现差异化：把参数校验
   从"运行时解析 JSON 再检查"收敛到**编译期类型 + 集中式校验**两层
4. **`src/tools/` 内置本地工具**：`list_dir`、`read_file`、`grep_text`、`calc`，
   所有文件类工具限定工作区沙箱内（路径穿越、绝对路径、符号链接均需拦截）
5. **会话管理**：多轮对话的上下文截断、transcript 持久化
6. **CLI `moonlm agent --task "..."`**：能跑通"列目录 → 读文件 → 回答"的最小闭环

**验收标准（DoD）**

- [ ] 端到端演示：`moonlm agent --task "总结 docs/ 下的关键结论"`——在 **Qwen2.5-0.5B**
      上自主完成 列目录 → 读文件 → 总结（不再要求 135M 模型，理由见上）
- [ ] 引擎缺口修复有对拍验证：Q5_0 反量化对 llama.cpp 黄金向量 ≤1e-6；
      Qwen2 带 bias 的前向与 llama.cpp 逐位置 argmax 对拍，并记录一致率
- [ ] 所有工具调用 100% 经过 Schema 校验；注入非法参数的对抗测试全部被拒绝
- [ ] 沙箱逃逸测试全部拦截：`../../` 路径穿越、绝对路径（`/etc/passwd`、
      `C:\Windows\...`）、符号链接指向沙箱外
- [ ] 同一 AgentLoop 在 CLI（native）与浏览器（js）上行为一致
- [ ] **诚实性要求**：若 Qwen2.5-0.5B 在真实任务上的工具调用成功率偏低，
      记录实际成功率而非只报最好的例子；`docs/agent.md` 要写明模型能力边界

**风险与对策**

| 风险 | 对策 |
|---|---|
| Q5_0 反量化位布局写错 | 以 llama.cpp `dequantize_row_q5_0` 为唯一权威；黄金向量对拍（M0 已验证过这条路） |
| Qwen2 bias 加上后数值仍不对 | 用 llama.cpp 逐位置 argmax 对拍定位；Qwen2.5 的 GQA 比例是 7:1（14 query / 2 kv），比 SmolLM2 的 3:1 更激进，需重点验证 |
| 0.5B 模型工具调用成功率低 | 先测基线成功率再定架构；若偏低，把"重试 + 错误回传"作为必做项而非可选 |
| 浏览器侧跑 0.5B 太慢 | 0.5B Q4 约 300-400MB，浏览器加载与推理都吃力。**CLI 先行**，浏览器侧作为可选（可只演示小模型聊天，Agent 部分在 native 演示） |
| 工程量大（引擎缺口 + Agent 全套） | 分两批：先修引擎缺口并验证（可独立交付），再做 Agent 层 |

---

### M4 — 加速路径：llama.cpp FFI 双引擎（可选增强）

**目标**：证明架构抽象的价值——上层代码不变，换引擎即换性能档位。

**任务**
1. native FFI 绑定 llama.cpp 核心 C API（模型加载 / 上下文 / decode / 采样器）
2. `LlamaCppEngine` 实现 `Engine` trait；`--engine llama` / `--engine pure` 一键切换
3. 双引擎基准对比报告（tokens/s、内存、支持模型矩阵），写入 `benchmarks/`

**验收标准（DoD）**
- [ ] 同一 Agent/聊天代码在两个引擎下均可运行，仅切换一个参数
- [ ] 基准报告含 ≥ 3 个模型 × 2 引擎的数据表
- [ ] FFI 层内存边界清晰（谁分配谁释放），无泄漏（长会话回归测试）

---

### M5 — 提交打磨

**任务**
1. CI 完善（双后端构建 + 测试 + 基准 smoke）、README（双语）、架构图与调优笔记
2. 演示素材：3 分钟录屏（浏览器断网聊天 → Agent 文档问答 → 基准对比）
3. 对齐申报书口径：参考项目清单（llama.cpp、wllama、transformers.js、SmolLM2、pi-moonbit、moon_outlines）、原创性声明、许可证

**验收标准（DoD）**
- [ ] 新克隆仓库 → `moon build && moon test` 一次通过
- [ ] 演示脚本可由他人照着复现
- [ ] 申报书更新为本项目内容

## 7. 风险登记册

| 风险 | 概率 | 影响 | 对策 |
|---|---|---|---|
| 纯 MoonBit matmul 性能不达标 | 中 | 高 | 目标锁定 ≤0.5B 小模型；量化降低带宽；SIMD/布局优化；M4 native FFI 兜底；诚实公布基准数据 |
| 浏览器内存不足以加载大模型 | 中 | 中 | 浏览器档位只承诺 ≤135M 量化模型（实测 138MB Q8_0 可加载）；文档明确内存预算 |
| K-quant（Q4_K 等）位运算实现出错 | 中 | 中 | 黄金测试向量对拍；先 Q8_0/Q4_0 保证主线可跑，K-quant 并行攻坚 |
| tokenizer 细节不一致导致输出劣化 | 中 | 中 | 往返恒等测试 + 与 llama.cpp token 序列对拍 |
| 工期超支 | 高 | 中 | 阶段化裁剪：M4 整体可弃；浏览器 Demo 若吃力则退化为 native CLI 演示 + 截图 |
| 评委质疑"为什么要用 MoonBit 而不是直接用 llama.cpp" | — | — | 答辩口径：①Wasm-GC 目标 llama.cpp 需带完整 C 运行时，MoonBit 产物更小、启动更快；②纯类型安全的上层 Agent 框架是 llama.cpp 生态缺位；③MoonBit 版 GGUF/Agent 库可直接被 mooncakes 生态复用 |

## 8. 与比赛评审的对应

| 评审关注点 | 本项目回答 |
|---|---|
| 语言特性利用 | Wasm-GC 跨平台单二进制分发；`extern "C"` FFI；trait + 代数数据类型建模 Engine/Tool；错误处理用 `Result` 贯穿解析栈 |
| 生态贡献 | mooncakes 首个本地推理 + Agent 框架；`gguf/`、`tensor/`、`tokenizer/` 均为可独立复用的包 |
| 演示冲击力 | 断网浏览器跑模型 + Agent 自主完成文档问答，一眼看懂"零 API、零外传" |
| 完成度信号 | 双后端 CI、对拍测试、基准数据表、可复现演示脚本 |
