导读:如果说智能体编排系统是一栋大楼,那么 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 的价值

  • 标准化:统一的智能体描述语言
  • 可移植:支持导入导出和分享
  • 可执行:后端引擎可直接运行
  • 可版本化:支持版本管理和回滚

系列文章导航

  1. 架构篇 - 整体设计和技术选型
  2. DSL 设计篇 (本文)- 数据结构与序列化
  3. 📝 画布实现篇 - VueFlow 集成与交互
  4. 📝 节点系统篇 - 18 种节点的实现细节
  5. 📝 表单系统篇 - 动态表单与变量引用
  6. 📝 状态管理篇 - Pinia Store 设计
  7. 📝 高级特性篇 - 迭代/循环/嵌套
  8. 📝 实战篇 - 从零构建一个完整智能体

作者注:本文基于 agent-flow 项目的实际代码分析编写,力求还原真实的架构设计过程。欢迎在评论区讨论或提问!

下一篇从零开始设计一个智能体编排系统 - 画布实现篇(敬请期待)

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐