从零开始设计一个智能体编排系统 - DSL 设计篇
本文深入探讨了领域特定语言(DSL)的设计原理与实践,重点介绍了其在流程图保存和智能体编排中的应用。文章首先通过对比截图保存和结构化保存的优劣,阐明了DSL的核心价值:可编辑性、可执行性和可共享性。随后详细解析了DSL的四大组成部分:图结构、组件定义、全局变量和变量定义,并配以JSON示例说明节点数据、连线数据的存储格式。特别强调了组件作为"逻辑层"与节点"视图层&q
导读:如果说智能体编排系统是一栋大楼,那么 DSL 就是这栋大楼的设计图纸。本文带你深入了解如何用一种"语言"来描述复杂的智能体工作流。

一、什么是 DSL?为什么需要它?
1.1 从一个故事说起
想象你要向装修师傅描述你理想中的房子:
❌ 错误的描述方式:
“进门左手边有个开关,开关控制客厅的灯。客厅灯亮了之后,如果温度超过 28 度,空调自动打开。空调打开 30 分钟后,如果还有人,继续运行;如果没人,关闭空调…”
这样的描述又长又乱,师傅听得云里雾里。
✅ 正确的描述方式:
画一张电路图,上面标明了:
- 开关 → 灯
- 温度传感器 → 空调
- 人体传感器 → 空调
- 定时器 → 空调
一目了然!
DSL(Domain Specific Language,领域特定语言) 就是智能体编排系统的"电路图"。它用结构化的方式,描述智能体的执行流程。
1.2 DSL 的核心使命
┌─────────────────────────────────────────────────────┐
│ DSL 的三大使命 │
├─────────────────────────────────────────────────────┤
│ 1️⃣ 可视化 → 让流程图可以保存和加载 │
│ 2️⃣ 可执行 → 让后端引擎知道如何运行 │
│ 3️⃣ 可交换 → 支持导入导出和分享 │
└─────────────────────────────────────────────────────┘
二、DSL 长什么样?
2.1 一个完整的智能体 DSL
下面是一个"智能客服"智能体的 DSL 结构(简化版):
{
"title": "智能客服助手",
"graph": {
"nodes": [
{ "id": "begin", "type": "开始", "position": { "x": 100, "y": 200 } },
{ "id": "node1", "type": "知识检索", "position": { "x": 400, "y": 200 } },
{ "id": "node2", "type": "条件判断", "position": { "x": 700, "y": 200 } },
{ "id": "node3", "type": "AI 回答", "position": { "x": 1000, "y": 100 } },
{ "id": "node4", "type": "转人工", "position": { "x": 1000, "y": 300 } }
],
"edges": [
{ "from": "begin", "to": "node1" },
{ "from": "node1", "to": "node2" },
{ "from": "node2", "to": "node3", "condition": "找到答案" },
{ "from": "node2", "to": "node4", "condition": "未找到答案" }
]
},
"components": {
"node1": {
"type": "知识检索",
"params": {
"query": "用户的问题",
"knowledgeBase": "产品知识库",
"topResults": 5
}
},
"node2": {
"type": "条件判断",
"params": {
"condition": "检索结果数量 > 0"
}
}
// ... 其他组件
},
"variables": {
"userQuestion": { "type": "string", "value": "" },
"searchResult": { "type": "array", "value": [] }
}
}
2.2 DSL 的四层结构
┌────────────────────────────────────────────────┐
│ DSL 四层金字塔 │
├────────────────────────────────────────────────┤
│ ④ 变量层 │
│ 定义智能体中流动的数据 │
├────────────────────────────────────────────────┤
│ ③ 组件层 │
│ 每个节点的详细配置和参数 │
├────────────────────────────────────────────────┤
│ ② 图结构层 │
│ 节点 + 边,描述执行流程 │
├────────────────────────────────────────────────┤
│ ① 元数据层 │
│ 智能体名称、ID、版本等基础信息 │
└────────────────────────────────────────────────┘
三、核心设计:图结构
3.1 节点:智能体的"器官"
每个节点代表智能体的一个能力单元:
┌──────────────────────────────────────────────────────┐
│ 常见节点类型 │
├──────────────────────────────────────────────────────┤
│ │
│ 🟢 开始节点 │ 智能体的入口,接收用户输入 │
│ 🔵 AI 节点 │ 调用大模型生成回答 │
│ 🟣 检索节点 │ 从知识库查找相关信息 │
│ 🟠 条件节点 │ 根据条件走不同分支 │
│ 🟡 消息节点 │ 向用户输出消息 │
│ 🔴 工具节点 │ 调用外部 API 或工具 │
│ 🟤 循环节点 │ 重复执行某段流程 │
│ │
└──────────────────────────────────────────────────────┘
节点的三要素:
┌─────────────────────────┐
│ 节点结构 │
├─────────────────────────┤
│ 1. ID │
│ 唯一标识,如"node_001"│
├─────────────────────────┤
│ 2. 类型 │
│ 决定节点的功能 │
├─────────────────────────┤
│ 3. 配置参数 │
│ 节点的具体行为设置 │
└─────────────────────────┘
3.2 边:智能体的"神经"
边描述节点之间的连接关系,决定数据流向:
开始
│
▼
检索 ──────────┐
│ │
▼ │
判断 │
╱ ╲ │
╱ ╲ │
▼ ▼ │
AI 转人工 │
│ │ │
└───┬───┘ │
▼ │
输出 ◄─────────┘
边的关键信息:
{
"from": "node_001", // 从哪个节点出发
"to": "node_002", // 到哪个节点去
"fromHandle": "output", // 从哪个出口出来
"toHandle": "input" // 从哪个入口进去
}
3.3 特殊连接:多分支
有些节点有多个输出,比如条件判断节点:
条件判断
╱ │ ╲
╱ │ ╲
╱ │ ╲
▼ ▼ ▼
条件 A 条件 B 默认
│ │ │
▼ ▼ ▼
处理 A 处理 B 默认处理
DSL 中的表示:
{
"edges": [
{ "from": "switch", "to": "caseA", "handle": "Case 1" },
{ "from": "switch", "to": "caseB", "handle": "Case 2" },
{ "from": "switch", "to": "default", "handle": "default" }
]
}
四、组件:节点的"灵魂"
4.1 为什么需要组件层?
节点只描述了"有什么",组件描述"怎么做"。
比喻:
- 节点 = 汽车的外形和位置
- 组件 = 汽车的引擎、变速箱、轮胎等具体配置
4.2 组件的核心结构
┌─────────────────────────────────────────┐
│ 组件五要素 │
├─────────────────────────────────────────┤
│ 1. 组件名称 │
│ 如:"Agent", "Retrieval" │
├─────────────────────────────────────────┤
│ 2. 输入参数 │
│ 组件需要哪些数据 │
├─────────────────────────────────────────┤
│ 3. 输出参数 │
│ 组件产生哪些数据 │
├─────────────────────────────────────────┤
│ 4. 上游组件 │
│ 依赖哪些组件的输出 │
├─────────────────────────────────────────┤
│ 5. 下游组件 │
│ 输出给哪些组件使用 │
└─────────────────────────────────────────┘
4.3 典型组件示例
AI 组件:
{
"component_name": "Agent",
"params": {
"model": "gpt-4",
"temperature": 0.7,
"system_prompt": "你是一个专业的客服助手",
"max_tokens": 2048,
"tools": ["搜索工具", "计算器"],
"memory_window": 10
}
}
知识检索组件:
{
"component_name": "Retrieval",
"params": {
"knowledge_bases": ["kb_001", "kb_002"],
"similarity_threshold": 0.7,
"top_k": 5,
"query_variable": "user_question"
}
}
条件判断组件:
{
"component_name": "Switch",
"params": {
"conditions": [
{
"name": "高优先级",
"rules": [
{ "variable": "user_level", "operator": ">", "value": "5" }
],
"logic": "AND",
"goto": "vip_handler"
}
],
"default_goto": "normal_handler"
}
}
五、变量:数据的"血液"
5.1 变量的作用
变量让数据在智能体中流动起来:
用户输入 ──► [变量:user_input] ──► AI 处理 ──► [变量:ai_response] ──► 输出
5.2 变量的类型
┌────────────────────────────────────────────────┐
│ 常见变量类型 │
├────────────────────────────────────────────────┤
│ 📝 字符串 (String) │ 文本内容 │
│ 🔢 数字 (Number) │ 整数或小数 │
│ ✅ 布尔 (Boolean) │ true/false │
│ 📦 对象 (Object) │ 键值对集合 │
│ 📋 数组 (Array) │ 元素列表 │
└────────────────────────────────────────────────┘
5.3 变量的引用
关键设计:如何引用其他节点的输出?
我们使用 {节点 ID@输出名} 的格式:
{begin@user_input} → 引用开始节点的用户输入
{retrieval@results} → 引用检索节点的结果
{llm@response} → 引用 AI 节点的回答
示例:
{
"component_name": "Agent",
"params": {
"user_prompt": "请回答这个问题:{begin@question}",
"context": "参考这些信息:{retrieval@content}"
}
}
5.4 全局变量 vs 局部变量
┌─────────────────────────────────────────┐
│ 变量作用域对比 │
├─────────────────────────────────────────┤
│ │
│ 全局变量 局部变量 │
│ ───────── ──────── │
│ • 整个智能体可用 • 仅当前节点可用 │
│ • 生命周期长 • 生命周期短 │
│ • 如:用户 ID、会话 ID • 如:临时计算结果│
│ │
└─────────────────────────────────────────┘
全局变量示例:
{
"globals": {
"sys.user_id": "user_12345",
"sys.session_id": "session_abc",
"sys.conversation_turns": 5,
"sys.files": ["file1.pdf", "file2.doc"]
}
}
六、从可视化到 DSL
6.1 用户在画布上的操作
┌─────────────────────────────────────────────────────┐
│ 用户操作流程 │
├─────────────────────────────────────────────────────┤
│ │
│ 1. 拖拽节点到画布 │
│ ↓ │
│ 2. 配置节点参数 │
│ ↓ │
│ 3. 连接节点 │
│ ↓ │
│ 4. 点击保存 │
│ ↓ │
│ 5. 生成 DSL 并存储 │
│ │
└─────────────────────────────────────────────────────┘
6.2 转换过程
可视化数据(画布上的节点和连线):
[开始节点] ──► [AI 节点] ──► [输出节点]
x:100 x:400 x:700
y:200 y:200 y:200
转换后的 DSL:
{
"graph": {
"nodes": [
{ "id": "begin_001", "type": "beginNode", "position": { "x": 100, "y": 200 } },
{ "id": "agent_001", "type": "agentNode", "position": { "x": 400, "y": 200 } },
{ "id": "message_001", "type": "messageNode", "position": { "x": 700, "y": 200 } }
],
"edges": [
{ "source": "begin_001", "target": "agent_001" },
{ "source": "agent_001", "target": "message_001" }
]
},
"components": {
"begin_001": { "obj": { "component_name": "Begin", "params": {...} } },
"agent_001": { "obj": { "component_name": "Agent", "params": {...} } },
"message_001": { "obj": { "component_name": "Message", "params": {...} } }
}
}
6.3 关键转换规则
┌────────────────────────────────────────────────────┐
│ 转换规则对照表 │
├────────────────────────────────────────────────────┤
│ │
│ 画布元素 → DSL 元素 │
│ ───────── ───────── │
│ 节点位置 (x,y) → node.position │
│ 节点类型 → node.type │
│ 节点配置表单 → component.params │
│ 连线 → edge │
│ 连接点 ID → handle ID │
│ │
└────────────────────────────────────────────────────┘
七、DSL 的序列化与存储
7.1 为什么需要序列化?
┌─────────────────────────────────────────┐
│ 序列化的三大场景 │
├─────────────────────────────────────────┤
│ 1️⃣ 保存 → 存到数据库 │
│ 2️⃣ 导出 → 生成 JSON 文件 │
│ 3️⃣ 传输 → 前后端数据交换 │
└─────────────────────────────────────────┘
7.2 序列化流程
内存中的对象
↓
[序列化]
↓
JSON 字符串
↓
[压缩/加密](可选)
↓
存储到数据库/文件
7.3 存储结构
数据库表设计:
┌─────────────────────────────────────────┐
│ agents 表 │
├─────────────────────────────────────────┤
│ id │ 智能体 ID │
│ name │ 智能体名称 │
│ description │ 描述 │
│ graph │ 图结构(JSON) │
│ dsl │ 完整 DSL(JSON) │
│ version │ 版本号 │
│ created_at │ 创建时间 │
│ updated_at │ 更新时间 │
└─────────────────────────────────────────┘
八、DSL 的加载与还原
8.1 从存储到画布
数据库中的 DSL
↓
[反序列化]
↓
JSON 对象
↓
[解析转换]
↓
画布节点和边
↓
VueFlow 渲染
8.2 加载流程
// 伪代码示意
function loadAgent(agentId) {
// 1. 从数据库获取 DSL
const dsl = await fetchAgentDSL(agentId)
// 2. 解析图结构
const nodes = dsl.graph.nodes
const edges = dsl.graph.edges
// 3. 还原到画布
canvasData.nodes = nodes
canvasData.edges = edges
// 4. 重建组件映射
components.value = dsl.components
}
8.3 版本兼容
问题:DSL 格式升级后,旧数据如何加载?
解决方案:版本迁移
{
"version": "2.0",
"migration": {
"from": "1.0",
"rules": [
{ "field": "agentNode", "rename": "LLMNode" },
{ "field": "prompt", "move": "params.user_prompt" }
]
}
}
九、DSL 设计的挑战与解决
9.1 挑战一:循环引用
问题:节点 A 引用节点 B 的输出,节点 B 又引用节点 A,形成死循环。
┌──────────────┐
│ │
▼ │
节点 A ──► 节点 B ──┘
解决方案:
┌─────────────────────────────────────────┐
│ 循环检测机制 │
├─────────────────────────────────────────┤
│ 1. 构建有向图 │
│ 2. 使用 DFS/BFS 检测环 │
│ 3. 发现环时阻止连接 │
│ 4. 给用户明确提示 │
└─────────────────────────────────────────┘
用户提示:
⚠️ 无法连接:这会产生循环依赖。节点"AI 处理"的输出已经被"数据处理"使用,不能再连接回"AI 处理"。
9.2 挑战二:复杂条件表达式
问题:条件分支的判断逻辑可能很复杂。
如果 (用户等级 > 5 AND 余额 > 1000) OR (是 VIP)
则 → VIP 服务
否则
则 → 普通服务
解决方案:结构化的条件表示
{
"conditions": [
{
"name": "VIP 条件",
"items": [
{ "variable": "user_level", "operator": ">", "value": "5" },
{ "variable": "balance", "operator": ">", "value": "1000" }
],
"logic": "AND",
"goto": "vip_service"
},
{
"name": "VIP 会员",
"items": [
{ "variable": "is_vip", "operator": "==", "value": "true" }
],
"logic": "OR",
"goto": "vip_service"
}
],
"default_goto": "normal_service"
}
9.3 挑战三:嵌套结构
问题:迭代/循环节点内部还有子流程。
┌─────────────────────────────┐
│ 迭代节点 │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │子节点 1│ │子节点 2│ │子节点 3│ │
│ └─────┘ └─────┘ └─────┘ │
└─────────────────────────────┘
解决方案:父子节点关系
{
"nodes": [
{
"id": "iteration_001",
"type": "iterationNode",
"isContainer": true
},
{
"id": "child_001",
"type": "agentNode",
"parentId": "iteration_001",
"position": { "x": 50, "y": 100 }
}
]
}
十、总结
10.1 DSL 设计要点回顾
┌────────────────────────────────────────────────────┐
│ DSL 设计七大原则 │
├────────────────────────────────────────────────────┤
│ │
│ 1️⃣ 清晰性 → 结构清晰,易于理解 │
│ 2️⃣ 完整性 → 能描述所有智能体元素 │
│ 3️⃣ 一致性 → 命名和格式统一 │
│ 4️⃣ 可扩展性 → 支持新增节点类型 │
│ 5️⃣ 可序列化 → 方便存储和传输 │
│ 6️⃣ 可验证性 → 能校验合法性 │
│ 7️⃣ 可迁移性 → 支持版本升级 │
│ │
└────────────────────────────────────────────────────┘
10.2 DSL 的价值
- ✅ 标准化:统一的智能体描述语言
- ✅ 可移植:支持导入导出和分享
- ✅ 可执行:后端引擎可直接运行
- ✅ 可版本化:支持版本管理和回滚
系列文章导航:
- ✅ 架构篇 - 整体设计和技术选型
- ✅ DSL 设计篇 (本文)- 数据结构与序列化
- 📝 画布实现篇 - VueFlow 集成与交互
- 📝 节点系统篇 - 18 种节点的实现细节
- 📝 表单系统篇 - 动态表单与变量引用
- 📝 状态管理篇 - Pinia Store 设计
- 📝 高级特性篇 - 迭代/循环/嵌套
- 📝 实战篇 - 从零构建一个完整智能体
作者注:本文基于 agent-flow 项目的实际代码分析编写,力求还原真实的架构设计过程。欢迎在评论区讨论或提问!
下一篇:从零开始设计一个智能体编排系统 - 画布实现篇(敬请期待)
更多推荐


所有评论(0)