阅读说明:仅使用国内版 Trae,国际版的Trae 由于没法体验不做比较
总结
Trae 国内版本,在核心能力上跟 Cursor 和 Windsurf 还是有一定的差距,这里面最大的差距应该是模型本身,而非 IDE。
但 Trae 迭代很快,操作体验(非代码能力)也很好,功能上也是越来越丰富,配合字节大量的基建,如果字节整个公司都在使用 Trae(自愿也好,被迫也罢),那么就很像早期的飞书,潜力无限。
写单测
下面以一个真实的测试 case 来体验 Trae 写单测的能力。
测试源码在 simult-interpretation (这是扣子开放平台其中一个 SDK 的源码,可参考项目 README 跑起来以作验证),测试代码放在 simult-interpretation.spec.ts(线上放的是已经没问题的单测代码,测试时,可以先把这个文件清空),这个 case 是有点复杂的,它使用了异步的 Websocket、复杂的上游依赖还有一些浏览器特有的API,测试时,需要 Mock 掉所有的上游依赖,才能比较顺利通过。以下是两个文件的源码(可不看)
import { type WsSimultInterpretationClientOptions } from '../types';
import PcmRecorder from '../recorder/pcm-recorder';
import {
APIError,
CozeAPI,
type CreateSimultInterpretationsWsReq,
type CreateSimultInterpretationsWsRes,
type ErrorRes,
type WebSocketAPI,
WebsocketsEventType,
COZE_CN_BASE_WS_URL,
} from '../..';
class BaseWsSimultInterpretationClient {
//...
}
export default BaseWsSimultInterpretationClient;
import { v4 as uuid } from 'uuid';
import {
type AIDenoiserProcessorLevel,
type AIDenoiserProcessorMode,
} from '../recorder/pcm-recorder';
import {
type SimultInterpretationUpdateEvent,
WebsocketsEventType,
} from '../..';
import BaseWsSimultInterpretationClient from './base';
class WsSimultInterpretationClient extends BaseWsSimultInterpretationClient {
// ...
}
export default WsSimultInterpretationClient;
提供的提示语大致如下:
请参考其它单测代码 packages/coze-js/test , 为 packages/coze-js/src/ws-tools/simult-interpretation 写单测,代码放在 packages/coze-js/test/ws-tools/simult-interpretation.spec.ts, 要求:代码规范请参考项目代码,单测覆盖率 90% 以上,你可以在 packages/coze-js目录下支持 npm test 来验证测试结果
Cursor
起初用 Cursor 写,折腾了个把小时,尝试各种思路,就是没搞定,它的错误不是某个单测没通过,而是整个单测都没跑起来,分析了下就是引入了副作用(类似如下截图的错误),经过多次提示,还是没法解决这个问题,通常是解决一个问题引入新的问题,到了最后,实在没精力搞了就放弃了。
Windsurf
隔天,心血来潮,想着要不用 Windsurf 来试一下,同样的提示语与上下文,好家伙,第一次基本就搞定了,三次对话就顺利通过单测,并且单测覆盖率达到惊人的 95%+
Trae CN
第一次尝试(使用 DeepSeek),用上面的提示语,尽管后面补充了要 mock 所有上游依赖,还是没法搞定副作用,而且生成的测试代码仅有 100行,源码都不止 100行,即使通过单测覆盖率也达不到。
第二次尝试,增加了整个 Workspace 上下文,换了一个模型(Gemimi),这次生成的测试代码就丰富多了(700多行),但经过几轮对话,还是没法解决根本性问题,不再继续折腾
第三次,复杂的 case 应付不了,那么尝试一下简单的 case,接下来测试一个纯 utils 函数 utils.ts,经过小修小改就通过了,而且测试 case 的覆盖率还挺全的,生成的测试代码 180 多行,比原来的 90 行多一倍,一些以前没覆盖的场景也都覆盖了,还是挺完美的。
在使用国内版的 Trae,如果是用来写代码,最好还是用国外的模型(如 Gemini)。
编码体验
Tab 能力
修改代码的时候,像 Cursor 会根据上下文做联动修改,只需要按 Tab 键就能快速修复,但是 Trae 这方面能力还有待加强,比如下面,修改 label 标签的内容,理论上下面相关的也能一并修改,但目前不会,而且手动去修改的时候,也无法给出智能提示,全部都要自己复制粘贴
MCP 使用
Figma 还原设计稿
这次使用到了 Figma MCP,安装还是挺简单,用 Figma MCP 还原设计稿,之前使用 Cursor 进行了多次测试,效果不是很完美的,体现两点:
-
- 生成一大堆样式,模型并不太清楚组件的样式,所以生成代码的时候,为了以防唯一,都会给每个 div 加一个 className,这些样式大部分是不需要的,但加了也没啥问题。
-
- 没法像素级别对齐,关键的一些位置信息(如 margin、padding、字体、颜色等)会和 Figma 对不上。看看 Trae 表现如何。
第一次尝试,使用了之前的提示语,发现生成的代码,根本不是按设计稿的来的(又是根据推理得来的),可能是 figma mcp 无法正确读取数据。
第二次尝试,使用截图代替 MCP,就能正确生成出来了,上面提到的两个问题照样存在,其它倒是没啥特别的,中规中矩,基本符合预期。
GitHub MCP
再试一下 github mcp,看看效果如何。
使用 Cursor,提示语:“使用 github mcp,告诉我 coze-js 这个仓库下的, coze/api 这个 SDK如何使用流式请求”,这个要求挺简单的,只要搜索相关的 README 和 示例,就能给出答案
async function streamChat() {
const stream = await client.chat.stream({
bot_id: 'your_bot_id', // 必填,要进行会话的 Bot ID
user_id: 'user123', // 可选,用户ID,若不提供会自动生成
additional_messages: [{
role: RoleType.User,
content: '你好,请介绍一下自己',
content_type: 'text',
}],
// 其他可选参数
// conversation_id: '已有的会话ID',
// auto_save_history: true,
// custom_variables: { key: 'value' },
});
// 使用 for await...of 循环处理流式响应
for await (const part of stream) {
if (part.event === ChatEventType.CONVERSATION_CHAT_CREATED) {
console.log('会话开始');
} else if (part.event === ChatEventType.CONVERSATION_MESSAGE_DELTA) {
// 实时输出消息片段
process.stdout.write(part.data.content);
} else if (part.event === ChatEventType.CONVERSATION_MESSAGE_COMPLETED) {
// 某条消息完成
console.log('\n消息完成:', part.data.content);
} else if (part.event === ChatEventType.CONVERSATION_CHAT_COMPLETED) {
// 会话完成,此时可以获取 token 使用情况
console.log('使用情况:', part.data.usage);
} else if (part.event === ChatEventType.DONE) {
// 流式响应结束
console.log('响应结束');
} else if (part.event === ChatEventType.ERROR) {
// 处理错误
console.error('错误:', part.data);
}
}
}
上面是 Cursor 给的回复(还有其它内容就不贴了),答案是正确的。
下面是 Trae 给的回复,答案明显是错误的,还是之前的问题,幻觉太多了,模型需要根据已有事实给出答案,而不是存在大量推理成分。
import { CozeAPI, ChatEventType, RoleType } from '@coze/api';
// 假设 client 已经初始化
// const client = new CozeAPI({ pat: 'your_personal_access_token' });
async function streamChat() {
const stream = await client.chat.stream({
bot_id: 'your_bot_id', // 替换为你的 Bot ID
user: 'user_id', // 替换为你的用户 ID
query: '你好!', // 用户的查询
stream: true, // 明确启用流式响应
// 如果需要,可以添加聊天历史
// chat_history: [
// { role: RoleType.User, content: '之前的消息', content_type: 'text' },
// { role: RoleType.Assistant, content: '之前的回复', content_type: 'text' },
// ],
// 如果需要,可以添加附加消息
// additional_messages: [{
// role: RoleType.User,
// content: '附加消息内容',
// content_type: 'text',
// }],
});
for await (const part of stream) {
// 检查事件类型是否为消息增量
if (part.event === ChatEventType.MESSAGE_DELTA) {
// part.data.message.content 包含了流式返回的消息片段
process.stdout.write(part.data.message.content); // 实时打印响应片段
} else if (part.event === ChatEventType.ERROR) {
// 处理错误事件
console.error('Stream error:', part.data);
}
// 你可以根据需要处理其他事件类型,例如 ChatEventType.MESSAGE, ChatEventType.DONE 等
}
console.log('\nStream finished.');
}
streamChat();
写 SDK 使用文档
在开源项目中,现在大部分的 README 和 SDK 使用文档都是在 AI 的辅助下完成的,接下来看看 Trae 在这方面的能力如何。
在开源项目 Coze-Js 中,有一个 SDK 和配套的 Demo,结合这两份源码生成一份 SDK 使用文档。
它生成的文档如下(可跳过):
# WsChatClient SDK 文档
`WsChatClient` 是 Coze JS SDK 的一部分,提供通过 WebSocket 与 Coze Bot 进行实时语音和文本聊天的能力。
## 安装
`WsChatClient` 包含在 `@coze/api` 包中。请确保已安装该包:
```bash
npm install @coze/api
# 或者
yarn add @coze/api
```
## 快速上手
以下是一个基本示例,展示如何初始化客户端、连接、发送消息并处理响应:
```typescript
import { WsChatClient, WsChatEventNames, type WsChatEventData } from '@coze/api/ws-tools';
import { WebsocketsEventType, RoleType } from '@coze/api';
async function runChat() {
let client: WsChatClient | null = null;
try {
// 1. 初始化客户端
client = new WsChatClient({
botId: 'YOUR_BOT_ID', // 替换为你的 Bot ID
token: 'YOUR_AUTH_TOKEN', // 替换为你的认证 Token
// voiceId: 'YOUR_VOICE_ID', // 可选:指定语音合成的音色 ID
allowPersonalAccessTokenInBrowser: true, // 可选:允许在浏览器环境使用个人访问令牌,默认为 false
debug: true, // 可选:开启调试日志,默认为 false
});
// 2. 监听事件 (示例:监听所有服务端事件)
client.on(WsChatEventNames.ALL, (eventName: string, event: WsChatEventData) => {
console.log(`[${eventName}]`, event);
// 处理收到的消息 (文本或语音)
if (event.event_type === WebsocketsEventType.CONVERSATION_MESSAGE_DELTA) {
console.log('收到文本消息片段:', event.data.content);
}
if (event.event_type === WebsocketsEventType.CONVERSATION_MESSAGE_COMPLETED) {
console.log('文本消息接收完成');
}
if (event.event_type === WebsocketsEventType.CONVERSATION_AUDIO_DELTA) {
// SDK 内部会自动处理并播放语音
console.log('收到语音消息片段');
}
if (event.event_type === WebsocketsEventType.CONVERSATION_AUDIO_COMPLETED) {
console.log('语音消息接收完成');
}
if (event.event_type === WebsocketsEventType.ERROR) {
console.error('发生错误:', event.data.code, event.data.msg);
}
});
// 3. 建立连接 (连接成功后会自动开始录音,除非 audioMutedDefault 为 true)
await client.connect();
console.log('连接成功!');
// 4. 发送用户消息
client.sendTextMessage('你好,请介绍一下你自己');
// 5. (可选) 在适当的时候断开连接
// setTimeout(() => {
// client?.disconnect();
// console.log('连接已断开');
// }, 30000); // 30秒后断开
} catch (error) {
console.error('发生错误:', error);
// 断开连接以清理资源
await client?.disconnect();
}
}
runChat();
```
## 核心概念
### 初始化 (`new WsChatClient(options)`)
创建 `WsChatClient` 实例时,需要传入配置对象 `WsChatClientOptions`:
* `botId` (string, **必需** ): 你的 Coze Bot ID。
* `token` (string | (() => string | Promise<string>), **必需** ): 用于认证的 Token。可以是字符串,也可以是一个返回 Token 或 Promise<Token> 的函数。
* `workflowId` (string, 可选): 指定要使用的 Bot 工作流 ID。
* `voiceId` (string, 可选): 指定语音合成(TTS)使用的音色 ID。如果未提供,可能不会进行语音合成。
* `allowPersonalAccessTokenInBrowser` (boolean, 可选): 是否允许在浏览器环境中使用个人访问令牌(PAT)。默认为 `false`,出于安全考虑,建议在生产环境的服务端使用 PAT。
* `debug` (boolean, 可选): 是否在控制台打印详细的调试日志。默认为 `false`。
* `baseWsURL` (string, 可选): 指定 WebSocket 连接的基础 URL。默认为中国区的 URL (`wss://api.coze.cn/open-platform/ws/chat/v1`)。
* `websocketOptions` (object, 可选): 传递给底层 WebSocket 库的额外选项。
* `audioCaptureConfig` (object, 可选): 音频捕获配置 (详见高级配置)。
* `aiDenoisingConfig` (object, 可选): AI 降噪配置 (详见高级配置)。
* `mediaStreamTrack` (MediaStreamTrack, 可选): 使用外部提供的音频轨道,而不是 SDK 内部创建。
* `wavRecordConfig` (object, 可选): WAV 录制配置 (详见高级配置)。
* `deviceId` (string, 可选): 指定要使用的音频输入设备 ID。默认为系统默认设备。
* `audioMutedDefault` (boolean, 可选): 初始化时是否默认静音。默认为 `false` (不静音)。
### 连接 (`connect(options?)`)
调用 `connect()` 方法与 Coze Bot 建立 WebSocket 连接。
* 该方法返回一个 Promise,在连接成功建立并收到 `chat_created` 事件后 resolve。
* 连接成功后,如果 `audioMutedDefault` 为 `false` 且未使用外部 `mediaStreamTrack`,SDK 会自动请求麦克风权限并开始录音。
* 可以传入一个可选的 `chatUpdate` 对象来自定义连接时的 `chat.update` 事件内容,例如设置输入/输出音频格式、开启 VAD 等。
```typescript
await client.connect({
chatUpdate: {
// 可选的 chat.update 事件数据
data: {
// ... 自定义配置
}
}
});
```
### 发送消息
* **`sendTextMessage(text: string)`** : 发送纯文本用户消息。
```typescript
client.sendTextMessage('今天天气怎么样?');
```
* **`sendMessage(data: CreateChatWsReq)`** : 发送原始的 WebSocket 消息对象。这允许发送更复杂的消息类型,例如包含附件或特定指令的消息。
```typescript
import { v4 as uuid } from 'uuid';
import { WebsocketsEventType, RoleType } from '@coze/api';
client.sendMessage({
id: uuid(), // 消息的唯一 ID
event_type: WebsocketsEventType.CONVERSATION_MESSAGE_CREATE,
data: {
role: RoleType.User,
content_type: 'text',
content: '这是通过 sendMessage 发送的消息',
},
});
```
### 接收事件 (`on(event, callback)`)
使用 `on()` 方法监听来自服务端或其他 SDK 内部触发的事件。
* `event` (string | string[]): 要监听的事件名称或名称数组。
* `callback` ((eventName: string, event: WsChatEventData) => void): 事件触发时的回调函数。
**常用事件:**
* `WsChatEventNames.ALL`: 监听所有事件。
* `WsChatEventNames.CONNECTED`: 连接成功时触发。
* `WsChatEventNames.DISCONNECTED`: 连接断开时触发。
* `WsChatEventNames.AUDIO_MUTED`: 调用 `setAudioEnable(false)` 成功后触发。
* `WsChatEventNames.AUDIO_UNMUTED`: 调用 `setAudioEnable(true)` 成功后触发。
* `WsChatEventNames.AUDIO_INPUT_DEVICE_CHANGED`: 音频输入设备切换后触发。
* `WsChatEventNames.AUDIO_INPUT_DUMP`: 当配置了 `wavRecordConfig` 时,录制的 WAV 文件生成后触发。
* `server.${WebsocketsEventType}`: 监听特定的服务端 WebSocket 事件,例如:
* `server.conversation.message.delta`: 收到文本消息片段。
* `server.conversation.message.completed`: 文本消息接收完成。
* `server.conversation.audio.delta`: 收到语音消息片段 (Base64 编码的 PCM 数据)。SDK 内部会自动解码并使用 `WavStreamPlayer` 播放。
* `server.conversation.audio.completed`: 语音消息接收完成。
* `server.conversation.chat.requires_action`: 需要外部工具调用。
* `server.conversation.chat.canceled`: 对话被取消。
* `server.error`: 发生服务端错误。
```typescript
// 监听文本消息片段
client.on(`server.${WebsocketsEventType.CONVERSATION_MESSAGE_DELTA}`, (eventName, event) => {
if ('content' in event.data) {
console.log('Text Delta:', event.data.content);
}
});
// 监听错误
client.on(`server.${WebsocketsEventType.ERROR}`, (eventName, event) => {
console.error('Server Error:', event.data.code, event.data.msg);
});
```
### 移除监听器 (`off(event, callback)`)
使用 `off()` 方法移除之前添加的事件监听器。
### 断开连接 (`disconnect()`)
调用 `disconnect()` 方法关闭 WebSocket 连接,停止录音和播放,并清理相关资源。
```typescript
await client.disconnect();
```
### 音频控制
* **`setAudioEnable(enable: boolean)`** : 控制麦克风的开启和关闭。
* `enable = true`: 取消静音,开始/恢复录音。
* `enable = false`: 静音,暂停录音。
* 成功切换后会触发 `AUDIO_UNMUTED` 或 `AUDIO_MUTED` 事件。
```typescript
await client.setAudioEnable(false); // 静音
await client.setAudioEnable(true); // 取消静音
```
* **`setAudioInputDevice(deviceId: string)`** : 切换音频输入设备。
* `deviceId`: 要切换到的设备 ID。可以使用 `RealtimeUtils.getAudioDevices()` (来自 `@coze/realtime-api` 包,如果同时使用) 或 `navigator.mediaDevices.enumerateDevices()` 获取设备列表。传入 `'default'` 使用系统默认设备。
* 切换成功后会触发 `AUDIO_INPUT_DEVICE_CHANGED` 事件。
```typescript
// 假设 newDeviceId 是从设备列表中获取的 ID
await client.setAudioInputDevice(newDeviceId);
```
* **`setAIDenoising(mode: AIDenoiserProcessorMode, level: AIDenoiserProcessorLevel)`** : 配置 AI 降噪。
* `mode`: 降噪模式 (`'balanced'` 或 `'aggressive'`)。
* `level`: 降噪级别 (1 到 5)。
* 需要确保初始化时已配置 `aiDenoisingConfig`。
```typescript
await client.setAIDenoising('aggressive', 3);
```
### 打断 (`interrupt()`)
调用 `interrupt()` 方法可以打断当前 Bot 的语音或文本输出。这通常用于用户在 Bot 回答过程中插入新的指令或问题。
```typescript
client.interrupt();
```
## 事件参考
**客户端事件 (`WsChatEventNames`)**
* `ALL`: 所有事件的通配符。
* `CONNECTED`: 连接成功。
* `DISCONNECTED`: 连接断开。
* `AUDIO_MUTED`: 已静音。
* `AUDIO_UNMUTED`: 已取消静音。
* `AUDIO_INPUT_DEVICE_CHANGED`: 音频输入设备已更改。
* `AUDIO_INPUT_DUMP`: (需要配置) 录制的 WAV 文件已生成。
**服务端事件 (`WebsocketsEventType`)** (通过 `server.${eventType}` 监听)
* `CHAT_CREATED`: WebSocket 连接建立并初始化成功。
* `CONVERSATION_MESSAGE_DELTA`: 文本消息增量片段。
* `CONVERSATION_MESSAGE_COMPLETED`: 文本消息接收完成。
* `CONVERSATION_AUDIO_DELTA`: 语音消息增量片段 (PCM Base64)。
* `CONVERSATION_AUDIO_COMPLETED`: 语音消息接收完成。
* `INPUT_AUDIO_BUFFER_SPEECH_STARTED`: 检测到用户开始说话。
* `INPUT_AUDIO_BUFFER_SPEECH_ENDED`: 检测到用户停止说话 (VAD)。
* `CONVERSATION_CHAT_REQUIRES_ACTION`: 需要外部进行工具调用。
* `CONVERSATION_CHAT_CANCELED`: 对话被取消。
* `ERROR`: 发生错误。
* ... (其他可能的事件类型)
## 错误处理
* **连接错误** : `connect()` 方法可能会 reject 一个 `APIError` 对象,如果 WebSocket 连接建立失败或收到初始的 `error` 事件。
* **运行时错误** : 在连接期间,服务端可能会发送 `error` 事件,可以通过监听 `server.${WebsocketsEventType.ERROR}` 来捕获。`APIError` 包含 `code` 和 `msg` 字段。
* **SDK 内部错误** : 调用 SDK 方法(如 `setAudioEnable`)时也可能抛出错误,例如设备访问失败。
建议使用 `try...catch` 块包裹 SDK 调用,并监听 `server.error` 事件来处理潜在问题。
## 高级配置
### `audioCaptureConfig`
控制音频捕获的参数:
```typescript
{
// 音频处理块大小 (samples)
bufferSize?: number; // default 4096
// 音频通道数
channelCount?: number; // default 1
// 采样率 (Hz)
sampleRate?: number; // default 16000
// 是否启用回声消除
echoCancellation?: boolean; // default true
// 是否启用自动增益控制
autoGainControl?: boolean; // default true
// 是否启用噪声抑制 (浏览器内置)
noiseSuppression?: boolean; // default true
}
```
### `aiDenoisingConfig`
启用和配置 AI 降噪功能:
```typescript
{
// 是否启用 AI 降噪
enabled: boolean; // default false
// 降噪模式 ('balanced' | 'aggressive')
mode?: AIDenoiserProcessorMode; // default 'balanced'
// 降噪级别 (1-5)
level?: AIDenoiserProcessorLevel; // default 3
// AI 降噪模型文件的 URL
modelUrl?: string;
// WebAssembly 文件的 URL
wasmUrl?: string;
// Worklet 脚本文件的 URL
workletUrl?: string;
}
```
**注意:** 启用 AI 降噪需要提供模型、Wasm 和 Worklet 文件的 URL。
### `wavRecordConfig`
配置录制 WAV 文件:
```typescript
{
// 是否启用 WAV 录制
enabled: boolean; // default false
// 录制模式 ('continuous' | 'vad')
mode?: 'continuous' | 'vad'; // default 'continuous'
// 'vad' 模式下,静音检测的阈值 (ms)
vadSilenceTimeout?: number; // default 1000
// 'vad' 模式下,语音活动检测的阈值 (0-1)
vadThreshold?: number; // default 0.5
}
```
启用后,录制的 WAV 文件会通过 `WsChatEventNames.AUDIO_INPUT_DUMP` 事件回调。
### `websocketOptions`
传递给底层 WebSocket 实现的选项,例如自定义 Header:
```typescript
{
headers: {
'X-Custom-Header': 'value'
}
}
```
这份文档,先不谈专业性如何,单看目录结构,没啥问题,但是细看里面的内容,就完全乱套了,里面很多内容都是无中生有的,很明显,模型没有根据事实(源码)整理,而完全是推理出来的,贴几个例子:
| 截图 | 说明 |
|---|---|
| SDK 没有 setAIDenoising 这个方法,SDK中是拆成两个方法的(setDenoiserLevel 和 setDenoiserMode) | |
audioCaptureConfig 这个配置项没有那么多的配置(框出来的配置是不存在的),类似的还有 aiDenoisingConfig和 wavRecordConfig,都不是基于事实写出来的 |
之前用 Cursor 写,虽然写得也马马虎虎,后续还要一些补充才行,但起码不会无中生有,捏造一些不存在的 API 出来。
最近 DeepWiki 很火,我让 DeepWiki 来写一份 “请帮我总结一份关于 WsChatClient 的使用指南”,结果很惊艳、很全面,条理很清晰,最重要的是没有事实错误,比之前用 Cursor 写得都好。
文案替换
如果项目是国际化的,那么文案替换是一项很常见的工作,以前需要一个一个替换,现在有了 AI ,可以批量将需要替换的文案找出来,然后交给 PM 录入,拿到一批 key后,就可以让 AI 一键替换了,现在试下 Trae 效果如何。
以下也是一个实际业务中的代码,让 Trae 帮忙找出所有的文案:
一开始使用豆包模型,结果非常差,后面换成 Gemini 效果就好很多了,如下,基本没啥问题。
Trae 没有自动选择模型的功能,感觉挺有必要的,或者能够给出每个模型适合什么样的场景,不然,一些刚开始使用的用户,很可能就因为模型选择不对导致效果很差,从而弃坑了。