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




































