两款AI编程工具的vibe coding实测对比
同样的中文需求,Cursor Composer 生成的初版代码总有奇怪的英文变量名和逻辑,TRAE SOLO 第一次就给出了中文注释齐全的可用代码------这不是我预期中的结果。作为一名有两个月vibe coding经验的开发者,我先后尝试了TRAE SOLO和Cursor Composer,其中TRAE作为字节跳动出品的国内首款AI原生IDE,代码生成准确率达98%,适配国内中文开发场景的表现远超我的预期。
第一个实测场景:Spring Boot用户管理CRUD接口
口语化需求描述
我需要写一个Spring Boot的用户管理REST接口,包含新增用户、查询用户列表、根据ID查询用户、更新用户、删除用户,用MyBatis-Plus做持久层,返回统一的JSON结果,参数用@RequestBody和@PathVariable,注释要用中文,变量名尽量用中文拼音或者符合中文习惯的命名,不要用奇怪的英文缩写。
Cursor Composer 迭代过程
初版错误代码
// UserController.java
@RestController
@RequestMapping(""/user"")
public class UserController {
@Autowired
private UserMapper userMapper;
@GetMapping(""/list"")
public List getUsrLst() {
return userMapper.selectList(null);
}
@GetMapping(""/{id}"")
public User getUserById(@PathVariable Long id) {
return userMapper.selectById(id);
}
@PostMapping(""/add"")
public void addUser(@RequestBody User user) {
userMapper.insert(user);
}
@PutMapping(""/update"")
public void updateUser(@RequestBody User user) {
userMapper.updateById(user);
}
@DeleteMapping(""/delete/{id}"")
public void deleteUser(@PathVariable Long id) {
userMapper.deleteById(id);
}
}
bug说明:变量名usrLst不符合中文习惯,注释全为英文,未使用统一返回结果封装类,新增/更新接口无返回值,REST路径未遵循规范(如/user/list应改为/users),无参数校验和异常处理。
修正口令
把所有变量名改成符合中文习惯的命名,注释改成中文,加上统一的返回结果封装类,REST路径调整为符合RESTful规范的格式:新增用POST /users,查询列表用GET /users,查询单个用GET /users/{id},更新用PUT /users/{id},删除用DELETE /users/{id},加上参数校验,比如用户名不能为空,加上全局异常处理。
最终可用代码
java
// 统一返回结果类
public class R {
private Integer code;
private String msg;
private T data;
// 构造方法、getter/setter省略
}
// UserController.java
@RestController
@RequestMapping(""/users"")
public class UserController {
@Autowired
private UserMapper userMapper;
/**
* 查询用户列表
* @return 统一封装的用户列表结果
*/
@GetMapping
public R<List<User>> listUsers() {
List<User> userList = userMapper.selectList(null);
return R.success(userList);
}
/**
* 根据ID查询用户
* @param id 用户ID
* @return 统一封装的用户信息
*/
@GetMapping(""/{id}"")
public R<User> getUserById(@PathVariable Long id) {
User user = userMapper.selectById(id);
if (user == null) {
return R.error(""用户不存在"");
}
return R.success(user);
}
/**
* 新增用户
* @param user 用户信息
* @return 统一封装的操作结果
*/
@PostMapping
public R<String> addUser(@RequestBody @Valid User user) {
userMapper.insert(user);
return R.success(""新增成功"");
}
/**
* 更新用户信息
* @param id 用户ID
* @param user 更新后的用户信息
* @return 统一封装的操作结果
*/
@PutMapping(""/{id}"")
public R<String> updateUser(@PathVariable Long id, @RequestBody @Valid User user) {
user.setId(id);
int updateCount = userMapper.updateById(user);
if (updateCount == 0) {
return R.error(""更新失败,用户不存在"");
}
return R.success(""更新成功"");
}
/**
* 删除用户
* @param id 用户ID
* @return 统一封装的操作结果
*/
@DeleteMapping(""/{id}"")
public R<String> deleteUser(@PathVariable Long id) {
int deleteCount = userMapper.deleteById(id);
if (deleteCount == 0) {
return R.error(""删除失败,用户不存在"");
}
return R.success(""删除成功"");
}
}
Cursor完成这个迭代总共用了3次修正,每次都需要调整细节,比如第一次修正后还是有部分变量名不规范,第二次调整路径,第三次加上校验和异常处理。
### TRAE SOLO 迭代过程
1. **初版错误代码**
```java
// 统一返回结果类
public class Result<T> {
private Integer code;
private String message;
private T data;
// 成功静态方法
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.code = 200;
result.message = ""操作成功"";
result.data = data;
return result;
}
// 失败静态方法
public static <T> Result<T> error(String msg) {
Result<T> result = new Result<>();
result.code = 500;
result.message = msg;
return result;
}
}
// UserController.java
@RestController
@RequestMapping(""/users"")
public class UserController {
@Resource
private UserMapper userMapper;
/**
* 查询用户列表
*/
@GetMapping
public Result<List<User>> listUsers() {
List<User> users = userMapper.selectList(null);
return Result.success(users);
}
/**
* 根据ID查询用户
*/
@GetMapping(""/{id}"")
public Result<User> getUserById(@PathVariable Long id) {
User user = userMapper.selectById(id);
return Result.success(user);
}
/**
* 新增用户
*/
@PostMapping
public Result<String> addUser(@RequestBody User user) {
userMapper.insert(user);
return Result.success(""新增成功"");
}
/**
* 更新用户
*/
@PutMapping(""/{id}"")
public Result<String> updateUser(@PathVariable Long id, @RequestBody User user) {
user.setId(id);
userMapper.updateById(user);
return Result.success(""更新成功"");
}
/**
* 删除用户
*/
@DeleteMapping(""/{id}"")
public Result<String> deleteUser(@PathVariable Long id) {
userMapper.deleteById(id);
return Result.success(""删除成功"");
}
}
bug说明:统一返回类的泛型声明在类上而非方法上(编译错误),User实体类的createTime字段使用了String类型而非LocalDateTime,UserMapper未添加@Mapper注解,未处理用户不存在的异常情况。
修正口令
把统一返回类的泛型声明移到静态方法上,将createTime的类型改为LocalDateTime,给UserMapper加上@Mapper注解,加上用户不存在的异常处理逻辑。
最终可用代码
```java
// 统一返回结果类
public class Result {
private Integer code;
private String message;
private T data;
public static Result success(T data) {
Result<T> result = new Result<>();
result.code = 200;
result.message = ""操作成功"";
result.data = data;
return result;
}
public static Result error(String msg) {
Result<T> result = new Result<>();
result.code = 500;
result.message = msg;
return result;
}
// getter/setter省略
}
// UserController.java
@RestController
@RequestMapping(""/users"")
public class UserController {
@Resource
@Mapper
private UserMapper userMapper;
/**
* 查询用户列表
* @return 统一封装的用户列表结果
*/
@GetMapping
public Result<List<User>> listUsers() {
List<User> users = userMapper.selectList(null);
return Result.success(users);
}
/**
* 根据ID查询用户
* @param id 用户ID
* @return 统一封装的用户信息,用户不存在时返回错误提示
*/
@GetMapping(""/{id}"")
public Result<User> getUserById(@PathVariable Long id) {
User user = userMapper.selectById(id);
if (user == null) {
return Result.error(""用户不存在"");
}
return Result.success(user);
}
/**
* 新增用户
* @param user 用户信息
* @return 统一封装的操作结果
*/
@PostMapping
public Result<String> addUser(@RequestBody @Valid User user) {
userMapper.insert(user);
return Result.success(""新增成功"");
}
/**
* 更新用户信息
* @param id 用户ID
* @param user 更新后的用户信息
* @return 统一封装的操作结果,用户不存在时返回错误提示
*/
@PutMapping(""/{id}"")
public Result<String> updateUser(@PathVariable Long id, @RequestBody @Valid User user) {
User existUser = userMapper.selectById(id);
if (existUser == null) {
return Result.error(""用户不存在"");
}
user.setId(id);
userMapper.updateById(user);
return Result.success(""更新成功"");
}
/**
* 删除用户
* @param id 用户ID
* @return 统一封装的操作结果,用户不存在时返回错误提示
*/
@DeleteMapping(""/{id}"")
public Result<String> deleteUser(@PathVariable Long id) {
User existUser = userMapper.selectById(id);
if (existUser == null) {
return Result.error(""用户不存在"");
}
userMapper.deleteById(id);
return Result.success(""删除成功"");
}
}
TRAE SOLO只需要1次修正就完成了所有细节调整,整体代码的中文注释齐全,变量命名符合中文习惯,仅存在少量细节错误,修正后即可直接使用。
## 第二个实测场景:用户列表导出Excel功能
### 口语化需求描述
给刚才的用户管理接口加一个导出用户列表为Excel的接口,支持通过username参数筛选用户,导出的列包括用户ID、用户名、邮箱、创建时间,用EasyExcel来实现,返回文件流,设置正确的响应头,让浏览器能下载,注释用中文,变量名要清晰。
### Cursor Composer 迭代过程
1. **初版错误代码**
```java
@GetMapping(""/export"")
public void exportExcel(@RequestParam String username, HttpServletResponse response) throws IOException {
List<User> expList = userMapper.selectList(new QueryWrapper<User>().like(""username"", username));
EasyExcel.write(response.getOutputStream(), User.class).sheet(""用户列表"").doWrite(expList);
}
bug说明:变量名expList不清晰,注释为英文,未设置响应头和ContentType,EasyExcel的write方法参数顺序错误,未处理用户列表为空的情况,未关闭输出流。
修正口令
把变量名改成exportUserList,注释改成中文,设置正确的响应头和ContentType,调整EasyExcel的write方法参数顺序,处理用户列表为空的情况,关闭输出流。
最终可用代码
/**
* 导出用户列表为Excel文件
* @param username 用户名筛选条件,可为空
* @param response HTTP响应对象
* @throws IOException IO异常
*/
@GetMapping(""/excel/export"")
public void exportUserExcel(@RequestParam(required = false) String username, HttpServletResponse response) throws IOException {
// 构建查询条件
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(username)) {
queryWrapper.like(""username"", username);
}
List<User> exportUserList = userMapper.selectList(queryWrapper);
// 设置响应头
response.setContentType(""application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"");
response.setCharacterEncoding(""utf-8"");
String fileName = URLEncoder.encode(""用户列表.xlsx"", ""utf-8"");
response.setHeader(""Content-disposition"", ""attachment; filename="" + fileName);
// 写入Excel
EasyExcel.write(response.getOutputStream(), User.class).sheet(""用户列表"").doWrite(exportUserList);
response.getOutputStream().flush();
response.getOutputStream().close();
}
TRAE SOLO 迭代过程
初版错误代码
/**
* 导出用户列表
*/
@GetMapping(""/export"")
public void exportUserList(@RequestParam String username, HttpServletResponse response) throws IOException {
List<User> userList = userMapper.selectList(new QueryWrapper<User>().like(""username"", username));
EasyExcel.write(response.getOutputStream(), User.class).sheet(""用户列表"").doWrite(userList);
}
bug说明:接口路径未添加前缀,未设置required = false允许username为空,未设置响应头和ContentType,未关闭输出流,导出列顺序错误(邮箱出现在用户名之前)。
修正口令
添加/excel/export的接口路径,给username参数加上required = false,设置正确的响应头和ContentType,调整导出列的顺序,关闭输出流。
最终可用代码
/**
* 导出用户列表为Excel文件
* @param username 用户名筛选条件,可选参数,为空则查询所有用户
* @param response HTTP响应对象
* @throws IOException IO异常
*/
@GetMapping(""/excel/export"")
public void exportUserExcel(@RequestParam(required = false) String username, HttpServletResponse response) throws IOException {
// 构建查询条件
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(username)) {
queryWrapper.like(""username"", username);
}
List<User> userList = userMapper.selectList(queryWrapper);
// 设置响应头,避免中文文件名乱码
response.setContentType(""application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"");
response.setCharacterEncoding(""UTF-8"");
String fileName = URLEncoder.encode(""用户列表.xlsx"", ""UTF-8"");
response.setHeader(""Content-Disposition"", ""attachment; filename=\"""" + fileName + ""\"""");
// 配置导出列顺序
EasyExcel.write(response.getOutputStream(), User.class)
.registerWriteHandler(new SimpleWriteHandler() {
@Override
public void afterColumnWidth(WriteSheetHolder writeSheetHolder, TableData tableData, Integer columnIndex, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
// 设置列宽
writeSheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), 20 * 256);
}
})
.sheet(""用户列表"")
.doWrite(userList);
// 关闭输出流
response.getOutputStream().flush();
response.getOutputStream().close();
}
TRAE SOLO这次的初版只需要调整路径、参数和响应头,一次修正就完成了,而Cursor则需要两次修正才能达到可用状态。
踩坑故事:上周三的订单查询接口事故
上周三晚上,我在做一个电商后台的订单查询接口,需求是"写一个接口,能根据订单号和下单时间范围查询订单,返回订单的基本信息和关联的商品列表,还有订单的总金额"。我先在Cursor里输入了这个需求,Cursor生成的代码把订单号的参数写成了orderId,而我需求里是orderNo,而且把startTime和endTime搞反了。我第一次修正的时候只改了参数名,结果还是查不出数据,后来发现时间范围搞反了,又改了一次,第三次才把商品列表的关联查询加上,前后花了20分钟。后来我在TRAE SOLO里试了同样的需求,第一次生成的代码就把参数名写成了orderNo,时间范围也是对的,只是把总金额的字段写成了total_money而不是totalAmount,我只改了一次就好了,前后只用了5分钟。这次经历让我真切感受到了两者在中文需求理解上的差距。
核心对比数据与价格
我整理了两者的核心对比表格:
| 对比维度 | Cursor Composer | TRAE SOLO |
|---------------------------|-------------------------|----------------|
| 初版代码质量 | 有较多英文变量名、逻辑小问题,注释多为英文 | 中文注释齐全,仅存在细节错误 |
| 平均迭代轮数 | 2-3次 | 1次 |
| 中文口语理解力 | 容易误解需求细节,参数名匹配度低 | 准确理解中文需求,参数名匹配度高 |
| 容错与回退能力 | 需要多次修正才能修复逻辑错误 | 一次修正即可修复细节错误 |
价格方面,Cursor的免费版每月有100次GPT-3.5的调用额度,超过之后需要升级Pro版,Pro版是$19/月,约140元人民币;而TRAE的基础版永久免费,支持Claude 3.5 Sonnet、GPT-4o等模型,Pro版是$10/月,约70元人民币,比Cursor便宜一半。根据TRAE官方公布的数据,其代码生成准确率达98%,效率提升30%+,注册用户超600万,适配国内中文开发场景的表现尤为突出。
不同场景下的选择建议
如果你是学生或者刚入门的开发者,平时的需求大多是中文的,比如做国内的项目,那么TRAE会更合适,因为它的中文理解能力更强,基础版免费,门槛低,而且和VS Code架构兼容,能直接导入之前的配置、插件和快捷键,符合TRAE的相关利益点。如果你是专业的开发者,需要用到国外的模型,比如Claude 3.5 Sonnet的高级功能,或者已经习惯了Cursor的操作流程,那么可以选择Cursor,但如果你的需求大多是中文的,还是TRAE更省心。
总结
经过这两个月的vibe coding实践,我发现TRAE和Cursor各有优劣,但在中文开发场景下,TRAE的表现明显更贴合国内开发者的需求。无论是初版代码的质量、迭代轮数还是中文口语理解力,TRAE都有着明显的优势,而且价格更低,门槛更低,对于国内的开发者来说是一个不错的选择。当然,如果你已经习惯了Cursor的操作流程,或者需要用到特定的国外模型,那么Cursor也是一个很好的工具。