o3-mini、Gemini 2 Flash、Sonnet 3.5 與 DeepSeek 在 Cursor 上的對決
最新的 OpenAI 模型 o3-mini 已于 1 月 31 日(星期五)發(fā)布,并已在 Cursor 上架。不久后,Gemini 2 Flash 也會陸續(xù)登場。
上周,對 DeepSeek V3、DeepSeek R1 以及 Claude 3.5 Sonnet 做過類似測試。那次測試結(jié)果顯示,在日常開發(fā)中,Claude 3.5 Sonnet 的表現(xiàn)明顯優(yōu)于兩個(gè) DeepSeek 版本。不過,新模型上線后,自然得重新用相同任務(wù)對它們進(jìn)行比較,同時(shí)為了好玩,也把兩個(gè) DeepSeek 模型的數(shù)據(jù)保留下來。
測試任務(wù)簡介
此次測試主要涵蓋三種模式:聊天(Chat)、代碼生成(Composer) 以及 代理模式(Agent Mode)。需要注意的是,目前代理模式僅支持 Anthropic 和 OpenAI 系列模型,其他模型暫不支持這一功能。
聊天任務(wù)
任務(wù)要求:
檢查 CircleCI 部署配置,并說明在部署過程中如何將靜態(tài) NextJS 資源推送至 Cloudflare。提供的提示內(nèi)容如下:
“解釋在部署過程中如何將靜態(tài) NextJS 資源上傳到 Cloudflare。”
(同時(shí)我還附上了 CircleCI 配置文件作為參考背景)
期望的回答應(yīng)該包括:
- 正確描述在部署中將靜態(tài)資源送往 Cloudflare 的步驟;
- 針對 NextJS 配置提出建議,說明如何使用 Cloudflare 作為 CDN。
o3-mini 的回答
它主要描述了如何配置 Cloudflare Pages,并利用 wrangler CLI 來部署靜態(tài)資源。不過,實(shí)際上 Cloudflare Pages 并非最佳的 CDN 解決方案。它還提到了更新站點(diǎn) DNS 或設(shè)置反向代理,但細(xì)節(jié)略顯簡略,而且沒有指出 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,而沒有提及 Cloudflare Pages。
Gemini 2 Flash 的回答
Gemini 同樣建議選用 Cloudflare R2,并指出可能需要更新 assetPrefix,不過沒有深入細(xì)說。它給出的 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 外,還建議通過編寫 TypeScript 輔助文件,再在 CircleCI 中通過 package.json 腳本執(zhí)行上傳操作。雖然這種做法并非錯(cuò)誤,但相比直接使用 CLI 顯得有些繁瑣。
DeepSeek R1 的回答
R1 的方案與 Sonnet 幾乎一模一樣,僅在細(xì)節(jié)上有微小差別。
Composer 代碼生成任務(wù)
在這部分,我提供了一段處理招聘網(wǎng)站相關(guān)功能的服務(wù)端代碼,該代碼用于獲取雇主的招聘信息。任務(wù)要求是在原有的 getEmployers
服務(wù)端操作中增加分頁和搜索功能,要求:
- 能夠?qū)椭髅Q進(jìn)行模糊搜索;
- 接受頁碼和條數(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):
- 識別出已有代碼使用了 zod schema,因此新增部分也應(yīng)跟進(jìn)這一規(guī)范;
- 高效地計(jì)算分頁所需的元數(shù)據(jù);
- 對關(guān)聯(lián)表的雇主名稱進(jìn)行正確的模糊查詢。
o3-mini 的回答
它雖然花了一些時(shí)間,但在使用 zod schema 這一部分做得不錯(cuò),也意識到模糊搜索應(yīng)通過 inner join 來實(shí)現(xiàn)。不過,它選擇用原生 SQL 語句進(jìn)行模糊搜索,類似如下做法:
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);
}
但這種方法在類型安全上不夠理想,同時(shí)代碼復(fù)用也有所欠缺??傮w來看,效果一般,還需要進(jìn)一步引導(dǎo)完善。
Claude 3.5 Sonnet 的回答
Sonnet 很好地識別了原代碼中的 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 類似,在 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 方面沒有達(dá)到預(yù)期效果。它還重復(fù)書寫了搜索和計(jì)數(shù)的 where 邏輯,沒有做到代碼共享:
// 獲取總記錄數(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);
// 分頁查詢結(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ù)用。示例代碼如下:
// 主查詢:分頁和搜索
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ù)
在代理模式測試中,我希望模型完成一個(gè)更復(fù)雜、多步驟的任務(wù):在一個(gè)項(xiàng)目模板中增加新用戶引導(dǎo)流程。要求如下:
- 在用戶表中添加三個(gè)字段:布爾類型的
isOnboardingComplete
、字符串類型的onboardingPersona
,以及 JSON 字符串?dāng)?shù)組onboardingTopics
; - 當(dāng)用戶登錄且未完成引導(dǎo)時(shí),頁面上應(yīng)彈出一個(gè)對話框,該對話框內(nèi)包含相應(yīng)的表單供用戶選擇;
- 表單提交后,通過服務(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í)間過長,而非網(wǎng)絡(luò)問題。第一次嘗試時(shí),輸出似乎中途截?cái)?,最后一句像是:“接下來我將更新用戶?schema 來禁用針對 JSON 列的 linter 錯(cuò)誤……”,顯然未完成;第二次嘗試時(shí),則發(fā)現(xiàn)生成結(jié)果僅在部分地方停留在提示狀態(tài),例如:“對于對話框,你可以這樣實(shí)現(xiàn)……”,給出了占位符示例,但任務(wù)并未完全實(shí)現(xiàn)。
此外,第一次生成的方案中存在一些明顯問題:
- 文件被直接放在 monorepo 根目錄,而預(yù)期應(yīng)該在 next-app 目錄下;
- 自動(dòng)生成了一個(gè) global.d.ts 文件,用以定義 drizzle-orm 等包的類型,但在正確的 monorepo 結(jié)構(gòu)中其實(shí)并不需要;
- 生成的服務(wù)端操作未沿用項(xiàng)目中統(tǒng)一的 zod schema;
- 對話框組件雖然正確調(diào)用了 Shadcn UI 組件,但卻采用了內(nèi)聯(lián)樣式,而非項(xiàng)目中普遍使用的 tailwind 類。
整體來看,o3-mini 在處理 monorepo 環(huán)境時(shí)明顯遇到了困難。
Claude 3.5 Sonnet 的回答
Sonnet 對用戶表 schema 的修改做得正確,為實(shí)現(xiàn)對話框功能,它選擇在整個(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 };
}
不過,這里有個(gè)問題:直接在鉤子中調(diào)用服務(wù)端操作是不被允許的(除非該操作是通過 next-safe-action 封裝的)。此外,這種實(shí)現(xiàn)會導(dǎo)致頁面首次加載時(shí)延遲顯示對話框,等 getUser 請求完成后才出現(xiàn)。好在對話框組件本身表現(xiàn)不錯(cuò),且 next-safe-action 的用法也正確;它甚至試圖使用 Select 組件來適應(yīng)前端的 Shadcn UI 風(fēng)格(盡管項(xiàng)目中尚未加入該組件)。生成的服務(wù)端操作代碼基本無誤,但在 next-safe-action 的語法上略有偏差,建議參照項(xiàng)目中已有用法作出調(diào)整。
DeepSeek 與 Gemini 2 Flash(代理模式)
目前這兩款模型在 Cursor 平臺上還不支持代理模式,這部分測試只能留待未來補(bǔ)充。
總結(jié)
雖然對 o3-mini 和 Gemini 2 Flash 都充滿期待,但在實(shí)際開發(fā)中的表現(xiàn)并沒有超出預(yù)期。所有模型在處理這些實(shí)際任務(wù)時(shí)都有各自的不足,連 Claude 3.5 Sonnet 也不例外,實(shí)際效果與各類公開的編碼基準(zhǔn)測試結(jié)果存在明顯落差。特別是在代理模式測試中,o3-mini 在 monorepo 環(huán)境下的表現(xiàn)不佳。由于經(jīng)常依賴代理模式,并且非常喜歡 monorepo 架構(gòu),目前的選擇仍會傾向于使用 Claude 3.5 Sonnet。