概述
Kimi是大家常用的一个人工智能助手,本文使用Kimi开发文档,以node作为后端,开发与一个问答系统
实现效果
Kimi简介
Kimi是由Moonshot AI
开发的人工智能助手,擅长中文和英文对话。目标是帮助用户解决问题、提供信息和执行任务。无论是回答问题、处理文件还是进行网络搜索,都能提供支持。
如下图,点击用户中心,在API Key管理
可以添加key。
在用量限制
可查看账户的用量和剩余数量。
图中的相关名字解释如下:
- 并发: 同一时间内我们最多处理的来自您的请求数
- RPM: request per minute 指一分钟内您最多向我们发起的请求数
- TPM: token per minute 指一分钟内您最多和我们交互的token数
- TPD: token per day 指一天内您最多和我们交互的token数
实现
后端实现
后端是通过node的Express框架实现的。
核心实现步骤与代码如下:
1. 初始化与安装依赖
# 创建目录
mkdir kimi-server && cd kimi-server
# 初始化package.json文件
npm init -y
# 安装依赖
npm i express openai -S
2. 修改package.json
修改package.json
文件中的scripts
节点的内容如下:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
+ "dev": "nodemon ./app.js",
+ "server": "pm2 start ./app.js --name kimi"
},
3. app.js
const express = require("express");
const kimiRouter = require("./router/kimi.js");
const app = express();
// 自定义跨域中间件
const allowCors = function (req, res, next) {
res.header("Access-Control-Allow-Origin", req.headers.origin);
res.header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,OPTIONS");
res.header("Access-Control-Allow-Headers", "Content-Type");
res.header("Access-Control-Allow-Credentials", "true");
next();
};
app.use(allowCors); // 使用跨域中间件
app.use(express.static("public"));
app.use("/kimi", kimiRouter);
app.listen(18080, () => {
console.log("express server running at http://127.0.0.1:18080");
});
4. kimi.js
const express = require("express");
const OpenAI = require("openai");
const R = require("../R");
const router = express.Router();
let r = new R();
const client = new OpenAI({
apiKey: "你的key",
baseURL: "https://api.moonshot.cn/v1",
});
let history = [];
async function chat(prompt = '') {
console.time("prompt", prompt);
let response = "";
if (prompt) {
history.push({
role: "user",
content: prompt,
});
const completion = await client.chat.completions.create({
model: "moonshot-v1-8k",
messages: history,
});
history = history.concat(completion.choices[0].message);
response = completion.choices[0].message.content;
} else {
response = "哈喽,你好!我是Kimi,由 Moonshot AI 提供的人工智能助手。";
history.push({
role: "system",
content: response,
});
}
console.log({
prompt,
response,
});
console.timeEnd("prompt");
return response;
}
router.get("/chat", async function (req, res) {
const { prompt } = req.query;
const reply = await chat(prompt);
res.send(
r.success({
reply: reply,
})
);
});
module.exports = router;
前端页面
1. index.html
前端页面通过CDN引入Vue
、Element Plus
和markdown.js
,实现代码如下:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>chat</title>
<!-- Import style -->
<link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css" />
<link rel="stylesheet" href="./md.css" />
<!-- Import Vue 3 -->
<script src="//unpkg.com/vue@3"></script>
<!-- Import component library -->
<script src="//unpkg.com/element-plus"></script>
<script src="//cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
html,
body,
#app {
height: 100%;
margin: 0;
padding: 0;
}
::-webkit-scrollbar {
width: 5px;
height: 5px;
background-color: #eee;
}
::-webkit-scrollbar-track {
background-color: #eee;
}
::-webkit-scrollbar-thumb {
background: #787878;
border-radius: 10px;
}
.chat-container {
display: flex;
flex-direction: column;
padding: 1rem;
height: calc(100% - 2rem);
}
.chat-messages {
flex-grow: 1;
overflow-y: auto;
padding: 10px;
}
.message {
overflow: hidden;
margin-bottom: 1rem;
}
.message:last-child {
margin-bottom: 0;
}
.message p {
margin: 0;
}
.message-bubble {
padding: 10px;
border-radius: 10px;
display: inline-block;
position: relative;
max-width: 80%;
text-align: justify;
line-height: 1.5;
}
.message-bubble:after {
content: ' ';
border: 10px solid transparent;
position: absolute;
top: 0.5rem;
}
.message-bubble.received {
background-color: #f0f0f0;
margin-left: 0.3rem;
}
.message-bubble.received:after {
border-right-color: #f0f0f0;
left: -16px;
}
.message-bubble.sent {
background-color: #007bff;
color: white;
float: right;
margin-right: 1rem;
}
.message-bubble.sent:after {
border-left-color: #007bff;
right: -16px;
}
.chat-input {
margin-top: 1rem;
display: flex;
}
.chat-input input {
flex: 1;
padding: 1.5rem 0.3rem;
}
.chat-input button {
padding: 1.5rem 1rem;
border: none;
border-radius: 5px;
background-color: #007bff;
color: white;
cursor: pointer;
}
</style>
</head>
<body>
<div id="app">
<div class="chat-container">
<div class="chat-messages" ref="messages">
<div v-for="message in messages" :key="message.id" class="message">
<div :class="['message-bubble', message.type]" v-html="message.text">
</div>
</div>
</div>
<div class="chat-input">
<el-input :disabled="loading" v-model="newMessage" placeholder="请输入您的问题..."
@keyup.enter="sendMessage"></el-input>
<el-button style="margin-left: 0.5rem;" type="primary" :loading="loading" @click="sendMessage">{{
loading ? '回答...' : '点击发送'
}}</el-button>
</div>
</div>
</div>
<script>
let url = 'http://127.0.0.1:18080/kimi/chat?prompt='
const storageKey = 'history-messages'
const App = {
data() {
return {
messages: [],
newMessage: '',
loading: false
};
},
mounted() {
const messages = JSON.parse(localStorage.getItem(storageKey) || '[]')
if (messages.length > 0) {
this.messages = messages
this.scrollToBottom()
} else {
this.sendMessage(true)
}
},
methods: {
getMessage(msg = '') {
return new Promise(resolve => {
this.loading = true
fetch(`${url}${msg}`).then(res => res.json()).then(res => {
this.loading = false
if (res.code == 200) {
resolve(res.data.reply)
} else {
resolve(res.msg)
}
})
})
},
scrollToBottom() {
setTimeout(() => {
this.$refs.messages.scrollTop = this.$refs.messages.scrollHeight
}, 100)
},
sendMessage(init = false) {
if (this.newMessage.trim() !== '') {
this.messages.push({
id: this.messages.length + 1,
text: this.newMessage,
type: 'sent',
});
this.scrollToBottom()
this.getMessage(this.newMessage).then(msg => {
this.messages.push({
id: this.messages.length + 1,
text: marked.parse(msg),
type: 'received',
});
localStorage.setItem(storageKey, JSON.stringify(this.messages));
this.newMessage = '';
this.scrollToBottom()
})
} else {
this.getMessage().then(msg => {
this.messages.push({
id: this.messages.length + 1,
text: marked.parse(msg),
type: 'received',
});
localStorage.setItem(storageKey, JSON.stringify(this.messages));
this.scrollToBottom()
})
}
},
},
};
const app = Vue.createApp(App);
app.use(ElementPlus);
app.mount("#app");
</script>
</body>
</html>
2. md.css
md.css
为优化markdown
样式的外部引用,代码如下:
body {
font-family: "Microsoft Yahei", Helvetica, arial, sans-serif;
font-size: 14px;
line-height: 1.6;
padding-top: 10px;
padding-bottom: 10px;
background-color: white;
padding: 30px;
color: #516272;
}
body>*:first-child {
margin-top: 0 !important;
}
body>*:last-child {
margin-bottom: 0 !important;
}
a {
color: #4183C4;
}
a.absent {
color: #cc0000;
}
a.anchor {
display: block;
padding-left: 30px;
margin-left: -30px;
cursor: pointer;
position: absolute;
top: 0;
left: 0;
bottom: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 20px 0 10px;
padding: 0;
font-weight: bold;
-webkit-font-smoothing: antialiased;
cursor: text;
position: relative;
}
h1:hover a.anchor,
h2:hover a.anchor,
h3:hover a.anchor,
h4:hover a.anchor,
h5:hover a.anchor,
h6:hover a.anchor {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA09pVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoMTMuMCAyMDEyMDMwNS5tLjQxNSAyMDEyLzAzLzA1OjIxOjAwOjAwKSAgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OUM2NjlDQjI4ODBGMTFFMTg1ODlEODNERDJBRjUwQTQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OUM2NjlDQjM4ODBGMTFFMTg1ODlEODNERDJBRjUwQTQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5QzY2OUNCMDg4MEYxMUUxODU4OUQ4M0REMkFGNTBBNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5QzY2OUNCMTg4MEYxMUUxODU4OUQ4M0REMkFGNTBBNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PsQhXeAAAABfSURBVHjaYvz//z8DJYCRUgMYQAbAMBQIAvEqkBQWXI6sHqwHiwG70TTBxGaiWwjCTGgOUgJiF1J8wMRAIUA34B4Q76HUBelAfJYSA0CuMIEaRP8wGIkGMA54bgQIMACAmkXJi0hKJQAAAABJRU5ErkJggg==) no-repeat 10px center;
text-decoration: none;
}
h1 tt,
h1 code {
font-size: inherit;
}
h2 tt,
h2 code {
font-size: inherit;
}
h3 tt,
h3 code {
font-size: inherit;
}
h4 tt,
h4 code {
font-size: inherit;
}
h5 tt,
h5 code {
font-size: inherit;
}
h6 tt,
h6 code {
font-size: inherit;
}
h1 {
font-size: 28px;
color: #2B3F52;
}
h2 {
font-size: 24px;
border-bottom: 1px solid #DDE4E9;
color: #2B3F52;
}
h3 {
font-size: 18px;
color: #2B3F52;
}
h4 {
font-size: 16px;
color: #2B3F52;
}
h5 {
font-size: 14px;
color: #2B3F52;
}
h6 {
color: #2B3F52;
font-size: 14px;
}
p,
blockquote,
ul,
ol,
dl,
li,
table,
pre {
margin: 15px 0;
color: #516272;
}
hr {
background: transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAYAAACtBE5DAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OENDRjNBN0E2NTZBMTFFMEI3QjRBODM4NzJDMjlGNDgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OENDRjNBN0I2NTZBMTFFMEI3QjRBODM4NzJDMjlGNDgiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4Q0NGM0E3ODY1NkExMUUwQjdCNEE4Mzg3MkMyOUY0OCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4Q0NGM0E3OTY1NkExMUUwQjdCNEE4Mzg3MkMyOUY0OCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PqqezsUAAAAfSURBVHjaYmRABcYwBiM2QSA4y4hNEKYDQxAEAAIMAHNGAzhkPOlYAAAAAElFTkSuQmCC) repeat-x 0 0;
border: 0 none;
color: #cccccc;
height: 4px;
padding: 0;
}
body>h2:first-child {
margin-top: 0;
padding-top: 0;
}
body>h1:first-child {
margin-top: 0;
padding-top: 0;
}
body>h1:first-child+h2 {
margin-top: 0;
padding-top: 0;
}
body>h3:first-child,
body>h4:first-child,
body>h5:first-child,
body>h6:first-child {
margin-top: 0;
padding-top: 0;
}
a:first-child h1,
a:first-child h2,
a:first-child h3,
a:first-child h4,
a:first-child h5,
a:first-child h6 {
margin-top: 0;
padding-top: 0;
}
h1 p,
h2 p,
h3 p,
h4 p,
h5 p,
h6 p {
margin-top: 0;
}
li p.first {
display: inline-block;
}
li {
margin: 0;
}
ul,
ol {
padding-left: 30px;
}
ul :first-child,
ol :first-child {
margin-top: 0;
}
dl {
padding: 0;
}
dl dt {
font-size: 14px;
font-weight: bold;
font-style: italic;
padding: 0;
margin: 15px 0 5px;
}
dl dt:first-child {
padding: 0;
}
dl dt> :first-child {
margin-top: 0;
}
dl dt> :last-child {
margin-bottom: 0;
}
dl dd {
margin: 0 0 15px;
padding: 0 15px;
}
dl dd> :first-child {
margin-top: 0;
}
dl dd> :last-child {
margin-bottom: 0;
}
blockquote {
border-left: 4px solid #ECF0F3;
/*padding: 0 15px;*/
padding: 15px;
background-color: #F7F9FA;
color: #2B3F52;
}
blockquote> :first-child {
margin-top: 0;
}
blockquote> :last-child {
margin-bottom: 0;
}
table {
padding: 0;
border-collapse: collapse;
}
table tr {
border-top: 1px solid #cccccc;
background-color: white;
margin: 0;
padding: 0;
}
table tr:nth-child(2n) {
background-color: #f8f8f8;
}
table tr th {
font-weight: bold;
border: 1px solid #cccccc;
margin: 0;
padding: 6px 13px;
}
table tr td {
border: 1px solid #cccccc;
margin: 0;
padding: 6px 13px;
}
table tr th :first-child,
table tr td :first-child {
margin-top: 0;
}
table tr th :last-child,
table tr td :last-child {
margin-bottom: 0;
}
img {
max-width: 100%;
}
span.frame {
display: block;
overflow: hidden;
}
span.frame>span {
border: 1px solid #dddddd;
display: block;
float: left;
overflow: hidden;
margin: 13px 0 0;
padding: 7px;
width: auto;
}
span.frame span img {
display: block;
float: left;
}
span.frame span span {
clear: both;
color: #333333;
display: block;
padding: 5px 0 0;
}
span.align-center {
display: block;
overflow: hidden;
clear: both;
}
span.align-center>span {
display: block;
overflow: hidden;
margin: 13px auto 0;
text-align: center;
}
span.align-center span img {
margin: 0 auto;
text-align: center;
}
span.align-right {
display: block;
overflow: hidden;
clear: both;
}
span.align-right>span {
display: block;
overflow: hidden;
margin: 13px 0 0;
text-align: right;
}
span.align-right span img {
margin: 0;
text-align: right;
}
span.float-left {
display: block;
margin-right: 13px;
overflow: hidden;
float: left;
}
span.float-left span {
margin: 13px 0 0;
}
span.float-right {
display: block;
margin-left: 13px;
overflow: hidden;
float: right;
}
span.float-right>span {
display: block;
overflow: hidden;
margin: 13px auto 0;
text-align: right;
}
code,
tt {
margin: 0 2px;
padding: 0 5px;
white-space: nowrap;
border: 1px solid #eaeaea;
background-color: #f8f8f8;
border-radius: 3px;
}
pre code {
margin: 0;
padding: 0;
white-space: pre;
border: none;
background: transparent;
}
.highlight pre {
background-color: #f8f8f8;
border: 1px solid #cccccc;
font-size: 13px;
line-height: 19px;
overflow: auto;
padding: 6px 10px;
border-radius: 3px;
}
pre {
background-color: #f8f8f8;
border: 1px solid #cccccc;
font-size: 13px;
line-height: 19px;
overflow: auto;
padding: 6px 10px;
border-radius: 3px;
}
pre code,
pre tt {
background-color: transparent;
border: none;
}
sup {
font-size: 0.83em;
vertical-align: super;
line-height: 0;
}
code {
white-space: pre-wrap;
word-break: break-all;
display: inline-block;
}
* {
-webkit-print-color-adjust: exact;
}
@media screen and (min-width: 914px) {
body {
width: 960px;
margin: 0 auto;
}
}
@media print {
table,
pre {
page-break-inside: avoid;
}
pre {
word-wrap: break-word;
}
}