基于 DeepSeek 实现简易AI对话功能

660 阅读7分钟

前言

在当今快速发展的技术领域中,AI已经成为了推动创新的核心动力之一。今天我们就基于最近火爆的DeepSeek大模型来搭建一个简单的AI对话应用。

Chat bot

一个典型的全栈AI应用通常由前端展示层、后端服务层以及大型语言模型(LLM)构成。在这个框架中,前端负责用户交互界面,后端提供API接口和业务逻辑处理,而LLM则承担着复杂的自然语言处理任务。在这个项目中我们选择使用deepseek大模型来担此重任。

image.png

前端

在前端部分,我们可以选择vite来快速构建项目,并选择react这个前端开发框架;

// 初始化前端项目
npm init vite

react 开发

对于前端部分,我们选择了React框架来构建动态的用户界面。React组件化的特点使得它非常适合用于创建具有高度交互性的聊天机器人UI。通过Axios库,前端可以轻松地向后端发送HTTP请求,并获取来自DeepSeek的响应数据以更新视图。

import React, { useEffect, useState } from "react";
import axios from 'axios';
import './App.css';

const App = () => {
  return (
    <div>
      chatbot app
    </div>
  )
}

export default App;

在前端中,我们还需要封装一个方法去向后端api接口获取大模型的回答;

const chatApi = async (message) => {
  // 请求行
  // 5173 -> 3000 跨域 同源策略
  // post 复杂请求
  const response = await axios.post('http://localhost:3000/chatai',
    // 请求体
    message, {
    // 请求头
    headers: {
      'Content-Type': 'application/json'
    }
  }
  )
  return response.data;
}

当前端需要将用户的聊天消息发送给后端处理时,会调用这个函数 chatApi(message),其中 message 是用户输入的内容,并且我们自定义了请求头的数据格式,告知服务器我们这次传输的数据格式是json。

注意,在上面的这个方法中,我们是在前端(port: 5173)去发请求到后端(port: 3000)接口去拿deepseek返回的结果,这里会涉及到跨域的问题,因为浏览器同源策略的影响,端口号不同的网址不同源,在互相访问的时候就会受到限制,从而导致服务器端的数据响应不回来。这时,我们就要在后端去解决跨域的问题了。

useState、useEffect

接下来,我们就是用react中的hook去开发页面了,useState去设置响应式的数据;

import React, { useEffect, useState } from "react";
import axios from 'axios';
import './App.css';

const chatApi = async (message) => {
  // 请求行
  // 5173 -> 3000 跨域 同源策略
  // post 复杂请求
  const response = await axios.post('http://localhost:3000/chatai',
    // 请求体
    message, {
    // 请求头
    headers: {
      'Content-Type': 'application/json'
    }
  }
  )
  return response.data;
}

const App = () => {
  // useEffect 中不能直接使用 async
  const [question, setQuestion] = useState('');
  // 会话记录管理
  const [conversation, setConversation] = useState([]);
  const [loading, setLoading] = useState(false);
  const [disabled, setDisabled] = useState(false);
  
  // 在页面挂载后执行一次
  useEffect(() => {
    // 本地储存
    const storedConversation = localStorage.getItem('conversation')
    if (storedConversation) {
      setConversation(JSON.parse(storedConversation));
    }
  }, [])

  const askQuestion = async () => {
    // 参数校验
    if (!question.trim()) {
      return;
    }
    setDisabled(true);
    // 函数式更新
    setConversation((preConversation) => [...preConversation, {
      question: question,
      answer: '等待回答...'
    }]);
    setLoading(true);
    try {
      const message = `你是一个聪明的机器人,我想知道${question}`;
      const response = await chatApi({message});
      setConversation((preConversation) => {
        preConversation[preConversation.length - 1].answer = response
        // 存储字符串
        localStorage.setItem('conversation', JSON.stringify(preConversation))
        // 返回一个全新的状态 独立状态
        return [
          ...preConversation
        ]
      })
    } catch (error) {
      console.warn(error);
    } finally {
      setLoading(false);
      setQuestion('');
      setDisabled(false);
    }
  }

  return (
    <div className="chat-container" style={{ position: 'relative' }}>
      <p className="chat-title">我是ollama + deepseek 本地大模型</p>
      {
        conversation?.map((item, index) => (
          <div key={index} className="chat-message">
            <div className="chat-question">
              <span className="el-tag el0tag--large">me:</span> {item.question}
            </div>
            <div className="chat-answer">
              {item.answer.content}
              <span className="el-tag eltag--large">ai:</span>
            </div>
          </div>
        ))
      }
      <div className="chat-input">
        <input type="text"
          value={question}
          onChange={(e) => { setQuestion(e.target.value) }}
          onKeyUp={(e) => e.key === 'Enter' && askQuestion()}
          style={{ width: "80%" }}
        />
        <button onClick={askQuestion} disabled={disabled}>提交</button>
      </div>
      {loading && (
        <div className="loading-container">
          <p>加载中</p>
        </div>
      )}
    </div>
  )
}

export default App;

前端运行效果如下图;

image.png

后端 server

然后我们新建server文件夹去放置后端文件,先初始化一个npm项目到文件夹中;

// 初始化npm 项目
npm init -y

koa 框架

后端这块我们选择node的一个后端框架koa去搭建后端服务;Koa不仅支持异步函数的使用,还允许开发者通过中间件来组织代码逻辑

// 安装koa
npm install koa

// 搭建一个简单的后端服务
// node 最简单的后端框架
const koa = require('koa');
// 实例化
const app = new koa();
app.listen(3000, ()=>{
    console.log('服务器启动成功');
})

后端路由 koa-router

后端服务我们选择跑在3000端口上面,在koa中,我们如果要给前端提供api接口服务的话,还需要去引入后端路由koa-router;然后通过注册路由,挂载路由到koa实例上面去使用它。

// 安装koa-router
npm install koa-router
npm install axios  // 用于发送接口请求

// node 最简单的后端框架
const koa = require('koa');
const axios = require('axios');
// 实例化
const app = new koa();
const Router = require('koa-router');
const router = new Router();

// 注册路由
// 前端react axios 向 /chatai 发送请求
router.post('/chatai', async (ctx) => {
    // 拿到前端发送过来的问题
    // 再向ollama 发送请求
    const message = ctx.request.body.message || '';
    const data = {
        model: 'deepseek-r1:1.5b',
        messages: [
            {
                role: 'user',
                content: message
            }
        ],
        stream: false
    }

    // axios 发送请求 转发请求
    const response = await axios.post(
        "http://localhost:11434/api/chat", data
    ).then(response => {
        // console.log(response.data);
        ctx.body = {
            code: 200,
            content: response.data.message.content
        }
    })

})

// 挂载路由
app.use(router.routes())

app.listen(3000, ()=>{
    console.log('服务器启动成功');
})

通过后端的路由去给前端提供接口服务,上面封装的/chatai接口就是连接了deepseek大模型的接口服务,前端通过调用它,可以拿到deepseek大模型的回答。

接下来,我们可以启动上面的后端服务并去进行接口测试;

image.png

可以看到,接口成功响应了。

解决跨域

在后端我们可以用 CORS 方法去解决跨域的问题;在koa框架中可以通过use中间件的方式去实现各种功能,跨域也是。

// 跨域支持
app.use(async (ctx, next) => {
    // 设置 http 响应头
    ctx.set('Access-Control-Allow-Origin', '*');
    // 只读 服务器端的安全
    ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, Cookie');
    // 预检请求处理
    if (ctx.method === 'OPTIONS'){
        // 204 no content
        ctx.status = 204;
        return ;
    }
    await next();
})

同源策略会限制其读取响应内容以防信息泄露。而CORS(跨域资源共享)机制允许服务器通过特定的HTTP响应头告知浏览器该请求是被允许的,从而解除这种限制。因此,虽然同源策略会限制JavaScript读取跨域请求的响应,但不会直接“block”请求本身;相反,它控制的是能否访问请求的结果。

在上面的代码中,我们添加了一个预检请求处理,这是因为前端中是通过axios.post方法发送的接口请求,这是一个复杂的请求,浏览器在发送某些特定的跨域请求之前,会自动发出的一个探测性请求(OPTIONS 请求) ,目的是确认服务器是否允许该跨域请求。这时我们在后端中就要去做预检请求的处理,如果不做处理,请求可能也会因为缺少预检响应头而失败。

image.png

解析请求参数

我们可以使用koa-bodyParser这个中间件去解析从前端传来的请求参数。

// bodyParser 中间件
npm install koa-bodyParser

// node 最简单的后端框架
const koa = require('koa');
const axios = require('axios');
const bodyParser = require('koa-bodyparser');
// 实例化
const app = new koa();
const Router = require('koa-router');
const router = new Router();

// 跨域支持
app.use(async (ctx, next) => {
    // 设置 http 响应头
    ctx.set('Access-Control-Allow-Origin', '*');
    // 只读 服务器端的安全
    ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, Cookie');
    // 预检请求处理
    if (ctx.method === 'OPTIONS'){
        // 204 no content
        ctx.status = 204;
        return ;
    }
    await next();
})

// 解析请求体
app.use(bodyParser());

// 前端react axios 向 /chatai 发送请求
router.post('/chatai', async (ctx) => {
    // 拿到前端发送过来的问题
    // 再向ollama 发送请求
    const message = ctx.request.body.message || '';
    const data = {
        model: 'deepseek-r1:1.5b',
        messages: [
            {
                role: 'user',
                content: message
            }
        ],
        stream: false
    }

    // axios 发送请求 转发请求
    const response = await axios.post(
        "http://localhost:11434/api/chat", data
    ).then(response => {
        // console.log(response.data);
        ctx.body = {
            code: 200,
            content: response.data.message.content
        }
    })

})

// 挂载路由
app.use(router.routes())

app.listen(3000, ()=>{
    console.log('服务器启动成功');
})

现在我们可以来看看运行效果;

image.png