本文的目标是使用 nextjs 构建一个 ai 对话页面,默认已经有一个 nextjs 项目了
选择一个 AI
昨天逛掘金的时候发现了一个对比各家 ai 收费的网站 猛击访问
可以看到智谱 AI 的 GLM-4-Flash 模型是免费的,好用吗?一般般吧!但是我们自己玩一玩已经足够了
目前市面上大多数 AI 模型的调用都兼容 openAI 对接完成后换成其他模型也很方便
也可以使用 deepseek 的 AI 四舍五入算是不要钱!能力也不错。
或者选择阿里系模型价格也很便宜 猛击直达
这里我们使用智谱 AI 的 GLM-4-Flash 模型
首先需要注册猛击直达,进入控制台
点击 API 密钥创建一个密钥
AI 调用
这里我们使用 vercel 的 ai sdk 它对 AI 调用做了一些封装猛击直达 极大的简化了调用的过程,与之类似的还有 langchain
写到这里发现智谱有自己的 sdk 但是我不用!因为如果换一个 AI 就需要重新对接但是 vercel 的 ai sdk 和 langchain 则不需要
执行 pnpm add ai @ai-sdk/openai
安装需要的依赖
然后根据路径 app/api/ai/chat/route.ts
创建对应的文件, nextjs 的 api 接口需要写在 api 目录下的 route.ts 中否则无法识别
写入如下内容
import { CoreMessage, streamText } from 'ai' // 导入AI相关的核心类型和流式文本处理函数
import { createOpenAI } from '@ai-sdk/openai' // 导入创建OpenAI客户端的函数
// POST请求处理函数
export async function POST(req: Request) {
// 从请求体中解构获取消息数组
const { messages }: { messages: CoreMessage[] } = await req.json()
// 创建智谱AI的客户端实例
const openai = createOpenAI({
baseURL: 'https://open.bigmodel.cn/api/paas/v4/', // 智谱AI的API基础URL
compatibility: 'strict', // 设置兼容模式为严格模式
apiKey: process.env.ZHI_PU_AI_API_KEY // 从环境变量获取API密钥
})
// 使用streamText函数创建流式文本响应
const result = await streamText({
model: openai('glm-4-flash'), // 使用智谱AI的GLM-4-Flash模型
system: '你是一名优秀的开发工程师', // 设置AI助手的系统角色提示
messages // 传入用户消息历史
})
// 将结果转换为数据流响应并返回
return result.toDataStreamResponse()
}
是不是很简单但是需要明白的是,简单是因为 vercel 的 ai sdk 帮我们做了很多事情!
调用下接口看看
因为我们设置的是流失返回所以 AI 的回复并不是一次性返回的
我们把代码修改成等 AI 生成完再返回,再试一下
import { CoreMessage, generateText } from 'ai' // 导入AI相关的核心类型和流式文本处理函数
import { createOpenAI } from '@ai-sdk/openai' // 导入创建OpenAI客户端的函数
// POST请求处理函数
export async function POST(req: Request) {
// 从请求体中解构获取消息数组
const { messages }: { messages: CoreMessage[] } = await req.json()
// 创建智谱AI的客户端实例
const openai = createOpenAI({
baseURL: 'https://open.bigmodel.cn/api/paas/v4/', // 智谱AI的API基础URL
compatibility: 'strict', // 设置兼容模式为严格模式
apiKey: process.env.ZHI_PU_AI_API_KEY // 从环境变量获取API密钥
})
// 主要修改的部分 =============================
const { text } = await generateText({
model: openai('glm-4-flash'),
messages: [{ role: 'system', content: '你是一名优秀的开发工程师' }, ...messages]
})
// 返回JSON响应
return Response.json({
content: text,
role: 'assistant'
})
}
使用两个 gif 来对比下和流式返回的区别
流式返回
结果都是一样的,不同的是流式返回可以很快的响应进行输入,非流式则需要等 AI 生成完毕后一次返回需要等待一些时间
具体选择哪一个则需要具体场景具体分析,目前大部分的 AI 应用目前都是流式返回
对话页面
上边接口我们已经搞定了,那么下边看下页面如何构建
根据路径 app/aiChat/page.tsx
创建对应的文件,写入如下内容
'use client'
import { useState, useRef, useEffect } from 'react'
import { useChat, Message } from 'ai/react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Card } from '@/components/ui/card'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Loader2, Send, StopCircle, User } from 'lucide-react'
import { BytemdViewer } from '@/components/bytemd/viewer'
import { Icon } from '@iconify/react'
import Link from 'next/link'
function PageHeader() {
return (
<div className="mb-4 flex items-center justify-between">
<h1 className="text-2xl font-bold">
{process.env.NEXT_PUBLIC_GITHUB_USER_NAME} blog AI 小助手
</h1>
<Link href="/">
<Button size="icon">
<Icon icon="lets-icons:refund-back" width="20px" />
</Button>
</Link>
</div>
)
}
function UserMessage({ message }: { message: Message }) {
return (
<>
<Card className="max-w-[80%] p-2">{message.content ?? ''}</Card>
<Avatar>
<AvatarFallback>
<User className="h-4 w-4" />
</AvatarFallback>
<AvatarImage src="/user-avatar.png" alt="User Avatar" />
</Avatar>
</>
)
}
function AssistantMessage({ message }: { message: Message }) {
return (
<>
<Avatar>
<AvatarFallback>AI</AvatarFallback>
<AvatarImage src="/ai-avatar.png" alt="AI Avatar" />
</Avatar>
<Card className="max-w-[80%] p-2">
<BytemdViewer content={message.content ?? ''}></BytemdViewer>
</Card>
</>
)
}
function MessageInfo({ message }: { message: Message }) {
return (
<div
key={message.id}
className={`flex items-start space-x-2 mb-4 ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
{message.role === 'assistant' ? (
<AssistantMessage message={message}></AssistantMessage>
) : (
<UserMessage message={message}></UserMessage>
)}
</div>
)
}
function MessageList({ messages }: { messages: Message[] }) {
return messages.map((message) => <MessageInfo key={message.id} message={message}></MessageInfo>)
}
export default function AIChatPage() {
const { messages, input, handleInputChange, handleSubmit, isLoading, stop } = useChat({
api: '/api/ai/chat',
keepLastMessageOnError: true
})
const [chatStarted, setChatStarted] = useState(false)
const scrollAreaRef = useRef<HTMLDivElement>(null)
const [scrollHeight, setScrollHeight] = useState(0)
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (input.trim()) {
handleSubmit(e)
if (!chatStarted) setChatStarted(true)
}
}
useEffect(() => {
const scrollElement = scrollAreaRef.current?.querySelector('[data-radix-scroll-area-viewport]')
if (scrollElement) {
const observer = new ResizeObserver((entries) => {
for (let entry of entries) {
if (entry.target === scrollElement) {
setScrollHeight(entry.target.scrollHeight)
entry.target.scrollTop = entry.target.scrollHeight
}
}
})
observer.observe(scrollElement)
return () => {
observer.disconnect()
}
}
}, [messages])
return (
<div className="flex flex-col h-screen w-full mx-auto p-4 md:w-10/12">
<PageHeader></PageHeader>
<Card className="flex-grow mb-4 overflow-hidden">
<ScrollArea
className="h-[calc(100vh-160px)]"
ref={scrollAreaRef}
style={{ height: `calc(100vh - 160px)`, maxHeight: `calc(100vh - 160px)` }}
>
<div className="p-4" style={{ minHeight: `${scrollHeight}px` }}>
{!chatStarted && (
<div className="text-center text-gray-500 mt-8">开始与 AI 助手对话</div>
)}
<MessageList messages={messages}></MessageList>
{isLoading && (
<div className="flex justify-center items-center mt-4">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
)}
</div>
</ScrollArea>
</Card>
<form onSubmit={onSubmit} className="flex space-x-2">
<Input
value={input}
onChange={handleInputChange}
placeholder="在此处输入您的消息..."
disabled={isLoading}
className="flex-grow"
/>
<Button type="submit" disabled={isLoading || !input.trim()}>
<Send className="h-4 w-4 mr-2" />
Send
</Button>
{isLoading && (
<Button type="button" variant="outline" onClick={() => stop()}>
<StopCircle className="h-4 w-4 mr-2" />
Stop
</Button>
)}
</form>
</div>
)
}
整体代码不算多,我们来分析一下
其中 AssistantMessage 使用了封装的 bytemd 组件来展示 AI 的回复,因为 AI 的回复是 md 格式的。
AIChatPage 这里同样使用 vercel 的 ai sdk
指定我们上边写的 api 接口路径
使用
然后有人可能对这一段代码有些疑问
他的作用是监听消息列表变化,自动滚动到底部
总结
本文实践了如何用 Next.js 和 Vercel AI SDK 搭建一个简单的对话式 AI 页面。
我们选择了智谱 AI 的 GLM-4-Flash 模型(免费)并通过 Vercel AI SDK 简化 AI 模型的调用过程。
文章会带你一步步完成 API 配置、Next.js 后端设置和对话页面的搭建,让你轻松实现一个 AI 对话页面。
往期文章
- Vue3组件二次封装的小技巧
- 从零到一建立属于自己的前端组件库
- 求求你们了,对自己代码质量有点要求!
- 不要用vue2的思维写vue3
- openlayers 实战离线地图
- 一个开源的leafletjs示例项目
- vue3 JSX 从零开始
- 手摸手,配置项目中全局loading
- vue + element-ui动态主题及网站换肤2021,亲测可用!!!
- vue3+elementPlus主题动态切换2022,亲测可用!
- go ➕ “蓝兔支付”实现个人网上支付
答疑
使用 vercel 部署 api 10s 左右就会自动停止
这是因为 vercel 默认最大持续时间是 10s 有多种办法可以解决 猛击直达 vercel 文档