Agentcade

主 Agent 负责语音聊天和游戏选择,通过 A2A 与独立游戏 Agent 通信。目前提供“你画我猜”和“猜数字”。src/games/ 下的每个游戏目录启动时自动接入,新增游戏只需要新增一个目录。

说“有哪些游戏”查看目录,说“开始猜数字”或“玩第二个”选择游戏。游戏期间可以聊天,也可以直接继续回答;说“继续游戏”查询当前进度。等待用户输入期间保持游戏状态。

同时只能运行一个游戏。说“结束游戏”退出,结束播报和设备操作完成后才能开始其他游戏。直接要求换游戏时,机器人会提示先退出当前游戏。

安装后第一次使用

从应用广场安装并启动后,浏览器会自动打开设置页面(http://127.0.0.1:8790/settings)。应用用的是你自己的服务账号,需要填三样密钥,各栏分别填写:

密钥 用途 填在哪一栏
阿里云百炼(DashScope)的 API Key 语音识别和语音合成 主 Agent
大模型的 API Key(OpenAI 兼容接口,默认 DeepSeek) 聊天、选择游戏、主持游戏 主 Agent 和每个游戏
Typesafe 的 API Key 你画我猜里判断答得对不对 你画我猜

保存后应用自动继续启动,机器人说出开场白就可以对它说话了。密钥只保存在这台电脑上,这些服务按各自的账号计费。打开 http://127.0.0.1:8790 可以看到对话内容和它用了哪些工具。

已知问题:机器人固件每开关一次麦克风会少一点内存且不归还,连续玩一个多小时后机器人会听不到声音或者屏幕不再变化,给机器人断电重启就能恢复。目前只在 macOS 上验证过。

你画我猜

对机器人说“我们来玩你画我猜”开始。

机器人说话时不收音,播报完成后恢复收音。

猜数字

机器人随机选择 1 到 20 的整数,玩家有五次回答机会。回答后会告知数字过大或过小;说“提示”可以获得奇偶信息。答对或机会用完后结束,也可以随时说“结束游戏”。

运行

需要 Python 3.11+、uv,以及一台和电脑在同一局域网的机器人,固件支持 expression.runtime.v3、expression.vector_accessory.v1、microphone、audio.stream、motion。

cp src/agentcade/.env.example src/agentcade/.env
cp src/games/guess_animal/.env.example src/games/guess_animal/.env
cp src/games/guess_number/.env.example src/games/guess_number/.env
# 分别填写三个文件中的配置
uv sync
uv run watcherobot robot setup    # 仅首次连接设备
uv run watcherobot app run .

按 Ctrl+C 或执行 uv run watcherobot app stop 停止。

配置

主 Agent 和每个游戏 Agent 各有一份配置,互相不共用:相同的变量名在不同 Agent 里互不影响,运行时也不会读取别的 Agent 的配置。读取时不修改进程环境,不读取根目录 .env,也不使用同名进程环境变量覆盖。配置值按字面读取,不展开 ${...}。

配置项的声明

每个 Agent 读取哪些环境变量,写在它自己目录的 settings.yaml 里,主 Agent 和游戏 Agent 用同一种格式:

options:
  - key: TYPESAFE_API_KEY
    label: Typesafe 的 API Key,用于判断用户答得对不对
    secret: true
  - key: JEV_MODEL
    label: 判题模型
    default: "jev-latest"
    advanced: true
字段 含义
key 环境变量名,大写字母、数字和下划线
label 设置页面上这一项显示的说明
default 默认值,写成带引号的字符串;不写表示必填
secret true 表示密钥,页面上不回显
advanced true 表示一般不用改,页面上收在“更多设置”里

这份文件是唯一的来源:设置页面的表单、读取时补默认值和检查必填项都按它来。文件里不能有重名的项和上表以外的字段,写错时应用启动会报错。

代码里只需要一个配置类说明自己在哪个目录、怎样把读到的值转换成配置对象(src/agentcade/config.py 的 AgentSettings):

@dataclass(frozen=True)
class Settings(AgentSettings):
    DIRECTORY: ClassVar[Path] = Path(__file__).parent

    llm: LLMSettings

    @classmethod
    def from_values(cls, values: dict[str, str]) -> "Settings":
        return cls(llm=LLMSettings.from_values(values))

各目录里的 .env.example 由 settings.yaml 生成,改了之后运行 uv run python scripts/write_env_examples.py。

配置保存在哪里

配置从两个地方之一读取:

设置页面

应用运行时打开 http://127.0.0.1:8790/settings,或在对话页面右上角点“设置”。配置还没有填好时,应用启动后会自动打开这个页面并等待,保存后自动继续启动,不用重新打开应用。

配置文件 用途
src/agentcade/.env 主 Agent 的文字 LLM、工具调用、ASR、TTS、音色和游戏目录路径
src/games/guess_animal/.env 你画我猜的游戏 Agent 与绘图模型、Jev、置信度和预计绘图时间
src/games/guess_number/.env 猜数字的游戏 Agent 模型
变量 用途
DASHSCOPE_API_KEY 主 Agent 的 Qwen ASR/TTS 接口凭据
QWEN_ASR_MODEL 默认 qwen-audio-3.1-asr-flash-streaming,持续识别用户语音
QWEN_TTS_MODEL 默认 qwen-audio-3.1-tts-flash,合成聊天和游戏播报
QWEN_TTS_VOICE TTS 音色,默认 yuxiaoyun_v3.1
SPEECH_TIMEOUT_SECONDS 单次语音合成最长等待秒数,默认 90
ASR_SILENCE_MS ASR 判定说话结束的静音时间,默认 800 毫秒
ASR_INPUT_GAIN 麦克风 PCM 输入增益,默认 4,范围 1 至 8;通过 FFmpeg volume 处理,保持采样率和时间信息
TYPESAFE_API_KEY 你画我猜的 JEV 答案判定凭据
JEV_MODEL / JEV_CONFIDENCE_THRESHOLD 你画我猜的 JEV 模型与判定置信度,默认 jev-latest / 0.65
LLM_API_KEY 当前配置文件所属 Agent 的 OpenAI 兼容接口凭据
GAME_CATALOG 远程游戏的注册文件路径,默认项目中的 games.json;相对路径以应用目录为基准
OBSERVABILITY_PORT 可观测性页面的本机端口,默认 8790;不能使用 SDK 守护进程占用的 8765 和 8767

DASHSCOPE_BASE_URL 默认是百炼的公共地址;使用专属业务空间时改成对应的地址,换 key 时记得一起改。

文字模型

主 Agent 和游戏 Agent 各自在自己的配置里设置 LLM_MODEL_NAME、LLM_BASE_URL、LLM_API_KEY 和请求参数。全部 LLM 调用通过 OpenAI SDK 访问兼容 Chat Completions 的接口,使用原生 function calling 执行注册工具。主 Agent 的普通聊天回复直接交给 TTS。

OpenAI 兼容接口在对应 Agent 的配置文件中填写,例如:

LLM_BASE_URL=https://api.deepseek.com
LLM_API_KEY=填写你的密钥
LLM_MODEL_NAME=deepseek-flash
LLM_EXTRA_BODY_JSON={"thinking":{"type":"enabled"},"reasoning_effort":"low"}

更换服务时,将 LLM_BASE_URL、LLM_API_KEY、LLM_MODEL_NAME 改成目标服务的配置。这三项必须填写,接口需要支持 JSON 对象输出和 function calling。LLM_EXTRA_BODY_JSON 默认是 {},用于填写所选服务支持的可选参数,如思考模式、输出长度或服务等级。它不能覆盖模型、消息、响应格式、流式模式或工具参数。LLM_TIMEOUT_SECONDS 控制单次调用超时,默认 180 秒。

修改配置后重新启动应用。本地游戏的 A2A 等待时间为该游戏模型超时的三倍加 30 秒。远程游戏在 games.json 中单独配置 timeout_seconds,默认 210 秒。

题目生成失败或超时会自动重试,最多三次,仍然失败时机器人会请用户再试一次。每次生成的 JSON 和去重记录存在 .artifacts/,重启后仍然有效。

你画我猜的生成提示词要求模型优先选择常见动物,并使用常用、容易读写的名称和别名,避免名称中含有不常见的生僻字。

对话与轨迹页面

应用运行期间打开 http://127.0.0.1:8790,端口由 OBSERVABILITY_PORT 设置,服务只监听本机。端口上已有其他服务时应用启动失败并提示修改端口。页面顶部有两个标签,显示的是同一批实时事件:

记录 内容
用户 ASR 的最终识别文字,每条开启一轮
主 Agent · 回复 主 Agent 直接回复用户的聊天文字
主 Agent · 工具、游戏 Agent · 工具 各自发起的工具调用,包含完整参数、返回或错误、耗时
A2A →、A2A ← 主 Agent 发给游戏的 GameInput,以及游戏返回的每一条 GameReply:中间回复和最后一条回复
设备执行 实际播报、绘图、表情、动作与完成状态,关联发起操作的游戏工具

设备记录带有图案时,检查器显示对应笔画。A2A 回复展示面向用户的文字和本局是否结束,可与主 Agent 的播报原文核对。检查器底部列出关联的上级和下级记录,点击即可跳转。一次 A2A 请求及其游戏工具、设备执行均归入发起请求的那一轮。程序退出时自动发送的 end 显示在“轮次之间”。远程游戏的设备 trace 由游戏部署方记录。

应用事件在内存中保留最近 2000 条。页面最多显示最近 3000 条记录,断线后保留已收到的记录并自动重连。事件包含对话内容,不包含密钥。

每一轮的标题显示从用户说完到最后一条记录的耗时,时间线一列画出各条记录在这一轮中的起止位置:工具调用和 A2A 请求是时间段,其余记录是时间点。轨迹标签上方可以搜索记录、收起或展开全部轮次;收起后每轮显示用户原话和记录条数。上下方向键切换选中的记录。

检查

uv run ruff check app.py src scripts
uv run watcherobot app check .

真机端到端测试:

uv run python scripts/e2e_robot.py

脚本启动应用,用电脑扬声器替用户说话,机器人收音并回应,再根据可观测性事件核对每一步:查询游戏、猜数字的开局、猜数、提示和结束、游戏中的闲聊,以及你画我猜的开局(先回复稍等、显示进度、画出题目)、提示、作答和结束。运行前连接机器人并放在电脑扬声器附近,保持环境安静;全程大约五分钟,会调用真实的语音和模型服务。合成的用户语音、应用日志、轨迹和报告保存在 .artifacts/e2e/<时间>/。

除此之外没有其他自动化测试。uv run watcherobot app run scripts/eye_preview 可以在真机上逐个预览眼型,不调用模型。

发布

uv run publish            # 登录(没登录时)并发布源码,发布前会停下来确认
uv run publish --submit   # 发布之后提交到官方名单审核

默认发布到 Hugging Face,加 --provider gitee 发布到 Gitee。第一次运行会显示一个网址和验证码,在浏览器里完成授权后继续。源码发布到自己名下的公开仓库 WatcherRobot-<应用 ID>;提交审核后由官方人工合并,合并后应用才会出现在应用广场里。.env、.watcherignore 里列出的文件和 scripts/ 不会发布。更新版本时先改 app.json 的 version。

实现

flowchart TD
    Mic[机器人麦克风 16 kHz PCM] --> ASR[Qwen ASR\n服务端判断说话结束]
    ASR -->|用户原话| Main[主 Agent:聊天与工具调用\n内部包含 A2A Client]
    Catalog[src/games/ 下的游戏目录和 Agent Card] --> Main
    Main -->|A2A JSON-RPC| Animal[你画我猜 Agent]
    Main -->|A2A JSON-RPC| Number[猜数字 Agent]
    Main -->|A2A JSON-RPC| Extension[新增游戏 Agent]
    Animal --> Drawing[绘图 LLM 与 Jev]
    Animal --> Result[一条或多条面向用户的回复]
    Number --> Result
    Extension --> Result
    Main --> Queue[聊天与游戏回复输出队列]
    Queue --> Output[公共设备执行模块]
    Animal --> Output
    Number --> Output
    Extension --> Output
    Result --> Main
    Output -->|聊天与游戏播报文本| Speech[Qwen TTS]
    Speech --> Audio[SDK PCM 音频播放]
    Output --> Device[屏幕、表情、头部动作]

主 Agent 使用持续的 ASR WebSocket 会话识别麦克风语音,由 ASR 服务判定说话结束。主 Agent 根据原话、历史和当前游戏,直接回答,或者调用 list_games、start_game、end_game、send_message。每轮模型调用都会将当前已注册游戏的名称、ID、别名和介绍按目录顺序写入系统提示词,新增游戏目录后自动使用。普通聊天由主 Agent 直接回复,游戏介绍和播报使用简短的日常口语。

主 Agent 通过 A2A 给游戏 Agent 发一条消息,游戏 Agent 把它当作用户输入,回复一条或多条要播报的话,主 Agent 原样交给 TTS。第一条回复播完后恢复收音,其余回复到达时继续播报。游戏还在处理上一条输入时,新的游戏输入不会转发,主 Agent 会请用户稍等。消息格式见 通信设计。

游戏 Agent 是由 SKILL.md 驱动的 ReAct Agent。SKILL.md 写明规则和整体流程,模型按流程连续调用游戏自己的工具:规则工具返回题目、提示、答案判定和计分等事实,展示画面的工具直接操作屏幕。答对答错时的表情和动作不经过模型,由规则工具在判定时直接触发。耗时操作之前,模型用 say 先说一句话。聊天期间游戏状态保持不变,等待用户输入期间没有计时提醒、机会扣除或自动结束。

本地游戏通过官方 a2a-sdk 启动仅监听 127.0.0.1 的 HTTP 服务,并通过 Agent Card 发现。context_id 标识一局游戏,同一局的请求逐条处理;end 会取消正在处理的请求。主 Agent 收到本局结束的回复后清除当前会话。

文件 职责
app.py SDK 入口
src/agentcade/device.py 设备启动确认、音频发送顺序与设备状态记录
src/agentcade/application.py 收音、主 Agent 与输出队列的运行周期
src/agentcade/speech_input.py 持续 ASR 输入、语音增益和识别结果路由
src/agentcade/tools.py 工具注册、JSON Schema、参数校验和执行记录
src/agentcade/protocol.py 主 Agent 与游戏 Agent 的统一通信格式、游戏声明和设备输出 schema
src/agentcade/main_agent.py 主 Agent 决策、聊天历史、单游戏限制和游戏回复的播报
src/agentcade/game_client.py 主 Agent 一侧:发现游戏目录、Agent Card 校验和 A2A 回复流
src/agentcade/game_server.py 游戏一侧:本地 A2A 服务、会话与请求的顺序和取消
src/agentcade/task_store.py 已完成 A2A Task 的保留数量
src/agentcade/skill.py SKILL.md 的读取与校验
src/agentcade/skill_agent.py 所有游戏共用的 ReAct 运行时、say 和公共设备工具
src/agentcade/output.py 统一播报、屏幕、表情、动作与麦克风管理
src/games/<game_id>/SKILL.md 游戏声明、规则和流程
src/games/<game_id>/agent.py 游戏入口 GameAgent:状态和工具
src/games/<game_id>/config.py 游戏独立配置读取与检查
games.json 远程游戏的注册文件,本地游戏不需要登记
src/games/guess_animal/game.py 出题、计分、提示
src/games/guess_animal/generator.py 提示词、输出校验、去重历史
src/games/guess_animal/animals.py 动物数据结构
src/games/guess_animal/judge.py Jev 动物答案判定
src/agentcade/providers.py OpenAI 兼容接口的结构化输出和工具调用
src/agentcade/vector.py 公共笔画结构和矢量编码
src/agentcade/expressions.py 表情 SVG 转换和进度条
src/agentcade/eyes.py 待机眼睛和眼型预览参数
src/agentcade/speech.py ASR/TTS WebSocket 协议、语音合成和识别时间关联
src/agentcade/audio_stream.py SDK PCM 播放、完成确认和取消处理
src/agentcade/microphone.py 麦克风收音和播报时静音
src/agentcade/config.py 读取各 Agent 的 settings.yaml 和配置值(AgentSettings)、主 Agent 的配置和检查
src/agentcade/settings.yaml、src/games/<game_id>/settings.yaml 各 Agent 读取的配置项声明
src/agentcade/settings_page.py 设置页面的后端:按各 Agent 的 settings.yaml 生成表单、保存配置、启动时等配置填好
src/agentcade/assets/emojis/ 公共表情 SVG
src/observability/events.py 事件总线、轮次与上下级关联、工具调用和 A2A 通信的记录入口
src/observability/server.py 本机 HTTP 服务与 SSE 事件流,支持按事件 ID 续传,并推送“正在听到的话”这类只看当前值的状态
src/observability/web/settings.html 设置页面:填写各 Agent 的服务配置
src/observability/web/index.html 对话与轨迹页面:聊天样式的对话视图,按轮次分组的记录表与检查器
scripts/ 真机端到端测试、眼型预览工具、生成 .env.example 的脚本

添加游戏

每个游戏位于 src/games/<game_id>/,目录包含 __init__.py、SKILL.md、入口文件 agent.py、声明配置项的 settings.yaml 和读取它的 config.py。游戏专用模块放在对应游戏目录内;公共语音、模型和显示能力位于 src/agentcade/。

SKILL.md 的 frontmatter 声明游戏,正文写规则和流程,启动时整体写入游戏 Agent 的系统提示词:

---
id: guess_number
name: 猜数字
aliases: [数字游戏, 猜数]
description: 我会选一个1到20的整数,你来猜,最多有五次机会。
required_capabilities: [motion]
---

## 规则

……

## 流程

### 开局(event 为 start)

调用 start_game,然后告诉用户数字范围和机会数,请他开始猜。

agent.py 的入口类是 GameAgent,继承 agentcade.skill_agent.SkillAgent:

工具返回可以转成 JSON 的事实,例如判定结果、提示和剩余次数,由模型组织成回复。无法执行的操作返回带 outcome 的结果,模型据此向用户说明。

目录放好后重新启动应用就会接入,不需要登记,也不需要改主 Agent。启动时 src/games/ 下每个同时带有 SKILL.md 和 agent.py 的子目录都会被当作一个游戏,目录名要和 SKILL.md 里的 id 一致,游戏按目录名排序。

src/games/guess_number/ 是完整示例。增加使用现有语音、屏幕和动作能力的游戏,只需要新增游戏目录,主 Agent 的提示词、选择逻辑、开场白和输出处理自动使用游戏声明。游戏用到新的 Python 依赖时,还要加进 pyproject.toml 和 app.json。

独立部署的游戏在 games.json 里登记,例如 [{"kind":"remote","id":"my_game","url":"https://example.com"}]。服务提供 A2A Agent Card、流式 JSON-RPC 接口,以及标记 desktop-game/5 的 skill,并按 通信设计 收发消息。部署方通过 serve_game(factory, settings, device) 注入真实设备执行函数。设备能力通过 capability:motion 等 tag 声明,认证由部署环境提供。

License

MIT