人工智能(四)- Function Calling 核心原理与实战

人工智能(三)- 提供 API,搭建智能客服系统

大模型虽然擅长自然语言理解与生成,但在处理实时数据查询、精准数学计算、外部系统交互等场景时往往力不从心。Function Calling(函数调用)作为大模型连接外部工具的核心能力,让模型能够"调用工具"解决原本无法直接回答的问题,成为构建智能应用的关键技术。

一、Function Calling 核心工作原理

Function Calling 本质是大模型与外部工具的协作交互流程 ,通过标准化的多轮对话机制,让模型能够自主决策是否调用工具、调用哪个工具,并基于工具返回结果生成最终回答。完整流程分为5个核心步骤:

1.1 首次模型调用:传递问题与工具清单

应用程序向大模型发起请求,请求内容包含两部分核心信息:

  • 用户的原始问题(如"北京今天天气怎么样?")
  • 模型可调用的工具清单(包含工具名称、功能描述、入参规范)

1.2 模型决策:返回工具调用指令或直接回答

模型基于用户问题和工具清单进行判断:

  • 需要调用工具:返回 JSON 格式的工具调用指令,包含「要调用的工具名称」和「工具所需入参」
  • 无需调用工具:直接返回自然语言格式的回答(如回答常识性问题)

1.3 应用端执行工具调用

应用程序解析模型返回的工具调用指令,调用对应的外部工具(如天气 API、计算器、数据库查询接口),获取工具执行结果。

1.4 二次模型调用:传入工具执行结果

将工具返回的结果(如"北京今天是晴天")添加到对话上下文(messages)中,再次调用大模型。

1.5 生成最终回答

模型结合用户原始问题和工具返回的精准数据,生成自然、准确的最终回复。


二、实战案例:实现天气查询 AI 助手

下面以 Java 实现的天气查询助手为例,完整展示 Function Calling 的落地流程(基于阿里云通义千问 API)。

2.1 第一步:定义工具(核心)

工具是大模型与外部系统的桥梁,需同时定义「模型可识别的工具描述」和「本地执行的工具逻辑」。

2.1.1 工具执行逻辑(模拟天气查询)
java 复制代码
/**
 * 执行天气查询(模拟真实 API 调用)
 * @param arguments 模型传入的参数 JSON 字符串,格式:{"location": "查询地点"}
 * @return 工具执行结果,格式:"{位置}今天是{天气}"
 */
public String execute(String arguments) {
    try {
        // 解析模型传入的参数
        JsonNode argsNode = objectMapper.readTree(arguments);
        String location = argsNode.get("location").asText();

        // 模拟调用真实天气 API,返回随机天气结果
        List<String> weatherConditions = Arrays.asList("晴天", "多云", "雨天");
        String randomWeather = weatherConditions.get(new Random().nextInt(weatherConditions.size()));

        return location + "今天是" + randomWeather + "。";
    } catch (IOException e) {
        // 异常处理:参数解析失败时返回友好提示
        return "无法解析地点参数,请检查输入格式。";
    }
}
2.1.2 定义模型可识别的工具描述

模型需要通过标准化的 JSON 结构识别工具,核心字段说明:

json 复制代码
{
    "type": "function",
    "function": {
        "name": "get_current_weather",
        "description": "查询指定城市/县区的实时天气,适用于用户询问天气相关问题时。",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "城市或县区名称,例如:北京市、杭州市、余杭区"
                }
            },
            "required": ["location"]
        }
    }
}
  • type:字段固定为"function";
  • function:字段为 Object 类型;
    • name:字段为自定义的工具函数名称,建议使用与函数相同的名称,如get_current_weather或get_current_time;
    • description:字段是对工具函数功能的描述,大模型会参考该字段来选择是否使用该工具函数。
    • parameters:字段是对工具函数入参的描述,类型是 Object ,大模型会参考该字段来进行入参的提取。如果工具函数不需要输入参数,则无需指定parameters参数。
      • type:字段固定为"object";
      • properties:字段描述了入参的名称、数据类型与描述,为 Object 类型,Key 值为入参的名称,Value 值为入参的数据类型与描述;
    • required:字段指定哪些参数为必填项,为 Array 类型。

2.2 第二步:完整代码实现

2.2.1 创建Maven工程

使用IntelliJ IDEA创建一个Maven工程,完成工程初始化。

2.2.2 引入核心依赖
xml 复制代码
<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>

引入依赖后,点击IDEA的「Refresh」按钮,下载并加载依赖包,确保工程无依赖报错。

2.2.3 工程配置文件

在src/main/resources目录下创建application.yml文件,配置服务端口(默认8080,可根据需求修改):

yaml 复制代码
server:
  port: 8080
2.2.4 解决跨域问题

由于前端页面与后端服务可能存在跨域(CORS)问题,导致前端无法正常调用后端API,因此需要添加跨域过滤器。创建MyFilter类,实现Filter接口,具体代码如下:

java 复制代码
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;
        // 允许所有域名跨域访问(生产环境建议指定具体域名,提升安全性)
        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");
        // 继续执行过滤链
        chain.doFilter(request, response);
    }
}
2.2.5 天气工具封装类
java 复制代码
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;

/**
 * 天气查询工具封装
 * 包含:模型识别的工具定义 + 本地执行逻辑
 */
@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", "城市或县区名称,例如:北京市、杭州市、余杭区");

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

        // 构建参数规范
        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("查询指定城市/县区的实时天气,适用于用户询问天气相关问题时。")
                .parameters(functionParameters)
                .build();

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

    /**
     * 执行天气查询(模拟真实 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.2.6 核心对话控制器
java 复制代码
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.function.Function;

/**
 * 通义千问 Function Calling 核心控制器
 * 实现:多轮对话 + 工具调用完整流程
 */
@RestController
@RequestMapping("/chat")
public class ChatController {
    // 配置项(建议从配置文件/环境变量读取,此处为演示)
    private static final String API_KEY = "xxx";
    private static final String BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1";
    private static final String LLM_MODEL = "qwen-plus";
    private static final String SYSTEM_PROMPT = "你是一个智能天气查询助手,仅在用户询问天气时调用天气工具,其他问题直接友好回复。";

    // 对话历史(单用户演示,多用户需按会话ID隔离)
    private static final List<ChatCompletionMessageParam> messageHistoryList = new ArrayList<>();
    // OpenAI 客户端(兼容通义千问)
    private static final OpenAIClient client = OpenAIOkHttpClient.builder()
            .apiKey(API_KEY)
            .baseUrl(BASE_URL)
            .build();
    private final ObjectMapper objectMapper = new ObjectMapper();

    // 工具列表 + 工具执行器映射(便于扩展)
    private static final List<ChatCompletionTool> toolList = new ArrayList<>();
    private static final Map<String, ToolExecutor> toolExecutorMap = new HashMap<>();

    // 初始化工具
    static {
        try {
            WeatherTool weatherTool = new WeatherTool();
            toolList.add(weatherTool.buildToolDefinition());
            toolExecutorMap.put(WeatherTool.FUNCTION_NAME, weatherTool::execute);
        } catch (Exception e) {
            throw new RuntimeException("工具初始化失败", e);
        }
    }

    // 工具执行器函数式接口(简化工具扩展)
    @FunctionalInterface
    public interface ToolExecutor {
        String execute(String arguments);
    }

    /**
     * 处理用户消息,返回最终回复
     * @param request 请求体,格式:{"message": "用户问题"}
     * @return 模型最终回复
     */
    @PostMapping("")
    public String chat(@RequestBody Map<String, Object> request) {
        String userInput = request.get("message").toString();

        // 1. 添加用户消息到对话历史
        ChatCompletionUserMessageParam userMessage = ChatCompletionUserMessageParam.builder()
                .role(JsonValue.from("user"))
                .content(userInput)
                .build();
        messageHistoryList.add(ChatCompletionMessageParam.ofUser(userMessage));

        try {
            // 2. 第一次调用模型,判断是否需要调用工具
            ChatCompletion completion = callModel();
            ChatCompletionMessage assistantMsg = completion.choices().get(0).message();
            addAssistantMessageToHistory(assistantMsg);

            // 3. 无需调用工具,直接返回结果
            if (!hasToolCall(assistantMsg)) {
                return assistantMsg.content().orElse("抱歉,我暂时无法回答这个问题。");
            }

            // 4. 需要调用工具,循环处理(支持多轮工具调用)
            while (hasToolCall(assistantMsg)) {
                ChatCompletionMessageToolCall toolCall = assistantMsg.toolCalls().get().get(0);
                String toolId = toolCall.function().get().id();
                String funcName = toolCall.function().get().function().name();
                String args = toolCall.function().get().function().arguments();

                // 执行工具调用
                String toolResult = toolExecutorMap.getOrDefault(funcName, arg -> "未找到工具:" + funcName).execute(args);

                // 5. 将工具结果加入对话历史
                ChatCompletionToolMessageParam toolMsg = ChatCompletionToolMessageParam.builder()
                        .role(JsonValue.from("tool"))
                        .toolCallId(toolId)
                        .content(toolResult)
                        .build();
                messageHistoryList.add(ChatCompletionMessageParam.ofTool(toolMsg));

                // 6. 二次调用模型,生成最终回答
                completion = callModel();
                assistantMsg = completion.choices().get(0).message();
                addAssistantMessageToHistory(assistantMsg);
            }

            return assistantMsg.content().orElse("");
        } catch (Exception e) {
            e.printStackTrace();
            return "处理请求时出错:" + e.getMessage();
        }
    }

    /**
     * 调用大模型核心方法
     */
    private ChatCompletion callModel() {
        ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
                .addSystemMessage(SYSTEM_PROMPT)
                .messages(messageHistoryList)
                .model(LLM_MODEL)
                .tools(toolList)
                .parallelToolCalls(true) // 支持并行工具调用
                .build();
        return client.chat().completions().create(params);
    }

    /**
     * 将助手消息加入对话历史
     */
    private void addAssistantMessageToHistory(ChatCompletionMessage msg) {
        ChatCompletionAssistantMessageParam.Builder builder = ChatCompletionAssistantMessageParam.builder()
                .role(JsonValue.from("assistant"))
                .content(msg.content().orElse(""));
        if (hasToolCall(msg)) {
            builder.toolCalls(msg.toolCalls().get());
        }
        messageHistoryList.add(ChatCompletionMessageParam.ofAssistant(builder.build()));
    }

    /**
     * 判断模型回复是否包含工具调用
     */
    private boolean hasToolCall(ChatCompletionMessage msg) {
        return msg.toolCalls().isPresent() && !msg.toolCalls().get().isEmpty();
    }
}
2.2.7 启动类开发

创建StartServer类,作为Spring Boot工程的启动入口,代码如下:

typescript 复制代码
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搭建聊天界面,实现用户输入提问、展示客服回复的功能。创建index.html文件,代码如下(可直接在浏览器中打开使用):

xml 复制代码
<!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;
        }

        /* 快捷按钮区域样式 */
        .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>
        
        <!-- 新增快捷按钮区域 -->
        <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()">发送</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';
        });

        // 快捷消息发送函数
        function sendQuickMessage(message) {
            // 将快捷消息填入输入框
            messageInput.value = message;
            messageInput.style.height = 'auto';
            messageInput.style.height = (messageInput.scrollHeight > 45 ? messageInput.scrollHeight : 45) + 'px';
            
            // 自动发送该消息
            sendMessage();
        }

        // 发送消息函数
        async function sendMessage() {
            const message = messageInput.value.trim();
            
            // 验证输入内容
            if (!message) {
                alert('请输入咨询内容!');
                return;
            }

            // 禁用发送按钮和输入框
            sendButton.disabled = true;
            messageInput.disabled = true;

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

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

                // 添加加载状态
                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 })
                });

                // 移除加载状态
                removeLoadingMessage(loadingId);

                // 处理响应
                if (response.ok) {
                    const data = await response.text();
                    // 添加客服回复到聊天窗口
                    addMessageToChat(data, 'bot');
                } else {
                    addMessageToChat('抱歉,服务器暂时无法响应,请稍后再试!', 'bot');
                    console.error('接口请求失败:', response.status);
                }
            } catch (error) {
                // 移除加载状态
                const loadingElements = document.querySelectorAll('.loading-message');
                loadingElements.forEach(el => el.remove());
                
                addMessageToChat('网络错误,请检查您的网络连接!', 'bot');
                console.error('请求出错:', error);
            } finally {
                // 恢复发送按钮和输入框
                sendButton.disabled = false;
                messageInput.disabled = false;
                messageInput.focus();
                
                // 滚动到最新消息
                scrollToBottom();
            }
        }

        // 添加消息到聊天窗口
        function addMessageToChat(content, sender) {
            const messageDiv = document.createElement('div');
            messageDiv.className = `message ${sender}-message`;
            
            // 创建头像元素
            const avatarImg = document.createElement('img');
            avatarImg.className = 'message-avatar';
            avatarImg.src = sender === 'user' ? USER_AVATAR : BOT_AVATAR;
            avatarImg.alt = sender === 'user' ? '用户头像' : '客服头像';
            
            // 创建消息内容元素
            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';
            
            // 创建客服头像
            const avatarImg = document.createElement('img');
            avatarImg.className = 'message-avatar';
            avatarImg.src = BOT_AVATAR;
            avatarImg.alt = '客服头像';
            
            // 创建加载内容
            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;
        }

        // 监听输入框回车事件(兼容)
        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>

四、系统测试


五、总结

  1. Function Calling 的核心是大模型+外部工具的协作流程,通过"问题传递→模型决策→工具执行→结果整合"四步解决大模型无法直接处理的问题;
  2. 落地 Function Calling 的关键是标准化的工具定义 (让模型识别)和灵活的工具执行器映射(便于扩展);
  3. 生产环境中需注意对话历史隔离 (多用户场景)、配置项解耦异常友好处理三大核心点。
相关推荐
Mars酱2 小时前
1分钟编写贪吃蛇 | JSnake贪吃蛇单机版
java·后端·开源
进击的野人2 小时前
MCP协议:让AI应用像插USB一样连接外部世界
人工智能·agent·mcp
清空mega2 小时前
动手学深度学习——SSD
人工智能·深度学习
tinochen2 小时前
我用 OpenClaw 搭了一套全自动 AI 行业简报系统,每天零干预推送到飞书
人工智能
何陋轩2 小时前
OpenAI Codex深度解析:终端里的AI代码特工,一个指令重构整个项目
人工智能·面试
jarvisuni2 小时前
三大编程智能体的RULES和SKILLS规范!
人工智能·ai编程
默 语2 小时前
Records、Sealed Classes这些新特性:Java真的变简单了吗?
java·开发语言·python
Kel2 小时前
PydanticAI 源码深潜:类型安全依赖注入与图执行引擎的双核架构解析
人工智能·python·架构
后端开发基础免费分享2 小时前
Claude Code 最全使用指南:CLAUDE.md、rules、skills、memory 一次讲清
人工智能·ai·claude·claudecode