05 · Ainet 语言设计
目标:给两类人同一套语义—— 研究者/系统工程师用 Core 精确描述网与规则; Agent 工程师用 Flow 写流水线、扇出、竞速、监督,不被迫先学 λ 演算。 两者编译到同一个 AIN IR。
5.1 设计目标
| 目标 | 具体要求 |
|---|---|
| 可读 | 网/端口/规则是显式的;不要求用户手写组合子 |
| 可验证 | 线性端口、session 协议、effect row、预算都能静态检查 |
| 可移植 | 编译到 aind 字节码、WASM(Netbook)、以及可选的 HVM/IC 后端 |
| 可调试 | 每个源位置映射到 cell/rule;错误能定位到图 |
| 可扩展 | 宿主函数/工具/模型通过 effect 接口接入,不改核心 |
| 渐进 | 先写 Flow;需要时下探到 Core;再需要时写裸 IR |
5.2 两层语言
Layer 1:Ainet Core(显式网)
核心概念直接暴露:agent、port、state、rule、net、effect。
// 类型
type Task = { id: Text, prompt: Text }
type Draft = { text: Text, cites: List<Text> }
type Report = { text: Text, approved: Bool }
// 效果(由宿主提供实现)
effect LLM {
input { model: Text, prompt: Text, max_tokens: Nat }
output { text: Text, tokens: Nat, usd: Float }
}
effect Search {
input { query: Text, k: Nat }
output List<{ title: Text, url: Text, snippet: Text }>
}
// Agent:一个带状态与端口的 cell
agent Researcher {
ports {
inbox: ?Message // principal:接收消息
out: !Draft // auxiliary:发送结果
llm: !LLM // auxiliary:能力(capability)
search: !Search
}
state Idle | Searching { prompt: Text } | Thinking { docs: List<Doc> } | Done
}
// 收到任务 → 先搜索;回复回到 inbox(复用同一个端口)
rule Researcher[Idle](inbox, out, llm, search) <> Task(t) =>
Researcher[Searching(prompt: t.prompt)](inbox, out, llm, search)
| search.call(query: t.prompt, k: 5, reply: inbox)
// 搜索完成 → 再调用 LLM
rule Researcher[Searching { prompt }](inbox, out, llm, search) <> Search.Response(docs) =>
Researcher[Thinking(docs: docs)](inbox, out, llm, search)
| llm.call(
model: "fast",
prompt: render(prompt, docs),
reply: inbox
)
// LLM 完成 → 输出草稿
rule Researcher[Thinking { docs }](inbox, out, llm, search) <> LLM.Response(r) =>
Researcher[Done](inbox, out, llm, search)
| Send(out, Draft{ text: r.text, cites: urls(docs) })
注意:每个状态只接受一种回复。如果同时发出 search 与 llm 两个调用,两条回复会竞争同一个
inbox,其中一个会因为没有匹配规则而卡住。这是故意的约束:Agent 的状态应当明确它此刻在等什么;需要并发等待时,用Join(2)而不是复用同一个 principal 端口。
构造子即 cell:
Task(t)、Search.Response(docs)、Draft{...}不是"结构体字面量",而是携带字段的构造子 cell(agent)。t.prompt是规则对消息字段的投影。这样数据流和消息流是同一种图结构。能力与 Effect 的关系:
llm是一个 capability cell(内部由Effect<LLM>实现),llm.call(...)展开为EffectCall(llm, op, input, reply)+ 一条 reply wire;运行时把 reply 注入到reply所在的端口。
要点:
inbox是 principal 端口;其余是 auxiliary。llm.call(..., reply: inbox)是一个 EffectCall cell:principal 连到llm能力,aux 连到 payload 与 reply wire。回复被注入到reply所连的端口(这里是新 agent 的inbox),形成新的活跃对。- 规则 RHS 是网表达式:
cell | cell | ...;Send(port, value)把一个值送到辅助端口。 - 状态展开由编译器完成:
Researcher[Idle]/Researcher[Working]/Researcher[Done]是不同 agent 符号。
Layer 2:Ainet Flow(Agent DSL)
大多数用户不写规则,只写"有哪些 Agent、怎么连、预算多少":
flow Research {
input question: Text
output report: Report
budget { usd: 0.80, tokens: 250_000, wall: 120s }
agent R = Researcher(model: "fast")
agent C = Critic(model: "strong")
agent W = Writer(model: "fast")
stage retrieve = R.search(question, k: 5)
stage draft = R.draft(question, retrieve)
stage review = C.review(draft)
stage write = W.write(draft, review)
draft -> review -> write -> report
}
(retrieve 作为 draft 的参数传入;箭头只表示输出到下一 stage,不重复连接。)
Flow 的编译结果是一张网:
Question ─▶ Researcher ─draft─▶ Critic ─verdict─▶ Writer ─▶ Report
│ │ │
└── LLM cap ──────┴── LLM cap ─────┘
组合子
// 扇出 + join
par {
branch a = R.draft(question)
branch b = R.draft(question)
} -> join { a, b } as both
// 竞速(第一个成功)
race {
M1.answer(question)
M2.answer(question)
} -> first
// 超时
with timeout(30s) {
R.draft(question)
}
// 监督
supervise C {
policy: restart_one
max_restarts: 3
window: 60s
}
下探
Flow 的每个 stage 都可以标注 core: 覆盖生成的规则;Core 的每个 agent 也可以在 Flow 里当 stage 用。二者不是两门语言,而是同一 AST 的两种表面语法。
5.3 语法(Core,EBNF 摘要)
program ::= decl*
decl ::= typeDecl | effectDecl | agentDecl | ruleDecl | flowDecl | importDecl
typeDecl ::= "type" ID "=" type ";"
type ::= ID | "{" field,* "}" | "List" "<" type ">" | "Result" "<" type "," type ">"
| "(" type,* ")" | type "->" type
effectDecl ::= "effect" ID "{" "input" type "output" type "}"
agentDecl ::= "agent" ID "{" portDecl* stateDecl? "}"
portDecl ::= ID ":" ("!" | "?") type ";"
stateDecl ::= "state" variant ("|" variant)*
variant ::= ID ("{" field,* "}")?
ruleDecl ::= "rule" pattern "<>" pattern "=>" net ";"
pattern ::= ID ("[" statePat "]")? "(" ID,* ")"
statePat ::= ID | "_"
net ::= cell ("|" cell)*
cell ::= ID ("[" statePat "]")? "(" arg,* ")"
arg ::= ID ":" expr | expr
expr ::= literal | ID | expr "." ID | "render" "(" expr ")"
flowDecl ::= "flow" ID "{" flowItem* "}"
flowItem ::= "input" ID ":" type
| "output" ID ":" type
| "budget" "{" budgetField,* "}"
| "agent" ID "=" ID "(" arg,* ")"
| "stage" ID "=" call
| portChain
call ::= ID "." ID "(" arg,* ")"
portChain ::= ID ("->" ID)+ ("->" ID)?
语法细节(运算符优先级、注释、字符串插值)在
specs/grammar.md固化;本文只保证示例可读。
5.4 类型系统
5.4.1 端口类型
!T:发送T(output/capability)。?T:接收T(input)。- 端口是线性的:每个端口在一条规则里恰好被消费一次;free port 不能被复制。
- 需要共享时,显式使用
dup(编译器插入 δ)或Refcell;类型系统会记录共享点。
5.4.2 Session 协议
type LLM = session {
send Request;
recv Response;
end
}
session { ... }是类型级糖,等价于!Request.?Response.end;完整语法见specs/grammar.md。v1 支持send/recv/end/rec。
或内联:
!{ request: Request; response: Response; end }
v1 支持:
send T/recv T/endrec X. ...(递归协议)choice { ... }(内部选择)与offer { ... }(外部选择)——可选- 对偶检查:连接的两端必须互补;否则类型错误。
5.4.3 Effect row
agent Researcher effects { llm, search } {
...
}
- 规则只能调用声明过的 effect。
- 运行时 handler 必须覆盖所有声明的 effect;缺失则在启动时报错。
- 纯 agent(无 effect)可以编译到纯网后端(HVM/WASM),获得更高性能。
5.4.4 状态与数据
state是有限 ADT;编译器展开为 agent 变体。- 数据类型是结构化 JSON-ish;大 payload 存堆/对象存储,cell 里只放引用。
Result<T,E>是一等类型;Error是它的构造子,不是异常。
5.4.5 预算类型
agent Researcher budget { usd: 0.20, tokens: 80_000 } {
...
}
- 预算是上限,不是保证;运行时在 effect 前检查。
- 子 agent 的预算从父预算扣减;超支返回
Denied。 - 类型检查保证每个 effect 调用都有对应的预算来源(或显式
unbounded,仅限测试)。
5.5 编译流水线
source (.ain)
│ parse (rowan / chumsky)
▼
AST ── resolve names ──▶ typed AST
│ linear + session + effect + budget checks
▼
Core AST (agents/rules/nets)
│ lower: state expansion, effect calls, join/select, supervision
▼
AIN IR (cells, ports, wires, rules; SSA-ish)
│ optimize: inline, specialize, fuse, dead-cell elim, placement
▼
Bytecode / WASM / HVM-IC (optional)
关键 lowering
| 源构造 | IR |
|---|---|
agent Researcher[Idle] |
agent symbol Researcher_Idle |
state 转移 |
规则 RHS 替换为新符号 |
llm.call(...) |
EffectCall cell + reply wire + budget check |
par { ... } |
δ 复制输入 + 多个子网 + Join |
race { ... } |
δ + 多个子网 + Select + cancel |
supervise |
Supervisor cell + 子 Exit 端口 |
with timeout |
Timeout cell 包装子网 |
flow 的 -> |
端口对接 + 消息 cell |
stage |
一个 agent 实例 + 输入/输出 wire |
优化(v1 只做安全子集)
- 内联:单次使用的 agent 可以内联其规则(减少 cell)。
- 特化:状态已知时消去不可达分支。
- 融合:相邻的
Send/Receive合并。 - 死 cell 消除:没有路径到 free port 的子网擦除(ε)。
- 放置:按
@loc/租户/通信图分配。
5.6 示例:三 Agent 研究流水线(Flow)
flow Research {
input question: Text
output report: Report
budget { usd: 0.80, tokens: 250_000 }
agent R = Researcher(model: "fast", temperature: 0.3)
agent C = Critic(model: "strong", temperature: 0.0)
agent W = Writer(model: "fast", temperature: 0.5)
stage docs = R.search(question, k: 8)
stage draft = R.draft(question, docs)
stage review = C.review(draft)
if review.approved {
stage final = W.write(draft, review)
final -> report
} else {
stage revised = R.revise(draft, review)
stage review2 = C.review(revised)
W.write(revised, review2) -> report
}
}
编译后的网(简化):
Question
│
▼
Researcher ──docs──▶ Researcher ──draft──▶ Critic ──review──┐
│ │ │ │
└── Search cap ──────┴── LLM cap ─────────┴── LLM cap ───┤
▼
┌── approved? ──┐
│ yes │ no
▼ ▼
Writer Researcher.revise
│ │
└─────▶ Report ◀┘
if在 IR 里是Select/Switchcell;两个分支的网都被构造,未选分支被擦除(或取消)。review.approved是一个普通数据字段;分支选择由Switch规则完成。- 预算在
Researcher/Critic/Writer之间分配;Search与LLM调用前检查。
5.7 示例:扇出 + join + 竞速(Core)
// 扇出:把问题复制给两个模型
rule Fanout[Idle](in, outA, outB) <> Question(q) =>
Fanout[Done](in, outA, outB)
| Send(outA, Question(q))
| Send(outB, Question(q))
// 竞速:第一个答案获胜,另一个取消
rule Race[Idle](in, a, b, out) <> Question(q) =>
Race[Waiting](in, a, b, out)
| Select2(a, b, out, cancel: in)
Select2是 L2 的Select(2)语法糖;失败/取消的输入收到Cancel消息。上游EffectCall/Agent必须声明cancel端口,否则类型错误。
5.8 示例:监督 + 重试
supervise Worker {
policy: restart_one
max_restarts: 3
window: 60s
agent W = Worker(model: "fast")
stage job = W.run(task)
job -> result
}
编译为:
Supervisor ──spawn──▶ Worker
▲ │
└──── Exit(ok|err) ┘
restart_one:只重启失败的子 agent;保留其他子网。max_restarts/window由Supervisorcell 的计数器状态实现。- 超过阈值 →
Escalate到父 supervisor;没有父则终止整个 flow 并返回Error。
5.9 TypeScript SDK
面向不写 .ain 的开发者:
import { defineAgent, flow, llm, tool } from "@ainet/sdk";
const researcher = defineAgent({
name: "Researcher",
input: Task,
output: Draft,
capabilities: {
llm: llm({ model: "fast", budget: { usd: 0.2 } }),
search: tool("search", { schema: SearchInput }),
},
async run(task, ctx) {
const docs = await ctx.search({ query: task.prompt, k: 5 });
const draft = await ctx.llm.ask(render(task.prompt, docs));
return { text: draft.text, cites: docs.map(d => d.url) };
},
});
const research = flow("research")
.input("question", Task)
.output("report", Report)
.stage("draft", researcher)
.stage("review", critic)
.stage("write", writer)
.budget({ usd: 0.8, tokens: 250_000 });
- SDK 在进程内运行
ain-runtime(napi/wasm),或连到aind服务。 defineAgent.run是普通 async 函数;SDK 把它编译成状态机 cell(等价于 Core 的规则)。- 能力(capability)通过
ctx注入,不是全局注册表;没有ctx.search就调不到。 flow是 DSL builder;最终生成同一份 AIN IR。
5.10 错误信息与 DX
目标:错误要能"指到图上"。
示例:
error[E-LINEAR-01]: port `out` is used twice
--> research.ain:24:18
|
24 | | Send(out, draft) | Send(out, review)
| ^^^ ^^^ `out` already consumed here
|
help: duplicate the value explicitly:
| | dup(draft) as d1, d2
| | Send(out, d1) | Send(out, d2)
其他错误:
E-SESSION-01:协议对偶不匹配(发送 vs 发送)。E-EFFECT-01:调用了未声明的 effect。E-BUDGET-01:effect 没有预算来源。E-DEADLOCK-01:Join/Select 的环形等待(保守检查)。E-STATE-01:状态未覆盖(缺失规则)。E-PORT-01:端口类型/方向不匹配。
每个错误带:源位置、相关 cell/port、最小复现网(可粘贴到 Netbook)、修复建议。
5.11 模块、包、版本
project/
├── ainet.toml
├── src/
│ ├── main.ain
│ ├── agents/
│ └── flows/
└── tests/
ainet.toml:
[package]
name = "research"
version = "0.1.0"
edition = "2026"
[dependencies]
ainet-std = "0.1"
- 包 = 一组 agent/effect/flow;可发布到 registry(v2)。
- 版本化:IR 有
version字段;运行时拒绝不兼容的字节码。 - 能力声明在包级别:安装包时能看到它请求的 effect(类似权限清单)。
5.12 与 AIN IR 的映射(摘要)
| Ainet | AIN IR |
|---|---|
agent + state |
多个 agent 符号 |
ports |
principal/aux 端口 + 类型 |
rule |
(α,β) -> template |
Send(port, v) |
Message cell 接到 aux 端口 |
EffectCall |
Effect<Op> cell + job |
par |
δ + Join |
race |
δ + Select + Cancel |
supervise |
Supervisor + Exit 端口 |
budget |
Budget cell + 准入检查 |
flow |
网拓扑 + 初始消息 |
5.13 v1 实现范围
必须:
- Core 的 agent/rule/net/effect/type。
- Flow 的 agent/stage/
->/par/race/timeout/supervise/budget。 - 线性 + session(
!/?/end/rec)+ effect row 检查。 - 编译器到
aind字节码 + WASM。 - 错误信息带源位置与网片段。
可以延后:
- 泛型/子类型/row 推断。
- 完整
&/⊕session 子类型。 - 热更新/动态路由表。
- 包 registry。
- Python SDK(先 TS)。