esp_llm_tongyi_agent_app

Example of the component espressif/adf_examples v0.2.8
# ESP LLM 通义智能体

- [English](./README.md)
- 例程难度:⭐⭐⭐

## 例程简介

> **定位说明**:本例程是基于 `esp_llm_tongyi` 的 **DashScope 多模态 WebSocket 智能体**示例。**Dialog** 模式为双工语音;**Vision** 模式在板子具备相机与 LCD 时提供预览,并通过 `snap` 发送单张 JPEG(非连续视频上行)。板级缺少相机或 LCD 时,视觉路径自动跳过,语音仍可用。

- 通过 WebSocket 连接 DashScope 应用,完成双工语音对话。
- Vision:LCD 实时预览 + 控制台 `snap` / `snap <prompt>` 就图提问;`check_name` 探测不到相机或 LCD 时不建视觉路径,语音仍可用。
- 使用前配置 API Key、Workspace ID、Application ID(Vision 需 `mm_` 类应用)。

官方文档:[DashScope 多模态 WebSocket](https://help.aliyun.com/zh/model-studio/websocket-for-multimodal-dialogue)。

### 功能

下面每一项能力都在 `idf.py menuconfig` 中开启:

| 能力 | 作用 | 配置项 | 默认 |
| --- | --- | --- | --- |
| 语音对话 | 与 DashScope 应用进行双工语音交流 | 两种应用模式下都可用 | 开 |
| 相机预览 | 在 LCD 上显示实时相机画面 | `Application mode` 选为 vision | 仓库预置配置下开 |
| 就照片提问 | `snap <prompt>` 抓拍一张 JPEG,并就这张图向模型提问 | `Application mode` 选为 vision | vision 下开 |
| 静默更新图像 | 不带参数的 `snap` 只刷新会话图像,不请求回复 | `Default snapshot prompt` 留空 | vision 下开 |
| 唤醒词 | 唤醒词之前保持麦克风关闭,唤醒后取消当前回复 | `Dialog mode` 选为先唤醒词再对话 | 关 |
| 回声消除与噪声抑制 | 上行 AEC、NS 默认开启 | 编译固定开启 | 开 |
| 语音活动检测 | 判断语音起止 | `Enable voice activity detection` | 开 |

### 典型场景

- 由 DashScope 语音应用驱动的双工语音助手
- 带屏设备,显示实时相机预览,并就画面内容向模型提问
- 唤醒词门控的对话,WakeNet 触发前麦克风保持关闭

### 媒体能力

| 项目 | 本例程的支持情况 |
| --- | --- |
| 音频上行 | OPUS,16 kHz,单声道,24000 bit/s |
| 音频下行 | OPUS,16 kHz,单声道,32 kbit/s |
| 视频上行 | 没有连续视频轨道;每次 `snap` 命令只发送一张 JPEG |
| 视频下行 | 不支持 |
| 相机 | 仅 Vision 模式,MJPEG,分辨率由 `TONGYI_VISION_CAMERA_WIDTH/HEIGHT` 决定,默认 320x240,ESP32-S31 上为 640x480 |
| LCD 预览 | 仅 Vision 模式,全屏显示。按相机完整分辨率采集 RGB565 并缩放到屏幕,帧率取 `TONGYI_VISION_CAMERA_FPS`,默认 15 |
| 音频前端 | AEC、NS 固定开启;VAD 由 `TONGYI_AUDIO_VAD` 控制,默认开 |
| 唤醒词 | WakeNet,仅当 `Dialog mode` 选为 `Wake word then conversation` 时加入 |
| 上行门控 | 唤醒词对话模式下,唤醒词触发前音频上行保持关闭 |
| 播放器流 | stream 0 承载通义 TTS;stream 1 空闲,可用于 URL 或提示音 |

Vision 为 **单张 JPEG 快照**,不建立连续 `VIDEO_UP`。预览为 RGB565 写 LCD;性能与双路采集说明见 [FAQ](#faq)。

### 应用模式

`Application mode` 决定固件构建成什么形态:

| 模式 | Kconfig | 行为 |
| --- | --- | --- |
| Dialog | `TONGYI_APP_MODE_DIALOG` | 仅双工语音;不带相机和 LCD |
| Vision | `TONGYI_APP_MODE_VISION` | 增加相机采集、LCD 预览和 `snap` CLI 命令 |

Kconfig 的默认值是 Dialog,但仓库中的 `sdkconfig.defaults` 设置了 `CONFIG_TONGYI_APP_MODE_VISION=y`。因此新执行一次 `idf.py set-target` 得到的是 Vision 构建。要构建 Dialog 版本,请在 menuconfig 中修改 `Application mode`。

两种模式需要不同的 DashScope 应用。Dialog 需要纯语音应用 ID,而 Vision 预置的 `mm_` 应用是多模态应用,在从不发送相机帧的情况下会返回 `images must not be empty`。

### 文件结构

```
esp_llm_tongyi_agent/
├── main/
│   ├── app_main.c            平台初始化、Wi-Fi、SNTP 和应用生命周期
│   ├── app_agent.c           通义配置、AFE 掩码和事件处理
│   ├── app_agent.h           智能体创建 / 启动 / 销毁接口
│   ├── app_cli.c             UART CLI(`esp_cli_service`);Vision 额外注册 `snap`
│   ├── app_cli.h             CLI 启动 / 停止接口
│   ├── libc_compat.c         兼容旧版 IDF 的 stderr 补丁
│   ├── Kconfig.projbuild     应用 menuconfig 选项
│   └── idf_component.yml
├── components/
│   ├── tongyi_app_media/     板级设备、采集、播放器和 LCD 预览
│   └── gen_bmgr_codes/       由 esp_board_manager 生成的板级元数据
├── partitions.csv
├── sdkconfig.defaults
├── sdkconfig.defaults.esp32p4
├── sdkconfig.defaults.esp32s3
├── sdkconfig.defaults.esp32s31
├── README.md
└── README_CN.md
```

## 环境配置

### 硬件要求

- 提供 `audio_adc` 和 `audio_dac` 设备的开发板
- 接到板级音频输出的扬声器或耳机
- Vision 模式下开发板还需提供 `camera` 和 `display_lcd` 设备
- 带 PSRAM,ESP32-S3 和 ESP32-S31 需 16 MB Flash,ESP32-P4 需 8 MB Flash

| 芯片 | 开发板 | 默认相机 |
| --- | --- | --- |
| `esp32s3` | `esp32_s3_korvo_2_3` | OV3660 DVP JPEG 320x240 @ 25 fps |
| `esp32s31` | `esp32_s31_korvo_1` | SC101IOT DVP YUV422 YUYV 640x480 @ 15 fps |
| `esp32p4` | `esp32_p4_function_ev_board` | SC2336 MIPI CSI RAW8 1024x600 @ 30 fps |

固件使用自定义分区表,应用分区 `factory` 为 6 MB。

### 默认 IDF 分支

本例程支持 IDF release/v5.4 (>= v5.4.3) 与 release/v5.5 (>= v5.5.2) 分支。

### 软件要求

- DashScope API key、workspace ID 和应用 ID
- 能访问 DashScope 端点和 NTP 服务器的 Wi-Fi 网络

`Kconfig.projbuild` 预置了内部实验室 API key 和 workspace ID,因此例程无需修改即可编译。在该实验室账号之外使用本例程前,请把两者替换为自己的 DashScope 凭据。

## 编译和下载

### 编译准备

编译本例程前请先配置好 ESP-IDF。如果已经配置完成,直接进入工程目录即可;否则在 ESP-IDF 根目录下执行以下脚本。所用芯片的说明请参考 [ESP-IDF 编程指南](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/)。

```
./install.sh
. ./export.sh
```

- 进入例程目录:

```
cd adf_examples/ai_agent/esp_llm_tongyi_agent
```

- 先选芯片,再选开发板:

```bash
idf.py set-target esp32s3
idf.py bmgr -b esp32_s3_korvo_2_3
```

`idf.py set-target` 要在 `idf.py bmgr` 之前执行,因为它会同时加载 `sdkconfig.defaults` 和携带相机默认项的 `sdkconfig.defaults.<chip>` 文件。本例程使用 [ESP Board Manager](https://github.com/espressif/esp-board-manager):`idf.py bmgr -l` 列出当前可见的开发板,`idf.py bmgr -b <board_index|board_name>` 选择其中一个。

### 项目配置

```bash
idf.py menuconfig
```

在 `Tongyi LLM agent application` 下设置:

- `DashScope API key`、`Tongyi workspace ID` 和 `Tongyi application ID`:要接入的 DashScope 账号和应用
- `Application mode`:Dialog 为纯音频,Vision 为相机预览加 `snap` 命令
- `Camera JPEG width` / `height` / `Camera / LCD preview FPS`:仅 Vision 模式,尺寸必须匹配一个 esp-video JPEG 传感器 mode
- `Default snapshot prompt`:仅 Vision 模式,`snap` 不带参数时使用
- `Dialog mode`:直接对话,或先唤醒词再对话
- `Audio front-end features`:AEC、NS 编译固定开启;VAD 默认开启
- `Audio output volume`:0 到 100,默认 80
- `Wi-Fi`:SSID、密码和连接超时(毫秒)
- `Run time before clean shutdown`:0 表示一直运行;非零值会走完整的停止、断开和销毁流程

凭据会被编译进固件,请勿提交真实值。Wi-Fi 配置档由 `esp_wifi_service_request_connect()` 持久化到 NVS。

### 编译与烧录

- 编译例程

```
idf.py build
```

- 烧录并打开监视器(将 PORT 替换为实际串口):

```
idf.py -p PORT flash monitor
```

- 按 `Ctrl-]` 退出监视器

## 如何使用例程

### 功能和用法

1. 启动后应用先连接 Wi-Fi,然后等待 SNTP 同步完成
2. 随后日志会提示通义智能体就绪,表示设备可以开始对话
3. 直接对话模式下可随时说话,由服务端判断一次提问何时结束
4. 唤醒词对话模式下先说唤醒词。唤醒后麦克风打开,正在进行的回复被取消
5. 回复以 TTS 形式到达播放器 stream 0;朗读文本同时通过 `RESPONDING_CONTENT` 事件给出,对话状态跳变通过 `DIALOG_STATE` 给出
6. UART CLI 提示符是 `tongyi>`(`esp_cli_service`)。Dialog 提供 `help` / `sys_heap`。Vision 额外提供 `snap`

```text
snap
snap 镜头前面是什么
```

`snap` 不带参数时把缓存的 JPEG 作为 `UpdateInfo` 发送,只更新会话图像而不请求回复。`snap <prompt>` 发送 `RequestToRespond`,请求模型就这张图作答。设置了 `Default snapshot prompt` 后,不带参数的 `snap` 会使用它,因此同样会请求回复。

## 故障排除

### 应用在连接前就停止

查看日志定位失败的步骤。Wi-Fi SSID 为空会报 `Start network failed: Wi-Fi SSID is empty`,API key、workspace ID 或应用 ID 缺失则在创建会话之前就被拒绝。

### 缺少板级设备名

编译或启动时报未知设备句柄,说明没有为所选芯片生成板级元数据。用与目标芯片匹配的开发板重新执行 `idf.py bmgr -b <board>`。

### WebSocket 连接失败

确认网络能访问 DashScope 端点、API key 和 workspace ID 有效、时钟已同步。本例程把 `CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN` 提高到 16384,TLS 缓冲区需要 PSRAM。

### 模型返回 `images must not be empty`

在 Dialog 模式下用了多模态 `mm_` 应用 ID,而这种模式从不发送图像。请把 `Application mode` 切到 Vision,或者改用纯语音应用 ID。

### 出现 `V4L2_SRC: Fail to open device`

可能是所选芯片的相机默认项没有生效,也可能是板上实装的传感器与这些默认项描述的型号不一致。先用 `idf.py set-target` 重新生成 `sdkconfig` 以加载芯片文件。芯片文件只会选定一种传感器和一种 mode:ESP32-S3 为 DVP 接口的 OV3660、JPEG 320x240,ESP32-S31 为 DVP 接口的 SC101IOT、YUV422 YUYV 640x480,ESP32-P4 为 MIPI CSI 接口的 SC2336、RAW8 1024x600。如果板上是别的传感器,请在 menuconfig 中改为启用该传感器,并让 `TONGYI_VISION_CAMERA_WIDTH` 和 `TONGYI_VISION_CAMERA_HEIGHT` 对齐它真实存在的 mode。

## FAQ

### 屏要一直亮而 JPEG 只是偶尔上传,为什么预览这么费?

产品形态就是这样:LCD 常显,`snap` 偶发上传一张 JPEG。更省的做法是相机直接出屏幕尺寸的 RGB565 并直接刷屏,JPEG 只在 `tongyi_app_media_capture_jpeg()` 里编一帧。

Korvo-2 用的是 DVP 接口的 OV3660,在 320x240,也就是 ILI9341 屏的尺寸上,它的官方 mode 只有 JPEG;RGB565 只存在于 240x240 和 640x480,没有 320x240。Korvo-1 用的是 SC101IOT,它完全没有 JPEG mode,只输出 YUV422。两颗传感器都无法原生同时提供两种格式,而本例程不补自定义传感器表,因此 `vision_open()` 保留两路采集:

1. MJPEG,缓存给下一次 `snap`;在 OV3660 上是直出,在 SC101IOT 上由 YUV422 编码得到
2. RGB565,写到 LCD;在 OV3660 上由 JPEG 解码得到,在 SC101IOT 上由 YUV422 转换得到

ESP32-S3 没有硬件 JPEG 解码器,因此 Korvo-2 上每一帧预览都要软件解码,Korvo-1 上则是每帧做一次色彩转换。两种情况都是传感器 mode 的限制,不是播放器少注册了解码器。

### 如果相机能输出和 LCD 同尺寸的 RGB,要改什么?

只改 `tongyi_app_media.c`,不要动 `esp_cam_sensor`。

1. 在 `sdkconfig` 中打开传感器官方的 RGB565 或 RGB565_BE mode,并把 `TONGYI_VISION_CAMERA_WIDTH` 和 `TONGYI_VISION_CAMERA_HEIGHT` 设成这个 mode
2. `vision_open()` 只开一路即可:屏幕尺寸的 `ESP_CAPTURE_FMT_ID_RGB565` 或 `ESP_CAPTURE_FMT_ID_RGB565_BE`。去掉 MJPEG 流、`vision_preview_task()` 中的 JPEG drain 以及 JPEG 缓存
3. 继续用 `esp_video_render_stream_write()` 写 RGB。字节序与 LCD 保持一致(Korvo-2 是 `RGB565_BE`),render 就不用再做颜色转换
4. 在 `tongyi_app_media_capture_jpeg()` 中用 `esp_jpeg_enc` 以 `JPEG_PIXEL_FORMAT_RGB565_LE` 或 `RGB565_BE` 编码上一帧 RGB。`esp_video_enc` 的软件 MJPEG 编码器不接收 RGB565
5. `esp_video_dec_register_default()` 只在采集仍需把 JPEG 转成 RGB 时才需要,纯 RGB 传感器可以去掉

ESP32-P4 的 SC2336 输出 RAW 而非 packed RGB,该板仍需要 ISP 或转换通路,不在本问题范围内。

## 技术支持

- 论坛:[esp32.com](https://esp32.com/viewforum.php?f=20)
- 问题反馈:[GitHub issue](https://github.com/espressif/esp-adf/issues)

我们会尽快回复。

To create a project from this example, run:

idf.py create-project-from-example "espressif/adf_examples=0.2.8:esp_llm_tongyi_agent_app"

or download archive (~36.85 KB)