AI 自动编程评测:Cline vs GitHub Copilot vs Cursor

标签:AI

最近 Cursor 很火,而我一直觉得免费的自动补全工具就已经够用了,花钱使用自动编程工具似乎有些浪费。然而最近的一次经历让我对其大为改观:Cline + Claude 3.5 Sonnet 在 5 分钟内就帮我解决了一个几天都没有搞定的问题。

好奇之下,我准备评测一番各种自动编程的工具,看看谁是目前的王者。

这次的任务背景如下:
公司有一个服务需要调用多种大语言模型的 API,在接入其他模型时,都能通过 HTTP 接口异步调用,唯独 AWS Redrock 提供的 Claude 模型没有。
我花了几天时间去搜索文档和寻找资料,只得到这个结论:AWS Redrock 没有提供 HTTP 的接口文档,官方只提供了同步库(如 boto3)来进行调用,没有异步实现。
大约半年前,我曾向 AWS 的工程师请求帮助,得到了一份复杂的代码片段,介绍了如何为 HTTP 接口调用添加签名。我简单尝试了一下,发现签名仍然错误。也许再多花几个小时修改代码,我有可能可以解决这个问题,但当时业务重心偏向 GPT-4o 模型,所以我就将其搁置了。
如今有了 AI 自动编程工具,我决定让它们来解决这个问题。

以下是简化后的代码片段:
import asyncio
import json
from datetime import UTC, datetime, timedelta
from typing import Iterable, Sequence

import boto3
from botocore.credentials import RefreshableCredentials
from botocore.session import get_session

from app.config import config
from app.schemas.claude import ClaudeContent, ClaudeMessage
from app.schemas.completion_result import CompletionResult, Usage
from app.schemas.message import Message
from app.schemas.model_name import ModelName
from app.schemas.role import Role

from .base import AIModel


class Claude(AIModel):
    def __init__(self) -> None:
        refreshable_credentials = RefreshableCredentials.create_from_metadata(
            metadata=self.get_credentials(),
            refresh_using=self.get_credentials,
            method='sts-assume-role',
        )

        session = get_session()
        session._credentials = refreshable_credentials
        autorefresh_session = boto3.Session(botocore_session=session)
        self.client = autorefresh_session.client(
            service_name='bedrock-runtime',
            region_name=config.CLAUDE_REGION_NAME,
        )

    def get_credentials(self):
        session = boto3.Session()
        frozen_credentials = session.get_credentials().get_frozen_credentials()
        credentials = {
            'access_key': frozen_credentials.access_key,
            'secret_key': frozen_credentials.secret_key,
            'token': frozen_credentials.token,
            'expiry_time': (datetime.now(UTC) + timedelta(hours=11)).isoformat(),
        }
        return credentials

    async def shutdown(self) -> None:
        await asyncio.to_thread(self.client.close)

    @classmethod
    def dump_messages(cls, messages: Iterable[ClaudeMessage]) -> Iterable:
        return [m.model_dump() for m in messages]

    def sync_chat(
        self,
        messages: Iterable[ClaudeMessage],
        temperature: float | None = None,
        top_p: float | None = None,
        model_name: ModelName = ModelName.CLAUDE_3_5_SONNET,
    ) -> CompletionResult:
        data = {
            'anthropic_version': 'bedrock-2023-05-31',
            'messages': [message.model_dump() for message in messages],
        }
        if temperature is not None:
            data['temperature'] = temperature
        if top_p is not None:
            data['top_p'] = top_p
        response = self.client.invoke_model(
            modelId=model_name,
            body=json.dumps(data),
        )

        body = json.loads(response['body'].read())
        usage = body['usage']
        model = body['model']
        result = CompletionResult(
            content='',
            model=model,
            usage=Usage(**usage),
        )
        content = body.get('content')
        if content and len(content) > 0:
            result.content = content[0]['text']

        return result

    async def chat(
        self,
        messages: Iterable[ClaudeMessage],
        temperature: float | None = None,
        top_p: float | None = None,
        model_name: ModelName = ModelName.CLAUDE_3_5_SONNET,
    ) -> CompletionResult:
        return await asyncio.to_thread(self.sync_chat, messages, temperature, top_p, model_name)

首先登场的是一号选手 Cline。它可以接入各种模型的 API,其中就包括 AWS Redrock 提供的 Claude 模型。
由于公司的 AWS 账号只启用了 anthropic.claude-3-5-sonnet-20240620-v1:0 模型,还没启用更新的 anthropic.claude-3-5-sonnet-20241022-v2:0 版本,我就先用老版本来测试吧。配置好 API KEY 并赋予所有权限后,评测正式开始。

先问一个问题:可以把Claude类改成使用HTTP异步调用的实现吗?
Cline 在 2 分钟内自问自答了 5 次后,代码已被它修改成功,花费约 $0.3。
我测试了一下单元测试,发现失败了:Access to Anthropic models is not allowed from unsupported countries, regions, or territories。
我直接把异常栈发给它,它又给我加上了 try ... except ...
于是我直白地告诉它,需要使用代理,于是它又加上了 aiohttp_socks,并贴心地把 requirements.txt 文件和我的配置文件也改了。
现在单元测试已经通过,消耗 734k / 12k (输入 / 输出) tokens,花费约 $2.38(这个模型无 prompt caching 优惠),一共耗时约 5 分钟。
但是还有一个需求没有实现,我继续命令它:之前的同步实现中,我使用了自动刷新凭证,现在的异步实现中是否能实现?
这次消耗 60k / 2k tokens,花费约 $0.21,完美完成要求。
我手动调整了一下代码,去掉 aiohttp_socks,最终结果如下:
import json
import logging
from datetime import UTC, datetime, timedelta
from typing import Iterable, Sequence

from aiohttp import ClientSession
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
from botocore.credentials import RefreshableCredentials
from botocore.session import get_session

from app.config import config
from app.schemas.claude import ClaudeContent, ClaudeMessage
from app.schemas.completion_result import CompletionResult, Usage
from app.schemas.message import Message
from app.schemas.model_name import ModelName
from app.schemas.role import Role

from .base import AIModel


class Claude(AIModel):
    def __init__(self) -> None:
        self.session = ClientSession(trust_env=True)  # 使用环境变量配置代理,Claude 禁止中国访问
        self.botocore_session = get_session()
        self.credentials = RefreshableCredentials.create_from_metadata(
            metadata=self._get_credentials(), refresh_using=self._get_credentials, method='sts-assume-role'
        )
        self.botocore_session._credentials = self.credentials

    def _get_credentials(self):
        frozen_credentials = self.botocore_session.get_credentials().get_frozen_credentials()
        return {
            'access_key': frozen_credentials.access_key,
            'secret_key': frozen_credentials.secret_key,
            'token': frozen_credentials.token,
            'expiry_time': (
                datetime.now(UTC) + timedelta(hours=11)
            ).isoformat(),  # 默认的 credentials 过期时间是 12 小时,这里让它 11 小时刷新一次
        }

    def _sign_request(self, method: str, url: str, data: dict) -> dict:
        request = AWSRequest(method=method, url=url, data=json.dumps(data))
        SigV4Auth(self.credentials, 'bedrock', config.CLAUDE_REGION_NAME).add_auth(request)
        return dict(request.headers)

    async def shutdown(self) -> None:
        await self.session.close()

    @classmethod
    def dump_messages(cls, messages: Iterable[ClaudeMessage]) -> Iterable:
        return [m.model_dump() for m in messages]

    async def chat(
        self,
        messages: Iterable[ClaudeMessage],
        temperature: float | None = None,
        top_p: float | None = None,
        model_name: ModelName = ModelName.CLAUDE_3_5_SONNET,
    ) -> CompletionResult:
        data = {
            'anthropic_version': 'bedrock-2023-05-31',
            'messages': [message.model_dump() for message in messages],
        }
        if temperature is not None:
            data['temperature'] = temperature
        if top_p is not None:
            data['top_p'] = top_p
        url = f'https://bedrock-runtime.{config.CLAUDE_REGION_NAME}.amazonaws.com/model/{model_name}/invoke'
        headers = self._sign_request('POST', url, data)
        headers.update(
            {
                'Content-Type': 'application/json',
                'Accept': 'application/json',
            }
        )

        async with self.session.post(url, json=data, headers=headers) as response:
            response.raise_for_status()

            try:
                body = await response.json()
                usage = body['usage']
            except Exception:
                logging.exception('Failed to parse Claude response: %s', response.text)
                raise

        usage = body['usage']
        model = body['model']
        result = CompletionResult(
            content='',
            model=model,
            usage=Usage(**usage),
        )
        content = body.get('content')
        if content and len(content) > 0:
            result.content = content[0]['text']

        return result

一号选手的表现非常不错,5 分钟完成了我几天都没干完的活。
我看了下日志,发现它会发送项目里的文件列表和打开的文件列表给模型参考,所以项目里少放一些不相关的文件,以及少打开一些文件,可能会对节省 tokens 有帮助。
另外,如果有继续修改的意见,直接问会附带历史记录,导致 tokens 开销很大,回答也慢。在不需要上下文时,新开一个任务可能更加好。

接下来换二号选手 Cline + gemini-2.0-flash-thinking-exp-1219 上场。这是一个号称和 OpenAI o1 相当的模型,而且是免费的。虽然有频率限制(每分钟 10 次,每天 1500 次),但是用于编程肯定是用不完的。
然而结果却让我大失所望,它自问自答了 20 次后,达到了默认的上限停下来了。看日志就是一个劲地尝试错误方法,有点钻牛角尖。
但是让它在一号选手的初版代码的基础上加上代理,它却会使用 trust_env=True 参数来设置,比添加 aiohttp_socks 库要更好。

然后换三号选手 Cline + DeepSeek-V2.5-1210 上场。DeepSeek 注册就送一个月 500 万 token(10 CNY)的额度,而且缓存的提示词价格是普通的 1/10(其他家大部分是 1/2)。
它的表现明显不是很聪明,经常带来各种小问题,需要反复提问和修改。在日志中也可以看到模型生成的 diff 经常不正确,导致需要重试。并且有一点非常影响体验:在执行命令后不能自动继续运行,需要人工告诉它已经执行完了,请继续修改。
我来来回回花了约半小时,终于得到了一个可以正常执行的版本:
import asyncio
import json
from typing import Iterable, Sequence

import aioboto3
from botocore.config import Config

from app.config import config
from app.schemas.claude import ClaudeContent, ClaudeMessage
from app.schemas.completion_result import CompletionResult, Usage
from app.schemas.message import Message
from app.schemas.model_name import ModelName
from app.schemas.role import Role

from .base import AIModel


class Claude(AIModel):
    def __init__(self) -> None:
        self.session = aioboto3.Session()
        self.bedrock_config = Config(retries={'max_attempts': 3, 'mode': 'standard'})
        self.bedrock = None

    async def shutdown(self) -> None:
        if self.bedrock is not None:
            try:
                await self.bedrock.__aexit__(None, None, None)
            except Exception:
                pass
        await asyncio.sleep(0.25)  # 等待连接关闭

    @classmethod
    def dump_messages(cls, messages: Iterable[ClaudeMessage]) -> Iterable:
        return [m.model_dump() for m in messages]

    async def chat(
        self,
        messages: Iterable[ClaudeMessage],
        temperature: float | None = None,
        top_p: float | None = None,
        model_name: ModelName = ModelName.CLAUDE_3_5_SONNET,
    ) -> CompletionResult:
        data = {
            'anthropic_version': 'bedrock-2023-05-31',
            'messages': [message.model_dump() for message in messages],
        }
        if temperature is not None:
            data['temperature'] = temperature
        if top_p is not None:
            data['top_p'] = top_p

        if not self.bedrock:
            self.bedrock = await self.session.client(
                'bedrock-runtime', region_name=config.CLAUDE_REGION_NAME, config=self.bedrock_config
            ).__aenter__()
        response = await self.bedrock.invoke_model(modelId=model_name, body=json.dumps(data))
        body = json.loads(await response['body'].read())
        usage = body['usage']
        model = body['model']
        result = CompletionResult(
            content='',
            model=model,
            usage=Usage(**usage),
        )
        content = body.get('content')
        if content and len(content) > 0:
            result.content = content[0]['text']

        return result
这里它并没有用异步 HTTP 实现,而是用 aioboto3 这个非官方的异步库,勉强也算是异步实现吧。
到此已经消耗了 1.5M / 11k tokens,和官网的计费有少许差异,其中大概 85% 是缓存的 tokens。

最大的问题是代码里直接调用了 __aenter____aexit__,理论上应该用 async with 来封装的。我让它修改一下,结果遇到了 tokens 超过 64k 的错误提示,只好新开一个任务。
然而又花了约半小时,各种修改后,它仍然没有解决这个问题。
最终官网统计用了 1.85M / 17k tokens,其中 1.6M 命中缓存,实际费用 0.42 CNY。要说费用的话,大概只有 Claude 3.5 Sonnet 的 1/40,但是算上时间成本的话,我觉得 Claude 3.5 Sonnet 完胜。

试完三名 Cline 选手后,我换成了 GitHub Copilot。它前几天刚推出了免费版,每个月可以对话 50 次,有 2000 次的代码补全。收费版则是 $10/月或 $100/年,没有对话和补全的限制,且可以使用 o1 等高级模型。由于我是免费用户,只能选择目前最好的 Claude 3.5 Sonnet (Preview) 模型。
它的体验明显差了很多,代码是在聊天对话框中返回的,需要点击 Apply in Editor 按钮并等待一段时间才能插入代码编辑器,然后再像 git diff 一样选择是否接受修改。如果没有接受或拒绝的话,是不能继续提问的。
它采用的实现方案也是用 aioboto3,但是我花了十几分钟,最终仍没有得到可以正常执行的代码。虽然都是用 Claude 3.5 Sonnet 模型,但是体验和智能上都相差很多。

下面来试试久负盛名的 Cursor。它每月可以使用 250 次快速的高级模型,费用是 $20/月 或 $192/年(8 折)。
我先使用了 claude-3-5-sonnet-20241022 模型。
它有 chat 和 composer 两种模式,前者的体验类似 GitHub Copilot,但是有一些体验上的改进:
  • 虽然也需要手动点击按钮来 apply,但是 apply 的过程可以即时完成。
  • diff 的界面更清晰些。
  • 有快捷键可以 accept 代码片段或整个文件的改动。
  • 复制代码或日志时,会自动添加到上下文中。
  • 上下文里可以手动选择文件。
一番操作后,我问了 10 个问题,任务还是没有完成。它反复地在尝试自己实现签名,失败多次后,我提示它可以用 botocore.auth.SigV4Auth 来处理签名,但结果还是签名错误。
Composer 模式则智能很多,可以分步骤自动完成,但最终还需要用户手动 accept。在这种模式下,我用了 5 次问答,终于通过测试用例了。其中 3 次是签名不正确,我让它修改,但它总是认为是代理导致了签名错误,直到我告诉它我认为代理不会影响后,它才终于改对,生成代码如下:
import asyncio
import json
import logging
from datetime import UTC, datetime, timedelta
from typing import Iterable, Sequence
from urllib.parse import urlparse

import aiohttp
import boto3
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
from botocore.credentials import Credentials

from app.config import config
from app.schemas.claude import ClaudeContent, ClaudeMessage
from app.schemas.completion_result import CompletionResult, Usage
from app.schemas.message import Message
from app.schemas.model_name import ModelName
from app.schemas.role import Role

from .base import AIModel

logger = logging.getLogger(__name__)


class Claude(AIModel):
    def __init__(self) -> None:
        # 配置代理
        self.proxy = None
        if hasattr(config, 'HTTP_PROXY') and config.HTTP_PROXY:
            self.proxy = config.HTTP_PROXY
            connector = aiohttp.TCPConnector(ssl=False)  # 如果需要忽略 SSL 证书验证
            self.session = aiohttp.ClientSession(
                connector=connector,
            )
        else:
            self.session = aiohttp.ClientSession()

        self.endpoint = f'https://bedrock-runtime.{config.CLAUDE_REGION_NAME}.amazonaws.com'
        self.service = 'bedrock'
        self.region = config.CLAUDE_REGION_NAME

    def get_credentials(self) -> Credentials:
        session = boto3.Session()
        frozen_credentials = session.get_credentials().get_frozen_credentials()
        return Credentials(
            access_key=frozen_credentials.access_key,
            secret_key=frozen_credentials.secret_key,
            token=frozen_credentials.token,
        )

    def sign_request(self, request_url: str, body: str) -> dict:
        credentials = self.get_credentials()
        parsed_url = urlparse(request_url)
        host = parsed_url.netloc

        # 计算请求体的 SHA256 哈希
        import hashlib

        body_hash = hashlib.sha256(body.encode('utf-8')).hexdigest()

        headers = {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
            'Host': host,
            'X-Amz-Content-Sha256': body_hash,
        }

        request = AWSRequest(
            method='POST',
            url=request_url,
            data=body.encode('utf-8'),
            headers=headers,
        )

        signer = SigV4Auth(credentials, self.service, self.region)
        signer.add_auth(request)
        return dict(request.headers)

    async def shutdown(self) -> None:
        await self.session.close()

    @classmethod
    def dump_messages(cls, messages: Iterable[ClaudeMessage]) -> Iterable:
        return [m.model_dump() for m in messages]

    async def sync_chat(
        self,
        messages: Iterable[ClaudeMessage],
        temperature: float | None = None,
        top_p: float | None = None,
        model_name: ModelName = ModelName.CLAUDE_3_5_SONNET,
    ) -> CompletionResult:
        data = {
            'anthropic_version': 'bedrock-2023-05-31',
            'messages': [message.model_dump() for message in messages],
        }
        if temperature is not None:
            data['temperature'] = temperature
        if top_p is not None:
            data['top_p'] = top_p

        request_url = f'{self.endpoint}/model/{model_name}/invoke'
        body = json.dumps(data)
        headers = self.sign_request(request_url, body)
        logger.debug('Request URL: %s', request_url)
        logger.debug('Request headers: %s', headers)
        logger.debug('Request body hash: %s', headers.get('X-Amz-Content-Sha256'))

        proxy = self.proxy
        try:
            async with self.session.post(request_url, headers=headers, data=body, proxy=proxy) as response:
                if not response.ok:
                    error_text = await response.text()
                    logger.error('Request failed with status %d: %s', response.status, error_text)
                    raise aiohttp.ClientError(f'Request failed with status {response.status}: {error_text}')
                body = await response.json()
        except Exception as e:
            logger.exception('Request failed')
            raise

        usage = body['usage']
        model = body['model']
        result = CompletionResult(
            content='',
            model=model,
            usage=Usage(**usage),
        )
        content = body.get('content')
        if content and len(content) > 0:
            result.content = content[0]['text']

        return result

    async def chat(
        self,
        messages: Iterable[ClaudeMessage],
        temperature: float | None = None,
        top_p: float | None = None,
        model_name: ModelName = ModelName.CLAUDE_3_5_SONNET,
    ) -> CompletionResult:
        return await self.sync_chat(messages, temperature, top_p, model_name, app_name)
大致的问题有:
  1. 已经改成异步了,函数名还叫 sync_chat
  2. 配置代理太复杂。
  3. 在函数内 import。
  4. 加了太多日志和异常相关代码。

最后有请压轴选手 Cursor + o1-mini(免费用户不能使用 o1 和 o1-preview)登场。
这次它终于在 4 次对话后,得到了一个槽点满满的实现:
import asyncio
import json
import os
from datetime import UTC, datetime, timedelta
from typing import Iterable, Sequence

import aiohttp
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
from botocore.exceptions import NoCredentialsError, PartialCredentialsError
from botocore.session import get_session

from app.config import config
from app.schemas.claude import ClaudeContent, ClaudeMessage
from app.schemas.completion_result import CompletionResult, Usage
from app.schemas.message import Message
from app.schemas.model_name import ModelName
from app.schemas.role import Role

from .base import AIModel


class Claude(AIModel):
    def __init__(self) -> None:
        self.base_url = f'https://bedrock.{config.CLAUDE_REGION_NAME}.amazonaws.com'
        self.credentials, self.region = self.get_credentials()
        proxy = os.getenv('HTTP_PROXY') or os.getenv('HTTPS_PROXY')
        connector = aiohttp.TCPConnector(ssl=False) if proxy else aiohttp.TCPConnector()
        self.session = aiohttp.ClientSession(connector=connector)

    def get_credentials(self):
        """
        从 ~/.aws/credentials 和 ~/.aws/config 自动读取 AWS 凭证和区域信息。
        """
        session = get_session()
        try:
            credentials = session.get_credentials()
            if credentials is None:
                raise NoCredentialsError
            # 获取区域信息
            region = session.get_config_variable('region')
            if region is None:
                region = config.DEFAULT_AWS_REGION  # 确保在配置中有默认区域
            return credentials, region
        except (NoCredentialsError, PartialCredentialsError) as e:
            raise e

    async def shutdown(self) -> None:
        await self.session.close()

    @classmethod
    def dump_messages(cls, messages: Iterable[ClaudeMessage]) -> Iterable:
        return [m.model_dump() for m in messages]

    def create_signed_headers(self, method: str, url: str, body: str) -> dict:
        """
        使��� SigV4 对请求进行签名,并返回带有认证信息的头部。
        """
        parsed_url = aiohttp.helpers.URL(url)
        request = AWSRequest(method=method, url=url, data=body, headers={'Content-Type': 'application/json'})
        SigV4Auth(self.credentials, 'bedrock', self.region).add_auth(request)
        return dict(request.headers)

    async def sync_chat(
        self,
        messages: Iterable[ClaudeMessage],
        temperature: float | None = None,
        top_p: float | None = None,
        model_name: ModelName = ModelName.CLAUDE_3_5_SONNET,
    ) -> CompletionResult:
        data = {
            'anthropic_version': 'bedrock-2023-05-31',
            'messages': [message.model_dump() for message in messages],
        }
        if temperature is not None:
            data['temperature'] = temperature
        if top_p is not None:
            data['top_p'] = top_p

        body = json.dumps({'modelId': model_name.value, 'body': data})

        url = f'{self.base_url}/invoke_model'

        headers = self.create_signed_headers('POST', url, body)

        proxy = os.getenv('HTTP_PROXY') or os.getenv('HTTPS_PROXY')

        async with self.session.post(url, headers=headers, data=body, proxy=proxy) as response:
            if response.status != 200:
                error_text = await response.text()
                raise aiohttp.ClientResponseError(
                    status=response.status,
                    message=error_text,
                    request_info=response.request_info,
                    history=response.history,
                )
            body = await response.json()
            usage = body.get('usage', {})
            model = body.get('model', '')
            result = CompletionResult(
                content='',
                model=model,
                usage=Usage(**usage),
            )
            content = body.get('content')
            if content and len(content) > 0:
                result.content = content[0].get('text', '')

            return result

    async def chat(
        self,
        messages: Iterable[ClaudeMessage],
        temperature: float | None = None,
        top_p: float | None = None,
        model_name: ModelName = ModelName.CLAUDE_3_5_SONNET,
    ) -> CompletionResult:
        return await self.sync_chat(messages, temperature, top_p, model_name, app_name)
我大致列一些不足之处吧:
  1. 已经改成异步了,函数名还叫 sync_chat
  2. 生成了乱码。
  3. 读取 AWS 凭证时加了一堆额外代码。
  4. 自己实现了环境变量的读取。
  5. usage 的默认值不对。
  6. 缺少自动刷新。
这个模型不支持 composer 模式,因此评测就到此为止了。

综上所述,Cline 的用户体验才是最棒的,可以通过自问自答来一步步完成任务,不需要手动接受改动,会自己寻找并修改所需的文件,并且可以自动执行本地命令来实现安装库和修改文件等操作。
而冠军选手则毫无争议是 Cline + Claude 3.5 Sonnet,花的时间最少,生成的代码也最好,再贵也比工资便宜。
省钱也可以使用 Cursor + Claude 3.5 Sonnet 的 composer 模式,体验稍差,需要人工修改的地方稍多,但也能干活。
顺带一提,Windsurf 比 Cursor 的 composer 模式更接近 Cline 的体验,且价格更便宜($15/月)。但是因为付费用户才能用 Claude 3.5 Sonnet 模型,这里我就不测试了。

2024年12月27日更新:
DeepSeek 于昨日夜间突然更新到 DeepSeek-V3 模型,于是继续用 Cline 测试了一下。它尝试了 3 次自己实现签名,但是都失败了,直到我提示它用 botocore.auth.SigV4Auth 来处理签名,才终于改对。
5 次会话共消耗 280k / 10k tokens,花费 0.11 CNY(优惠期过后大约会涨到 0.3 CNY)。
评价是有所改观,但仍然不如 Claude 3.5 Sonnet。


2025年1月7日更新:
可以用 Roo-Cline 替代 Cline,它可以自定义运行自动执行的命令。
Cursor 在代码补全上的体验确实更好,它会根据全文来补完其他位置的修改(包括添加、删除和替换),而不只是补充光标后的文本。另外,在配置中启用 yolo 模式后,也可以自动执行命令。

0条评论 你不来一发么↓

    想说点什么呢?