前后端 + Kimi + 多轮聊天实现

0 阅读3分钟

技术选型

前端

  • Vue3 + Vite

后端

  • Python + FastAPI

模型接入

  • OpenAI Python SDK + Moonshot base_url

Moonshot 支持使用 OpenAI SDK 进行接入,并通过 base_url 指向 Kimi API。(platform.moonshot.cn)


目录结构

ai-companion-agent/
├─ server/
│  ├─ app.py
│  ├─ requirements.txt
│  └─ .env
└─ web/
   └─ 先用 vite 创建

第一步:后端

1)创建 server/requirements.txt

fastapi
uvicorn
python-dotenv
openai

2)创建 server/.env

MOONSHOT_API_KEY=你的月之暗面API_KEY
MOONSHOT_BASE_URL=https://api.moonshot.cn/v1
MOONSHOT_MODEL=kimi-k2-0905-preview

3)创建 server/app.py

import os
from typing import List, Literal, Optional

from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from openai import OpenAI

load_dotenv()

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

client = OpenAI(
    api_key=os.getenv("MOONSHOT_API_KEY"),
    base_url=os.getenv("MOONSHOT_BASE_URL", "https://api.moonshot.cn/v1"),
)

MODEL_NAME = os.getenv("MOONSHOT_MODEL", "kimi-k2-0905-preview")


class Message(BaseModel):
    role: Literal["system", "user", "assistant"]
    content: str


class ChatRequest(BaseModel):
    messages: List[Message]
    session_id: Optional[str] = None


class ChatResponse(BaseModel):
    reply: str


@app.get("/health")
def health():
    return {"ok": True}


@app.post("/api/chat", response_model=ChatResponse)
def chat(req: ChatRequest):
    completion = client.chat.completions.create(
        model=MODEL_NAME,
        messages=[m.model_dump() for m in req.messages],
        temperature=0.7,
    )

    reply = completion.choices[0].message.content or ""
    return ChatResponse(reply=reply)

4)启动

进入 server 目录:

pip install -r requirements.txt
uvicorn app:app --reload --port 8000

浏览器打开:

http://127.0.0.1:8000/health

image.png

说明后端起来了。


第二步:前端创建聊天页面

1)创建项目

npm create vite@latest web

选择:

Vue
JavaScript

然后:

cd web
npm install
npm install axios

2)改 web/src/App.vue

<template>
  <div class="page">
    <div class="container">
      <h1>AI Companion Agent</h1>

      <div class="chat-box">
        <div
          v-for="(item, index) in messages"
          :key="index"
          :class="['msg', item.role]"
        >
          <div class="role">{{ item.role === 'user' ? '我' : 'AI' }}</div>
          <div class="content">{{ item.content }}</div>
        </div>

        <div v-if="loading" class="msg assistant">
          <div class="role">AI</div>
          <div class="content">思考中...</div>
        </div>
      </div>

      <div class="input-area">
        <textarea
          v-model="inputValue"
          placeholder="输入你想说的话"
          @keydown.enter.exact.prevent="sendMessage"
        />
        <button :disabled="loading || !inputValue.trim()" @click="sendMessage">
          发送
        </button>
      </div>
    </div>
  </div>
</template>

<script setup>
import axios from 'axios'
import { ref } from 'vue'

const inputValue = ref('')
const loading = ref(false)

const messages = ref([
  {
    role: 'system',
    content: '你是一个温柔、聪明、会长期陪伴用户的AI伙伴。',
  },
  {
    role: 'assistant',
    content: '你好,我已经准备好了。你今天想聊什么?',
  },
])

const sendMessage = async () => {
  const text = inputValue.value.trim()
  if (!text || loading.value) return

  messages.value.push({
    role: 'user',
    content: text,
  })
  inputValue.value = ''
  loading.value = true

  try {
    const res = await axios.post('http://127.0.0.1:8000/api/chat', {
      messages: messages.value,
    })

    messages.value.push({
      role: 'assistant',
      content: res.data.reply,
    })
  } catch (error) {
    messages.value.push({
      role: 'assistant',
      content: '请求失败,请检查后端或API Key配置。',
    })
    console.error(error)
  } finally {
    loading.value = false
  }
}
</script>

<style scoped>
.page {
  min-height: 100vh;
  background: #f5f7fb;
  padding: 24px;
  box-sizing: border-box;
}
.container {
  width: 900px;
  margin: 0 auto;
  background: #fff;
  border-radius: 16px;
  padding: 24px;
  box-sizing: border-box;
}
h1 {
  margin: 0 0 20px;
}
.chat-box {
  height: 520px;
  overflow-y: auto;
  border: 1px solid #e5e7eb;
  border-radius: 12px;
  padding: 16px;
  background: #fafafa;
}
.msg {
  margin-bottom: 16px;
}
.msg.system {
  display: none;
}
.role {
  font-size: 12px;
  color: #666;
  margin-bottom: 6px;
}
.content {
  display: inline-block;
  max-width: 75%;
  line-height: 1.7;
  padding: 12px 14px;
  border-radius: 12px;
  word-break: break-word;
}
.user .content {
  background: #dbeafe;
}
.assistant .content {
  background: #f3f4f6;
}
.input-area {
  margin-top: 16px;
  display: flex;
  gap: 12px;
}
textarea {
  flex: 1;
  min-height: 100px;
  resize: vertical;
  border: 1px solid #d1d5db;
  border-radius: 12px;
  padding: 12px;
  font-size: 14px;
  outline: none;
}
button {
  width: 100px;
  border: none;
  border-radius: 12px;
  background: #111827;
  color: #fff;
  cursor: pointer;
}
button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
</style>

3)启动

npm run dev

粗糙但是 能用

image.png

image.png

插曲 账户没钱了 直接 失败

image.png

现在这个版本拿到了什么

这个 V1 虽然简单,但已经有 3 个关键能力:

1. 真正接上了 Kimi

Moonshot 官方支持 OpenAI SDK 兼容接入,这样你后面切模型、加 tool calling 都很方便。(platform.moonshot.cn)

2. 已经是多轮对话

因为你把整个 messages 列表持续传给模型了。Moonshot 官方多轮对话指南也是这个思路。(platform.moonshot.cn)

3. 后续能自然扩展到 Agent

Moonshot 当前文档支持 tool calling,后续你要做搜索、记忆检索、天气工具都可以往上加。(platform.moonshot.cn)