o3-mini、Gemini 2 Flash、Sonnet 3.5 與 DeepSeek 在 Cursor 上的對(duì)決
最新的 OpenAI 模型 o3-mini 已于 1 月 31 日(星期五)發(fā)布,并已在 Cursor 上架。不久后,Gemini 2 Flash 也會(huì)陸續(xù)登場(chǎng)。
上周,對(duì) DeepSeek V3、DeepSeek R1 以及 Claude 3.5 Sonnet 做過(guò)類(lèi)似測(cè)試。那次測(cè)試結(jié)果顯示,在日常開(kāi)發(fā)中,Claude 3.5 Sonnet 的表現(xiàn)明顯優(yōu)于兩個(gè) DeepSeek 版本。不過(guò),新模型上線后,自然得重新用相同任務(wù)對(duì)它們進(jìn)行比較,同時(shí)為了好玩,也把兩個(gè) DeepSeek 模型的數(shù)據(jù)保留下來(lái)。
測(cè)試任務(wù)簡(jiǎn)介
此次測(cè)試主要涵蓋三種模式:聊天(Chat)、代碼生成(Composer) 以及 代理模式(Agent Mode)。需要注意的是,目前代理模式僅支持 Anthropic 和 OpenAI 系列模型,其他模型暫不支持這一功能。
聊天任務(wù)
任務(wù)要求:
檢查 CircleCI 部署配置,并說(shuō)明在部署過(guò)程中如何將靜態(tài) NextJS 資源推送至 Cloudflare。提供的提示內(nèi)容如下:
“解釋在部署過(guò)程中如何將靜態(tài) NextJS 資源上傳到 Cloudflare。”
(同時(shí)我還附上了 CircleCI 配置文件作為參考背景)
期望的回答應(yīng)該包括:
- 正確描述在部署中將靜態(tài)資源送往 Cloudflare 的步驟;
- 針對(duì) NextJS 配置提出建議,說(shuō)明如何使用 Cloudflare 作為 CDN。
o3-mini 的回答
它主要描述了如何配置 Cloudflare Pages,并利用 wrangler CLI 來(lái)部署靜態(tài)資源。不過(guò),實(shí)際上 Cloudflare Pages 并非最佳的 CDN 解決方案。它還提到了更新站點(diǎn) DNS 或設(shè)置反向代理,但細(xì)節(jié)略顯簡(jiǎn)略,而且沒(méi)有指出 NextJS 配置中需要更新的部分。
Claude 3.5 Sonnet 的回答
Sonnet 給出的方案包括安裝 AWS CLI 的步驟,并建議在 NextJS 配置中按如下方式修改:
const nextConfig = {
output: 'standalone',
assetPrefix: process.env.PUBLIC_ASSETS_BASE_URL,
// 其它配置項(xiàng)……
}
同時(shí),它推薦使用 Cloudflare R2,而沒(méi)有提及 Cloudflare Pages。
Gemini 2 Flash 的回答
Gemini 同樣建議選用 Cloudflare R2,并指出可能需要更新 assetPrefix,不過(guò)沒(méi)有深入細(xì)說(shuō)。它給出的 NextJS 配置示例如下:
const nextConfig = {
// 其它配置……
images: {
domains: ['your-site-static-assets-production.r2.dev', 'your-site-static-assets-qa.r2.dev'],
},
};
DeepSeek V3 的回答
DeepSeek V3 除了建議使用 Cloudflare R2,并清楚描述了如何更新 assetPrefix 外,還建議通過(guò)編寫(xiě) TypeScript 輔助文件,再在 CircleCI 中通過(guò) package.json 腳本執(zhí)行上傳操作。雖然這種做法并非錯(cuò)誤,但相比直接使用 CLI 顯得有些繁瑣。
DeepSeek R1 的回答
R1 的方案與 Sonnet 幾乎一模一樣,僅在細(xì)節(jié)上有微小差別。
Composer 代碼生成任務(wù)
在這部分,我提供了一段處理招聘網(wǎng)站相關(guān)功能的服務(wù)端代碼,該代碼用于獲取雇主的招聘信息。任務(wù)要求是在原有的 getEmployers
服務(wù)端操作中增加分頁(yè)和搜索功能,要求:
- 能夠?qū)椭髅Q(chēng)進(jìn)行模糊搜索;
- 接受頁(yè)碼和條數(shù)限制;
- 返回包含總記錄數(shù)及是否有更多記錄的元數(shù)據(jù)。
現(xiàn)有的代碼如下:
export const getEmployers = actionClient.action(async () => {
const profile = await getActiveProfileOrThrowError();
if (profile.type !== "jobBoard") {
throw new Error("Unauthorized");
}
const applications = await db.query.employerJobBoardApplications.findMany({
where: eq(employerJobBoardApplications.jobBoardId, profile.id),
with: {
employer: true,
},
});
return applications;
});
預(yù)期輸出應(yīng)滿足以下幾點(diǎn):
- 識(shí)別出已有代碼使用了 zod schema,因此新增部分也應(yīng)跟進(jìn)這一規(guī)范;
- 高效地計(jì)算分頁(yè)所需的元數(shù)據(jù);
- 對(duì)關(guān)聯(lián)表的雇主名稱(chēng)進(jìn)行正確的模糊查詢。
o3-mini 的回答
它雖然花了一些時(shí)間,但在使用 zod schema 這一部分做得不錯(cuò),也意識(shí)到模糊搜索應(yīng)通過(guò) inner join 來(lái)實(shí)現(xiàn)。不過(guò),它選擇用原生 SQL 語(yǔ)句進(jìn)行模糊搜索,類(lèi)似如下做法:
if (search) {
conditions.push(sql`"employer"."name" ILIKE ${`%${search}%`}`);
}
let totalRecords: number;
if (search) {
const totalCountRes = await db
.select({ count: sql<number>`count(*)` })
.from(employerJobBoardApplications)
.innerJoin(
employers,
eq(employerJobBoardApplications.employerId, employers.id),
)
.where(and(...conditions));
totalRecords = Number(totalCountRes[0]?.count ?? 0);
} else {
const totalCountRes = await db
.select({ count: sql<number>`count(*)` })
.from(employerJobBoardApplications)
.where(baseCondition);
totalRecords = Number(totalCountRes[0]?.count ?? 0);
}
但這種方法在類(lèi)型安全上不夠理想,同時(shí)代碼復(fù)用也有所欠缺。總體來(lái)看,效果一般,還需要進(jìn)一步引導(dǎo)完善。
Claude 3.5 Sonnet 的回答
Sonnet 很好地識(shí)別了原代碼中的 zod schema,并在新代碼中沿用了這一規(guī)范。它將搜索與計(jì)數(shù)的 where 邏輯進(jìn)行了復(fù)用,但在使用 Drizzle ORM 時(shí),inner join 的處理上不夠到位。示例代碼如下:
const where = [eq(employerJobBoardApplications.jobBoardId, profile.id)];
if (search) {
where.push(
ilike(employerJobBoardApplications.employer.name, `%${search}%`),
);
}
const [applications, totalCount] = await Promise.all([
db.query.employerJobBoardApplications.findMany({
where: and(...where),
with: {
employer: true,
},
limit,
offset,
}),
db.query.employerJobBoardApplications.count({
where: and(...where),
}),
]);
在我提示要求使用 inner join 后,它也做出了相應(yīng)調(diào)整。
Gemini 2 Flash 的回答
這款模型反應(yīng)迅速,采用了 zod schema,但和 Sonnet 類(lèi)似,在 inner join 這一環(huán)節(jié)處理不當(dāng),其代碼示例如下:
const [applications, total] = await Promise.all([
db.query.employerJobBoardApplications.findMany({
where: and(...whereConditions),
with: {
employer: true,
},
limit,
offset,
}),
db
.select({
count: sql<number>`count(*)`.mapWith(Number),
})
.from(employerJobBoardApplications)
.where(and(...whereConditions))
.limit(1)
.then((res) => res[0]?.count ?? 0),
]);
DeepSeek V3 的回答
DeepSeek V3 同樣正確使用了 zod schema,但在 inner join 方面沒(méi)有達(dá)到預(yù)期效果。它還重復(fù)書(shū)寫(xiě)了搜索和計(jì)數(shù)的 where 邏輯,沒(méi)有做到代碼共享:
// 獲取總記錄數(shù)
const totalCount = await db
.select({ count: sql<number>`count(*)` })
.from(employerJobBoardApplications)
.where(
and(
eq(employerJobBoardApplications.jobBoardId, profile.id),
search
? ilike(employerJobBoardApplications.employer.name, `%${search}%`)
: undefined,
),
)
.then((res) => res[0]?.count ?? 0);
// 分頁(yè)查詢結(jié)果
const applications = await db.query.employerJobBoardApplications.findMany({
where: and(
eq(employerJobBoardApplications.jobBoardId, profile.id),
search
? ilike(employerJobBoardApplications.employer.name, `%${search}%`)
: undefined,
),
with: {
employer: true,
},
limit,
offset,
});
DeepSeek R1 的回答
R1 同樣使用了 zod schema,但盡管思考得較多,inner join 的實(shí)現(xiàn)還是未能完善,需要進(jìn)一步提示以優(yōu)化 where 邏輯的復(fù)用。示例代碼如下:
// 主查詢:分頁(yè)和搜索
const data = await db.query.employerJobBoardApplications.findMany({
where: (apps, { and, eq, or }) =>
and(
eq(apps.jobBoardId, profile.id),
search ? ilike(apps.employer.name, `%${search}%`) : undefined,
),
with: {
employer: true,
},
offset,
limit,
});
// 總記錄數(shù)查詢
const [countResult] = await db
.select({
count: sql<number>`count(*)`,
})
.from(employerJobBoardApplications)
.where(
and(
eq(employerJobBoardApplications.jobBoardId, profile.id),
search
? ilike(employerJobBoardApplications.employer.name, `%${search}%`)
: undefined,
),
);
代理模式任務(wù)
在代理模式測(cè)試中,我希望模型完成一個(gè)更復(fù)雜、多步驟的任務(wù):在一個(gè)項(xiàng)目模板中增加新用戶引導(dǎo)流程。要求如下:
- 在用戶表中添加三個(gè)字段:布爾類(lèi)型的
isOnboardingComplete
、字符串類(lèi)型的onboardingPersona
,以及 JSON 字符串?dāng)?shù)組onboardingTopics
; - 當(dāng)用戶登錄且未完成引導(dǎo)時(shí),頁(yè)面上應(yīng)彈出一個(gè)對(duì)話框,該對(duì)話框內(nèi)包含相應(yīng)的表單供用戶選擇;
- 表單提交后,通過(guò)服務(wù)端操作(采用 next-safe-action)更新引導(dǎo)狀態(tài)。
需要注意的是,用戶表定義在 Drizzle ORM 的 schema 文件中,模型需要自動(dòng)找到并修改相關(guān)定義,同時(shí)確保引導(dǎo)流程能夠正常工作,且 next-safe-action 的使用與項(xiàng)目中其它部分保持一致。
o3-mini 的回答
o3-mini 在這部分的表現(xiàn)較差。首先,它響應(yīng)較慢,可能是內(nèi)部“思考”時(shí)間過(guò)長(zhǎng),而非網(wǎng)絡(luò)問(wèn)題。第一次嘗試時(shí),輸出似乎中途截?cái)啵詈笠痪湎袷牵骸敖酉聛?lái)我將更新用戶表 schema 來(lái)禁用針對(duì) JSON 列的 linter 錯(cuò)誤……”,顯然未完成;第二次嘗試時(shí),則發(fā)現(xiàn)生成結(jié)果僅在部分地方停留在提示狀態(tài),例如:“對(duì)于對(duì)話框,你可以這樣實(shí)現(xiàn)……”,給出了占位符示例,但任務(wù)并未完全實(shí)現(xiàn)。
此外,第一次生成的方案中存在一些明顯問(wèn)題:
- 文件被直接放在 monorepo 根目錄,而預(yù)期應(yīng)該在 next-app 目錄下;
- 自動(dòng)生成了一個(gè) global.d.ts 文件,用以定義 drizzle-orm 等包的類(lèi)型,但在正確的 monorepo 結(jié)構(gòu)中其實(shí)并不需要;
- 生成的服務(wù)端操作未沿用項(xiàng)目中統(tǒng)一的 zod schema;
- 對(duì)話框組件雖然正確調(diào)用了 Shadcn UI 組件,但卻采用了內(nèi)聯(lián)樣式,而非項(xiàng)目中普遍使用的 tailwind 類(lèi)。
整體來(lái)看,o3-mini 在處理 monorepo 環(huán)境時(shí)明顯遇到了困難。
Claude 3.5 Sonnet 的回答
Sonnet 對(duì)用戶表 schema 的修改做得正確,為實(shí)現(xiàn)對(duì)話框功能,它選擇在整個(gè)應(yīng)用外層包裹一個(gè)包裝組件,其示例代碼如下:
export function OnboardingWrapper({ children }: Props) {
const { isOpen } = useOnboarding();
return (
<>
<OnboardingDialog isOpen={isOpen} />
{children}
</>
);
}
包裝組件中用到的 useOnboarding
鉤子定義如下:
import { useEffect, useState } from "react";
import { getUser } from "../actions/user";
export function useOnboarding() {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
const checkOnboarding = async () => {
const user = await getUser();
if (user && !user.isOnboardingComplete) {
setIsOpen(true);
}
};
checkOnboarding();
}, []);
return { isOpen };
}
不過(guò),這里有個(gè)問(wèn)題:直接在鉤子中調(diào)用服務(wù)端操作是不被允許的(除非該操作是通過(guò) next-safe-action 封裝的)。此外,這種實(shí)現(xiàn)會(huì)導(dǎo)致頁(yè)面首次加載時(shí)延遲顯示對(duì)話框,等 getUser 請(qǐng)求完成后才出現(xiàn)。好在對(duì)話框組件本身表現(xiàn)不錯(cuò),且 next-safe-action 的用法也正確;它甚至試圖使用 Select 組件來(lái)適應(yīng)前端的 Shadcn UI 風(fēng)格(盡管項(xiàng)目中尚未加入該組件)。生成的服務(wù)端操作代碼基本無(wú)誤,但在 next-safe-action 的語(yǔ)法上略有偏差,建議參照項(xiàng)目中已有用法作出調(diào)整。
DeepSeek 與 Gemini 2 Flash(代理模式)
目前這兩款模型在 Cursor 平臺(tái)上還不支持代理模式,這部分測(cè)試只能留待未來(lái)補(bǔ)充。
總結(jié)
雖然對(duì) o3-mini 和 Gemini 2 Flash 都充滿期待,但在實(shí)際開(kāi)發(fā)中的表現(xiàn)并沒(méi)有超出預(yù)期。所有模型在處理這些實(shí)際任務(wù)時(shí)都有各自的不足,連 Claude 3.5 Sonnet 也不例外,實(shí)際效果與各類(lèi)公開(kāi)的編碼基準(zhǔn)測(cè)試結(jié)果存在明顯落差。特別是在代理模式測(cè)試中,o3-mini 在 monorepo 環(huán)境下的表現(xiàn)不佳。由于經(jīng)常依賴(lài)代理模式,并且非常喜歡 monorepo 架構(gòu),目前的選擇仍會(huì)傾向于使用 Claude 3.5 Sonnet。