我试用了4个AI代码助手3周:这就是我真正发现的

发布: (2026年3月9日 GMT+8 07:10)
14 分钟阅读
原文: Dev.to

Source: Dev.to

请提供您希望翻译的完整文本内容(除代码块和 URL 之外),我将把它翻译成简体中文并保持原有的 Markdown 格式。谢谢!

介绍

三周前,我在调试一个只在生产环境出现的错误。一个 FastAPI 端点在用户同时按多列排序时返回了不一致的分页数据。我们的 ORM(SQLAlchemy 2.0.36)会根据缓存结果是新对象还是已填充对象而生成不同的查询。经典案例。

让我注意到的并不是 bug 本身——这类问题我已经见过千百次——而是 在调试过程中不同 AI 助手帮助我的方式之间的巨大差异。有的在第二次尝试时就给出了我需要的完整代码;有的则在十分钟内兜圈子。于是我开始思考:我们对这些助手的认知到底有多少是来自精心打磨的演示,而非真实使用?

于是我花了接下来的三周进行系统化测试。我所在的团队有四人,构建的是一个 B2B 分析平台——主要技术栈:FastAPI + React + PostgreSQL,部署在 AWS 上。我使用的是日常待办中的任务,而不是特意编造的练习。

评估候选人

助手版本 / 配置
GitHub Copilot在设置面板中启用了 Claude Sonnet 4.5 后端
Cursor0.47
Claude Code (CLI)Sonnet 4.5
Windsurf1.9.2

我如何设计有意义的测试

Lo primero que descarté fueron los benchmarks de autocompletado de funciones aisladas. HumanEval y SWE‑bench son útiles para comparar modelos base, pero no me dicen si un asistente me va a ayudar un martes por la tarde con un servicio que tiene 4 000 líneas de historia y dependencias raras.

Definí 四类任务

  1. 广泛上下文理解 – darle un módulo de 800 + líneas y pedirle que añada una feature que respete los patrones existentes.
  2. 带部分信息的调试 – pegarle un stack trace y el fragmento de código relevante, sin el contexto completo del repo.
  3. 受限重构 – “cambia esto sin romper la API pública, y sin cambiar los tests existentes”.
  4. 生成测试 – escribir tests de integración para código legacy con acoplamiento fuerte.

Para cada categoría seleccioné tres tareas reales distintas. La evaluación fue subjetiva (lo sé) en tres dimensiones:

  • ¿El primer intento era usable directamente?
  • ¿Cuántas iteraciones necesité?
  • ¿Introdujo bugs que no estaban antes?

No medí velocidad de tokens por segundo; no me importa que sea rápido si el output está mal.

对上下文的理解:这里是好人与平庸之分

我用的测试最能区分好坏的是这个:我有一个数据导出模块(data_export/pipeline.py,约 1 100 行),它使用 通过装饰器注册的插件模式。我让每位助理按照完全相同的模式为一个新导出格式添加支持。

# 现有模式,助理需要识别并复制
@export_registry.register("csv")
class CSVExporter(BaseExporter):
    """
    Exporta datos en formato CSV con soporte para encodings custom.
    El registry inyecta config via __init_subclass__ — no tocar ese flujo.
    """

    def export(self, queryset: QuerySet, options: ExportOptions) -> BytesIO:
        # La lógica de chunking está en BaseExporter.stream_chunks()
        # Los exporters solo deben implementar _serialize_chunk()
        buffer = BytesIO()
        for chunk in self.stream_chunks(queryset, options.chunk_size):
            buffer.write(self._serialize_chunk(chunk, options))
        return buffer

    def _serialize_chunk(self, data: list[dict], options: ExportOptions) -> bytes:
        # ... implementación real omitida por brevedad

结果

助理评论迭代次数大致时间
Claude Code (CLI)在第二次尝试时——第一次缺少注册装饰器——生成了一个使用 stream_chunks() 正确且遵循 __init_subclass__ 约定的 Parquet 导出器。我只改了两行代码。2~6 分钟
Cursor结果在功能上是正确的,但忽略了 stream_chunks() 模式,自己从头实现了分块循环。虽然能运行,但现在产生了新的技术债务,因为如果我修改 BaseExporter,该导出器不会继承这些更改。1
Copilot与 Cursor 类似。它理解装饰器,但不明白为什么要把 _serialize_chunk() 设为单独的方法。第一次尝试把所有逻辑都写在 export() 里。1
Windsurf正面惊喜——它捕捉到了模式,甚至比我预期的更好。结果与 Claude Code 同等水平,只是用了更多的迭代(四次交流 vs. 两次)。4

结论:
对于内部模式不明显的代码,模型处理上下文的窗口以及它对上下文的理解,比自动补全的速度更为关键。如果你的代码库有自己的约定——两年后的任何代码库几乎都有——你会明显感受到这种差异。

部分信息调试:凌晨 11 点的堆栈追踪场景

这是我在实际工作中最常遇到的用例。某个功能在 staging 环境崩溃,我手上只有 traceback 和相关代码,没时间提供完整的上下文。

我使用了过去一个月的三个真实 bug。最能说明问题的一个是 RecursionError,它出现在 Pydantic v2 的序列化过程中,当模型通过 Optional 字段并使用 model_rebuild() 形成循环引用时。

个人备注: 这里我犯了一个让自己浪费时间的错误:我默认任何助手都能自行区分 Pydantic v1v2,而不明确说明。Copilot 给了我三种在 v1 可行但在 v2 已废弃或直接删除的解决方案——validator 替代 field_validator__fields__ 替代 model_fields。这些错误表明模型混用了两个版本的知识。如果你不熟悉 Pydantic,可能根本不会察觉,结果花半小时实现根本不可用的代码。

从那以后,我在最初的提示中总会明确写出确切的版本号。这个细节本该一开始就写明。

Claude Code 是最能应对这种歧义的助手——部分原因是它以 CLI 方式运行并能访问完整仓库,能够读取 pyproject.toml 并在回答前确认你安装的版本。Windsurf 也有类似的功能。Copilot 的聊天模式默认并不会这么做。

始终在上下文中提供确切的版本信息。 如果助手能够访问你的项目配置文件,请充分利用——这会彻底改变回复的质量。

有约束的重构:大多数人会失败的地方

这是最让人沮丧的测试类别,因为助手们往往过于野心勃勃。你让他们重构一个函数,却把整个模块都重写了。

**任务:**我们有一个通知服务(notifications/dispatcher.py),其中有一个 120 行的函数做了太多事。我想把限流逻辑提取到一个单独的类中,但不能更改函数的公共签名,也不能改动现有的测试——27 个集成测试,运行需要 40 秒,我不想动它们。

# La firma que NO podía cambiar:
async def dispatch_notification(
    user_id: int,
    event: NotificationEvent,
    channels: list[NotificationChannel],
    *,
    priority: Priority = Priority.NORMAL,
    metadata: dict | None = None
) -> DispatchResult:
    ...

Copilot 和 Cursor——以不同程度的激进性——都提出了会破坏 API 的改动。Cursor 建议把 channelslist 改为 *args,看似小改动,却会破坏所有传入已构建列表的调用。Copilot 添加了一个没有默认值的新参数,显然会把所有东西都弄坏。

Claude Code 遵守了限制。Windsurf 也遵守了,尽管它的限流器实现有一个微妙的 bug:它只使用 user_id 作为限流键,忽略了 channel,于是如果用户收到大量电子邮件通知,SMS 也会被限流。它进行了一次额外的交流来修正这个问题。

有件事让我意外:当我明确告诉 Cursor “不要更改 dispatch_notification 的公共签名,也不要改变其可观察行为” 时,它的表现显著提升。指令的精准度比我想象的更重要。并不是 Cursor 不能做正确的重构——而是默认情况下,它会假设你允许它改动所有内容,除非你明确说明相反的要求。

生成测试:最具可变性的用例

这里的差异非常大,取决于你正在测试的代码类型。一个一致的模式是:所有助手在为新且结构良好的代码生成测试时都表现出色;但在面对遗留代码和全局依赖时则表现平平。这是有道理的——如果代码难以测试,从外部推理它也会变得困难。

区别各助手的关键在于当遗留代码出现问题时它们的处理方式:它们是直接告诉你,还是仅仅生成看似不错但实际上并未测试你所期望内容的测试?

Claude Code 是唯一在两次不同场合中对我说过类似的话的助手:

“这个函数在全局单例中有副作用;如果不先对其进行 mock,生成的测试会很脆弱——你想让我这么做吗?”

这非常有用。其余助手则直接生成测试并把它们呈现为正确的。我的样本虽小,但足够一致,足以让我注意到这一点。

我的真实推荐 — 直截了当

如果你主要在 IDE 中工作,且代码库规模中等(< 100 k 行)

  • Cursor 仍然是最集成的选项,整体体验更好。编辑器中的自动补全比其他方案更快、更自然。
  • 将后端模型切换为 Claude Sonnet 4.5 —— 输出质量的提升足以抵消额外成本,且延迟在可接受范围。

如果你大量进行架构设计、大规模重构或难度高的 bug 调试

  • Claude Code CLI。它能够读取整个仓库、执行命令并在长会话中保持上下文,这在复杂任务中提供了真实优势。虽然没有集成 IDE 那么舒适——需要适应曲线——但在高复杂度工作中结果显著更好。

Copilot

  • 老实说,如果你已经有 GitHub 订阅,且主要需求是快速自动补全和行内建议,它表现良好。
  • 对于复杂推理或非标准模式的代码理解,它始终落后。Claude 后端在 Copilot 中的集成相对较新——可能会改进。

Windsurf

  • 团队的积极惊喜。对于不想为 Cursor Pro 付费且希望比标准 Copilot 更强大的团队,这是一个认真的选择。
  • 他们用于仓库上下文的 Cascade 模型,表现比预期更好,且价格合理。

有一件事我不会改变,无论使用哪种助手

学习为你的特定上下文编写优秀提示“重构这个”“重构函数 dispatch_notification,将限流逻辑提取到一个单独的类中,保持公共签名不变,并兼容 test_dispatcher.py 中的测试” 之间的差别,就是得到一个无用的结果和一个可以直接使用的结果的区别。助手很强大,但它不是预言家。

这些结果反映了我的技术栈(FastAPI / Python / React)、我的工作风格以及我待办事项中的任务。如果你使用 Rust 或者在一个拥有 200 万行代码的 Java 单体仓库中工作,排名可能会有所不同。但方法论——用真实代码而不是演示来尝试——同样适用。

0 浏览
Back to Blog

相关文章

阅读更多 »