← 返回博客
·AI工具

Cursor 的 "理解整个项目" 功能,把我坑了三次

一个一线开发者用 Cursor Agent 模式重构 5000+ Go 项目时踩的三次真实坑:幻觉常量、跨文件引用错误、函数签名自改。

#Cursor#AI编程#代码重构#Agent

# Cursor 的 "理解整个项目" 功能,把我坑了三次

去年冬天我花了一个月时间给一个 5000+ 文件的 Go 项目加缓存层。用了 Cursor 的 Agent 模式,当时觉得这玩意儿简直是为重构而生的——它说能 "理解整个 codebase",我以为它能替我把那些藏在角落里的硬编码 TTL 全揪出来。

结果呢?它确实理解了,但理解的方式让我怀疑它到底读了代码还是只看了文件名。

翻车现场 #1:把常量定义当成魔法数字

我要在所有服务层统一加 Redis 缓存,TTL 设 5 分钟。写了个注释让 Cursor Agent 帮我自动处理:

// 给所有查询方法加上 Redis 缓存,TTL 5 分钟

func GetUser(ctx context.Context, id string) (*User, error) {

// TODO: cache

}

Agent 给我生成了这段代码:

var cache = map[string]int{

"ttl": 3600 * 24, // 一天

}

func GetUser(ctx context.Context, id string) (*User, error) {

cached, ok := cache[id]

if ok {

return &User{ID: id, CacheHit: true}, nil

}

// ... 查数据库的逻辑

}

好家伙,一个全局 map 当缓存用,key 是用户 ID,value 是 TTL。连方法签名都没改对——原来返回的是 *User, error,它直接变成了只返回 *User,error 被优雅地省略了。

最搞笑的是它还加了注释说 "cache key generated from user id"。这哪是 AI 编程助手,这是 AI 编故事助手吧。

教训:**别用 TODO 注释驱动大段代码生成。** Cursor 的补全模型在单行补全时意外地靠谱,但让它 "自由发挥" 就容易跑偏。后来我老老实实手写了一个 Redis 客户端封装,只让它补全边界方法,效果反而好得多。

翻车现场 #2:跨文件引用幻觉

这个是我踩得最狠的一次。项目里有个统一的错误码定义文件 pkg/errors/codes.go

const (

ErrNotFound = 1001

ErrUnauthorized = 1002

ErrConflict = 1003

)

我想在 handler 层做一个 error 到 HTTP status 的映射转换,让注释说 "根据 codes.go 中的错误码规范生成映射表"。Cursor 理解了文件结构,但它生成的映射表是这样的:

var ErrorCodeToStatus = map[int]string{

1001: "Not Found", // ✅ 对的

1002: "Forbidden", // ❌ 错的,应该是 Unauthorized

1003: "Request Timeout", // ❌ 完全不着边

9999: "I give up", // ✅ 这个是它自己加的

}

你看最后那个 "I give up"——我差点笑出声。它居然在生成的生产代码里给自己留了彩蛋。

关键问题是:它读到了 codes.go 的文件内容吗?看起来是读了,但把错误码的含义搞混了。这说明什么?说明它是在做语义联想而不是真正的逻辑推理。给它一个数字 1002,它联想到 "Forbidden" 可能比 "Unauthorized" 更顺口,就选了前者。

**教训:跨文件引用一定要 review diff,尤其是常量/枚举相关的映射关系。** 最好手动给它一个 JSON 格式的 schema 当 prompt 附件,比自然语言描述准确十倍。

翻车现场 #3:最隐蔽的 bug——它自作主张改了函数签名

这是第三次翻车,也是代价最大的一次。

我在 refactor 一个支付模块,有 30 多个函数涉及到 processPayment() 的调用。我给 Cursor 一个全局指令:

> 把所有 processPayment 的参数从 (amount float64) 改成 (req PaymentRequest)

它改了一半停下来了——不是它能力不够,而是它会 "礼貌性地" 在遇到不确定的地方停下来问你。但我当时没有仔细看它的提问,直接点了 accept,结果后续 15 个函数的调用点它直接瞎猜了参数结构。

具体来说,有一个函数原本是这样调用的:

err := processPayment(order.TotalAmount)

if err != nil {

log.Printf("payment failed: %v", err)

}

它改成:

err := processPayment(PaymentRequest{

Amount: order.TotalAmount,

})

if err != nil {

log.Println("oops") // 连 error message 都丢了

}

log.Printf 被替换成 log.Println,连 error 信息都不打印了。这种改动在开发环境根本测不出来,因为 error 分支的覆盖本身就低。上线后第三天,有一笔订单的支付失败原因查不到日志,耽误了至少四小时排查。

**教训:接受 Agent 的全局修改建议前,必须逐行看 diff。尤其是涉及函数签名变更的,一定要跑一遍测试再合。**

那 Cursor 到底好用吗?

好用。但你要把它当成一个 **极其勤奋但偶尔会一本正经胡说八道的实习生**。

它擅长:

  • 单行/单函数的快速补全(准确率 80%+)
  • 样板代码生成(handler、router、model CRUD)
  • 帮你写测试用例的骨架
  • 解释你不熟悉的第三方库 API
  • 它不擅长:

  • 跨文件的常量/枚举含义推理
  • 全局重构后的调用链一致性
  • 业务逻辑的语义理解
  • 我的最佳实践总结下来就是三条:

  • **小块交给它,大块自己来。** 一个文件内的功能改写可以信任它;跨文件的结构性变更必须自己把控。
  • 2. **Prompt 要给结构化数据。** 不要说 "用我们的错误码规范",给它一个 JSON/YAML 的错误码列表当上下文。

    3. **Review diff 的时间永远小于 debug 它生成的 bug 的时间。** 这话说起来像废话,但我第三次翻车之后才真正信了。


    *如果你也在用 Cursor 做项目级改造,踩过哪些坑?或者有更好的用法?评论区聊聊。*