Java2026年7月29日· 约 119 分钟

谁说 Java 老项目不能"玩" AI ?

#AI集成#HTTP请求#RAG检索#会话持久化
Twitter 微博

谁说 Java 老项目不能"玩" AI ?

保姆级教程:用原生 HTTP 接入阿里云百炼,实现 RAG + 会话持久化 + Tool Calls

不用任何 AI SDK,纯手写 HTTP 请求,从零搭建一个能对话、能检索、能调用后端函数的智能系统。后面会介绍一些AI接入的框架(Langchain4J、SpringAI等),那些就是固定模板使用就行,原生HTTP随意性比较大。

Gitee代码来了,我已经在本地可以正常跑通的:https://gitee.com/sun-guo-qiang/student_management_http_ai.git


写在前面#

最近接了个需求(我自己给自己加的,为了验证能否实现):给现有的学生管理系统加个 AI 助手。学生问"我数学考了多少分",系统能直接回答,不用自己翻页面。

一开始想着直接用官方 SDK 省事,但后来一想,SDK 封装太多,出了问题不好排查,而且很多公司出于合规考虑不让随便引入第三方依赖。于是决定用原生 HTTP 请求直接调百炼 API。

折腾了几天,跑通了三个核心功能:

  • RAG 检索增强:把 FAQ 文档转成向量,用户提问时先搜相关知识再回答
  • 会话 & 消息持久化:聊天记录存 MySQL,刷新页面还能接着聊
  • Tool Calls 工具调用:AI 能自动调用后端函数,比如查成绩、做计算

这篇文章把整个实现过程从头到尾捋一遍,代码都是跑通的,可以直接拿去用。


一、环境准备#

1.1 开通阿里云百炼#

  1. 登录 阿里云百炼控制台
  2. 开通百炼服务,获取 API KeyWorkspace ID
  3. 确认你需要的模型(我用的是 qwen3.7-plus)和向量化模型(text-embedding-v4

1.2 创建 Spring Boot 项目#

用 IDEA 或者 Spring Initializr 创建一个 Spring Boot 项目,引入以下依赖:

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
&lt;!-- OkHttp(HTTP 客户端) --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;com.squareup.okhttp3&lt;/groupId&gt;
    &lt;artifactId&gt;okhttp&lt;/artifactId&gt;
    &lt;version&gt;class="hljs-number">4.12.class="hljs-number">0&lt;/version&gt;
&lt;/dependency&gt;

&lt;!-- Jackson(JSON 序列化) --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;com.fasterxml.jackson.core&lt;/groupId&gt;
    &lt;artifactId&gt;jackson-databind&lt;/artifactId&gt;
&lt;/dependency&gt;

&lt;!-- MyBatis Plus(数据库操作) --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;com.baomidou&lt;/groupId&gt;
    &lt;artifactId&gt;mybatis-plus-boot-starter&lt;/artifactId&gt;
    &lt;version&gt;class="hljs-number">3.5.class="hljs-number">5&lt;/version&gt;
&lt;/dependency&gt;

&lt;!-- MySQL 驱动 --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;com.mysql&lt;/groupId&gt;
    &lt;artifactId&gt;mysql-connector-j&lt;/artifactId&gt;
&lt;/dependency&gt;

&lt;!-- Lombok(减少样板代码) --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
    &lt;artifactId&gt;lombok&lt;/artifactId&gt;
    &lt;optional&gt;true&lt;/optional&gt;
&lt;/dependency&gt;

</dependencies>

1.3 配置文件#

application.yml 里配置好数据库和百炼的参数:

server:
  port: 8081

spring: datasource: url: jdbc:mysql://localhost:3306/student_management?useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

bailian: api: api-key: sk-ws-你的APIKey # 替换成你自己的 workspace-id: ws-你的WorkspaceId # 替换成你自己的 model: qwen3.7-plus embedding-model: text-embedding-v4 chat: max-history-messages: 10 # 最多保留10条历史消息


二、用 OkHttp 调通第一个百炼 API#

这是最基础的一步——不依赖任何 SDK,直接发 HTTP 请求调百炼

2.1 理解百炼 API 的调用方式#

百炼的 API 是 OpenAI 兼容 的,也就是说请求格式和 OpenAI 的 /v1/chat/completions 完全一样。

  • 请求地址https://{workspaceId}.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/chat/completions
  • 认证方式:Header 里传 Authorization: Bearer {apiKey}
  • 请求方法:POST
  • Content-Typeapplication/json

2.2 构造请求体#

百炼的 Chat Completions API 接收一个 JSON 对象,核心字段如下:

{
  "model": "qwen3.7-plus",
  "messages": [
    {"role": "system", "content": "你是一个助手"},
    {"role": "user", "content": "你好"}
  ],
  "stream": false
}

在 Java 里,我们用 DTO 类来映射这个 JSON:

// dto/bailian/ChatRequest.java
@Data
public class ChatRequest {
private String model;                    class=class="hljs-string">"hljs-comment">// 模型名称
private List&lt;Message&gt; messages;          class=class="hljs-string">"hljs-comment">// 对话消息列表
private Boolean stream = false;          class=class="hljs-string">"hljs-comment">// 是否流式输出

class=class="hljs-string">"hljs-comment">// 消息内部类
@Data
public static class Message {
    private String role;                 class=class="hljs-string">"hljs-comment">// user / assistant / system
    private String content;              class=class="hljs-string">"hljs-comment">// 消息内容

    class=class="hljs-string">"hljs-comment">// 静态工厂方法,方便创建
    public static Message user(String content) {
        Message m = new Message();
        m.setRole(class="hljs-string">"user");
        m.setContent(content);
        return m;
    }

    public static Message system(String content) {
        Message m = new Message();
        m.setRole(class="hljs-string">"system");
        m.setContent(content);
        return m;
    }

    public static Message assistant(String content) {
        Message m = new Message();
        m.setRole(class="hljs-string">"assistant");
        m.setContent(content);
        return m;
    }
}

}

2.3 构造响应体#

API 返回的 JSON 长这样:

{
  "id": "chatcmpl-xxx",
  "model": "qwen3.7-plus",
  "choices": [
    {
      "index": 0,
      "message": {"role": "assistant", "content": "你好!有什么可以帮你的?"},
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 20,
    "completion_tokens": 15,
    "total_tokens": 35
  }
}

对应的 Java DTO:

// dto/bailian/ChatResponse.java
@Data
@JsonIgnoreProperties(ignoreUnknown = true)   // 忽略未知字段,提高兼容性
public class ChatResponse {
private String id;
private String model;
private List&lt;Choice&gt; choices;
private Usage usage;

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Choice {
    private Integer index;
    private Message message;
    @JsonProperty(class="hljs-string">"finish_reason")
    private String finishReason;
}

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Message {
    private String role;
    private String content;
}

@Data
public static class Usage {
    @JsonProperty(class="hljs-string">"prompt_tokens")
    private Integer promptTokens;
    @JsonProperty(class="hljs-string">"completion_tokens")
    private Integer completionTokens;
    @JsonProperty(class="hljs-string">"total_tokens")
    private Integer totalTokens;
}

}

2.4 用 OkHttp 发请求#

重头戏来了。BailianChatServiceImpl.java 是真正发 HTTP 请求的地方:

// service/impl/BailianChatServiceImpl.java
@Slf4j
@Service
public class BailianChatServiceImpl {
@Value(class="hljs-string">"${bailian.api.api-key}")
private String apiKey;
@Value(class="hljs-string">"${bailian.api.workspace-id}")
private String workspaceId;
@Value(class="hljs-string">"${bailian.api.model:qwen-plus}")
private String model;

private OkHttpClient httpClient;
private ObjectMapper objectMapper;

class=class="hljs-string">"hljs-comment">// 百炼 API 地址模板
private static final String BASE_URL =
        class="hljs-string">"https:class="hljs-commentclass="hljs-string">">//%s.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/chat/completions";

@PostConstruct
public void init() {
    class=class="hljs-string">"hljs-comment">// 初始化 OkHttp 客户端,设置超时时间
    this.httpClient = new OkHttpClient.Builder()
            .connectTimeout(class="hljs-number">30, TimeUnit.SECONDS)
            .readTimeout(class="hljs-number">60, TimeUnit.SECONDS)
            .writeTimeout(class="hljs-number">30, TimeUnit.SECONDS)
            .build();
    class=class="hljs-string">"hljs-comment">// 初始化 ObjectMapper,忽略未知字段
    this.objectMapper = new ObjectMapper()
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}

class=class="hljs-string">"hljs-comment">/**
 * 最基础的对话方法
 */
public String chat(String userMessage) {
    try {
        class=class="hljs-string">"hljs-comment">// class="hljs-number">1. 构造消息列表
        List&lt;ChatRequest.Message&gt; messages = new ArrayList&lt;&gt;();
        messages.add(ChatRequest.Message.system(class="hljs-string">"你是一个智能助手"));
        messages.add(ChatRequest.Message.user(userMessage));

        class=class="hljs-string">"hljs-comment">// class="hljs-number">2. 构造请求体
        ChatRequest request = new ChatRequest();
        request.setModel(model);
        request.setMessages(messages);
        request.setStream(false);

        class=class="hljs-string">"hljs-comment">// class="hljs-number">3. 序列化请求体为 JSON
        String jsonBody = objectMapper.writeValueAsString(request);

        class=class="hljs-string">"hljs-comment">// class="hljs-number">4. 拼接 URL(把 workspaceId 替换到子域名)
        String url = String.format(BASE_URL, workspaceId);

        class=class="hljs-string">"hljs-comment">// class="hljs-number">5. 构造 OkHttp 请求
        Request httpRequest = new Request.Builder()
                .url(url)
                .post(RequestBody.create(jsonBody,
                        MediaType.parse(class="hljs-string">"application/json")))
                .addHeader(class="hljs-string">"Authorization", class="hljs-string">"Bearer " + apiKey)
                .addHeader(class="hljs-string">"Content-Type", class="hljs-string">"application/json")
                .build();

        class=class="hljs-string">"hljs-comment">// class="hljs-number">6. 发送请求
        try (Response response = httpClient.newCall(httpRequest).execute()) {
            String responseBody = response.body().string();

            if (!response.isSuccessful()) {
                log.error(class="hljs-string">"调用失败, code={}, body={}", response.code(), responseBody);
                return class="hljs-string">"AI服务暂时不可用";
            }

            class=class="hljs-string">"hljs-comment">// class="hljs-number">7. 解析响应
            ChatResponse chatResponse = objectMapper.readValue(
                    responseBody, ChatResponse.class);

            if (chatResponse.getChoices() == null || chatResponse.getChoices().isEmpty()) {
                return class="hljs-string">"未获取到有效回复";
            }

            class=class="hljs-string">"hljs-comment">// class="hljs-number">8. 提取 AI 回复内容
            return chatResponse.getChoices().get(class="hljs-number">0).getMessage().getContent();
        }
    } catch (IOException e) {
        log.error(class="hljs-string">"调用异常", e);
        return class="hljs-string">"AI服务异常: " + e.getMessage();
    }
}

}

关键点说明:

  • @PostConstruct 里的 init() 方法在 Bean 初始化时执行,只创建一次 OkHttp 客户端和 ObjectMapper,避免重复创建
  • RequestBody.create(jsonBody, MediaType.parse("application/json")) 是 OkHttp 发送 POST 请求的标准写法
  • try (Response response = ...) 用了 try-with-resources,确保 response 被正确关闭,不会泄漏连接
  • @JsonIgnoreProperties(ignoreUnknown = true) 很重要,因为百炼返回的字段可能比我们的 DTO 多,不加这个会报错

2.5 写个 Controller 测试一下#

// controller/ChatController.java
@RestController
@RequestMapping("/api/chat")
public class ChatController {
@Autowired
private BailianChatServiceImpl chatService;

@PostMapping(class="hljs-string">"/simple")
public Map&lt;String, Object&gt; simpleChat(@RequestBody Map&lt;String, String&gt; req) {
    String reply = chatService.chat(req.get(class="hljs-string">"message"));
    Map&lt;String, Object&gt; result = new HashMap&lt;&gt;();
    result.put(class="hljs-string">"reply", reply);
    return result;
}

}

启动项目,用 Postman 或者 curl 测试:

curl -X POST http://localhost:8081/api/chat/simple \
  -H "Content-Type: application/json" \
  -d '{"message": "你好,请介绍一下你自己"}'

如果一切正常,你会收到 AI 的回复。


三、实现多轮对话(带历史记忆)#

上面的例子只能做单轮对话,每次都是全新的对话,AI 不记得之前说过什么。

3.1 多轮对话的原理#

多轮对话的核心是:把历史消息也发给 AI

比如用户先问"你好",AI 回答"你好!有什么可以帮你的?",然后用户又问"我数学考了多少分"。

第二次请求时,messages 应该是这样的:

{
  "messages": [
    {"role": "user", "content": "你好"},
    {"role": "assistant", "content": "你好!有什么可以帮你的?"},
    {"role": "user", "content": "我数学考了多少分"}
  ]
}

这样 AI 就知道上下文了。

3.2 用内存存历史消息#

最简单的方案是用 ConcurrentHashMap 存每个用户的对话历史:

// service/impl/ConversationServiceImpl.java
@Slf4j
@Service
public class ConversationServiceImpl {
@Value(class="hljs-string">"${bailian.chat.max-history-messages:class="hljs-number">10}")
private int maxHistoryMessages;

class=class="hljs-string">"hljs-comment">// key=userId, value=历史消息列表
private final Map&lt;Integer, List&lt;ChatRequest.Message&gt;&gt; store = new ConcurrentHashMap&lt;&gt;();
private final AtomicInteger idGen = new AtomicInteger(class="hljs-number">1);

class=class="hljs-string">"hljs-comment">/**
 * 获取用户的历史消息
 */
public List&lt;ChatRequest.Message&gt; getHistory(Integer userId) {
    if (userId == null) return Collections.emptyList();
    List&lt;ChatRequest.Message&gt; history = store.get(userId);
    return history == null ? Collections.emptyList() : new ArrayList&lt;&gt;(history);
}

class=class="hljs-string">"hljs-comment">/**
 * 添加消息到历史
 */
public void addMessage(Integer userId, String role, String content) {
    if (userId == null || content == null || content.trim().isEmpty()) return;

    List&lt;ChatRequest.Message&gt; history = store.computeIfAbsent(
            userId, k -&gt; new ArrayList&lt;&gt;());
    ChatRequest.Message msg = new ChatRequest.Message();
    msg.setRole(role);
    msg.setContent(content);
    history.add(msg);

    class=class="hljs-string">"hljs-comment">// 超过最大数量时,截断旧消息
    if (history.size() &gt; maxHistoryMessages) {
        List&lt;ChatRequest.Message&gt; newHistory = new ArrayList&lt;&gt;(
                history.subList(history.size() - maxHistoryMessages, history.size()));
        store.put(userId, newHistory);
    }
}

class=class="hljs-string">"hljs-comment">/**
 * 清除用户历史
 */
public void clearHistory(Integer userId) {
    if (userId != null) store.remove(userId);
}

class=class="hljs-string">"hljs-comment">/**
 * 生成新用户 ID
 */
public Integer generateUserId() {
    return idGen.getAndIncrement();
}

}

3.3 改造 ChatService,支持多轮#

// service/impl/BailianChatServiceImpl.java(改造后)
@Autowired
private ConversationServiceImpl conversationService;

public String chatWithMemory(Integer userId, String userMessage) { // 1. 获取历史消息 List<ChatRequest.Message> messages = conversationService.getHistory(userId);

class=class="hljs-string">"hljs-comment">// class="hljs-number">2. 添加 system 消息(可选)
if (messages.isEmpty()) {
    messages.add(class="hljs-number">0, ChatRequest.Message.system(class="hljs-string">"你是一个智能助手"));
}

class=class="hljs-string">"hljs-comment">// class="hljs-number">3. 添加当前用户消息
messages.add(ChatRequest.Message.user(userMessage));

class=class="hljs-string">"hljs-comment">// class="hljs-number">4. 调用 API
String reply = doChat(messages);

class=class="hljs-string">"hljs-comment">// class="hljs-number">5. 保存用户消息和 AI 回复到历史
conversationService.addMessage(userId, class="hljs-string">"user", userMessage);
conversationService.addMessage(userId, class="hljs-string">"assistant", reply);

return reply;

}

这样用户就能体验到"有记忆"的对话了。不过内存存储有个问题:服务重启后数据全丢


四、会话 & 消息持久化到 MySQL#

4.1 建表#

先建两张表,一张存会话,一张存消息:

-- 会话表
CREATE TABLE chat_session (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    session_id VARCHAR(64) NOT NULL UNIQUE,
    user_id INT NOT NULL,
    title VARCHAR(255),
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted TINYINT DEFAULT 0
);

-- 消息表 CREATE TABLE chat_message ( id BIGINT AUTO_INCREMENT PRIMARY KEY, session_id VARCHAR(64) NOT NULL, role VARCHAR(20) NOT NULL, -- user / assistant content TEXT, tool_calls TEXT, -- 工具调用 JSON(后面会用到) created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_session_id (session_id) );

4.2 实体类#

// entity/ChatSession.java
@Data
@TableName("chat_session")
public class ChatSession {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String sessionId;
    private Integer userId;
    private String title;
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createdAt;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updatedAt;
    @TableLogic
    private Integer deleted;
}

// entity/ChatMessage.java @Data @TableName("chat_message") public class ChatMessage { @TableId(type = IdType.AUTO) private Long id; private String sessionId; private String role; private String content; private String toolCalls; @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt; }

4.3 Mapper 接口#

// mapper/ChatSessionMapper.java
@Mapper
public interface ChatSessionMapper extends BaseMapper<ChatSession> {
    List<ChatSession> selectByUserIdOrderByUpdated(@Param("userId") Integer userId);
}

// mapper/ChatMessageMapper.java @Mapper public interface ChatMessageMapper extends BaseMapper<ChatMessage> { List<ChatMessage> selectBySessionIdOrderByTime(@Param("sessionId") String sessionId); }

对应的 XML:

<!-- mapper/ChatSessionMapper.xml -->
<mapper namespace="com.sun.student_management_http_ai.mapper.ChatSessionMapper">
    <select id="selectByUserIdOrderByUpdated" resultType="ChatSession">
        SELECT * FROM chat_session
        WHERE user_id = #{userId} AND deleted = 0
        ORDER BY updated_at DESC
    </select>
</mapper>

<!-- mapper/ChatMessageMapper.xml --> <mapper namespace="com.sun.student_management_http_ai.mapper.ChatMessageMapper"> <select id="selectBySessionIdOrderByTime" resultType="ChatMessage"> SELECT * FROM chat_message WHERE session_id = #{sessionId} ORDER BY created_at ASC </select> </mapper>

4.4 持久化服务#

// service/impl/ChatPersistenceServiceImpl.java
@Service
@RequiredArgsConstructor
public class ChatPersistenceServiceImpl {
private final ChatSessionMapper sessionMapper;
private final ChatMessageMapper messageMapper;

class=class="hljs-string">"hljs-comment">/**
 * 创建新会话
 */
public String createSession(Integer userId, String firstMessage) {
    String sessionId = UUID.randomUUID().toString();
    ChatSession session = new ChatSession();
    session.setSessionId(sessionId);
    session.setUserId(userId);
    class=class="hljs-string">"hljs-comment">// 首条消息截取前class="hljs-number">50字作为标题
    if (firstMessage != null &amp;&amp; firstMessage.length() &gt; class="hljs-number">50) {
        session.setTitle(firstMessage.substring(class="hljs-number">0, class="hljs-number">50));
    } else {
        session.setTitle(firstMessage);
    }
    sessionMapper.insert(session);
    return sessionId;
}

class=class="hljs-string">"hljs-comment">/**
 * 保存消息
 */
public void saveMessage(String sessionId, String role, String content,
                        String toolCallsJson) {
    ChatMessage msg = new ChatMessage();
    msg.setSessionId(sessionId);
    msg.setRole(role);
    msg.setContent(content);
    msg.setToolCalls(toolCallsJson);
    msg.setCreatedAt(LocalDateTime.now());
    messageMapper.insert(msg);

    class=class="hljs-string">"hljs-comment">// 更新会话的 updated_at
    LambdaQueryWrapper&lt;ChatSession&gt; wrapper = new LambdaQueryWrapper&lt;&gt;();
    wrapper.eq(ChatSession::getSessionId, sessionId);
    ChatSession session = sessionMapper.selectOne(wrapper);
    if (session != null) {
        session.setUpdatedAt(LocalDateTime.now());
        sessionMapper.updateById(session);
    }
}

class=class="hljs-string">"hljs-comment">/**
 * 获取会话的所有消息(按时间升序)
 */
public List&lt;ChatMessage&gt; getSessionMessages(String sessionId) {
    return messageMapper.selectBySessionIdOrderByTime(sessionId);
}

class=class="hljs-string">"hljs-comment">/**
 * 获取 LLM 格式的历史消息(只取最近 maxHistory 条)
 */
public List&lt;ChatRequest.Message&gt; getHistoryForLLM(String sessionId, int maxHistory) {
    List&lt;ChatMessage&gt; messages = getSessionMessages(sessionId);
    List&lt;ChatRequest.Message&gt; history = new ArrayList&lt;&gt;();
    int count = class="hljs-number">0;

    class=class="hljs-string">"hljs-comment">// 从后往前取,保证取到的是最近的消息
    for (int i = messages.size() - class="hljs-number">1; i &gt;= class="hljs-number">0; i--) {
        ChatMessage cm = messages.get(i);
        if (class="hljs-string">"user".equals(cm.getRole()) || class="hljs-string">"assistant".equals(cm.getRole())) {
            if (count &gt;= maxHistory) break;
            ChatRequest.Message m = new ChatRequest.Message();
            m.setRole(cm.getRole());
            m.setContent(cm.getContent());
            history.add(m);
            count++;
        }
    }
    Collections.reverse(history);  class=class="hljs-string">"hljs-comment">// 恢复时间顺序
    return history;
}

class=class="hljs-string">"hljs-comment">/**
 * 删除会话(逻辑删除)
 */
public void deleteSession(String sessionId, Integer userId) {
    LambdaQueryWrapper&lt;ChatSession&gt; wrapper = new LambdaQueryWrapper&lt;&gt;();
    wrapper.eq(ChatSession::getSessionId, sessionId)
           .eq(ChatSession::getUserId, userId);
    ChatSession session = sessionMapper.selectOne(wrapper);
    if (session != null) {
        sessionMapper.deleteById(session.getId());
    }
}

class=class="hljs-string">"hljs-comment">/**
 * 更新会话标题
 */
public void updateSessionTitle(String sessionId, Integer userId, String title) {
    LambdaQueryWrapper&lt;ChatSession&gt; wrapper = new LambdaQueryWrapper&lt;&gt;();
    wrapper.eq(ChatSession::getSessionId, sessionId)
           .eq(ChatSession::getUserId, userId);
    ChatSession session = sessionMapper.selectOne(wrapper);
    if (session != null) {
        session.setTitle(title);
        session.setUpdatedAt(LocalDateTime.now());
        sessionMapper.updateById(session);
    }
}

class=class="hljs-string">"hljs-comment">/**
 * 检查会话是否存在
 */
public boolean sessionExists(String sessionId, Integer userId) {
    LambdaQueryWrapper&lt;ChatSession&gt; wrapper = new LambdaQueryWrapper&lt;&gt;();
    wrapper.eq(ChatSession::getSessionId, sessionId)
           .eq(ChatSession::getUserId, userId);
    return sessionMapper.selectCount(wrapper) &gt; class="hljs-number">0;
}

}

4.5 持久化对话流程#

public ChatResult chatWithPersistence(Integer userId, String sessionId,
                                       String userMessage) {
    // 1. 初始化用户ID
    if (userId == null) userId = generateUserId();
class=class="hljs-string">"hljs-comment">// class="hljs-number">2. 初始化会话ID
if (sessionId == null || sessionId.isEmpty()) {
    sessionId = persistenceService.createSession(userId, userMessage);
} else {
    if (!persistenceService.sessionExists(sessionId, userId)) {
        sessionId = persistenceService.createSession(userId, userMessage);
    }
}

class=class="hljs-string">"hljs-comment">// class="hljs-number">3. 获取历史消息
List&lt;ChatRequest.Message&gt; history = persistenceService.getHistoryForLLM(
        sessionId, maxHistoryMessages);

class=class="hljs-string">"hljs-comment">// class="hljs-number">4. 构建消息列表
List&lt;ChatRequest.Message&gt; messages = new ArrayList&lt;&gt;();
messages.add(ChatRequest.Message.system(class="hljs-string">"你是一个智能助手"));
messages.addAll(history);
messages.add(ChatRequest.Message.user(userMessage));

class=class="hljs-string">"hljs-comment">// class="hljs-number">5. 调用 AI
String reply = doChat(messages);

class=class="hljs-string">"hljs-comment">// class="hljs-number">6. 保存到数据库
persistenceService.saveMessage(sessionId, class="hljs-string">"user", userMessage, null);
persistenceService.saveMessage(sessionId, class="hljs-string">"assistant", reply, null);

return new ChatResult(userId, sessionId, reply);

}


五、RAG 检索增强生成#

RAG 是现在 AI 应用的标准配置。原理很简单:先搜相关知识,再把知识喂给 AI

5.1 RAG 的整体流程#

FAQ 文档 → 文本分割 → 向量化 → 存入向量库
                                    ↓
用户提问 → 向量化 → 相似度检索 → 取 Top3 → 拼入 System Prompt → 调 AI

5.2 文本分割#

先准备一份 FAQ 文档,放在 src/main/resources/docs/学生管理系统FAQ.txt

Q:如何修改密码?
A:在"个人中心"->"安全设置"中可修改登录密码,修改前需要���证原密码。

Q:如何选课? A:登录系统后,进入"选课管理"模块,查看可选课程列表,点击"选课"按钮即可。

Q:如何查成绩? A:进入"成绩查询"模块,选择学期和科目,点击"查询"即可查看成绩。

然后写一个分割器,把文档按 Q/A 拆成独立的 Document:

// service/impl/TextSplitterImpl.java
@Slf4j
@Service
public class TextSplitterImpl {
class=class="hljs-string">"hljs-comment">/**
 * 解析 QA 格式的文档
 */
public List&lt;Document&gt; parseQADocuments(String content) {
    List&lt;Document&gt; docs = new ArrayList&lt;&gt;();
    String[] lines = content.split(class="hljs-string">"\\n");
    String currentQ = null;
    StringBuilder currentA = new StringBuilder();

    for (String line : lines) {
        String trimmed = line.trim();
        if (trimmed.startsWith(class="hljs-string">"Q:") || trimmed.startsWith(class="hljs-string">"Q:")) {
            class=class="hljs-string">"hljs-comment">// 保存上一个 QA
            if (currentQ != null &amp;&amp; currentA.length() &gt; class="hljs-number">0) {
                docs.add(createDoc(currentQ, currentA.toString().trim()));
            }
            currentQ = trimmed.substring(class="hljs-number">2).trim();
            currentA = new StringBuilder();
        } else if (trimmed.startsWith(class="hljs-string">"A:") || trimmed.startsWith(class="hljs-string">"A:")) {
            currentA.append(trimmed.substring(class="hljs-number">2).trim()).append(class="hljs-string">" ");
        } else if (currentA.length() &gt; class="hljs-number">0) {
            currentA.append(trimmed).append(class="hljs-string">" ");
        }
    }
    class=class="hljs-string">"hljs-comment">// 处理最后一个 QA
    if (currentQ != null &amp;&amp; currentA.length() &gt; class="hljs-number">0) {
        docs.add(createDoc(currentQ, currentA.toString().trim()));
    }
    log.info(class="hljs-string">"解析 QA 格式完成,共 {} 条", docs.size());
    return docs;
}

class=class="hljs-string">"hljs-comment">/**
 * 创建 Document 对象
 */
public Document createDoc(String question, String answer) {
    Document doc = new Document();
    doc.setContent(question);  class=class="hljs-string">"hljs-comment">// content 存问题(用于向量检索)
    doc.getMetadata().put(class="hljs-string">"question", question);
    doc.getMetadata().put(class="hljs-string">"answer", answer);
    return doc;
}

}

// dto/bailian/rag/Document.java @Data public class Document { private String content; private Map<String, String> metadata = new HashMap<>(); }

5.3 文本向量化#

向量化就是把文本转成浮点数数组。百炼的 Embedding API 也是 OpenAI 兼容的:

  • 请求地址https://{workspaceId}.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/embeddings
  • 请求体{"model": "text-embedding-v4", "input": ["文本1", "文本2"]}
// service/impl/EmbeddingServiceImpl.java
@Slf4j
@Service
public class EmbeddingServiceImpl {
@Value(class="hljs-string">"${bailian.api.api-key}")
private String apiKey;
@Value(class="hljs-string">"${bailian.api.workspace-id}")
private String workspaceId;

private OkHttpClient httpClient;
private ObjectMapper objectMapper;
private static final String MODEL = class="hljs-string">"text-embedding-v4";
private static final String BASE_URL =
        class="hljs-string">"https:class="hljs-commentclass="hljs-string">">//%s.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/embeddings";
private static final int BATCH_SIZE = class="hljs-number">10;  class=class="hljs-string">"hljs-comment">// 每批最多class="hljs-number">10条

@PostConstruct
public void init() {
    this.httpClient = new OkHttpClient.Builder()
            .connectTimeout(class="hljs-number">30, TimeUnit.SECONDS)
            .readTimeout(class="hljs-number">60, TimeUnit.SECONDS)
            .build();
    this.objectMapper = new ObjectMapper();
}

class=class="hljs-string">"hljs-comment">/**
 * 单条文本向量化
 */
public float[] embed(String text) throws IOException {
    List&lt;float[]&gt; result = embedBatch(List.of(text));
    return result.isEmpty() ? new float[class="hljs-number">0] : result.get(class="hljs-number">0);
}

class=class="hljs-string">"hljs-comment">/**
 * 批量向量化(每批最多class="hljs-number">10条)
 */
public List&lt;float[]&gt; embedBatch(List&lt;String&gt; texts) throws IOException {
    if (texts == null || texts.isEmpty()) return new ArrayList&lt;&gt;();

    List&lt;float[]&gt; allVectors = new ArrayList&lt;&gt;();
    String url = String.format(BASE_URL, workspaceId);

    for (int i = class="hljs-number">0; i &lt; texts.size(); i += BATCH_SIZE) {
        List&lt;String&gt; batch = texts.subList(i,
                Math.min(i + BATCH_SIZE, texts.size()));

        class=class="hljs-string">"hljs-comment">// 构造请求体
        EmbeddingRequest req = new EmbeddingRequest();
        req.setModel(MODEL);
        req.setInput(batch);
        String json = objectMapper.writeValueAsString(req);

        class=class="hljs-string">"hljs-comment">// 发 HTTP 请求
        Request httpReq = new Request.Builder()
                .url(url)
                .post(RequestBody.create(json,
                        MediaType.parse(class="hljs-string">"application/json")))
                .addHeader(class="hljs-string">"Authorization", class="hljs-string">"Bearer " + apiKey)
                .build();

        try (Response response = httpClient.newCall(httpReq).execute()) {
            String body = response.body().string();
            if (!response.isSuccessful()) {
                throw new IOException(class="hljs-string">"Embedding 失败: " + body);
            }
            EmbeddingResponse embResp = objectMapper.readValue(
                    body, EmbeddingResponse.class);
            for (EmbeddingResponse.EmbeddingData d : embResp.getData()) {
                class=class="hljs-string">"hljs-comment">// Double 列表转 float[]
                float[] vec = new float[d.getEmbedding().size()];
                for (int j = class="hljs-number">0; j &lt; vec.length; j++) {
                    vec[j] = d.getEmbedding().get(j).floatValue();
                }
                allVectors.add(vec);
            }
        }
    }
    return allVectors;
}

}

// dto/bailian/rag/EmbeddingRequest.java @Data public class EmbeddingRequest { private String model; private List<String> input; }

// dto/bailian/rag/EmbeddingResponse.java @Data @JsonIgnoreProperties(ignoreUnknown = true) public class EmbeddingResponse { private List<EmbeddingData> data;

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class EmbeddingData {
    private Integer index;
    private List&lt;Double&gt; embedding;
}

}

5.4 内存向量库 + 余弦相似度检索#

向量库就是个 List<VectorEntry>,每个 Entry 存文本内容和对应的向量。检索时用余弦相似度

// service/MemoryVectorStore.java
@Slf4j
@Service
public class MemoryVectorStore implements ApplicationRunner {
@Autowired
private EmbeddingServiceImpl embeddingService;
@Autowired
private TextSplitterImpl textSplitter;

private final List&lt;VectorEntry&gt; vectorStore = new ArrayList&lt;&gt;();
private final AtomicBoolean ready = new AtomicBoolean(false);

private static final double THRESHOLD = class="hljs-number">0.65;  class=class="hljs-string">"hljs-comment">// 相似度阈值
private static final int TOP_K = class="hljs-number">3;             class=class="hljs-string">"hljs-comment">// 返回前class="hljs-number">3条

class=class="hljs-string">"hljs-comment">/**
 * 应用启动时异步加载知识库
 */
@Async
@Override
public void run(ApplicationArguments args) {
    try {
        class=class="hljs-string">"hljs-comment">// 读取 FAQ 文档
        ClassPathResource resource = new ClassPathResource(class="hljs-string">"docs/学生管理系统FAQ.txt");
        StringBuilder content = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                content.append(line).append(class="hljs-string">"\n");
            }
        }

        class=class="hljs-string">"hljs-comment">// 解析文档
        List&lt;Document&gt; documents = textSplitter.parseQADocuments(content.toString());

        class=class="hljs-string">"hljs-comment">// 向量化
        List&lt;String&gt; texts = documents.stream()
                .map(Document::getContent)
                .collect(Collectors.toList());
        List&lt;float[]&gt; vectors = embeddingService.embedBatch(texts);

        class=class="hljs-string">"hljs-comment">// 存入向量库
        for (int i = class="hljs-number">0; i &lt; documents.size(); i++) {
            VectorEntry entry = new VectorEntry();
            entry.setId(UUID.randomUUID().toString());
            entry.setContent(documents.get(i).getContent());
            entry.setVector(vectors.get(i));
            entry.setMetadata(documents.get(i).getMetadata());
            vectorStore.add(entry);
        }

        ready.set(true);
        log.info(class="hljs-string">"内存向量库初始化完成,共 {} 条记录", vectorStore.size());
    } catch (Exception e) {
        log.error(class="hljs-string">"向量库初始化失败", e);
    }
}

class=class="hljs-string">"hljs-comment">/**
 * 检索与查询最相关的文档
 */
public List&lt;VectorEntry&gt; search(String query) {
    if (!ready.get()) return Collections.emptyList();

    try {
        class=class="hljs-string">"hljs-comment">// 查询向量化
        float[] qVec = embeddingService.embed(query);

        class=class="hljs-string">"hljs-comment">// 计算余弦相似度
        List&lt;ScoredEntry&gt; scored = new ArrayList&lt;&gt;();
        for (VectorEntry entry : vectorStore) {
            double sim = cosineSimilarity(qVec, entry.getVector());
            scored.add(new ScoredEntry(entry, sim));
        }

        class=class="hljs-string">"hljs-comment">// 按相似度降序排列
        scored.sort((a, b) -&gt; Double.compare(b.similarity, a.similarity));

        class=class="hljs-string">"hljs-comment">// 取 Top3 且相似度 &gt;= 阈值
        List&lt;VectorEntry&gt; results = new ArrayList&lt;&gt;();
        for (ScoredEntry se : scored) {
            if (se.similarity &gt;= THRESHOLD &amp;&amp; results.size() &lt; TOP_K) {
                results.add(se.entry);
            }
        }
        return results;
    } catch (Exception e) {
        log.error(class="hljs-string">"检索失败", e);
        return Collections.emptyList();
    }
}

class=class="hljs-string">"hljs-comment">/**
 * 余弦相似度:cos(theta) = (A·B) / (|A| * |B|)
 */
private double cosineSimilarity(float[] a, float[] b) {
    double dot = class="hljs-number">0, na = class="hljs-number">0, nb = class="hljs-number">0;
    for (int i = class="hljs-number">0; i &lt; a.length; i++) {
        dot += a[i] * b[i];
        na += a[i] * a[i];
        nb += b[i] * b[i];
    }
    return dot / (Math.sqrt(na) * Math.sqrt(nb));
}

@Data
public static class VectorEntry {
    private String id;
    private String content;
    private float[] vector;
    private Map&lt;String, String&gt; metadata = new HashMap&lt;&gt;();
}

@Data
@AllArgsConstructor
private static class ScoredEntry {
    VectorEntry entry;
    double similarity;
}

}

5.5 把检索结果拼入 System Prompt#

// 在对话服务中
if (useRag) {
    List<VectorEntry> matched = vectorStore.search(userMessage);
    if (matched != null && !matched.isEmpty()) {
        StringBuilder context = new StringBuilder();
        for (VectorEntry entry : matched) {
            String answer = entry.getMetadata().get("answer");
            if (answer != null) {
                context.append("- ").append(answer).append("\n");
            }
        }
        String systemPrompt = "你是一个智能助手。请根据以下参考资料回答用户的问题," +
                "如果资料没有相关答案,结合最符合的资料进行补充回答," +
                "毫无相关资料则回答暂时无法回答建议提工单处理。\n\n" +
                "【参考资料】\n" + context.toString();
        messages.add(ChatRequest.Message.system(systemPrompt));
    }
}

这样,当用户问"如何修改密码"时,AI 会先检索到相关的 FAQ 答案,然后基于这个答案来回答用户。


六、Tool Calls — 让 AI 调用你的后端函数#

这是最有意思的部分。通过 Function Calling,AI 可以自动判断什么时候需要调用你的后端方法。

6.1 什么是 Tool Calls#

简单说,就是你在请求里告诉 AI:"我有这些工具可以用",然后 AI 在回复时如果判断需要调用工具,会返回一个 tool_calls 字段,你执行完工具后把结果再发给 AI,AI 再生成最终回复。

举个例子:

你发给 AI:
{
  "messages": [{"role": "user", "content": "2@4等于多少"}],
  "tools": [{
    "type": "function",
    "function": {
      "name": "calculate_at_operation",
      "description": "计算两个数字之间的特殊运算(@运算),公式是 a * b + 100",
      "parameters": {
        "type": "object",
        "properties": {
          "a": {"type": "integer", "description": "@符号左边的数字"},
          "b": {"type": "integer", "description": "@符号右边的数字"}
        },
        "required": ["a", "b"]
      }
    }
  }]
}

AI 返回: { "choices": [{ "message": { "role": "assistant", "tool_calls": [{ "id": "call_xxx", "function": { "name": "calculate_at_operation", "arguments": "{"a": 2, "b": 4}" } }] }, "finish_reason": "tool_calls" }] }

然后你执行 calculate_at_operation(2, 4) 得到结果 108,再发给 AI:

{
  "messages": [
    {"role": "user", "content": "2@4等于多少"},
    {"role": "assistant", "tool_calls": [...]},
    {"role": "tool", "tool_call_id": "call_xxx", "content": "计算结果: 2 @ 4 = 108"}
  ],
  "tools": [...]
}

AI 最终返回: {"choices": [{"message": {"content": "2@4 的计算结果是 108"}, "finish_reason": "stop"}]}

6.2 自定义注解#

为了优雅地注册工具方法,我们定义两个注解:

// annotation/ToolMethod.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ToolMethod {
    String name() default "";          // 工具名称
    String description() default "";   // 工具描述(给 AI 看的)
    boolean enabled() default true;    // 是否启用
}

// annotation/ToolParam.java @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ToolParam { String name() default ""; // 参数名称 String description() default ""; // 参数描述(给 AI 看的) boolean required() default true; // 是否必填 }

6.3 工具注册中心#

ToolRegistry 实现了 BeanPostProcessor,在 Spring 容器初始化 Bean 后自动扫描带 @ToolMethod 的方法:

// register/ToolRegistry.java
@Slf4j
@Component
public class ToolRegistry implements BeanPostProcessor, ApplicationContextAware {
private ApplicationContext applicationContext;
private final Map&lt;String, ToolRegistration&gt; registry = new ConcurrentHashMap&lt;&gt;();
private final ObjectMapper mapper = new ObjectMapper();

@Override
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
    this.applicationContext = ctx;
}

class=class="hljs-string">"hljs-comment">// Bean 初始化后自动扫描
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
    scanBean(bean);
    return bean;
}

private void scanBean(Object bean) {
    Class&lt;?&gt; clazz = bean.getClass();
    class=class="hljs-string">"hljs-comment">// 处理 CGLIB 代理
    if (clazz.getName().contains(class="hljs-string">"$$")) clazz = clazz.getSuperclass();
    for (Method method : clazz.getDeclaredMethods()) {
        ToolMethod tm = method.getAnnotation(ToolMethod.class);
        if (tm == null || !tm.enabled()) continue;
        ToolRegistration reg = buildRegistration(tm, method, bean);
        if (reg != null) {
            registry.put(reg.getName(), reg);
            log.info(class="hljs-string">"注册工具: {}", reg.getName());
        }
    }
}

private ToolRegistration buildRegistration(ToolMethod tm, Method method, Object bean) {
    String name = tm.name().isEmpty() ? method.getName() : tm.name();
    ToolRegistration reg = new ToolRegistration();
    reg.setName(name);
    reg.setDescription(tm.description());
    reg.setMethod(method);
    reg.setTarget(bean);

    List&lt;ToolParameter&gt; params = new ArrayList&lt;&gt;();
    for (Parameter p : method.getParameters()) {
        ToolParam tp = p.getAnnotation(ToolParam.class);
        ToolParameter param = new ToolParameter();
        if (tp != null) {
            param.setName(tp.name().isEmpty() ? p.getName() : tp.name());
            param.setDescription(tp.description());
            param.setRequired(tp.required());
        } else {
            param.setName(p.getName());
            param.setDescription(class="hljs-string">"参数 " + p.getName());
            param.setRequired(true);
        }
        param.setType(p.getType());
        param.setJsonType(mapType(p.getType()));
        params.add(param);
    }
    reg.setParameters(params);
    return reg;
}

class=class="hljs-string">"hljs-comment">// Java 类型 -&gt; JSON Schema 类型
private String mapType(Class&lt;?&gt; type) {
    if (type == String.class) return class="hljs-string">"string";
    if (type == Integer.class || type == int.class) return class="hljs-string">"integer";
    if (type == Long.class || type == long.class) return class="hljs-string">"integer";
    if (type == Double.class || type == double.class ||
        type == Float.class || type == float.class) return class="hljs-string">"number";
    if (type == Boolean.class || type == boolean.class) return class="hljs-string">"boolean";
    if (type == List.class || type.isArray()) return class="hljs-string">"array";
    return class="hljs-string">"object";
}

class=class="hljs-string">"hljs-comment">/**
 * 生成 Function Calling Schema(发给 AI 的 tools 定义)
 */
public List&lt;ChatRequest.Tool&gt; getToolDefinitions() {
    List&lt;ChatRequest.Tool&gt; tools = new ArrayList&lt;&gt;();
    for (ToolRegistration reg : registry.values()) {
        ChatRequest.Tool tool = new ChatRequest.Tool();
        tool.setType(class="hljs-string">"function");
        ChatRequest.FunctionDef func = new ChatRequest.FunctionDef();
        func.setName(reg.getName());
        func.setDescription(reg.getDescription());

        class=class="hljs-string">"hljs-comment">// 构造 JSON Schema 参数定义
        Map&lt;String, Object&gt; params = new LinkedHashMap&lt;&gt;();
        params.put(class="hljs-string">"type", class="hljs-string">"object");
        Map&lt;String, Object&gt; props = new LinkedHashMap&lt;&gt;();
        List&lt;String&gt; required = new ArrayList&lt;&gt;();
        for (ToolParameter p : reg.getParameters()) {
            Map&lt;String, Object&gt; prop = new LinkedHashMap&lt;&gt;();
            prop.put(class="hljs-string">"type", p.getJsonType());
            prop.put(class="hljs-string">"description", p.getDescription());
            props.put(p.getName(), prop);
            if (p.isRequired()) required.add(p.getName());
        }
        params.put(class="hljs-string">"properties", props);
        if (!required.isEmpty()) params.put(class="hljs-string">"required", required);
        func.setParameters(params);
        tool.setFunction(func);
        tools.add(tool);
    }
    return tools;
}

class=class="hljs-string">"hljs-comment">/**
 * 执行工具(反射调用)
 */
public String execute(String toolName, String arguments) {
    ToolRegistration reg = registry.get(toolName);
    if (reg == null) return class="hljs-string">"未知工具: " + toolName;
    try {
        JsonNode argsNode = mapper.readTree(arguments);
        Method method = reg.getMethod();
        Object[] args = new Object[method.getParameterCount()];
        for (int i = class="hljs-number">0; i &lt; method.getParameters().length; i++) {
            ToolParameter p = reg.getParameters().get(i);
            JsonNode val = argsNode.path(p.getName());
            args[i] = convert(val, p.getType());
        }
        Object result = method.invoke(reg.getTarget(), args);
        return result != null ? result.toString() : class="hljs-string">"执行成功(无返回值)";
    } catch (Exception e) {
        log.error(class="hljs-string">"执行工具失败: {}", toolName, e);
        return class="hljs-string">"工具执行失败: " + e.getMessage();
    }
}

private Object convert(JsonNode node, Class&lt;?&gt; type) {
    if (node.isNull()) return null;
    if (type == String.class) return node.asText();
    if (type == Integer.class || type == int.class) return node.asInt();
    if (type == Long.class || type == long.class) return node.asLong();
    if (type == Double.class || type == double.class) return node.asDouble();
    if (type == Boolean.class || type == boolean.class) return node.asBoolean();
    try {
        return mapper.treeToValue(node, type);
    } catch (Exception e) {
        return node.asText();
    }
}

}

// 工具注册信息类 @Data class ToolRegistration { private String name; private String description; private Method method; private Object target; private List<ToolParameter> parameters = new ArrayList<>();

@Data
static class ToolParameter {
    private String name;
    private String description;
    private boolean required;
    private Class&lt;?&gt; type;
    private String jsonType;
}

}

6.4 写一个实际的工具方法#

// tools/MathTool.java
@Slf4j
@Service
public class MathTool {
@ToolMethod(
        name = class="hljs-string">"calculate_at_operation",
        description = class="hljs-string">"计算两个数字之间的特殊运算(@运算)。" +
                class="hljs-string">"当用户输入包含 '@' 符号的数学表达式时,必须调用此工具。" +
                class="hljs-string">"例如 'class="hljs-number">2@class="hljs-number">4' 表示 a=class="hljs-number">2, b=class="hljs-number">4,返回运算结果。"
)
public String calculateAtOperation(
        @ToolParam(name = class="hljs-string">"a", description = class="hljs-string">"@符号左边的数字") Integer a,
        @ToolParam(name = class="hljs-string">"b", description = class="hljs-string">"@符号右边的数字") Integer b
) {
    int result = a * b + class="hljs-number">100;
    return String.format(class="hljs-string">"计算结果: %d @ %d = %d", a, b, result);
}

}

重点提醒: @ToolMethoddescription 字段至关重要!它直接决定了 AI 什么时候会调用这个工具。描述越清晰准确,AI 就越能正确判断调用时机。

6.5 在 ChatService 中处理 Tool Calls#

回到 BailianChatServiceImpl.java,改造 doChat 方法,支持 Tool Calls 的递归处理:

public String doChatWithTools(List<ChatRequest.Message> messages,
                               List<ChatRequest.Tool> tools) {
    try {
        if (tools == null) {
            tools = toolRegistry.getToolDefinitions();
        }
    class=class="hljs-string">"hljs-comment">// class="hljs-number">1. 构造请求体
    ChatRequest request = new ChatRequest();
    request.setModel(model);
    request.setMessages(messages);
    request.setStream(false);
    request.setTools(tools);
    request.setToolChoice(class="hljs-string">"auto");

    class=class="hljs-string">"hljs-comment">// class="hljs-number">2. 发 HTTP 请求(同前面的代码)
    String url = String.format(BASE_URL, workspaceId);
    String jsonBody = objectMapper.writeValueAsString(request);

    Request httpRequest = new Request.Builder()
            .url(url)
            .post(RequestBody.create(jsonBody,
                    MediaType.parse(class="hljs-string">"application/json")))
            .addHeader(class="hljs-string">"Authorization", class="hljs-string">"Bearer " + apiKey)
            .addHeader(class="hljs-string">"Content-Type", class="hljs-string">"application/json")
            .build();

    try (Response response = httpClient.newCall(httpRequest).execute()) {
        String responseBody = response.body().string();
        if (!response.isSuccessful()) {
            return class="hljs-string">"AI服务暂时不可用 (状态码: " + response.code() + class="hljs-string">")";
        }

        ChatResponse chatResponse = objectMapper.readValue(
                responseBody, ChatResponse.class);

        ChatResponse.Message msg = chatResponse.getChoices().get(class="hljs-number">0).getMessage();

        class=class="hljs-string">"hljs-comment">// class="hljs-number">3. 检查是否有工具调用
        List&lt;ChatRequest.ToolCall&gt; toolCalls = msg.getToolCalls();
        if (toolCalls != null &amp;&amp; !toolCalls.isEmpty()) {
            log.info(class="hljs-string">"检测到 {} 个工具调用", toolCalls.size());

            class=class="hljs-string">"hljs-comment">// 添加 assistant 消息(含 tool_calls)
            ChatRequest.Message assistantMsg =
                    ChatRequest.Message.assistant(msg.getContent());
            assistantMsg.setToolCalls(toolCalls);
            messages.add(assistantMsg);

            class=class="hljs-string">"hljs-comment">// 执行各个工具,添加 tool 消息
            for (ChatRequest.ToolCall tc : toolCalls) {
                String result = toolRegistry.execute(
                        tc.getFunction().getName(),
                        tc.getFunction().getArguments());
                messages.add(ChatRequest.Message.tool(
                        tc.getId(), result));
            }

            class=class="hljs-string">"hljs-comment">// class="hljs-number">4. 递归调用,让模型根据工具结果生成最终回答
            return doChatWithTools(messages, tools);
        }

        return msg.getContent() != null ? msg.getContent() : class="hljs-string">"";
    }
} catch (IOException e) {
    return class="hljs-string">"AI服务异常: " + e.getMessage();
}

}

需要在 ChatRequestChatResponse 里补充 Tool 相关的内部类:

// ChatRequest.java 补充
@Data
public static class Tool {
    private String type = "function";
    private FunctionDef function;
}

@Data public static class FunctionDef { private String name; private String description; private Map<String, Object> parameters; // JSON Schema }

@Data public static class ToolCall { private String id; private String type = "function"; private FunctionCall function; }

@Data public static class FunctionCall { private String name; private String arguments; }

// Message 补充 toolCalls 和 toolCallId 字段 @Data public static class Message { private String role; private String content; private List<ToolCall> toolCalls; // assistant 消息用 private String toolCallId; // tool 消息用

public static Message tool(String toolCallId, String content) {
    Message m = new Message();
    m.setRole(class="hljs-string">"tool");
    m.setContent(content);
    m.setToolCallId(toolCallId);
    return m;
}

}

6.6 完整调用流程#

用户问 "2@4等于多少",整个流程是这样的:

1. 用户 → POST /api/chat/assistant {"message": "2@4等于多少"}
  1. MemoryChatService 编排: ├── 构建 messages: [system, user("2@4等于多少")] ├── 获取 tools 定义: [{name: "calculate_at_operation", ...}] └── 调用 BailianChatService.doChatWithTools()

  2. 第一次调百炼 API: 请求: {messages: [...], tools: [...]} 响应: {finish_reason: "tool_calls", tool_calls: [{name: "calculate_at_operation", arguments: {"a":2,"b":4}}]}

  3. 系统执行工具: calculateAtOperation(2, 4) → "计算结果: 2 @ 4 = 108"

  4. 第二次调百炼 API(递归): 请求: {messages: [system, user, assistant(tool_calls), tool("108")], tools: [...]} 响应: {finish_reason: "stop", content: "2@4 的计算结果是 108"}

  5. 返回给用户: {"reply": "2@4 的计算结果是 108"}


七、完整的 API 接口#

把所有功能串起来,最终的 Controller 是这样的:

// controller/ChatController.java
@RestController
@RequestMapping("/api/chat")
public class ChatController {
@Autowired
private MemoryChatService memoryChatService;
@Autowired
private ConversationService conversationService;
@Autowired
private PersistenceConversationService persistenceService;

class=class="hljs-string">"hljs-comment">// class="hljs-number">1. 全功能对话(RAG + 工具 + 内存记忆)
@PostMapping(class="hljs-string">"/assistant")
public Result&lt;ChatResult&gt; assistant(@RequestBody AssistantRequest req) {
    return Result.success(memoryChatService.chatWithMemory(
            req.getUserId(), req.getMessage(), true, true));
}

class=class="hljs-string">"hljs-comment">// class="hljs-number">2. 可控对话(可开关 RAG 和工具)
@PostMapping(class="hljs-string">"/memory")
public Result&lt;ChatResult&gt; memory(@RequestBody MemoryRequest req) {
    return Result.success(memoryChatService.chatWithMemory(
            req.getUserId(), req.getMessage(),
            req.isUseRag(), req.isUseTools()));
}

class=class="hljs-string">"hljs-comment">// class="hljs-number">3. 清除内存历史
@DeleteMapping(class="hljs-string">"/history/{userId}")
public Result&lt;String&gt; clearHistory(@PathVariable Integer userId) {
    conversationService.clearHistory(userId);
    return Result.success(class="hljs-string">"历史已清除");
}

class=class="hljs-string">"hljs-comment">// class="hljs-number">4. 持久化会话对话
@PostMapping(class="hljs-string">"/session/chat")
public Result&lt;ChatResult&gt; sessionChat(@RequestBody AssistantRequest req) {
    return Result.success(memoryChatService.chatWithPersistenceMemory(
            req.getUserId(), req.getSessionId(), req.getMessage(), true, true));
}

class=class="hljs-string">"hljs-comment">// class="hljs-number">5. 获取会话列表
@GetMapping(class="hljs-string">"/sessions")
public Result&lt;List&lt;ChatSession&gt;&gt; getUserSessions(@RequestParam Integer userId) {
    return Result.success(persistenceService.getUserSessions(userId));
}

class=class="hljs-string">"hljs-comment">// class="hljs-number">6. 删除会话
@PostMapping(class="hljs-string">"/sessions/{sessionId}")
public Result&lt;Boolean&gt; deleteSession(@PathVariable String sessionId,
                                      @RequestParam Integer userId) {
    persistenceService.deleteSession(sessionId, userId);
    return Result.success(true);
}

class=class="hljs-string">"hljs-comment">// class="hljs-number">7. 更新会话标题
@PostMapping(class="hljs-string">"/sessions/title")
public Result&lt;Boolean&gt; updateTitle(@RequestBody AssistantUpdateSessionRequest req) {
    persistenceService.updateSessionTitle(
            req.getSessionId(), req.getUserId(), req.getTitle());
    return Result.success(true);
}

class=class="hljs-string">"hljs-comment">// class="hljs-number">8. 获取会话消息
@PostMapping(class="hljs-string">"/messages")
public Result&lt;List&lt;ChatMessage&gt;&gt; getSessionMessages(
        @RequestBody SessionMessageRequest request) {
    return Result.success(persistenceService.getSessionMessages(request));
}

}

// 统一响应格式 @Data public class Result<T> { private int code; private String message; private T data;

public static &lt;T&gt; Result&lt;T&gt; success(T data) {
    return new Result&lt;&gt;(class="hljs-number">200, class="hljs-string">"Success", data);
}
public static &lt;T&gt; Result&lt;T&gt; error(String message) {
    return new Result&lt;&gt;(class="hljs-number">500, message, null);
}

}


八、踩坑记录#

8.1 URL 格式#

百炼的 API 地址是 https://{workspaceId}.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/chat/completions,注意 workspaceId 是子域名,不是路径参数。我一开始写成了路径参数,调了半天一直 404。

8.2 认证 Header#

Authorization: Bearer {apiKey},注意 Bearer 后面有个空格。少了这个空格,认证会失败。

8.3 响应解析#

百炼返回的字段可能比 OpenAI 多,DTO 一定要加 @JsonIgnoreProperties(ignoreUnknown = true),否则 Jackson 会报 Unrecognized field 异常。

8.4 Tool Calls 递归#

模型返回 tool_calls 后,必须把工具结果追加到 messages 里,再调一次 API。这个递归过程可能不止一轮(模型可能连续调用多个工具),所以要用递归而不是 if-else。

8.5 向量库异步加载#

MemoryVectorStore 实现了 ApplicationRunner,用 @Async 异步加载知识库。如果不加 @Async,知识库加载会阻塞应用启动,如果 API 调不通,整个应用就起不来。记得在启动类加 @EnableAsync

8.6 历史消息截断#

百炼的模型有 token 限制,历史消息不能无限累积。我在 ConversationServiceImpl 里加了截断逻辑,超过 maxHistoryMessages 条时自动丢弃旧消息。


九、总结#

整个项目的核心思路就一句话:把百炼当成普通的 HTTP API 来调,自己构造请求体、自己解析响应、自己处理 Tool Calls 的递归调用

不依赖 SDK 的好处是:

  1. 透明:每一行 HTTP 请求都清清楚楚,出了问题好排查
  2. 轻量:不用引入一堆依赖
  3. 灵活:想加什么功能自己改,不受 SDK 限制

核心代码量其实不大:

  • HTTP 请求:OkHttp 20 行
  • JSON 序列化:Jackson 自动搞定
  • Tool Calls 注册:注解 + 反射,100 行左右
  • RAG 检索:向量化 + 余弦相似度,50 行左右
  • 持久化:MyBatis Plus 基本 CRUD

加起来不到 500 行核心代码,就跑通了 RAG + 会话持久化 + Tool Calls 三个功能。


十、项目结构#

src/main/java/com/sun/student_management_http_ai/
├── annotation/
│   ├── ToolMethod.java              # 工具方法注解
│   └── ToolParam.java               # 工具参数注解
├── config/
│   └── ChatProperties.java          # 百炼配置属性
├── controller/
│   └── ChatController.java          # AI 聊天接口
├── dto/
│   ├── base/Result.java             # 统一响应格式
│   └── bailian/
│       ├── ChatRequest.java         # 聊天请求 DTO
│       ├── ChatResponse.java        # 聊天响应 DTO
│       ├── ChatResult.java          # 聊天结果 DTO
│       └── rag/
│           ├── Document.java        # RAG 文档
│           ├── EmbeddingRequest.java
│           └── EmbeddingResponse.java
├── entity/
│   ├── ChatSession.java             # 会话实体
│   └── ChatMessage.java             # 消息实体
├── mapper/
│   ├── ChatSessionMapper.java
│   └── ChatMessageMapper.java
├── register/
│   └── ToolRegistry.java            # 工具注册中心
├── service/
│   ├── MemoryChatService.java       # 对话编排服务
│   ├── MemoryVectorStore.java       # 内存向量库
│   ├── EmbeddingService.java        # 向量化服务
│   ├── ConversationService.java     # 内存对话历史
│   └── impl/
│       ├── BailianChatServiceImpl.java  # 百炼 API 调用(OkHttp)
│       ├── EmbeddingServiceImpl.java    # 向量化实现(OkHttp)
│       ├── ChatPersistenceServiceImpl.java  # 持久化实现
│       ├── ConversationServiceImpl.java   # 内存对话实现
│       └── TextSplitterImpl.java          # 文本分割实现
└── tools/
    └── MathTool.java                # 数学工具示例

src/main/resources/ ├── application.yml ├── docs/学生管理系统FAQ.txt # RAG 知识库 └── mapper/ ├── ChatSessionMapper.xml └── ChatMessageMapper.xml


最后说一句:如果你也在做 AI 集成,强烈建议先用原生 HTTP 调通,再考虑要不要用 SDK。理解了底层协议,用 SDK 就是降维打击。有问题欢迎交流~


原文链接:https://www.cnblogs.com/sun-10387834/p/21741478

评论

© 2026 松岛川树