Agent扩展开发指南

概述

小蜜陪护机器人采用多 Agent 协作架构,通过任务规划专家将用户意图路由到专业 Agent 执行。本文详细介绍如何通过增加工作 Agent 和相应的工具来扩展系统功能,无需修改核心 C++ 代码,仅需配置 JSON 文件和编写 JavaScript 脚本。

与插件开发指南的区别

  • 插件开发指南:侧重 C++ DLL 插件开发,封装底层功能
  • Agent 扩展指南:侧重 JSON/JS 配置层面,将已有功能暴露给 Agent 使用

一、Agent 架构简述

1.1 整体架构

系统采用分层协作架构,数据流向如下:

用户输入(语音/文字)
    │
    ▼
TaskPlanner(任务规划专家)—— 解析意图,生成执行计划
    │
    └──→ WorkerAgent(专业 Agent)—— 执行具体任务
            │
            └──→ ToolManager(工具管理器)—— 加载和执行工具脚本
                    │
                    └──→ ScriptManager(脚本管理器)—— 运行 JavaScript
                            │
                            └──→ Plugin(插件)—— 底层功能实现

1.2 核心组件职责

组件 职责 文件位置
TaskPlanner 任务规划专家,负责意图解析和路由 bin/agentconfigs/agents/任务规划专家.agent
WorkerAgent 专业 Agent,执行具体任务 bin/agentconfigs/agents/*.agent
ToolManager 工具管理器,加载和执行工具 bin/agentconfigs/tools/*.tool
ScriptManager JavaScript 引擎,运行工具脚本 src/scriptmanager.cpp
Plugin C++ DLL 插件,封装底层功能 bin/agentconfigs/Plugins/*.dll

1.3 完整调用流程

用户说:"明天天气怎么样?"
    │
    ▼
1. ASR 语音识别 → "明天天气怎么样?"
    │
    ▼
2. TaskPlanner(任务规划专家)
   └── 解析意图:天气相关 → 路由到「天气专家」
    │
    ▼
3. WorkerAgent(天气专家)
   └── 根据指令,决定使用「天气查询」工具
    │
    ▼
4. ToolManager
   └── 加载并执行「天气查询.tool」的 JavaScript 脚本
    │
    ▼
5. ScriptManager
   └── 运行脚本,调用全局对象 weatherController.getTomorrowWeather()
    │
    ▼
6. Plugin(WeatherPlugin)
   └── 返回明天的天气数据
    │
    ▼
7. 结果返回
   └── Tool → Agent → TaskPlanner → TTS → 用户

二、扩展系统功能的步骤

2.1 扩展流程概览

要为系统增加新功能,需要完成以下三个核心步骤:

步骤1: 创建专业 Agent 配置文件
         │
         ▼
步骤2: 创建工具定义文件(包含 JS 脚本)
         │
         ▼
步骤3: 在任务规划专家中注册新 Agent

2.2 步骤详解

步骤1:创建专业 Agent 配置文件

在 bin/agentconfigs/agents/ 目录下创建新文件,命名为 {Agent名称}.agent,格式如下:

{
  "name": "Agent名称",
  "instructions": "Agent的角色定义和行为指令(System Prompt)",
  "tools": ["工具1", "工具2"]
}

字段说明:

字段 类型 说明
name string Agent 名称,用于路由和识别
instructions string System Prompt,定义 Agent 的角色、决策规则、输出格式等
tools array Agent 可以使用的工具列表
步骤2:创建工具定义文件

在 bin/agentconfigs/tools/ 目录下创建新文件,命名为 {工具名称}.tool,格式如下:

{
  "name": "工具名称",
  "description": "工具功能描述(供 LLM 理解何时调用)",
  "params": [
    {
      "type": "string/number",
      "name": "参数名",
      "description": "参数说明",
      "required": true
    }
  ],
  "auto_verified": true,
  "script_content": "JavaScript 脚本内容"
}

字段说明:

字段 类型 说明
name string 工具名称(中文,Agent 可见)
description string 工具功能描述,帮助 LLM 判断何时调用
params array 参数列表,定义输入参数
auto_verified boolean 是否自动验证(true = 工具结果无需模型再次验证)
script_content string JavaScript 脚本,实现工具逻辑
步骤3:在任务规划专家中注册

修改 bin/agentconfigs/agents/任务规划专家.agent,需要更新两处:

  1. 路由表:在路由规则中添加新意图到新 Agent 的映射
  2. agent 数组:将新 Agent 名称添加到 agent 数组中

三、工具调用插件的机制

3.1 脚本引擎与全局对象

ScriptManager 维护一个单例 JavaScript 引擎,插件加载时会将其 QObject 实例注册为全局对象:

// PluginManager 注册流程
if (plugin->scriptObject()) {
    QString objName = plugin->scriptObjectName();
    m_scriptManager->registerGlobalObject(objName, plugin->scriptObject());
}

3.2 已注册的全局对象

全局对象名 插件 功能
weatherController WeatherPlugin 天气查询
taskScheduler TaskSchedulerPlugin 定时任务
musicPlayer MusicPlayerPlugin 音乐播放
radioTvPlayer RadioTvPlayerPlugin 广播/电视播放
volumeController VolumeControlPlugin 音量控制
networkInfo NetworkInfoPlugin 网络信息查询

3.3 工具脚本调用插件的方式

工具脚本中直接调用全局对象的 Q_INVOKABLE 方法:

// 天气查询工具脚本示例
function 天气查询() {
    var obj = JSON.parse(params);
    var queryDate = obj.查询日期 || 'today';
    
    // 调用插件暴露的全局对象
    var result = weatherController.getTodayWeather();
    
    // 格式化结果
    result = JSON.parse(JSON.stringify(result));
    return JSON.stringify({
        success: true,
        result: result
    });
}
天气查询();

3.4 工具脚本编写要点

  1. 参数解析:从 params 全局变量获取 JSON 格式的参数
  2. 全局对象调用:直接使用插件注册的全局对象
  3. 返回格式:必须返回 JSON 字符串,包含 success 字段
  4. 异常处理:捕获并返回错误信息
  5. 日志输出:使用 printlog(level, message) 输出调试信息

四、完整示例:添加「笑话专家」Agent

4.1 功能需求

创建一个「笑话专家」Agent,能够:

  • 讲一个随机笑话
  • 根据主题讲笑话(如动物、职场、校园等)

4.2 步骤1:创建笑话专家 Agent 配置

创建文件 bin/agentconfigs/agents/笑话专家.agent

{
  "name": "笑话专家",
  "instructions": "你是一个幽默风趣的笑话专家,负责给用户讲笑话。\n\n## 决策规则\n1. 用户说「讲个笑话」或类似请求 → 调用讲笑话工具\n2. 用户指定主题 → 调用工具并传入主题参数\n3. 用户要求讲多个笑话 → 多次调用工具\n4. 用户只是闲聊 → 直接回应,不调用工具\n\n## 输出格式\n工具调用:{\"status\":\"tool_call\",\"tool_name\":\"讲笑话\",\"arguments\":{\"主题\":\"主题名称\"}}\n最终答案:{\"status\":\"done\",\"result\":\"笑话内容\"}\n\n## 回复风格\n- 笑话讲完后,可以加一句俏皮话或表情符号\n- 如果用户不笑,可以换一个继续讲\n- 保持轻松幽默的语气",
  "tools": [
    "讲笑话"
  ]
}

4.3 步骤2:创建讲笑话工具定义

创建文件 bin/agentconfigs/tools/讲笑话.tool

{
  "name": "讲笑话",
  "description": "讲一个笑话,可以指定主题:动物、职场、校园、夫妻、冷笑话",
  "params": [
    {
      "type": "string",
      "name": "主题",
      "description": "笑话主题:动物、职场、校园、夫妻、冷笑话,不指定则随机",
      "required": false
    }
  ],
  "auto_verified": true,
  "script_content": "function 讲笑话() {\n    var obj;\n    try { obj = JSON.parse(params); } catch(e) { return JSON.stringify({success: false, result: '参数格式错误'}); }\n    var topic = obj.主题 || '';\n    \n    var jokes = {\n        '动物': [\n            '为什么企鹅只有肚子是白的?因为手太短,洗澡只能洗到肚子!',\n            '大象和蚂蚁结婚,第二天大象死了。蚂蚁哭着说:这辈子再也不干这么累的活了!',\n            '乌龟和兔子赛跑,兔子中途睡着了。等它醒来,乌龟已经到终点了,兔子说:早知道我就不戴墨镜了!'\n        ],\n        '职场': [\n            '老板问员工:你觉得你值多少钱?员工说:我觉得我值年薪100万。老板:那我给你年薪50万,你干两份活。',\n            '程序员的老婆让他去买酱油,他回来说:超市里没有酱油接口,我无法完成购买请求。',\n            'HR问面试者:你最大的缺点是什么?面试者:诚实。HR:我不觉得这是缺点。面试者:我不在乎你怎么想。'\n        ],\n        '校园': [\n            '老师:小明,你知道为什么闪电总是比雷声快吗?小明:因为眼睛长在耳朵前面!',\n            '学生问老师:为什么要学数学?老师:因为数学能帮你在菜市场不被坑。学生:可是我可以用计算器啊!',\n            '考试时,小明偷看同桌的答案。老师走过来问:你在看什么?小明:我在看他的答案是不是和我的一样。'\n        ],\n        '夫妻': [\n            '老婆:你知道我为什么嫁给你吗?老公:因为我长得帅?老婆:因为你老实。老公:那现在呢?老婆:因为你傻。',\n            '老公回家晚了,老婆问:你去哪了?老公:加班。老婆:我刚才给你们公司打电话,他们说你早就走了。老公:那是因为我加班到一半太累了,去隔壁公司休息了一下。',\n            '老婆:如果你中了五百万,你会怎么样?老公:我会分你一半。老婆:那如果你中了一千万呢?老公:那我就分你五百万。'\n        ],\n        '冷笑话': [\n            '为什么海象总是很开心?因为它有一颗海象的心!',\n            '什么动物最容易摔倒?狐狸,因为它太狡猾(脚滑)了!',\n            '为什么苹果手机不会感冒?因为它有iOS(爱奥西斯)!'\n        ],\n        'default': [\n            '一位程序员走进酒吧,要了一杯酒。服务员问:需要加冰吗?程序员说:不用了,我自带了。',\n            '医生问病人:你哪里不舒服?病人说:我睡不着觉。医生:为什么?病人:因为我是程序员,我的生物钟是夜猫子模式。',\n            '甲:你知道为什么程序员不喜欢过情人节吗?乙:为什么?甲:因为他们分不清0和1哪个是真爱。',\n            '老师让同学们用「如果」造句。小明:如果我有一百万,我就买个大房子。小红:如果我有一百万,我就买好多好吃的。小刚:如果这是个问题,我就回答它。'\n        ]\n    };\n    \n    var pool = jokes[topic] || jokes['default'];\n    var joke = pool[Math.floor(Math.random() * pool.length)];\n    \n    return JSON.stringify({\n        success: true,\n        topic: topic || '随机',\n        result: joke\n    });\n}\n讲笑话();"
}

4.4 步骤3:在任务规划专家中注册

修改 bin/agentconfigs/agents/任务规划专家.agent

更新路由表(在路由规则中添加):

"路由表": "数学/计算/算术 → 数学专家\n时间/日期/星期/几号 → 时间专家\n定时/提醒/闹钟/倒计时 → 定时任务专家\n网络/IP/子网掩码/网关 → 网络专家\n广播/电视/收音机/电台/电视台/频道 → 广播电视播放专家\n音乐/歌曲/唱歌/听歌/切歌 → 音乐播放专家\n音量/声音/静音/TTS音量 → 音量控制专家\n天气/下雨/刮风/温度/气温/湿度 → 天气专家\n笑话/幽默/搞笑/段子 → 笑话专家\n其他/聊天/问候/不确定 → 贴心聊天助手"

更新 agent 数组(添加「笑话专家」):

"agent": ["数学专家","贴心聊天助手","时间专家","定时任务专家","网络专家","广播电视播放专家","音乐播放专家","音量控制专家","天气专家","笑话专家"]

4.5 预期交互效果

用户:讲个笑话
    │
    ▼
任务规划专家 → 识别意图:笑话/幽默 → 路由到「笑话专家」
    │
    ▼
笑话专家 → 决定调用「讲笑话」工具
    │
    ▼
工具脚本 → 返回随机笑话:"为什么程序员不喜欢过情人节吗?因为他们分不清0和1哪个是真爱。"
    │
    ▼
笑话专家 → 整理回答:"好的,给你讲一个:为什么程序员不喜欢过情人节吗?因为他们分不清0和1哪个是真爱。😂"
    │
    ▼
TTS → 语音输出

五、扩展技巧与最佳实践

5.1 Agent 指令编写技巧

  1. 明确角色定位:让 Agent 清楚自己的职责范围
  2. 定义决策规则:告诉 Agent 何时调用工具、何时直接回答
  3. 指定输出格式:必须包含 status 字段(tool_call 或 done
  4. 提供示例:帮助 LLM 理解期望的输出格式

5.2 工具脚本编写技巧

  1. 参数校验:检查参数是否完整、格式是否正确
  2. 错误处理:捕获异常并返回明确的错误信息
  3. 日志输出:使用 printlog() 记录关键步骤,便于调试
  4. 返回格式统一:始终返回包含 success 字段的 JSON
  5. 脚本独立:每个工具脚本应独立运行,不依赖其他脚本

5.3 调试方法

  1. 查看日志:检查 bin/logs/ 目录下的日志文件
  2. 脚本调试:在工具脚本中使用 printlog() 输出调试信息
  3. 测试工具:通过修改 Agent 指令,强制调用特定工具
  4. 验证注册:确认任务规划专家的路由表和 agent 数组已正确更新

5.4 常见问题

问题 原因 解决方案
Agent 未被调用 任务规划专家未注册该 Agent 检查 agent 数组和路由表
工具未被调用 Agent 指令未正确定义调用规则 检查 instructions 中的决策规则
脚本执行失败 JavaScript 语法错误 检查 script_content 的语法
参数解析失败 参数格式不正确 确保传入的参数是合法 JSON
全局对象未定义 插件未正确加载 检查插件 DLL 是否在正确目录

六、无插件场景:纯脚本工具

并非所有工具都需要插件支持。如果功能可以完全通过 JavaScript 实现(如计算、数据处理、本地文件操作等),可以直接编写纯脚本工具,无需开发 C++ 插件。

6.1 纯脚本工具示例

计算器工具(无需插件,纯 JavaScript 实现):

{
  "name": "计算器",
  "description": "执行基本数学运算:加法、减法、乘法、除法",
  "params": [
    {
      "type": "string",
      "name": "操作",
      "description": "要执行的数学操作,可选值:加、减、乘、除",
      "required": true
    },
    {
      "type": "number",
      "name": "a",
      "description": "第一个操作数",
      "required": true
    },
    {
      "type": "number",
      "name": "b",
      "description": "第二个操作数",
      "required": true
    }
  ],
  "auto_verified": true,
  "script_content": "function 计算器() { var obj = JSON.parse(params); var a = parseFloat(obj.a); var b = parseFloat(obj.b); var result; switch(obj.操作) { case '加': result = a + b; break; case '减': result = a - b; break; case '乘': result = a * b; break; case '除': if(b !== 0) { result = a / b; } else { return JSON.stringify({success: false, result: '除数不能为零'}); } break; default: return JSON.stringify({success: false, result: '不支持的操作'}); } return JSON.stringify({success: true, result: String(result)}); } 计算器();"
}

6.2 何时需要开发插件

场景 是否需要插件 说明
网络请求 需要 JavaScript 无法直接发起 HTTP 请求
系统调用 需要 JavaScript 无法直接调用系统 API
文件操作 需要 JavaScript 文件操作能力有限
数据计算 不需要 纯 JavaScript 即可实现
数据处理 不需要 纯 JavaScript 即可实现
逻辑判断 不需要 纯 JavaScript 即可实现

总结

通过 Agent + Tool 的配置方式,无需修改核心 C++ 代码即可扩展系统功能:

  1. 创建 Agent 配置:定义专业 Agent 的角色和行为
  2. 创建工具脚本:实现具体功能,可调用插件或纯 JS 实现
  3. 注册到规划专家:更新路由表和 agent 数组

这种设计使得系统具备高度的可扩展性和灵活性,非程序员也能通过修改 JSON 和 JS 文件定制系统行为。


相关文档

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐