AI对话功能之SpringBoot整合Vue3

分享一个APP中如何增加:AI智能聊天功能。

使用技术:SpringBoot 3.x 、Vue3

大模型使用:Deepseek

准备:申请 deepseek的 APIKey(需要deepseek充值,例如 充值1元)

1.后端开发:SpringBoot

使用IDEA创建一个SpringBoot项目,例如:aichatproject

1.1 Pom.xml 的设置

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.15</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.laoma</groupId>
    <artifactId>aichatproject</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>aichatproject</name>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>1.0.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-openai</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

1.2 application.yml

XML 复制代码
spring:
  ai:
    openai:
      api-key: 自己的API-KEY
      base-url: https://api.deepseek.com
      chat:
        options:
          model: deepseek-chat

1.3 ChatController

java 复制代码
package com.laoma.aichatproject.controller;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

/**
 * AI 对话接口
 * <p>
 * 对外只暴露一个接口:GET /api/chat/stream
 * 前端用 EventSource 连接,后端把大模型的回答一个字一个字地"推"过去
 * 也就是常见的"打字机效果"
 */
@RestController
@RequestMapping("/api/chat")
public class ChatController {

    // ChatClient 是 Spring AI 提供的"大模型客户端"
    // 所有跟大模型的交互都通过它来完成
    private final ChatClient chatClient;

    // 默认提示词 prompt
    private static final String BASE_SYSTEM = """
            【你能做的】==:可以修改这里的内容
            - 解答系统功能使用问题
            - 引导用户完成常见操作(如:如何下单购买、查看订单、申请退款等)
            - 解释页面上各字段、按钮的含义
            - 遇到系统故障或账号问题,引导用户联系人工客服:400-123-4567
            
            【你不能做的】
            - 不回答与本系统无关的问题,礼貌拒绝即可
            - 不能承诺系统未来会有某某功能
            - 不能替用户执行任何操作(只能指导)
            
            【回答风格】
            - 简洁友好,口语化,不超过200字
            - 操作步骤用数字列出,清晰易懂
            - 遇到不确定的问题,诚实说不知道,并提供人工客服联系方式
            """;

    /**
     * 构造方法:初始化 ChatClient
     * <p>
     * Spring Boot 启动时会自动把 ChatClient.Builder 注入进来
     * Builder 会读取 application.yml 里配置的 api-key、model 等参数
     */
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    public ChatController(ChatClient.Builder builder) {

        // ---- 第一步:配置对话记忆 ----
        // 大模型本身是"无状态"的,每次请求它都不知道之前说过什么
        // 所以需要我们自己把历史对话存起来,每次请求时一起带过去
        // InMemoryChatMemoryRepository = 把历史记录存在内存里(重启后清空)
        // MessageWindowChatMemory = 滑动窗口,只保留最近 N 条,避免超出 token 限制
        ChatMemory chatMemory = MessageWindowChatMemory.builder()
                .chatMemoryRepository(new InMemoryChatMemoryRepository())
                .maxMessages(20)   // 最多记住最近 20 条对话(10问10答)
                .build();

        // ---- 第二步:构建 ChatClient ----
        // MessageChatMemoryAdvisor 是一个"拦截器"
        // 它会在每次发请求前,自动把历史记录塞进去
        // 在收到回复后,自动把这轮对话存进记忆里
        // 你不需要手动管理消息列表,它全帮你做了
        this.chatClient = builder
                .defaultSystem(BASE_SYSTEM)
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
                .build();
    }

    /**
     * 流式对话接口
     * <p>
     * 请求示例:GET /api/chat/stream?message=你好&sessionId=abc123
     *
     * @param message   用户发送的消息内容
     * @param sessionId 会话ID,用来区分不同用户的对话记忆
     *                  同一个 sessionId = 同一个对话上下文
     *                  前端可以用 crypto.randomUUID() 生成,每个用户一个
     */
    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    // produces = TEXT_EVENT_STREAM_VALUE 告诉浏览器:
    // 这个接口返回的是 SSE 格式(Server-Sent Events),不是普通 JSON
    // 浏览器的 EventSource 就是专门接收这种格式的
    public SseEmitter stream(
            @RequestParam String message,
            @RequestParam(defaultValue = "default") String sessionId) {

        // SseEmitter 是 Spring MVC 提供的 SSE 推送工具
        // 可以理解为一根"水管",后端往里写数据,前端实时收到
        // 180_000L = 超时时间 180秒,超时后连接自动断开
        SseEmitter emitter = new SseEmitter(180_000L);

        chatClient.prompt()
                .user(message)    // 用户这次发送的消息内容
                // 把本次请求绑定到对应的会话 ID
                // MessageChatMemoryAdvisor 会根据这个 ID:
                //   1. 发请求前:自动把该会话的历史记录拼进去
                //   2. 收到回复后:自动把本轮对话存进记忆
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sessionId))
                // 开启流式模式
                // 大模型每生成一个 token 就立刻返回,而不是等全部生成完再返回
                // 这样前端才能实现"打字机"效果
                .stream()
                .content()
                // subscribe 订阅这个数据流,类似于"监听"
                // 大模型每吐出一段文字,就会触发下面对应的回调
                .subscribe(
                        // ① 每收到一个 token(一个字或几个字)就立刻推给前端
                        token -> {
                            try {
                                emitter.send(token);
                            } catch (Exception e) {
                                // 发送失败(比如用户关闭了页面),终止推送
                                emitter.completeWithError(e);
                            }
                        },
                        // ② 大模型返回报错时,把错误通知前端并关闭连接
                        emitter::completeWithError,
                        // ③ 大模型全部回答完毕,正常关闭连接
                        emitter::complete
                );

        // 立刻返回 emitter,连接保持打开
        // 后续数据会通过上面的 subscribe 回调异步推送
        return emitter;
    }
}

1.4 配置跨域

config/CorsConfig.java

java 复制代码
package com.laoma.aichatproject.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")  // 所有接口
                //.allowedOrigins("http://localhost:5173")
                .allowedOriginPatterns("*")  // 允许所有来源
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许请求方式
                .allowCredentials(true) // 允许携带Cookie/Token
                .maxAge(3600); // 预检请求有效期
    }
}

2.前端开发:Vue

2.1 创建Vue3项目

npm create vite@latest

2.2 删除HelloWorld.vue

2.3 安装依赖

2.3.1 安装富文本渲染组件 markdown-it

npm install markdown-it

2.3.2 安装element-plus

npm install element-plus

npm install @element-plus/icons-vue

2.4 配置 main.js

javascript 复制代码
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
// 1. 引入 Element Plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 2. 引入所有图标并全局注册
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

const app = createApp(App)

// 遍历注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

app.use(ElementPlus)
app.mount('#app')

2.5 项目根目录下,创建 .env.development 文件,内容如下:

javascript 复制代码
VITE_BASE_URL=http://localhost:8080

2.6 在compoments下创建 AiChat.vue,内容如下:(这个代码是让AI生成的)

javascript 复制代码
<template>
  <div class="ai-chat-fab" @click="openPanel" v-show="!visible">
    <el-icon :size="24" color="white"><ChatDotRound /></el-icon>
    <span v-if="unread > 0" class="fab-badge">{{ unread }}</span>
  </div>

  <!-- 面板改用 v-show,不再销毁 DOM -->
  <div :class="['ai-chat-panel', { fullscreen: isFullscreen, 'panel-enter': panelEnter, 'panel-leave': panelLeave }]"
       v-show="panelVisible">
    <div class="panel-header">
      <div class="header-avatar">
        <el-icon :size="18" color="white"><Avatar /></el-icon>
      </div>
      <div class="header-info">
        <span class="header-title">AI 智能助手</span>
        <span class="header-sub"><i class="online-dot" />在线</span>
      </div>
      <div class="header-actions">
        <el-icon class="action-icon" @click="clearMessages"><Delete /></el-icon>
        <el-icon class="action-icon" @click="toggleFullscreen">
          <FullScreen v-if="!isFullscreen" />
          <Aim v-else />
        </el-icon>
        <el-icon class="action-icon" @click.stop="closePanel"><Close /></el-icon>
      </div>
    </div>

    <div class="suggest-bar" v-if="messages.length <= 1">
      <span v-for="s in SUGGESTIONS" :key="s" class="suggest-tag" @click="sendSuggestion(s)">
        {{ s }}
      </span>
    </div>

    <div class="message-list" ref="messageListRef">
      <div v-for="(msg, index) in messages" :key="index" :class="['msg-row', msg.role]">
        <div class="msg-avatar">
          <el-icon v-if="msg.role === 'assistant'"><Cpu /></el-icon>
          <el-icon v-else><User /></el-icon>
        </div>
        <!-- assistant 用 v-html 渲染 md,user 保持纯文本 -->
        <div v-if="msg.role === 'assistant'" class="bubble assistant">
          <span v-html="renderMd(msg.content)"></span>
          <span v-if="index === messages.length - 1 && streaming" class="cursor-blink">|</span>
        </div>
        <div v-else class="bubble user">
          {{ msg.content }}
        </div>
      </div>
      <div class="msg-row assistant" v-if="waiting">
        <div class="msg-avatar"><el-icon><Cpu /></el-icon></div>
        <div class="bubble assistant typing-bubble">
          <span class="dot" /><span class="dot" /><span class="dot" />
        </div>
      </div>
    </div>

    <div class="input-area">
      <el-input
          v-model="inputText"
          type="textarea"
          :autosize="{ minRows: 1, maxRows: 4 }"
          placeholder="输入消息,Enter 发送"
          resize="none"
          @keydown.enter.exact.prevent="sendMessage"
          :disabled="streaming || waiting"
          class="chat-input"
      />
      <el-button
          type="primary"
          :icon="Promotion"
          circle
          :disabled="!inputText.trim() || streaming || waiting"
          @click="sendMessage"
          class="send-btn"
      />
    </div>
  </div>
</template>

<script setup>
import { ref, nextTick } from 'vue'
import { ChatDotRound, Avatar, Delete, Close, Cpu, User, Promotion, FullScreen, Aim } from '@element-plus/icons-vue'
import MarkdownIt from 'markdown-it'

const API_URL = import.meta.env.VITE_BASE_URL + '/api/chat/stream'
const GREETING = '你好!我是校园小卖部的智能客服小智😊,有什么可以帮你的吗?'
const SUGGESTIONS = ['如何下单购买', '查看订单', '申请退款', '联系人工客服']
const SESSION_ID = crypto.randomUUID()

const visible = ref(false)      // 控制 FAB 显隐
const panelVisible = ref(false) // 控制面板 v-show(包含动画期间)
const panelEnter = ref(false)
const panelLeave = ref(false)
const isFullscreen = ref(false)
const inputText = ref('')
const streaming = ref(false)
const waiting = ref(false)
const unread = ref(0)
const messageListRef = ref(null)
const messages = ref([{ role: 'assistant', content: GREETING }])
const md = new MarkdownIt()

function toggleFullscreen() {
  isFullscreen.value = !isFullscreen.value
  nextTick(scrollToBottom)
}

function openPanel() {
  visible.value = true
  panelVisible.value = true
  unread.value = 0
  // 触发入场动画
  nextTick(() => {
    panelEnter.value = true
    panelLeave.value = false
    nextTick(scrollToBottom)
  })
}

function closePanel() {
  // 1. 立即让 FAB 不可点击(pointer-events),避免穿透
  visible.value = false
  // 2. 触发离场动画
  panelEnter.value = false
  panelLeave.value = true
  // 3. 动画结束后才真正隐藏面板
  setTimeout(() => {
    panelVisible.value = false
    panelLeave.value = false
    isFullscreen.value = false
    // 动画彻底结束后才让 FAB 重新可见
    visible.value = false
  }, 200)
}

function clearMessages() {
  messages.value = [{ role: 'assistant', content: GREETING }]
}

function sendSuggestion(text) {
  inputText.value = text
  sendMessage()
}

async function sendMessage() {
  const text = inputText.value.trim()
  if (!text || streaming.value || waiting.value) return

  messages.value.push({ role: 'user', content: text })
  inputText.value = ''
  waiting.value = true
  await nextTick(scrollToBottom)

  const es = new EventSource(
      `${API_URL}?message=${encodeURIComponent(text)}&sessionId=${SESSION_ID}`
  )

  messages.value.push({ role: 'assistant', content: '' })
  const lastIdx = messages.value.length - 1
  waiting.value = false
  streaming.value = true

  es.onmessage = async ({ data }) => {
    messages.value[lastIdx].content += data
    console.log('当前content:', messages.value[lastIdx].content)  // 加这行
    await nextTick(scrollToBottom)
  }

  es.onerror = () => {
    es.close()
    streaming.value = false
    if (!visible.value) unread.value++
  }
}

function scrollToBottom() {
  if (messageListRef.value)
    messageListRef.value.scrollTop = messageListRef.value.scrollHeight
}

function renderMd(content) {
  return md.render(content || '')
}
</script>

<style scoped>
.ai-chat-fab {
  position: fixed;
  bottom: 28px;
  right: 28px;
  width: 54px;
  height: 54px;
  border-radius: 50%;
  background: linear-gradient(135deg, #409eff, #1d7fe4);
  box-shadow: 0 4px 20px rgba(64, 158, 255, .5);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 9997;
  transition: transform .2s, box-shadow .2s;
}

.ai-chat-fab:hover {
  transform: scale(1.1);
  box-shadow: 0 6px 28px rgba(64, 158, 255, .65);
}

.fab-badge {
  position: absolute;
  top: 0;
  right: 0;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  border-radius: 9px;
  background: #f56c6c;
  border: 2px solid white;
  font-size: 11px;
  color: white;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
}

.ai-chat-panel {
  position: fixed;
  bottom: 96px;
  right: 28px;
  width: 360px;
  height: 520px;
  background: white;
  border-radius: 16px;
  box-shadow: 0 8px 48px rgba(0, 0, 0, .18);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  z-index: 9998;
  /* 默认隐藏状态(初始无动画) */
  transform: scale(0.6) translateY(30px);
  opacity: 0;
  transform-origin: bottom right;
  pointer-events: none;
  transition: width .3s ease, height .3s ease, bottom .3s ease,
  right .3s ease, border-radius .3s ease;
}

/* 入场 */
.ai-chat-panel.panel-enter {
  animation: chatPopIn .28s cubic-bezier(.34, 1.56, .64, 1) forwards;
  pointer-events: auto;
}

/* 离场 */
.ai-chat-panel.panel-leave {
  animation: chatPopOut .2s ease-in forwards;
  pointer-events: none; /* 离场动画期间禁止点击,防止穿透 */
}

.ai-chat-panel.fullscreen {
  bottom: 0;
  right: 0;
  width: 100vw;
  height: 100vh;
  border-radius: 0;
}

@keyframes chatPopIn {
  from {
    transform: scale(.6) translateY(30px);
    opacity: 0;
    transform-origin: bottom right;
  }
  to {
    transform: scale(1) translateY(0);
    opacity: 1;
    transform-origin: bottom right;
  }
}

@keyframes chatPopOut {
  from {
    transform: scale(1) translateY(0);
    opacity: 1;
    transform-origin: bottom right;
  }
  to {
    transform: scale(.6) translateY(20px);
    opacity: 0;
    transform-origin: bottom right;
  }
}

.panel-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  flex-shrink: 0;
  background: linear-gradient(135deg, #409eff, #1d7fe4);
}

.header-avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: rgba(255, 255, 255, .2);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.header-info {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.header-title {
  font-size: 14px;
  font-weight: 600;
  color: white;
}

.header-sub {
  font-size: 11px;
  color: rgba(255, 255, 255, .75);
  display: flex;
  align-items: center;
  gap: 4px;
}

.online-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #67c23a;
  box-shadow: 0 0 5px #67c23a;
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}

.action-icon {
  color: rgba(255, 255, 255, .8);
  cursor: pointer;
  font-size: 16px;
  transition: color .15s;
}

.action-icon:hover {
  color: white;
}

.suggest-bar {
  display: flex;
  gap: 6px;
  padding: 8px 12px;
  overflow-x: auto;
  background: #f5f7fa;
  border-bottom: 1px solid #ebeef5;
  flex-shrink: 0;
}

.suggest-bar::-webkit-scrollbar {
  display: none;
}

.suggest-tag {
  font-size: 12px;
  padding: 4px 10px;
  white-space: nowrap;
  border: 1px solid #dcdfe6;
  border-radius: 12px;
  color: #606266;
  cursor: pointer;
  background: white;
  transition: border-color .15s, color .15s;
}

.suggest-tag:hover {
  border-color: #409eff;
  color: #409eff;
}

.message-list {
  flex: 1;
  overflow-y: auto;
  padding: 14px 12px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  background: #f8f9fa;
}

.message-list::-webkit-scrollbar {
  width: 4px;
}

.message-list::-webkit-scrollbar-thumb {
  background: #dcdfe6;
  border-radius: 2px;
}

.msg-row {
  display: flex;
  align-items: flex-end;
  gap: 8px;
}

.msg-row.user {
  flex-direction: row-reverse;
}

.msg-avatar {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: #e8f4ff;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  font-size: 14px;
  color: #409eff;
}

.bubble {
  max-width: min(600px, 60%);
  padding: 10px 13px;
  font-size: 13px;
  line-height: 1.6;
  word-break: break-word;
}

.bubble.assistant {
  background: white;
  color: #303133;
  border-radius: 2px 12px 12px 12px;
  box-shadow: 0 1px 6px rgba(0, 0, 0, .08);
}

.bubble.user {
  background: #409eff;
  color: white;
  border-radius: 12px 2px 12px 12px;
}

.typing-bubble {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 12px 16px;
}

.dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #c0c4cc;
  animation: dotBounce 1.4s infinite ease-in-out;
}

.dot:nth-child(2) { animation-delay: .16s; }
.dot:nth-child(3) { animation-delay: .32s; }

@keyframes dotBounce {
  0%, 80%, 100% { transform: translateY(0); background: #c0c4cc; }
  40% { transform: translateY(-7px); background: #409eff; }
}

.cursor-blink {
  color: #409eff;
  animation: cursorBlink .7s infinite;
}

@keyframes cursorBlink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0; }
}

.input-area {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  padding: 10px 12px;
  border-top: 1px solid #ebeef5;
  background: white;
  flex-shrink: 0;
}

.chat-input { flex: 1; }

.chat-input :deep(.el-textarea__inner) {
  border-radius: 8px;
  font-size: 13px;
  line-height: 1.5;
  padding: 8px 10px;
  resize: none;
}

.send-btn {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
}

@media (max-width: 480px) {
  .ai-chat-panel {
    right: 0;
    bottom: 0;
    width: 100vw;
    height: 70vh;
    border-radius: 16px 16px 0 0;
  }
  .ai-chat-fab {
    bottom: 20px;
    right: 20px;
  }
}

.bubble.assistant :deep(p) {
  margin: 0;
}
</style>

2.7 App.vue内容

javascript 复制代码
<script setup>
  import AiChat from './components/AiChat.vue'
</script>

<template>
  <h1>AI智能聊天功能实现</h1>
  <AiChat/>
</template>

3.启动测试结果

npm run dev

到此结束!

相关推荐
阿寻寻1 小时前
【人工智能学习260612-软件测试篇】小工具实现 [特殊字符] Prompt工程 + RAG思路 + API调用 + 自动化测试
人工智能·功能测试·学习·prompt
甲维斯1 小时前
测一波Kimi K2.7,消耗一周配额!
前端·人工智能·游戏开发
石山代码1 小时前
给照片装上 AI 引擎:ACDSee 2025 安装详细步骤
人工智能
chase_my_dream1 小时前
A-LOAM中scanRegistration.cpp详细讲解
c++·人工智能·自动驾驶
ai_xiaogui1 小时前
AI Starter全面开源在即!PanelAI测试版即将上线,客户端+后端全开源,本地AI一键部署神器
人工智能·panelai测试版上线·本地ai一键部署系统·客户端后端开源·ai starter全面开源·跨平台ai模型管理工具·ai starter开源
邵宇然1 小时前
Pin、Unpin 与 Tokio 异步运行时:自引用结构在异步环境中的内存安全保证
人工智能
武子康2 小时前
调查研究-174 什么是“红丸主义“:它为什么吸引人,又为什么容易把人带偏?
后端
逐米时代2 小时前
制造型企业AI智能体实施步骤详解:提升协同效率的实战指南
大数据·人工智能
神奇小汤圆2 小时前
白嫖DeepSeek V4 Pro!免费无限用,还能接入Claude-Code
后端