跳到主要内容

为什么是 DSL

每个把 Agent 用在生产环境的人,迟早都会撞上同一类事故:

流程跑到第七步,模型"优化"掉了一个校验;某个分支悄悄没走; 两个人排查两天,最后在对话记录里发现——第三轮的一段长上下文, 把指令冲淡了。

这不是模型能力问题。你把控制流交给了一条概率性的信道。 提示词适合承载判断力,不适合承载流程;流程需要确定性, 而确定性需要一种能被检查的形式

Agent Flow 的主张只有一句话:

能让机器确定执行的,就不要让模型看着办。

你现在的三种写法,各自缺什么

写法一:把流程写进提示词

症状你大概率见过:

  • 约束会失忆。改一个词行为就变;第十轮对话后,"先校验再继续" 变成了"看起来没问题就继续"
  • 不可 review。没人能证明一段 500 字的提示词覆盖了所有分支—— code review 在这里失明
  • 判定权在模型手里。提示词写"检查通过后继续"——是否通过, 模型说了算。这在生产环境等于没有验证
  • 不可复跑。同样的输入,两次执行路径可能不同;出问题无法回归定位

写法二:把流程写成 YAML / JSON

看起来"工程"了一些,实质只是把提示词换成了另一种没有语义的文本:

  • 没有类型:字段拼错、结构漂移,运行期才炸
  • 没有统一语义:每家框架自己发明 depends_on${ref}, 同一份 YAML 换个框架含义全变
  • 编排真正需要的东西——并发预算、输出契约、验证判定、成员资格—— 没有对应物,只能靠字段约定一层层打补丁

写法三:用通用语言写(LangGraph / Semantic Kernel / 裸 SDK)

表达力最强,于是危险也最多:

  • 工作流就是任意代码:沙箱形同虚设,不可信分发无从谈起
  • "这段流程会不会并发写同一个文件?"——需要人肉追踪每个回调, 没有静态分析
  • 并发闸、预算、超时、重试、顺序还原……每个约束都是一段手写模式, 漏写一处就是一次事故
  • 框架的约束靠自觉,语言的约束靠文法。前者会腐化,后者不会

同一个任务,四种写法

任务很普通:并发处理 20 封邮件——逐封分类,需要回复的草拟回复。 约束:并发 ≤ 4;总调用 ≤ 40 次;单次 2 分钟超时; 输出必须含 idcategory;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 等非路径元素的数组则在编译期被拒绝。通过类型检查 只代表请求形状合法,运行时仍会与宿主写入策略求交。

为什么必须是"一门语言"

因为有三样保证,只在语言层面成立,框架和规范都给不了:

  1. 文法:错误写不出来。stage 依赖不声明就引用、expect 一个 不存在的类型、保留字做变量名——这些在解析阶段就不存在。 不是 lint 建议,是不可能
  2. 类型:坏数据进不来。agent 的输出契约 = 类型 = JSON Schema, 运行时由 Ajv 强制,失败自动修复一次,再失败就抛错; 下游每一次字段访问都在编译期检查过
  3. 运行时:判定与预算不可绕过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 的甜区是:流程已经想清楚,要的是可靠、可审计、可复跑的执行。 把探索的混沌留给对话,把确定的骨架交给语言—— 这正是"确定性优先"的含义。

下一步