大家好,我是 luckySnail,你肯定用過 AI 聊天工具。例如: Gemini,ChatGPT,claude AI 等產品,我們通過它們的 web 網站或者手機應用提出問題,得到答案。在之前如果你想要構建一個這樣的 AI 聊天應用程序,是需要大量時間才能開發出來,但是接下來,我將使用 cursor + vercel 的 Next.js 和 ai-sdk 快速搭建屬于你自己的 AI chat 工具,通過這篇文章你可以看到 AI 強大的輔助編程能力和 vercel 家超贊的工具!同時也能了解如何使用 AI 從 0 到 1 構建一個 web 應用,先看一下最終的產品:
如果你想直接看源碼: https://github.com/coderPerseus/easyChat 我還使用 deepwiki 生成了對應的項目文檔: https://deepwiki.com/coderPerseus/easyChat
環境準備
在正式開發前,你的設備需要有如下環境:
-
Node >= 18.18,pnpm 作為依賴管理工具
-
postgreSQL ,可以是本地或者在線
-
Curosr ,用作 AI 輔助編程
-
chrome 游覽器,其他游覽器也可
你需要具備的知識:
-
前端基礎
-
數據庫基礎
-
計算機網絡基礎
-
熟悉 React 開發 當然,你還需要有良好的軟件開發素養,否則你會發現寫的代碼不好維護,或者不易理解
項目初始化
在真正開發項目之前,讓我們先進行需求分析 和 技術選型
需求分析
- 聊天頁面開發(基礎能力)
-
包含提示的輸入框和發送 / 停止按鈕
-
實現一個聊天區域來顯示對話記錄,一個列表展示會話歷史
-
開發 /agent API 來處理請求
-
確保每個對話的數據都被持久存儲
-
通過流式傳輸返回所有結果
-
- 高級能力
-
增強聊天組件,支持 markdown 渲染、自動滾動、圖片上傳等
-
實現函數調用,例如檢索當前時間
-
技術選型
根據需求,我選擇了我喜歡的并且也是主流的技術:
-
Next.js 作為全棧開發基礎框架
-
hono.js 作為后端框架,優化在 Next.js 中后端開發體驗
-
PostgreSQL 作為數據庫存儲對話記錄
-
DrizzleORM 作為 ORM ,更為便捷和高效的方式與數據庫進行交互
-
shadcn/ui 作為 UI 組件庫,tailwindcss 作為 css 框架
-
Vercel AI SDK 快速開發 AI 相關的服務,如果你也在開發 AI 相關的服務,強烈推薦使用它,能幫你減少 80% 的工作
-
Biome 進行代碼格式化和檢測(代替 ESLint + Prettier),需要你安裝 Biome 插件哦
-
zod : TypeScript 優先的數據驗證庫
對了,我們使用 Github 進行版本控制,維護代碼。使用 vercel 進行項目部署上線。
初始化
下面進行初始化項目,初始化項目完成后,我們應該就可以進行業務開發
1)根據 Next.js 官方文檔我們創建一個 Next.js 項目:
npx?create-next-app@latest
2)下面,根據官方文檔集成 shadcn/ui:
pnpm?dlx?shadcn@latest?init
pnpm?dlx?shadcn@latest?add?button
然后嘗試使用 Button 按鈕,發現集成成功!
注意這里有一個小細節就是我在入口的 layout 組件為 body 標簽添加了 suppressHydrationWarning ,作用是:抑制 React 在客戶端和服務器端渲染不匹配時產生的警告信息,這在處理動態內容(如日期、時間等)時特別有用,因為這些內容在服務器端和客戶端可能會有差異。
3)下面集成 Biome,保證相關代碼風格一致
pnpm?i?@biomejs/biome?-D
然后在 package.json 添加對應的腳本:
{"scripts":?{"lint":?"next?lint","format":?"biome?format?--write?.","lint:biome":?"biome?check?--apply.",}
}
下面設置編輯器的 Format Document With ,選擇 Configure Default Formatter 設置為 Biome。現在你的項目就又了格式化能力,你還可以在 git 提交的鏈路上進行預先 format 和 lint 等操作,保證提交的代碼是格式化的。 4)下面繼續集成 hono.js,我參考了文章思路: https://kuizuo.cn/blog/nextjs-with-hono/ 。首先根據官方文檔進行安裝
pnpm?i?hono
#?讓?hono?接管所有接口服務
mkdir?-p?"src/app/api/[[...route]]"?&&?touch?"src/app/api/[[...route]]/route.ts"
下面,開發 route.ts 內容,讓 hono 來接管接口服務
//?src/app/api/[[...route]]/route.ts
import?api?from?"@/server/api";
import?{?handle?}?from?"hono/vercel";
const?handler?=?handle(api);
export?{handler?as?GET,handler?as?POST,handler?as?PUT,handler?as?DELETE,handler?as?PATCH,
};
因為 Next.js 會自動掃描 app 下的文件夾進行熱更新,所以我們可以將服務端代碼放在根目錄的 server 文件夾下(其實你可以使用任何名稱),這里寫我們所有的服務端邏輯和接口,下面初始化一下服務端的基礎邏輯, 5)創建自定義校驗器,它的作用是進行請求數據驗證的工具函數,確保數據符合預期的格式和類型規范,并提供類型安全的驗證結果
//?src/server/api/validator.ts
import?type?{Context,MiddlewareHandler,Env,ValidationTargets,TypedResponse,Input,
}?from?"hono";
import?{?validator?}?from?"hono/validator";
import?type?{?z,?ZodSchema,?ZodError?}?from?"zod";export?type?Hook<T,E?extends?Env,P?extends?string,Target?extends?keyof?ValidationTargets?=?keyof?ValidationTargets,//?biome-ignore?lint/complexity/noBannedTypes:?<explanation>O?=?{}
>?=?(result:?(|?{?success:?true;?data:?T?}|?{?success:?false;?error:?ZodError;?data:?T?})?&?{target:?Target;},c:?Context<E,?P>
)?=>|?Response|?void|?TypedResponse<O>//?biome-ignore?lint/suspicious/noConfusingVoidType:?<explanation>|?Promise<Response?|?void?|?TypedResponse<O>>;type?HasUndefined<T>?=?undefined?extends?T???true?:?false;export?const?zValidator?=?<T?extends?ZodSchema,Target?extends?keyof?ValidationTargets,E?extends?Env,P?extends?string,In?=?z.input<T>,Out?=?z.output<T>,I?extends?Input?=?{in:?HasUndefined<In>?extends?true??{[K?in?Target]?:?K?extends?"json"??In:?HasUndefined<keyof?ValidationTargets[K]>?extends?true??{?[K2?in?keyof?In]?:?ValidationTargets[K][K2]?}:?{?[K2?in?keyof?In]:?ValidationTargets[K][K2]?};}:?{[K?in?Target]:?K?extends?"json"??In:?HasUndefined<keyof?ValidationTargets[K]>?extends?true??{?[K2?in?keyof?In]?:?ValidationTargets[K][K2]?}:?{?[K2?in?keyof?In]:?ValidationTargets[K][K2]?};};out:?{?[K?in?Target]:?Out?};},V?extends?I?=?I
>(target:?Target,schema:?T,hook?:?Hook<z.infer<T>,?E,?P,?Target>
):?MiddlewareHandler<E,?P,?V>?=>//?@ts-expect-error?not?typed?wellvalidator(target,?async?(value,?c)?=>?{const?result?=?await?schema.safeParseAsync(value);if?(hook)?{const?hookResult?=?await?hook({?data:?value,?...result,?target?},?c);if?(hookResult)?{if?(hookResult?instanceof?Response)?{return?hookResult;}if?("response"?in?hookResult)?{return?hookResult.response;}}}if?(!result.success)?{throw?result.error;}return?result.data?as?z.infer<T>;});
6)創建錯誤處理文件,給到客戶端更好的錯誤提示:
//?src/server/api/error.ts
import?{?z?}?from?"zod";
import?type?{?Context?}?from?"hono";
import?{?HTTPException?}?from?"hono/http-exception";
import?type?{?ContentfulStatusCode?}?from?"hono/utils/http-status";export?class?ApiError?extends?HTTPException?{public?readonly?code?:?ContentfulStatusCode;constructor({code,message,}:?{code?:?ContentfulStatusCode;message:?string;})?{super(code,?{?message?});this.code?=?code;}
}export?function?handleError(err:?Error,?c:?Context):?Response?{if?(err?instanceof?z.ZodError)?{const?firstError?=?err.errors[0];return?c.json({?code:?422,?message:?`\`${firstError.path}\`:?${firstError.message}`?},422);}/***?This?is?a?generic?error,?we?should?log?it?and?return?a?500*/return?c.json({code:?500,message:?"服務端錯誤,?請稍后再試。",},{?status:?500?});
}
下面我們創建我們的第一個接口,驗證 honojs 是否引入成功:
//?src/server/api/routes/hello.ts
import?{?Hono?}?from?"hono";
const?app?=?new?Hono().get("/hello",?(c)?=>c.json({?message:?"Hello,?luckyChat"?})
);
export?default?app;
7)最后,開發入口文件:
//?src/server/api/index.ts
import?{?handleError?}?from?"./error";
import?{?Hono?}?from?"hono";
import?helloRoute?from?"./routes/hello";
const?app?=?new?Hono().basePath("/api");
app.onError(handleError);
const?routes?=?app.route("/",?helloRoute);
export?default?app;
export?type?AppType?=?typeof?routes;
現在我們不僅有了接口,還有了服務端接口的類型聲明,我們可以非常方便的在客戶端進行類型安全的接口請求,我們不需要寫路由,也不需要寫類型相關的內容,真的是 amazing,我們趕緊在客戶端調用第一個接口吧!在調用接口前, 8)我們先封裝一個 fetch 方法:
//?src/lib/fetch.ts
import?type?{?AppType?}?from?"@/server/api";
import?{?hc?}?from?"hono/client";
import?ky?from?"ky";const?baseUrl?=process.env.NODE_ENV?===?"development"??"http://localhost:3000":?process.env.NEXT_PUBLIC_APP_URL;export?const?fetch?=?ky.extend({hooks:?{afterResponse:?[async?(_,?__,?response:?Response)?=>?{if?(response.ok)?{return?response;//?biome-ignore?lint/style/noUselessElse:?<explanation>}?else?{throw?await?response.json();}},],},
});export?const?client?=?hc<AppType>(baseUrl?as?string,?{fetch:?fetch,
});
ky 庫是一個基于瀏覽器原生 Fetch API 的輕量級HTTP客戶端庫,提供了更簡潔友好的接口,使用它更好的與 honojs 集成,這里我們使用 hc 和 AppType 創建了一個安全的接口請求方式:
// src/app/page.tsx
import { Button } from "@/components/ui/button";
import { Heart } from "lucide-react";
import { client } from "@/lib/fetch";
async function getData() {try {const res = await client.api.hello.$get();if (!res.ok) {// This will activate the closest `error.js` Error Boundarythrow new Error("Failed to fetch data");}return res.json();} catch (error) {console.error("獲取數據失敗:", error);return { message: "AI 助手" };}
}
export default async function Home() {const { message } = await getData();return (<div><div>{message}</div><Button><Heart className="mr-2 h-4 w-4" /> lucky Snail</Button></div>);
}
當我們在使用 client 的時候它會進行代碼提示告訴你目前可以使用哪些接口,并且在后面我們可以借助 InferResponseType 和 typeof 等 ts 關鍵字來使用接口對應的 ts 類型,我們只需要在服務端定義好類型聲明,在客戶端直接消費即可 👍。
9)下面進行最重要的一步,也就是數據庫初始化,有開發經驗的應該都知道數據庫設計的好,可以大大降低系統復雜度,減少不必要的代碼,這么重要的事情肯定是需要 AI 的參與的,我們把需求給到 AI,然后讓 AI 幫忙進行初步數據庫設計,下面在 cursor 中進行 ask:
提示詞:現在集成 DrizzleORM, and AI SDK.使用 postgreSQL 作為數據庫,驅動使用 postgres,數據庫名字叫 chatAI ,數據庫就一張表,存儲 AI 對話記錄,設計良好的數據庫表結構,最后開發 /agent API 來處理聊天請求,這里使用大模型為 Deepseek ,大模型的 key 存在 環境變量的 DEEPSEEK_API_KEY ,數據庫集成參考 @https://orm.drizzle.team/docs/get-started-postgresql ,先梳理需求,然后一步步進行實現
AI 給到的數據庫結構如下:
//?src/lib/db/schema.ts
import?{?pgTable,?serial,?text,?timestamp,?varchar?}?from?'drizzle-orm/pg-core';//?聊天消息類型
export?const?chatMessages?=?pgTable('chat_messages',?{id:?serial('id').primaryKey(),sessionId:?varchar('session_id',?{?length:?255?}).notNull(),role:?varchar('role',?{?length:?50?}).notNull(),?//?'user'?或?'assistant'content:?text('content').notNull(),createdAt:?timestamp('created_at').defaultNow().notNull(),
});//?會話信息
export?const?chatSessions?=?pgTable('chat_sessions',?{id:?serial('id').primaryKey(),sessionId:?varchar('session_id',?{?length:?255?}).unique().notNull(),title:?varchar('title',?{?length:?255?}),createdAt:?timestamp('created_at').defaultNow().notNull(),updatedAt:?timestamp('updated_at').defaultNow().notNull(),
});
這里 AI 幫我們創建好了數據庫的表結構,它能理解需求,并給出合理的數據庫設計:
-
chat_sessions 表:存儲聊天會話信息
-
chat_messages 表:存儲聊天消息 還幫我們在 package.json 添加了生成和運行遷移的腳本,我們在 env 配置到 DATABASE_URL,執行腳本即可初始化數據庫
現在我們完成了項目初始化,我們可以使用 cursor 提供的 /generate Cursor Rules
來生成項目開發指導,在后面業務能力開發中,我們每次都攜帶上這條 rules ,它能幫 AI 更好的生成內容
現在已經搭建好了前端和后端基礎能力,并且生成了項目開發的 rules ,下面就全部交給 AI 來進行業務開發,我們只需要做一個合格的測試和 code review 就好了!
核心能力開發
一個聊天應用最核心的就是輸入提示詞 => AI 大模型響應內容 => 展示內容 => 繼續對話
1)開發 chat,提示詞如下
下面 @project-structure.mdc 就是我們生成的項目開發 rules
@project-structure.mdc 使用 ai-sdk 開發 /agent API 來處理聊天請求,遵循 RESTful API 風格,這里使用大模型為 Deepseek ,大模型的 key 存在 環境變量的 DEEPSEEK_API_KEY ,然后在開發對應的 chat 頁面,一個輸入框,右側有發送和暫停按鈕,支持接收用戶的輸入,支持發送和停止能力,這里使用 @ai-sdk/react 快速進行開發,需要流式輸出 AI 生成內容。代碼組件化,模塊化,盡可能使用 shadui/cn 組件開發,先梳理需求,然后一步步進行實現
AI 可能需要比較漫長時間完成工作,在這個過程中,我們可以思考下一個提示詞,在 AI 生成完成后,我們需要進行檢查和修復 bug,當然你是可以借助 AI 來 fix error。這里需要額外注意的是當我們 chat 中斷的時候應該把已經生成的內容進行存儲數據庫
2)開發創建新會話能力,支持會話緩存到本地
@project-structure.mdc 支持創建新會話,并且會話 id 存儲 localStorage ,在頁面刷新的時候會話 id 依然存在,注意點擊停止需要把當前會話進行存儲
3)開發歷史會話列表展示,支持切換會話
@project-structure.mdc 開發歷史對話記錄功能,先進行接口開發,這里兩個接口:獲取所有會話列表和獲取指定 id 的會話信息,前端需要將會話列表封裝為單獨組件,點擊會話列表項能進入該對話,數據接口邏輯使用自定義 hooks,保證代碼清晰易理解。 注意客戶端需要使用封裝的 fetch 導出的 client 進行接口請求
4)支持 markdown 渲染 AI 生成的內容,優化頁面布局 UI,支持內容自動滾動底部 上面完成后,我們有了基本的 chat 功能頁面,但是可能這時候頁面比較丑不太美觀,現在進行優化
@project-structure.mdc 你是資深 UI 設計師,現在進行項目優化:
1,實現 markdown 渲染流式內容。添加在對話中自動滾動到底部的能力
2. 優化目前的頁面 UI,頁面布局為左邊的側邊欄,展示歷史記錄和新對話能力都放在左邊,當屏幕寬度小于 tailwindcss 的 lg 的時候就不展示側邊欄,右側對話框 UI ,也需要進行優化,注意不要新增元素和修改邏輯,僅僅是對元素布局和 UI進行優化,參考優秀的 chatbox UI 設計
5)支持 function call 能力,獲取當前時間
@project-structure.mdc @web 參考文檔支持 function call 能力,以獲取最新時間為例
優化
如果你做到這里,相信你的項目肯定還有一些 bug 和 UI 細節需要進行優化,你可以和 AI 一起進行優化,給大家看一下我和 AI 的 chat
不斷的修復和優化,最終我的 chatbox 就出現啦!
你打幾分呢?
總結
-
AI 目前編程能力已經超過 90% 的工程師了,它在開發功能安全性和兼容性考慮上十分的全面,但是它也有能力邊界,在面對復雜系統,奇怪的需求上是不如人的。所以程序員以后更多是做決策,做 AI 與需求的橋梁,通過我們的經驗和直覺去選擇接受還是拒絕 AI 的生成
-
日常 AI 編程開發中,推薦使用方式為 gemini 做設計和文檔類工作,寫代碼部分交給 claude
-
需要我們能夠合理的進行模塊拆分,能夠識別 AI 代碼是不是合理的
-
系統設計是 AI 時代程序員的必須要提升的技能,推薦一本書《軟件設計的哲學》第二版
-
知識廣度,全棧能力對前端有很大的幫助,借助 AI 快速將想法變成現實是程序員的紅利
-
持續學習,跟進最新的 AI 是保證自己有競爭力的關鍵
-
Vibe Coding 必然會成為新的編碼方式,純手工編程必然會像 php 一樣成為歷史
-
目前來看,編程智能體(Agent)真的很成功,讓我這種普通人能夠快速 開發出產品。
參考:
-
https://claude.ai/chat/fd4c29a3-3b5c-4965-9670-4380dcc28f98
-
https://www.youtube.com/watch?v=tlrf4lu8Myc
-
https://bigbang.easykol.com/search/following?platform=TIKTOK&url=https://www.tiktok.com/@meditationbuddhism