人工智能(五)- OpenAI 調用Ollama本地大模型

人工智能(四)- Function Calling 核心原理與實戰(zhàn)

一、本地安裝大模型

本地安裝大模型,可參考本地部署DeepSeek大模型,文章里安裝的是deepseek-r1:7b大模型,不支持 tool 工具。

我們需要安裝一個新的模型,這里我們使用 qwen3.5 大模型

ollama run qwen3.5

Ollama提供了兩個主要的API端點,它們在功能和協(xié)議兼容性上有所區(qū)別。原生接口是 /api/generate,它使用 prompt 字段,并且不支持 messages 這種對話列表格式。

特性 /api/generate (Ollama原生接口) /v1/completions (OpenAI兼容接口)
協(xié)議標準 Ollama自定義協(xié)議 遵循OpenAI API規(guī)范
兼容性 適用于Ollama專屬功能 便于與現有OpenAI生態(tài)工具(如LangChain)集成
上下文管理 需顯式傳遞context字段維護多輪對話狀態(tài) 通常通過messages數組傳遞歷史對話
流式響應 默認支持,通過stream: true啟用 通過stream: true參數啟用
適用場景 需要深度控制模型參數、低延遲流式輸出 項目從云端OpenAI遷移至本地Ollama,希望最小化代碼改動

注意:Ollama 的 OpenAI 兼容端點是 http://localhost:11434/v1/,SDK 會自動拼接出完整的http://localhost:11434/v1/completions。


二、實戰(zhàn)案例:實現天氣查詢 AI 助手

2.1 創(chuàng)建Maven工程

使用IntelliJ IDEA創(chuàng)建一個Maven工程,完成工程初始化。

2.2 引入核心依賴
<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 http://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>2.7.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>org.devpotato</groupId>
    <artifactId>chat-service</artifactId>
    <version>1.0-SNAPSHOT</version>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <packaging>jar</packaging>

    <name>ChatService</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <jackson.version>2.15.0</jackson.version>
    </properties>

    <dependencies>
        <!--        spring-boot        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--        openai        -->
        <dependency>
            <groupId>com.openai</groupId>
            <artifactId>openai-java</artifactId>
            <version>4.28.0</version>
            <scope>compile</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.jetbrains.kotlin</groupId>
                    <artifactId>kotlin-stdlib-jdk8</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.openai</groupId>
            <artifactId>openai-java-client-okhttp</artifactId>
            <version>4.28.0</version>
            <scope>compile</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.jetbrains.kotlin</groupId>
                    <artifactId>kotlin-stdlib-jdk8</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib-jdk8</artifactId>
            <version>2.3.20-RC3</version>
            <scope>compile</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.jetbrains.kotlin</groupId>
                    <artifactId>kotlin-stdlib</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- Source: https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-stdlib -->
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib</artifactId>
            <version>2.3.20-RC3</version>
            <scope>compile</scope>
        </dependency>

        <!--        jackson        -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>

        <!--        guava        -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>21.0</version>
        </dependency>
        
        <!--        lombok        -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <repositories>
        <repository>
            <id>public</id>
            <name>aliyun nexus</name>
            <url>https://maven.aliyun.com/repository/public</url>
            <releases>
                <enabled>true</enabled>
            </releases>
        </repository>
        <repository>
            <name>Central Portal Snapshots</name>
            <id>central-portal-snapshots</id>

            <url>https://central.sonatype.com/repository/maven-snapshots/</url>
            <releases>
                <enabled>false</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>
</project>
2.3 解決跨域問題

由于前端頁面與后端服務可能存在跨域(CORS)問題,導致前端無法正常調用后端API,因此需要添加跨域過濾器。創(chuàng)建MyFilter類,實現Filter接口,具體代碼如下:

import org.springframework.stereotype.Component;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class MyFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 解決Chrome等瀏覽器訪問本地服務的跨域問題
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        // 允許所有域名跨域訪問(生產環(huán)境建議指定具體域名,提升安全性)
        httpResponse.setHeader("Access-Control-Allow-Origin", "*");
        // 允許的請求方式
        httpResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        // 允許的請求頭
        httpResponse.setHeader("Access-Control-Allow-Headers", "Content-Type");
        // 繼續(xù)執(zhí)行過濾鏈
        chain.doFilter(request, response);
    }
}
2.4 工程配置文件

在src/main/resources目錄下創(chuàng)建application.yml文件,配置服務端口(默認8080,可根據需求修改):

server:
  port: 8080
2.5 天氣工具封裝類
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

/**
 * 天氣查詢工具封裝
 * 包含:模型識別的工具定義 + 本地執(zhí)行邏輯
 */
@Component
public class WeatherTool {
    // 工具名稱(需與注冊給模型的名稱一致)
    public static final String FUNCTION_NAME = "get_current_weather";
    private final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 構建供模型識別的工具定義
     */
    public ChatCompletionTool buildToolDefinition() {
        // 定義入參屬性
        Map<String, Object> locationProperty = new HashMap<>();
        locationProperty.put("type", "string");
        locationProperty.put("description", "城市或縣區(qū)名稱,例如:北京市、杭州市、余杭區(qū)");

        Map<String, Object> properties = new HashMap<>();
        properties.put("location", locationProperty);

        // 構建參數規(guī)范
        FunctionParameters functionParameters = FunctionParameters.builder()
                .putAdditionalProperty("type", JsonValue.from("object"))
                .putAdditionalProperty("properties", JsonValue.from(properties))
                .putAdditionalProperty("required", JsonValue.from(Arrays.asList("location")))
                .build();

        // 構建函數定義
        FunctionDefinition functionDefinition = FunctionDefinition.builder()
                .name(FUNCTION_NAME)
                .description("查詢指定城市/縣區(qū)的實時天氣,適用于用戶詢問天氣相關問題時。")
                .parameters(functionParameters)
                .build();

        // 構建完整的工具定義
        return ChatCompletionTool.ofFunction(ChatCompletionFunctionTool.builder()
                .type(JsonValue.from("function"))
                .function(functionDefinition)
                .build());
    }

    /**
     * 執(zhí)行天氣查詢(模擬真實 API 調用)
     */
    public String execute(String arguments) {
        try {
            JsonNode argsNode = objectMapper.readTree(arguments);
            String location = argsNode.get("location").asText();

            List<String> weatherConditions = Arrays.asList("晴天", "多云", "雨天");
            String randomWeather = weatherConditions.get(new Random().nextInt(weatherConditions.size()));

            return location + "今天是" + randomWeather + "。";
        } catch (IOException e) {
            return "無法解析地點參數,請檢查輸入格式。";
        }
    }
}
2.6 核心對話控制器
package org.devpotato.ollama;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.core.JsonValue;
import com.openai.models.chat.completions.ChatCompletion;
import com.openai.models.chat.completions.ChatCompletionAssistantMessageParam;
import com.openai.models.chat.completions.ChatCompletionCreateParams;
import com.openai.models.chat.completions.ChatCompletionMessage;
import com.openai.models.chat.completions.ChatCompletionMessageParam;
import com.openai.models.chat.completions.ChatCompletionMessageToolCall;
import com.openai.models.chat.completions.ChatCompletionTool;
import com.openai.models.chat.completions.ChatCompletionToolMessageParam;
import com.openai.models.chat.completions.ChatCompletionUserMessageParam;
import org.devpotato.chat.WeatherTool;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Ollama本地模型 API 的多輪對話
 */
@RestController
@RequestMapping("/chat")
public class OllamaChatController {
    /**
     * API Key(建議后續(xù)從配置文件或環(huán)境變量中讀?。?     */
    private static final String API_KEY = "";

    /**
     * Ollama本地模型兼容模式 Base URL
     */
    private static final String BASE_URL = "http://localhost:11434/v1/";

    /**
     * 使用的模型名稱
     */
    private static final String LLM_MODEL = "qwen3.5";

    /**
     * 系統(tǒng)提示詞
     */
    private static final String SYSTEM_PROMPT = "作為一個專業(yè)的電商售后支持客服,你具備深厚的問題解決能力。請針對用戶提出的問題,提供詳細的解答步驟或有效的解決方案,并且考慮到用戶的水平可能有所不同,請盡可能地簡化語言。";

    /**
     * Ollama本地模型 API 是無狀態(tài)的,不會保存對話歷史。
     * 要實現多輪對話,需在每次請求中顯式傳入歷史對話消息。
     * 實現多輪對話的核心是維護一個 messages 數組。每一輪對話都需要將用戶的最新提問和模型的回復追加到此數組中,并將其作為下一次請求的輸入。
     * <p>
     * 注意:當前為單用戶演示模式。如需支持多用戶,應按會話 ID 維護獨立的消息列表。
     */
    private static final List<ChatCompletionMessageParam> messageHistoryList = new ArrayList<>();

    private static final OpenAIClient client = OpenAIOkHttpClient.builder()
            .apiKey(API_KEY)
            .baseUrl(BASE_URL)
            .build();

    ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 定義工具列表
     */
    private static final List<ChatCompletionTool> toolList = new ArrayList<>();

    /**
     * 工具名稱 -> 工具執(zhí)行器 的映射,方便擴展新工具
     */
    private static final Map<String, ToolExecutor> toolExecutorList = new HashMap<>();

    static {
        try {
            WeatherTool weatherTool = new WeatherTool();
            toolList.add(weatherTool.buildToolDefinition());

            // 注冊工具執(zhí)行器
            toolExecutorList.put(WeatherTool.FUNCTION_NAME, weatherTool::execute);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 工具執(zhí)行器函數式接口,便于擴展注冊新工具
     */
    @FunctionalInterface
    public interface ToolExecutor {
        String execute(String arguments);
    }

    /**
     * 處理用戶消息,返回模型的最終回復
     *
     * @param map 用戶輸入的文本
     * @return 模型的回復文本
     */
    @RequestMapping(path = "", method = RequestMethod.POST)
    public String chat(@RequestBody Map<String, Object> map) {
        String message = map.get("message").toString();

        // 1. 構造用戶消息并加入對話歷史
        ChatCompletionUserMessageParam userMessage = ChatCompletionUserMessageParam.builder()
                .role(JsonValue.from("user"))
                .content(message)
                .build();
        messageHistoryList.add(ChatCompletionMessageParam.ofUser(userMessage));

        try {
            // 2. 第一次調用模型
            ChatCompletion chatCompletion = callModel();
            ChatCompletionMessage completionMessage = chatCompletion.choices().get(0).message();

            // 3. 將助手的回復加入對話歷史
            addAssistantMessage(completionMessage);

            // 4. 如果模型沒有調用工具,直接返回回復內容
            if (!hasToolCalls(completionMessage)) {
                return completionMessage.content().orElse("");
            }

            // 5. 循環(huán)處理工具調用(模型可能連續(xù)調用多次工具)
            while (hasToolCalls(completionMessage)) {
                System.out.printf("工具調用信息: %s", objectMapper.writeValueAsString(completionMessage));

                ChatCompletionMessageToolCall messageToolCall = completionMessage.toolCalls().get().get(0);

                // 從模型的回復中解析出工具調用的具體信息(要調用的函數名、參數)
                String toolCallId = messageToolCall.function().get().id();
                String funcName = messageToolCall.function().get().function().name();
                String arguments = messageToolCall.function().get().function().arguments();
                System.out.println("正在調用工具 [" + funcName + "],參數:" + arguments);

                // 通過注冊表查找并執(zhí)行對應的工具
                String toolResult = executeToolByName(funcName, arguments);

                /**
                 * 構造一個 role 為 "tool" 的消息,其中包含工具的執(zhí)行結果
                 * 將工具的返回結果也加入到對話歷史中
                 */
                ChatCompletionToolMessageParam toolMessage = ChatCompletionToolMessageParam.builder()
                        .role(JsonValue.from("tool"))
                        .toolCallId(toolCallId)
                        .content(toolResult)
                        .build();
                messageHistoryList.add(ChatCompletionMessageParam.ofTool(toolMessage));

                // 再次調用模型,讓它根據工具結果生成回復
                chatCompletion = callModel();
                completionMessage = chatCompletion.choices().get(0).message();

                // 將模型的回復也加入到對話歷史中
                addAssistantMessage(completionMessage);
            }

            return completionMessage.content().orElse("");
        } catch (Exception e) {
            System.err.println("Error occurred: " + e.getMessage());
            e.printStackTrace();
            return "抱歉,沒明白你的意思。";
        }
    }

    private ChatCompletion callModel() {
        ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
                .addSystemMessage(SYSTEM_PROMPT)
                .messages(messageHistoryList)
                .model(LLM_MODEL)
                .tools(toolList)
                .parallelToolCalls(true)// 并行工具調用
                .build();
        try {
            return client.chat().completions().create(params);
        } catch (Throwable e) {
            throw new RuntimeException("調用模型失敗", e);
        }
    }

    /**
     * 將助手消息加入對話歷史
     */
    private void addAssistantMessage(ChatCompletionMessage message) {
        ChatCompletionAssistantMessageParam.Builder builder = ChatCompletionAssistantMessageParam.builder()
                .role(JsonValue.from("assistant"))
                .content(message.content().orElse(""));

        // 如果包含工具調用信息,也一并記錄
        if (hasToolCalls(message)) {
            builder.toolCalls(message.toolCalls().get());
        }

        messageHistoryList.add(ChatCompletionMessageParam.ofAssistant(builder.build()));
    }

    /**
     * 判斷模型回復中是否包含工具調用
     */
    private boolean hasToolCalls(ChatCompletionMessage message) {
        return message.toolCalls() != null && message.toolCalls().isPresent()
                && !message.toolCalls().get().isEmpty();
    }

    /**
     * 根據工具名稱執(zhí)行對應的工具
     *
     * @param funcName  工具/函數名稱
     * @param arguments 參數 JSON 字符串
     * @return 工具執(zhí)行結果
     */
    private String executeToolByName(String funcName, String arguments) {
        ToolExecutor executor = toolExecutorList.get(funcName);
        if (executor == null) {
            return "未找到工具: " + funcName;
        }
        return executor.execute(arguments);
    }
}
2.7 啟動類開發(fā)

創(chuàng)建StartServer類,作為Spring Boot工程的啟動入口,代碼如下:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class StartServer {

    public static void main(String[] args) {
        // 啟動Spring Boot服務
        SpringApplication.run(StartServer.class, args);
        System.out.println("智能客服后端服務啟動成功,端口:8080");
    }
}

三、前端頁面搭建(簡易版)

前端采用簡單的HTML+JavaScript搭建聊天界面,實現用戶輸入提問、展示客服回復的功能。創(chuàng)建index.html文件,代碼如下(可直接在瀏覽器中打開使用):

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>電商客服中心</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: 'Microsoft YaHei', sans-serif;
        }

        body {
            background-color: #f5f7fa;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            padding: 20px;
        }

        .chat-container {
            width: 100%;
            max-width: 800px;
            height: 80vh;
            background-color: #fff;
            border-radius: 10px;
            box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }

        .chat-header {
            background-color: #409eff;
            color: #fff;
            padding: 15px 20px;
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .chat-header img {
            width: 40px;
            height: 40px;
            border-radius: 50%;
            background-color: #fff;
        }

        .chat-header h2 {
            font-size: 18px;
            font-weight: 600;
        }

        .chat-messages {
            flex: 1;
            padding: 20px;
            overflow-y: auto;
            background-color: #f9f9f9;
        }

        .message {
            margin-bottom: 15px;
            max-width: 70%;
            display: flex;
            animation: fadeIn 0.3s ease;
        }

        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(10px); }
            to { opacity: 1; transform: translateY(0); }
        }

        .user-message {
            margin-left: auto;
            flex-direction: row-reverse;
        }

        .message-avatar {
            width: 36px;
            height: 36px;
            border-radius: 50%;
            margin: 0 8px;
            flex-shrink: 0;
        }

        .user-message .message-content {
            background-color: #409eff;
            color: #fff;
            border-radius: 10px 10px 0 10px;
        }

        .bot-message .message-content {
            background-color: #fff;
            color: #333;
            border-radius: 10px 10px 10px 0;
            border: 1px solid #eee;
        }

        .message-content {
            padding: 10px 15px;
            word-wrap: break-word;
            line-height: 1.4;
        }

        /* 快捷按鈕區(qū)域樣式 */
        .quick-buttons {
            padding: 10px 15px;
            border-top: 1px solid #eee;
            background-color: #fafafa;
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
        }

        .quick-button {
            padding: 6px 15px;
            background-color: #e8f4ff;
            color: #409eff;
            border: 1px solid #d1e9ff;
            border-radius: 20px;
            cursor: pointer;
            font-size: 13px;
            transition: all 0.2s;
        }

        .quick-button:hover {
            background-color: #409eff;
            color: #fff;
            border-color: #409eff;
        }

        .chat-input {
            display: flex;
            padding: 15px;
            border-top: 1px solid #eee;
            background-color: #fff;
        }

        #message-input {
            flex: 1;
            padding: 12px 15px;
            border: 1px solid #ddd;
            border-radius: 25px;
            outline: none;
            font-size: 14px;
            resize: none;
            height: 45px;
            max-height: 120px;
        }

        #message-input:focus {
            border-color: #409eff;
            box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
        }

        #send-button {
            margin-left: 10px;
            padding: 0 20px;
            background-color: #409eff;
            color: #fff;
            border: none;
            border-radius: 25px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.2s;
        }

        #send-button:hover {
            background-color: #337ecc;
        }

        #send-button:disabled {
            background-color: #b3d8ff;
            cursor: not-allowed;
        }

        .loading {
            display: inline-block;
            width: 20px;
            height: 20px;
            border: 3px solid rgba(255,255,255,.3);
            border-radius: 50%;
            border-top-color: white;
            animation: spin 1s ease-in-out infinite;
        }

        @keyframes spin {
            to { transform: rotate(360deg); }
        }

        .empty-hint {
            text-align: center;
            color: #999;
            padding: 50px 0;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <div class="chat-container">
        <div class="chat-header">
            <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMjAiIGN5PSIyMCIgcj0iMjAiIGZpbGw9IiM0MDllZmYiLz4KPHBhdGggZD0iTTE1IDI1QzE1IDI3LjcxIDE2Ljk5IDI5IDE5IDI5QzIxLjAxIDI5IDIzIDI3LjcxIDIzIDI1QzIzIDIyLjc5IDIxLjAxIDIxIDE5IDIxQzE2Ljk5IDIxIDE1IDIyLjc5IDE1IDI1WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==" alt="客服圖標">
            <h2>在線客服中心</h2>
        </div>
        <div class="chat-messages" id="chat-messages">
            <div class="empty-hint" id="empty-hint">歡迎咨詢,我是您的專屬客服??</div>
        </div>
        
        <!-- 新增快捷按鈕區(qū)域 -->
        <div class="quick-buttons" id="quick-buttons">
            <div class="quick-button" onclick="sendQuickMessage('查看訂單')">查看訂單</div>
            <div class="quick-button" onclick="sendQuickMessage('查看物流')">查看物流</div>
            <div class="quick-button" onclick="sendQuickMessage('申請退款')">申請退款</div>
            <div class="quick-button" onclick="sendQuickMessage('修改收貨地址')">修改收貨地址</div>
            <div class="quick-button" onclick="sendQuickMessage('商品質量問題')">商品質量問題</div>
        </div>
        
        <div class="chat-input">
            <textarea id="message-input" placeholder="請輸入您想咨詢的問題..." onkeydown="if(event.keyCode===13&&!event.shiftKey){event.preventDefault();sendMessage();}"></textarea>
            <button id="send-button" onclick="sendMessage()">發(fā)送</button>
        </div>
    </div>

    <script>
        // 獲取DOM元素
        const messageInput = document.getElementById('message-input');
        const sendButton = document.getElementById('send-button');
        const chatMessages = document.getElementById('chat-messages');
        const emptyHint = document.getElementById('empty-hint');

        // 頭像URL(使用base64編碼的SVG,也可以替換為實際圖片URL)
        const BOT_AVATAR = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iMzYiIHZpZXdCb3g9IjAgMCAzNiAzNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMTgiIGN5PSIxOCIgcj0iMTgiIGZpbGw9IiM0MDllZmYiLz4KPHBhdGggZD0iTTEzIDIyQzEzIDI0LjIxIDE0Ljk5IDI2IDE3IDI2QzE5LjAxIDI2IDIxIDI0LjIxIDIxIDIyQzIxIDE5Ljc5IDE5LjAxIDE4IDE3IDE4QzE0Ljk5IDE4IDEzIDE5Ljc5IDEzIDIyWiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==';
        const USER_AVATAR = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iMzYiIHZpZXdCb3g9IjAgMCAzNiAzNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMTgiIGN5PSIxOCIgcj0iMTgiIGZpbGw9IiNmZmYwMDAiLz4KPHBhdGggZD0iTTEyIDIwQzEyIDIyLjcxIDEzLjk5IDI1IDE2IDI1QzE4LjAxIDI1IDIwIDIyLjcxIDIwIDIwQzIwIDE3Ljc5IDE4LjAxIDE1IDE2IDE1QzEzLjk5IDE1IDEyIDE3Ljc5IDEyIDIwWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTIwIDI5QzIwIDI5IDE3IDMwIDE3IDMwQzE0IDMwIDEyIDI5IDEyIDI5QzEyIDI5IDEyIDI3IDEyIDI3QzEyIDI3IDE0IDI2IDE2IDI2QzE4IDI2IDIwIDI3IDIwIDI3QzIwIDI3IDIwIDI5IDIwIDI5WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==';

        // 自動調整輸入框高度
        messageInput.addEventListener('input', function() {
            this.style.height = 'auto';
            this.style.height = (this.scrollHeight > 45 ? this.scrollHeight : 45) + 'px';
        });

        // 快捷消息發(fā)送函數
        function sendQuickMessage(message) {
            // 將快捷消息填入輸入框
            messageInput.value = message;
            messageInput.style.height = 'auto';
            messageInput.style.height = (messageInput.scrollHeight > 45 ? messageInput.scrollHeight : 45) + 'px';
            
            // 自動發(fā)送該消息
            sendMessage();
        }

        // 發(fā)送消息函數
        async function sendMessage() {
            const message = messageInput.value.trim();
            
            // 驗證輸入內容
            if (!message) {
                alert('請輸入咨詢內容!');
                return;
            }

            // 禁用發(fā)送按鈕和輸入框
            sendButton.disabled = true;
            messageInput.disabled = true;

            try {
                // 隱藏空提示
                emptyHint.style.display = 'none';

                // 添加用戶消息到聊天窗口
                addMessageToChat(message, 'user');
                
                // 清空輸入框并恢復高度
                messageInput.value = '';
                messageInput.style.height = '45px';

                // 添加加載狀態(tài)
                const loadingId = addLoadingMessage();

                // 調用后端接口
                const response = await fetch('http://127.0.0.1:8080/chat', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ message: message })
                });

                // 移除加載狀態(tài)
                removeLoadingMessage(loadingId);

                // 處理響應
                if (response.ok) {
                    const data = await response.text();
                    // 添加客服回復到聊天窗口
                    addMessageToChat(data, 'bot');
                } else {
                    addMessageToChat('抱歉,服務器暫時無法響應,請稍后再試!', 'bot');
                    console.error('接口請求失敗:', response.status);
                }
            } catch (error) {
                // 移除加載狀態(tài)
                const loadingElements = document.querySelectorAll('.loading-message');
                loadingElements.forEach(el => el.remove());
                
                addMessageToChat('網絡錯誤,請檢查您的網絡連接!', 'bot');
                console.error('請求出錯:', error);
            } finally {
                // 恢復發(fā)送按鈕和輸入框
                sendButton.disabled = false;
                messageInput.disabled = false;
                messageInput.focus();
                
                // 滾動到最新消息
                scrollToBottom();
            }
        }

        // 添加消息到聊天窗口
        function addMessageToChat(content, sender) {
            const messageDiv = document.createElement('div');
            messageDiv.className = `message ${sender}-message`;
            
            // 創(chuàng)建頭像元素
            const avatarImg = document.createElement('img');
            avatarImg.className = 'message-avatar';
            avatarImg.src = sender === 'user' ? USER_AVATAR : BOT_AVATAR;
            avatarImg.alt = sender === 'user' ? '用戶頭像' : '客服頭像';
            
            // 創(chuàng)建消息內容元素
            const contentDiv = document.createElement('div');
            contentDiv.className = 'message-content';
            contentDiv.textContent = content;
            
            // 組裝消息元素
            messageDiv.appendChild(avatarImg);
            messageDiv.appendChild(contentDiv);
            
            chatMessages.appendChild(messageDiv);
            
            // 滾動到最新消息
            scrollToBottom();
        }

        // 添加加載中的消息
        function addLoadingMessage() {
            const loadingId = 'loading-' + Date.now();
            const loadingDiv = document.createElement('div');
            loadingDiv.id = loadingId;
            loadingDiv.className = 'message bot-message loading-message';
            
            // 創(chuàng)建客服頭像
            const avatarImg = document.createElement('img');
            avatarImg.className = 'message-avatar';
            avatarImg.src = BOT_AVATAR;
            avatarImg.alt = '客服頭像';
            
            // 創(chuàng)建加載內容
            const contentDiv = document.createElement('div');
            contentDiv.className = 'message-content';
            contentDiv.innerHTML = '<div class="loading"></div>';
            
            // 組裝加載消息
            loadingDiv.appendChild(avatarImg);
            loadingDiv.appendChild(contentDiv);
            chatMessages.appendChild(loadingDiv);
            
            scrollToBottom();
            return loadingId;
        }

        // 移除加載中的消息
        function removeLoadingMessage(loadingId) {
            const loadingDiv = document.getElementById(loadingId);
            if (loadingDiv) {
                loadingDiv.remove();
            }
        }

        // 滾動到聊天底部
        function scrollToBottom() {
            chatMessages.scrollTop = chatMessages.scrollHeight;
        }

        // 監(jiān)聽輸入框回車事件(兼容)
        messageInput.addEventListener('input', function() {
            this.style.height = 'auto';
            this.style.height = (this.scrollHeight > 45 ? this.scrollHeight : 45) + 'px';
        });
        
        messageInput.addEventListener('keypress', function(e) {
            if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                sendMessage();
            }
        });
    </script>
</body>
</html>

四、測試

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容