为什么是 DSL
每个把 Agent 用在生产环境的人,迟早都会撞上同一类事故:
流程跑到第七步,模型"优化"掉了一个校验;某个分支悄悄没走; 两个人排查两天,最后在对话记录里发现——第三轮的一段长上下文, 把指令冲淡了。
这不是模型能力问题。你把控制流交给了一条概率性的信道。 提示词适合承载判断力,不适合承载流程;流程需要确定性, 而确定性需要一种能被检查的形式。
Agent Flow 的主张只有一句话:
能让机器确定执行的,就不要让模型看着办。
你现在的三种写法,各自缺什么
写法一:把流程写进提示词
症状你大概率见过:
- 约束会失忆。改一个词行为就变;第十轮对话后,"先校验再继续" 变成了"看起来没问题就继续"
- 不可 review。没人能证明一段 500 字的提示词覆盖了所有分支—— code review 在这里失明
- 判定权在模型手里。提示词写"检查通过后继续"——是否通过, 模型说了算。这 在生产环境等于没有验证
- 不可复跑。同样的输入,两次执行路径可能不同;出问题无法回归定位
写法二:把流程写成 YAML / JSON
看起来"工程"了一些,实质只是把提示词换成了另一种没有语义的文本:
- 没有类型:字段拼错、结构漂移,运行期才炸
- 没有统一语义:每家框架自己发明
depends_on与${ref}, 同一份 YAML 换个框架含义全变 - 编排真正需要的东西——并发预算、输出契约、验证判定、成员资格—— 没有对应物,只能靠字段约定一层层打补丁
写法三:用通用语言写(LangGraph / Semantic Kernel / 裸 SDK)
表达力最强,于是危险也最多:
- 工作流就是任意代码:沙箱形同虚设,不可信分发无从谈起
- "这段流程会不会并发写同一个文件?"——需要人肉追踪每个回调, 没有静态分析
- 并发闸、预算、超时、重试、顺序还原……每个约束都是一段手写模式, 漏写一处就是一次事故
- 框架的约束靠自觉,语言的约束靠文法。前者会腐化,后者不会
同一个任务,四种写法
任务很普通:并发处理 20 封邮件——逐封分类,需要回复的草拟回复。
约束:并发 ≤ 4;总调用 ≤ 40 次;单次 2 分钟超时;
输出必须含 id 与 category;id 不得重复。
提示词版
你是一个邮件助手。请把下面 20 封邮件逐封分类为 urgent / normal / spam,
对需要回复的草拟一封礼貌回复。注意:不要同时处理太多封,
总共不要超过 40 次调用,每封不要超过 2 分钟,结果用 JSON,
每项必须包含 id 和 category,id 不能重复,请务必遵守……
这些约束存活到第几轮对话?没有人知道。id 重复了,谁来拦?
YAML 版
steps:
- id: classify
foreach: ${emails}
concurrency: 4 # 谁执行?上限和别的 step 叠加吗?
output_schema: classified_email # 谁校验?失败后呢?
- id: draft
when: ${classify.needs_reply} # 字段存在吗?拼错了何时发现?
budget: 40 # "budget" 是这个框架的词吗?还是你想象中的?
每一行都依赖某个解释器的私有语义——换框架,重写。
代码版
const sem = new Semaphore(4);
let runs = 0;
const seen = new Set();
const out = await Promise.all(emails.map(async (e) => {
if (++runs > 40) throw new Error('budget'); // 重试路径里忘写了
const r = await withTimeout(call(e), 120_000); // 迟到的结果忘了失效
if (!r.id || seen.has(r.id)) throw new Error('dup'); // 你得记得写这些
return r;
}));
// 并发上限公平吗?输出顺序还原了吗?这段代码敢放进不可信沙箱吗?
每一行都是模式,漏一行就是事故;而且没有人能静态地检查你有没有漏。
Agent Flow 版
limits {
concurrency: 4
agent_runs: 40
duration: 20m
}
stage classify -> ClassifiedEmail[] {
let items = parallel map input.emails as item limit 4 {
agent(assistant) {
task "Classify one email: urgent, normal or spam"
input { email: item }
tools none
expect ClassifiedEmail // 输出契约:id 与 category 缺一不可
timeout 2m
}
}
require unique(items[*].id)
else fail "ids must stay unique"
return items
}
并发、预算、超时、契约、唯一性——每一条都是受检查的语法,不是约定。
写错任何一条,flow check 会带着行号拒绝你,而不是等到生产事故提醒你。
同一个 bug 的四种死法
下面这些失败模式,在不同的写法里死于不同的阶段——越早死,越便宜:
| 失败模式 | 提示词 | YAML / JSON | 通用代码 | Agent Flow |
|---|---|---|---|---|
| 拼错字段 / 结构漂移 | 生 产事故 | 运行时异常 | 也许 code review | 编译期 |
| 流程步骤被跳过 | 常态 | 罕见 | 罕见 | 不可能(文法) |
| 验证结论被伪造 | 无从察觉 | 宿主代码负责 | 代码负责 | 不可能(Runtime 判定) |
| 超预算 / 越权写 | 事后账单 | 无对应物 | 手写漏掉 | limits / write 强制 |
写权限在编译期形成类型契约
write 只接受静态 path[]。数组字面量会推断兼容的公共元素类型,
因此当 input.kb_path: path 时,[input.kb_path] 正确推断为 path[];
包含 text、number 等非路径元素的数组则在编译期被拒绝。通过类型检查
只代表请求形状合法,运行时仍会与宿主写入策略求交。
为什么必须是"一门语言"
因为有三样保证,只在语言层面成立,框架和规范都给不了:
- 文法:错误写不出来。stage 依赖不声明就引用、
expect一个 不存在的类型、保留字做变量名——这些在解析阶段就不存在。 不是 lint 建议,是不可能 - 类型:坏数据进不来。agent 的输出契约 = 类型 = JSON Schema, 运行时由 Ajv 强制,失败自动修复一次,再失败就抛错; 下游每一次字段访问都在编译期检查过
- 运行时:判定与预算不可绕过。
verification.passed只能由pass when计算;agent_runs/duration/tools/write是闸门,不是提示
第三条需要一个受控的执行环境:Agent Flow 编译为受限 JavaScript,
在子进程 isolate 里执行,唯一出口是宿主 ABI,跨界值只能是 JSON。
通用代码给不了你"不能做什么"——受限语言可以。
这也是"可嵌入、无厂商绑定"的技术底座:语言与模型之间的全部耦合,
只有 AgentRuntime 一个接口。
常见疑问
模型越来越强,提示词不就够了吗?
模型变强解决的是判断力,不是可靠性。再强的模型,也不应该拥有 "我检查过了"的判定权——那是架构问题,不是能力问题。恰恰相反: 模型越强,越值得给它一副确定性的骨架,让能力稳定兑现,而不是 随对话质量波动。
又要学一门语言,成本是不是太高?
43 个保留字、一页文法,一个下午可以读完,快速开始
十分钟跑通第一个 workflow。对比一下你现在维护的那个 800 字提示词——
版本控制里叫 v7_final_final.txt,每次改完都要人肉回归。
DSL 不是新增复杂度,是把已经在你脑子里流动的流程,
写进能被机器检查的形式 里。
为什么不直接用 LangGraph / Semantic Kernel?
它们解决"在通用语言里写编排顺不顺手";Agent Flow 解决
"编排能不能被信任"。定位不同:通用语言能力强但给不出边界,
受限语言放弃图灵完备,换回来静态检查、沙箱执行与不可绕过的闸门。
两者并不互斥——宿主与集成用通用语言写,工作流核心跑 .flow。
已有系统怎么接入?
语言不绑定任何模型与厂商。宿主实现 AgentRuntime 接口
(resolveAgent / resolveTeam / 调用执行),注入运行时即可;
详见包与架构。换模型、换供应商,
工作流一行不改。
它不适合什么(诚实的边界)
- 单轮问答、摘要、翻译一段文字——直接调 API,别引入任何中间层
- 探索期的多 agent 对话,流程本身还在演化——先在对话里跑通,
再固化成
.flow - 一次性脚本,没有治理与复跑要求——怎么快怎么来
DSL 的甜区是:流程已经想清楚,要的是可靠、可审计、可复跑的执行。 把探索的混沌留给对话,把确定的骨架交给语言—— 这正是"确定性优先"的含义。