大家好,我是印刻君。平时逛淘宝的时候,不知道大家有没有体验过平台的 AI 搜索功能。它能结合你的需求,做相对精准的商品推荐。

今天我想和大家分享一个入门级别的 Demo,用 Next.js 简单实现类似的推荐助手,内容偏基础。
我们最终实现的 Demo 视频如下,它主要就 3 项基础功能:
- 基础的 AI 聊天对话,支持流式消息回复;
- 简易的水果价格查询,支持价格升序、降序查询;
- 对话过程中嵌入商品卡片,引导用户购买。
一、先搭建基础的聊天机器人
在实现推荐功能之前,我们先搭建一个基础的 AI 聊天机器人。本次 Demo 以接入 DeepSeek 大模型为例,整体可以分为 4 个关键步骤:
1.1 设计聊天界面
一个聊天界面包括两大核心部分,
-
**消息展示区:**是用户消息和 AI 消息的列表,常规布局是 AI 消息靠左展示,用户消息靠右展示;
-
下方的输入框:包括文本输入框和发送按钮,一直固定在页面底部。
UI 布局很常规,我们不详细展开介绍,重点分析一个智能滚动交互------当消息增多时,消息展示区应该会自动滚动到底部,确保用户能第一时间查看到最新的回复。
要实现这个效果,我们可以在聊天消息列表的底部放一个元素,借助 useRef 绑定它,并配合 useEffect 监听消息。当消息变化时,通过 scrollIntoView 方法,触发自动滚动。
tsx
export function Chatbot() {
const messagesEndRef = useRef<HTMLDivElement>(null);
// ...
useEffect(() => {
messagesEndRef.current?.scrollIntoView({
behavior: "smooth"
});
}, [chat.messages]);
// ...
return (
<div>
{/* ... */}
<div ref={messagesEndRef} />
</div>
)
}
1.2 简化聊天状态管理
界面搭建好之后,我们需要处理消息的发送、接收、加载等逻辑,手动编写相关代码比较繁琐,推荐使用 @ai-sdk/react 提供的 useChat Hook,它可以帮助我们大幅简化状态管理。
useChat 返回的对象中,我们主要用到 3 个属性:
- messages:消息列表,包括用户发送的消息和 AI 的回复,可以直接遍历渲染;
- status:当前状态,表示消息是否正在发送或流式传输中,可以用来做加载状态的交互;
- sendMessage:将用户的输入发送给后端的方法,可以用来和 AI 对话。
tsx
import { useChat } from "@ai-sdk/react";
export function Chatbot() {
const chat = useChat({
transport: new DefaultChatTransport({
api: "/api/chat",
}),
});
const handleSend = async () => {
// ...
await chat.sendMessage({ text });
};
// ...
const isLoading = chat.status === "submitted"
|| chat.status === "streaming";
// ...
return (
<div>
{/* ... */}
</div>
)
}
1.3 搭建后端 API 服务
之前介绍的都是前端逻辑,想要真正调用 AI 模型,需要后端做中转。
后端主要做三件事:
- 转换消息格式:使用 convertToModelMessages 将前端格式的消息转换成大模型能识别的标准格式;
- 调用模型:使用 streamText 以流式方式调用大模型生成回复。
- 返回流式响应:使用 toUIMessageStreamResponse 将模型的流转换成符合 HTTP 规范的 SSE(Server-Sent Events)响应,前端可以实时接收并显示。
注意,需要在 DeepSeek 的控制台申请一个 API Key,并且放到 .env.local 文件中。@ai-sdk/deepseek 会自动读取该文件下的 DEEPSEEK_API_KEY
后端代码示例如下:
ts
import { convertToModelMessages, streamText } from "ai";
import { deepseek } from "@ai-sdk/deepseek";
export async function POST(request: Request) {
try {
const body = await request.json();
const { messages: uiMessages } = body;
const modelMessages = await convertToModelMessages(uiMessages);
const result = streamText({
model: deepseek("deepseek-chat"),
messages: modelMessages,
});
return result.toUIMessageStreamResponse();
} catch (error) {
// ...
}
}
1.4 美化消息展示
完成前 3 步后,其实已经搭建好了一个聊天机器人,可以和 deepseek 模型进行对话。
但实际使用中,大模型返回的内容常常是 Markdown 格式的,直接渲染会显得比较杂乱。
我们可以用 streamdown 来美化 AI 返回的信息,它是专门处理 AI 流式消息的组件。
通过 npm 安装 streamdown 之后,我们可以这样使用 Streamdown 组件:
- 从 useChat 返回的 messages 中提取出需要展示的文本消息;
- 将文本内容作为子元素传递给 Streamdown 组件,就可以自动完成格式解析。
核心渲染代码如下:
jsx
export function Chatbot() {
// ...
return (
{/* */}
{chat.messages.map((message) => {
// 从 parts 中提取 text
const textPart = message.parts?.find(
(p) => "text" in p && typeof p.text === "string",
) as { type: "text"; text: string } | undefined;
const content = textPart?.text || "";
return (
<div key={message.id}>
<div>
{message.role === "user" ? (
content
) : (
<Streamdown animated isAnimating={isLoading}>
{content}
</Streamdown>
)}
</div>
</div>
);
})}
)
}
到这里,我们便搭建了一个像模像样的聊天机器人。接下来我们基于这个机器人,扩展出商品推荐的功能。
二、实现 AI 工具调用,完成水果推荐
基础的聊天机器人只能对话,它不知道我们预设的水果数据,没办法做水果推荐。所以我们需要给 AI 加上工具调用(Tool Call)能力。让 AI 能主动调用我们的数据查询工具,获取水果相关数据后再推荐。
比如用户问最便宜的水果是什么?最贵的水果是什么?AI 就会调用我们的查询工具,拿到排序好的数据推荐给用户。
实现这个功能,你需要经过 3 个步骤:
- 定义 AI 调用的工具;
- 实现工具执行的函数;
- 渲染工具的结果。
2.1 定义 Tool
我们从 ai 的 npm 包中引入 tool 和 jsonSchema,定义一个专门查询水果的工具。主要参数有:
- description,告诉 AI 这个工具的用途;
- inputSchema,约束 AI 调用工具时输入的参数格式;
- execute,绑定的执行函数。
ts
import { tool, jsonSchema } from 'ai';
const tools = {
findFruits: tool({
description: "查询水果数据,并根据用户的要求返回数据",
inputSchema: jsonSchema<{
query?: string;
limit?: number;
sortBy?: SortStrategy;
}>({
// ...
},
required: ["query"],
additionalProperties: false,
}),
execute: findFruitsFunc,
}),
};
// ...
const result = streamText({
model: deepseek("deepseek-chat"),
system:
"你是一个专业的水果销售助手。当用户询问水果相关问题时,请先调用 findFruits 工具获取数据,然后根据返回的结果用友好的方式向用户推荐并说明理由。",
messages: modelMessages,
tools,
stopWhen: stepCountIs(5),
});
定义好工具后,在调用 streamText 时把工具传入,模型会自动分析用户的输入,判断是否需要调用该工具。
2.2 实现 tool 的执行函数
真正的查询逻辑,是在工具执行函数里进行的,本次 Demo 为了简化理解,我们用一个数组来代替数据库,我们的查询也只是实现了价格升序、价格降序两种查询。
真实场景中,这里会替换为数据库查询,接口调用等逻辑,但核心思路和这个 Demo 是一致的,大家可以自行拓展。
ts
const fruits = [
{
id: "apple",
name: "苹果",
price: 8.8,
unit: "斤",
desc: "红富士苹果,口感清爽",
tags: ["红富士", "水果", "新鲜"],
},
// ...
]
const findFruitsFunc = async ({
sortBy = "price_asc",
}: {
sortBy?: SortStrategy;
}) => {
const sortedFruits = fruits.toSorted((a, b) => {
return sortBy === "price_asc" ? a.price - b.price
: b.price - a.price;
});
return {
sortBy,
results: sortedFruits,
};
};
工具执行完毕后,结果会自动传递给大模型,AI 会解析结果内容,再生成相应的自然语言回复。
2.3 前端渲染工具结果
后端把工具查询结果和 AI 文本返回给前端后,前端需要区分普通文本和工具结果。
- 普通文本:利用 Streamdown 渲染;
- 工具结果:业务自定义,我们这里是水果推荐,因此会把工具结果渲染为水果推荐卡片。
怎么区分普通文本和工具结果呢,我们可以根据消息的 parts 中每一项的 type 来判断,如果是 text 则是文本,如果是 tool-findFruits 则是 findFruits tool 的结果,可以渲染水果卡片组件。
tsx
export function Chatbot() {
return (
<div>
{chat.messages.map((message) => {
return (
<div>
{message.parts?.map((part) => {
switch (part.type) {
case 'text': {
// ... Streamdown 相关逻辑
}
case 'tool-findFruits': {
// ... 返回前端水果卡片
}
default:
return null
}
})}
</div>
)
})}
</div>
)
}
三、总结
以上就是入门 Demo 的全部实现,通过 AI 聊天功能 + AI 工具调用,就可以简易模拟电商平台的 AI 搜索推荐功能。
当然,这个 Demo 只是入门演示级别,和真实电商推荐系统相比,还有很多可以完善的地方。比如对用户输入的校验、兼容、和真实数据库的对接、用户历史对话记忆等等。但这个 Demo,已涵盖了 "前端交互 + AI 流式调用 + 工具调用 + 结果可视化" 的核心链路。
我是印刻君,一位探索 AI 的前端程序员。关注我,让前端知识有温度,技术落地有深度。