本文正在参加金石计划附加挑战赛——第一(或二、三、四)期命题。
引言
随着人工智能技术的不断发展,大模型的应用越来越广泛。Kimi大模型作为一款功能全面、效果出色的智能助手,其强大的数据处理能力、广泛的知识覆盖、便捷的文件处理以及友好的用户交互体验,使其在众多大模型中脱颖而出。本文将详细介绍如何使用Vue3前端技术对接Kimi大模型的流式传输接口,实现实时的对话以及附加文件上传处理的功能。
Kimi大模型简介
Kimi大模型是由北京月之暗面科技有限公司(Moonshot AI)开发的一款智能助手,它具备高效的信息处理能力,支持200万字超长无损上下文,能够快速理解和回应用户的问题。它拥有广泛的知识库,能够提供包括科技、文化、历史、教育等多个领域的信息。此外,Kimi大模型还能读取多种格式的文件,如TXT、PDF、Word文档等,并能安全地访问互联网获取信息,确保用户隐私和数据安全。
目前,Kimi大模型拥有三种核心模型能力,分别是:
moonshot-v1-8k
: 它是一个长度为 8k 的模型,适用于生成短文本。moonshot-v1-32k
: 它是一个长度为 32k 的模型,适用于生成长文本。moonshot-v1-128k
: 它是一个长度为 128k 的模型,适用于生成超长文本。
Kimi大模型不仅是一个强大的AI助手,还提供了一个开放的开发平台,让开发者能够利用其先进的AI能力进行二次开发,主要提供接口:
- chat对话:提供流畅的实时对话体验,能够理解和回应用户的问题,适用于创建聊天机器人或客服系统。
- 工具调用:允许开发者调用各种内置工具,以执行特定的任务,如数据分析、信息检索等。
- 格式输出:支持多种数据格式的输出,方便开发者将AI处理的结果集成到不同的应用程序中。
- 文件接口:提供文件处理接口,使得开发者可以上传文件,AI助手能够阅读并回应文件内容,极大地扩展了应用的使用场景。
开发者注册
- 注册Kimi开发者平台账号。
- 在控制台管理中申请API Key。
- 输入名称获取密钥,用于API接口对接。
流式传输接口介绍
流式传输接口是一种基于网络传输的技术,它允许数据在不同设备间以连续、流式的方式实时传输,而不是一次性传输完整的数据块。。Kimi大模型提供的流式接口基于HTTP协议,使用Server-Sent Events(SSE)技术实现。SSE允许服务端主动向客户端发送数据,而客户端则通过EventSource事件源来监听并获取服务端的消息。
前端实现
前端使用Vue3来实现与Kimi大模型的流式传输对话,采用element-plus构建界面,使用markdown-it来渲染大模型返回来的md格式的对话。
项目搭建
首先,使用Vue CLI创建一个Vue3项目:
vue create vue-kimi-chat
cd vue-kimi-chat
安装依赖
由于axios不支持流事件,我们需要使用fetch或基于fetch实现的第三方请求库。这里推荐使用微软的fetch-event-source库:
npm install element-plus
npm install markdown-it
创建对话组件
在src/components
目录下创建一个名为ChatComponent.vue
的组件:
<template>
<div>
<main class="main">
<el-scrollbar
height="70%"
class="chat-log"
id="chatLog"
ref="scrollMenuRes"
>
<div
class="chat-item"
v-for="(item, index) in chatLogs"
:key="index"
:style="{
'justify-content':
item.type == 'user-message' ? 'flex-end' : 'flex-start',
}"
>
<img
src="../assets/ai-icon.png"
alt=""
v-if="item.type == 'ai-message'"
/>
<div :class="item.type" v-html="md.render(item.message)"></div>
<img
src="../assets/user-icon.jpg"
style="margin-left: 20px"
alt=""
v-if="item.type == 'user-message'"
/>
</div>
</el-scrollbar>
<div class="input-container">
<!-- v-loading="aiThink" element-loading-text="Ai 思考回答中..." -->
<div class="input-area">
<el-input
v-model="inputMessage"
class="input"
:autosize="{ minRows: 5, maxRows: 10 }"
type="textarea"
placeholder="请输入内容,按 Enter 发送,Shift + Enter 换行"
@keydown="sendMessage"
/>
<el-button
class="send-button"
type="primary"
:disabled="inputMessage.length === 0"
@click="sendButtonClick()"
><el-icon size="22">
<Promotion /> </el-icon
></el-button>
</div>
<div class="file-area">
<el-upload
v-model:file-list="fileList"
ref="upload"
action="https://api.moonshot.cn/v1/files"
:headers="{
Authorization: this.apiKey,
}"
:limit="10"
:on-success="success"
:show-file-list="false"
:auto-upload="true"
:on-exceed="handleExceed"
:on-progress="handleProgress"
>
<div>
<el-button class="file-button" type="info" size="small">
<el-icon size="14" color="#FFF">
<UploadFilled />
</el-icon>
{{ progressText }}
</el-button>
<div class="file-name">
<ul>
<li v-for="(item, index) in fileList" :key="index">
{{ item.name }}
</li>
</ul>
</div>
</div>
</el-upload>
<el-button
class="del-button"
type="danger"
size="small"
v-if="fileContent.length > 0"
@click="clearFiles"
>
<el-icon size="14" color="#FFF">
<DeleteFilled />
</el-icon>
清空上传
</el-button>
</div>
</div>
</main>
</div>
</template>
<script>
import { Promotion, UploadFilled, DeleteFilled } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import MarkdownIt from "markdown-it";
export default {
components: {
Promotion,
UploadFilled,
DeleteFilled,
},
data() {
return {
aiApiUlr: "https://api.moonshot.cn/v1/chat/completions",
apiKey: "sk-LgHXc8RZLALMrzL0SN81idZvKcMr2TI7DH8f11111", // 换成自己的key
chatLog: "",
inputMessage: "",
chatLogs: [
{
type: "ai-message",
message:
"Hi,我是 Kimi~很高兴遇见你!你可以随时把网址🔗或者文件📃发给我,我来帮你看看 😊",
},
],
aiThink: false,
fileList: [],
fileContent: [],
progressText: "文件上传",
md: new MarkdownIt(),
};
},
methods: {
// Enter发送消息
sendMessage(event) {
const message = this.inputMessage.trim();
if (message === "") {
return;
}
// 监听 Enter 键 和 Shift 键
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
if (this.aiThink) {
return ElMessage.error("Ai 正在思考中,请稍后发送");
}
this.chatLogs.push({ message: message, type: "user-message" });
this.scrollToBottom();
this.$refs.upload.clearFiles();
this.inputMessage = "";
this.sendRequestToChatGPT(message);
}
},
// 发送按钮点击
sendButtonClick() {
const message = this.inputMessage.trim();
if (message === "") {
return;
}
if (this.aiThink) {
return ElMessage.error("Ai 正在思考中,请稍后发送");
}
this.chatLogs.push({ message: message, type: "user-message" });
this.scrollToBottom();
this.$refs.upload.clearFiles();
this.inputMessage = "";
this.sendRequestToChatGPT(message);
},
// 滚动到底部
scrollToBottom() {
this.$nextTick(() => {
if (this.$refs.scrollMenuRes) {
const container = this.$refs.scrollMenuRes.$el.querySelector(
".el-scrollbar__wrap"
);
if (container) {
container.style.scrollBehavior = "smooth"; // 添加平滑滚动效果
container.scrollTop = container.scrollHeight; // 滚动到底部
}
}
});
},
// 构建发送请求参数
async sendRequestToChatGPT(message) {
this.aiThink = true;
// 构建请求对象
let request = {
model: "moonshot-v1-8k",
messages: [
{
role: "user",
content: message,
},
],
stream: true,
};
// 如果有文件,则将文件内容也发送给 ChatGPT
if (this.fileContent.length > 0) {
for (let i = 0; i < this.fileContent.length; i++) {
request.messages.push({ role: "user", content: this.fileContent[i] });
}
}
// 构建对话UI
this.chatLogs.push({
message: "",
type: "ai-message",
});
this.fetchStreamData(this.aiApiUlr, request);
},
// 清空文件
clearFiles() {
this.fileContent = [];
this.$refs.upload.clearFiles();
},
// 附件上传回调
async success(e) {
console.log("e", e);
let file_content = await fetch(
`https://api.moonshot.cn/v1/files/${e.id}/content`,
{
method: "GET",
headers: {
"Content-Type": "application/json", // 根据实际情况设置
Authorization: this.apiKey,
},
}
);
const reader = file_content.body.getReader();
const textDecoder = new TextDecoder("utf-8");
const { done, value } = await reader.read();
const chunk = textDecoder.decode(value, { stream: true });
this.fileContent.push(chunk);
this.progressText = "上传成功,继续上传";
console.log("fileContent", this.fileContent);
},
// 文件上传中回调
handleProgress() {
this.progressText = "上传中ing...";
},
// 文件上传超出个数回调
handleExceed() {
this.$refs.upload.clearFiles();
},
},
};
</script>
<style>
* {
padding: 0;
margin: 0;
}
.main {
width: 100%;
height: 100vh;
background-color: rgb(245, 247, 250);
display: flex;
flex-direction: column;
}
.input-container {
display: flex;
align-items: center;
width: 100%;
margin: 0 auto;
position: absolute;
bottom: 5%;
justify-content: center;
}
.input-area {
width: 40%;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
}
.input {
width: 100%;
margin-right: 1.25rem;
border-radius: 0.625rem;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.input:focus {
outline: none;
border: 0.0625 solid #409eff;
}
.chat-log {
width: 60%;
margin: 2.5rem auto;
background: #fff;
padding: 2.5rem;
border-radius: 0.625rem;
}
.chat-item {
display: flex;
align-items: start;
margin-bottom: 1.25rem;
line-height: 1.5rem;
}
.chat-item img {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.625rem;
margin-right: 1.25rem;
}
.send-button {
position: absolute;
width: 2.5rem;
height: 2.5rem;
bottom: 1.25rem;
right: 2.5rem;
transition: transform 0.3s ease;
}
.file-button {
/* position: absolute;
bottom: 1.25rem;
left: 1.25rem; */
transition: transform 0.3s ease;
}
.send-button:hover,
.file-button:hover {
transform: scale(1.1);
}
.file-name {
font-size: 0.75rem;
color: #409eff;
}
.file-name li {
list-style: none;
margin-top: 0.625rem;
}
.user-message {
background-color: #f0f8ff;
padding: 0.625rem 2rem;
border-radius: 0.3125rem;
}
.ai-message {
background-color: #f0fff0;
padding: 0.625rem 2rem;
border-radius: 0.3125rem;
}
.el-textarea__inner {
background-color: white;
padding: 1.25rem;
border-radius: 0.625rem;
font-size: 16px;
}
.file-area {
z-index: 99;
position: relative;
top: 250px;
width: 40%;
left: 10px;
min-height: 300px;
display: flex;
}
.el-upload-list {
width: 40%;
}
.del-button {
margin-left: 140px;
}
</style>
代码解释
以下是对代码中功能方法的详细解释:
- sendMessage(event): 此方法在用户发送消息时被调用。它检查输入的消息是否为空,并监听Enter键(非Shift+Enter)来发送消息。如果AI正在思考(
aiThink
为true
),则显示错误消息。否则,它将消息添加到聊天记录中,滚动到聊天窗口的底部,清除上传的文件,并发送请求到Kimi。 - sendButtonClick(): 当用户点击发送按钮时调用的方法,其功能与
sendMessage
类似,但不依赖于键盘事件。 - scrollToBottom(): 此方法用于滚动到聊天窗口的底部,以便用户可以看到最新的消息。使用Vue的
$nextTick
来确保DOM更新后再执行滚动操作。 - sendRequestToChatGPT(message): 此方法构建并发送请求到Kimi API。它首先将
aiThink
设置为true
,表示AI正在思考。然后,它构建一个请求对象,包括模型名称、用户消息和文件内容(如果有)。最后,它调用fetchStreamData
方法来发送请求并获取流数据。 - fetchStreamData(url, requestData): 此方法使用
fetch
API发送POST请求到指定的URL,并处理返回的流数据。它使用TextDecoder
来解码数据块,并调用processChunk
方法来处理每个数据块。当流结束时,它会重置一些状态并执行清理工作。 - processChunk(chunk, callback): 此方法用于处理从流中接收到的数据块。它尝试将数据块解析为JSON,并提取其中的内容。如果解析成功,它会调用回调函数并传入提取的内容。
- clearFiles(): 此方法用于清除已选择的文件和
fileContent
数组中的内容。 - success(e): 这是ElementPlus上传组件的一个异步方法,用于处理附件上传成功的回调。上传成功后,首先获取Kimi上传接口返回文件id,并通过
fetch
从指定的Kimi解析文件接口api.moonshot.cn/v1/files/${… 获取文件内容,然后使用TextDecoder
解码获取到的数据块,并将其添加到fileContent
数组中。最后,它更新进度文本并打印fileContent
。 - handleProgress(): 当文件上传中时调用的方法,用于更新进度文本。
- handleExceed(): 当上传的文件数量超过限制时调用的方法,它会清除已选择的文件。
集成组件到主应用
在src/App.vue
中引入并使用ChatComponent
组件:
<template>
<div id="app">
<ChatComponent />
</div>
</template>
<script>
import ChatComponent from './components/ChatComponent.vue';
export default {
name: 'App',
components: {
ChatComponent
}
};
</script>
运行项目
运行Vue项目
npm run dev
现在,你可以在浏览器中打开Vue应用,输入问题并敲击回车,即可看到Kimi大模型的实时回复,同时可以上传一个或多个文件,来实现文件的上传和解析。