保姆级教程:用原生 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 开通阿里云百炼
- 登录 阿里云百炼控制台
- 开通百炼服务,获取 API Key 和 Workspace ID
- 确认你需要的模型(我用的是
qwen3.7-plus)和向量化模型(text-embedding-v4)
1.2 创建 Spring Boot 项目
用 IDEA 或者 Spring Initializr 创建一个 Spring Boot 项目,引入以下依赖:
xml
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- OkHttp(HTTP 客户端) -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<!-- Jackson(JSON 序列化) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- MyBatis Plus(数据库操作) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- Lombok(减少样板代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
1.3 配置文件
application.yml 里配置好数据库和百炼的参数:
yaml
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-Type :
application/json
2.2 构造请求体
百炼的 Chat Completions API 接收一个 JSON 对象,核心字段如下:
json
{
"model": "qwen3.7-plus",
"messages": [
{"role": "system", "content": "你是一个助手"},
{"role": "user", "content": "你好"}
],
"stream": false
}
在 Java 里,我们用 DTO 类来映射这个 JSON:
java
// dto/bailian/ChatRequest.java
@Data
public class ChatRequest {
private String model; // 模型名称
private List<Message> messages; // 对话消息列表
private Boolean stream = false; // 是否流式输出
// 消息内部类
@Data
public static class Message {
private String role; // user / assistant / system
private String content; // 消息内容
// 静态工厂方法,方便创建
public static Message user(String content) {
Message m = new Message();
m.setRole("user");
m.setContent(content);
return m;
}
public static Message system(String content) {
Message m = new Message();
m.setRole("system");
m.setContent(content);
return m;
}
public static Message assistant(String content) {
Message m = new Message();
m.setRole("assistant");
m.setContent(content);
return m;
}
}
}
2.3 构造响应体
API 返回的 JSON 长这样:
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:
java
// dto/bailian/ChatResponse.java
@Data
@JsonIgnoreProperties(ignoreUnknown = true) // 忽略未知字段,提高兼容性
public class ChatResponse {
private String id;
private String model;
private List<Choice> choices;
private Usage usage;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Choice {
private Integer index;
private Message message;
@JsonProperty("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("prompt_tokens")
private Integer promptTokens;
@JsonProperty("completion_tokens")
private Integer completionTokens;
@JsonProperty("total_tokens")
private Integer totalTokens;
}
}
2.4 用 OkHttp 发请求
重头戏来了。BailianChatServiceImpl.java 是真正发 HTTP 请求的地方:
java
// service/impl/BailianChatServiceImpl.java
@Slf4j
@Service
public class BailianChatServiceImpl {
@Value("${bailian.api.api-key}")
private String apiKey;
@Value("${bailian.api.workspace-id}")
private String workspaceId;
@Value("${bailian.api.model:qwen-plus}")
private String model;
private OkHttpClient httpClient;
private ObjectMapper objectMapper;
// 百炼 API 地址模板
private static final String BASE_URL =
"https://%s.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/chat/completions";
@PostConstruct
public void init() {
// 初始化 OkHttp 客户端,设置超时时间
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
// 初始化 ObjectMapper,忽略未知字段
this.objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
/**
* 最基础的对话方法
*/
public String chat(String userMessage) {
try {
// 1. 构造消息列表
List<ChatRequest.Message> messages = new ArrayList<>();
messages.add(ChatRequest.Message.system("你是一个智能助手"));
messages.add(ChatRequest.Message.user(userMessage));
// 2. 构造请求体
ChatRequest request = new ChatRequest();
request.setModel(model);
request.setMessages(messages);
request.setStream(false);
// 3. 序列化请求体为 JSON
String jsonBody = objectMapper.writeValueAsString(request);
// 4. 拼接 URL(把 workspaceId 替换到子域名)
String url = String.format(BASE_URL, workspaceId);
// 5. 构造 OkHttp 请求
Request httpRequest = new Request.Builder()
.url(url)
.post(RequestBody.create(jsonBody,
MediaType.parse("application/json")))
.addHeader("Authorization", "Bearer " + apiKey)
.addHeader("Content-Type", "application/json")
.build();
// 6. 发送请求
try (Response response = httpClient.newCall(httpRequest).execute()) {
String responseBody = response.body().string();
if (!response.isSuccessful()) {
log.error("调用失败, code={}, body={}", response.code(), responseBody);
return "AI服务暂时不可用";
}
// 7. 解析响应
ChatResponse chatResponse = objectMapper.readValue(
responseBody, ChatResponse.class);
if (chatResponse.getChoices() == null || chatResponse.getChoices().isEmpty()) {
return "未获取到有效回复";
}
// 8. 提取 AI 回复内容
return chatResponse.getChoices().get(0).getMessage().getContent();
}
} catch (IOException e) {
log.error("调用异常", e);
return "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 测试一下
java
// controller/ChatController.java
@RestController
@RequestMapping("/api/chat")
public class ChatController {
@Autowired
private BailianChatServiceImpl chatService;
@PostMapping("/simple")
public Map<String, Object> simpleChat(@RequestBody Map<String, String> req) {
String reply = chatService.chat(req.get("message"));
Map<String, Object> result = new HashMap<>();
result.put("reply", reply);
return result;
}
}
启动项目,用 Postman 或者 curl 测试:
bash
curl -X POST http://localhost:8081/api/chat/simple \
-H "Content-Type: application/json" \
-d '{"message": "你好,请介绍一下你自己"}'
如果一切正常,你会收到 AI 的回复。
三、实现多轮对话(带历史记忆)
上面的例子只能做单轮对话,每次都是全新的对话,AI 不记得之前说过什么。
3.1 多轮对话的原理
多轮对话的核心是:把历史消息也发给 AI。
比如用户先问"你好",AI 回答"你好!有什么可以帮你的?",然后用户又问"我数学考了多少分"。
第二次请求时,messages 应该是这样的:
json
{
"messages": [
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "你好!有什么可以帮你的?"},
{"role": "user", "content": "我数学考了多少分"}
]
}
这样 AI 就知道上下文了。
3.2 用内存存历史消息
最简单的方案是用 ConcurrentHashMap 存每个用户的对话历史:
java
// service/impl/ConversationServiceImpl.java
@Slf4j
@Service
public class ConversationServiceImpl {
@Value("${bailian.chat.max-history-messages:10}")
private int maxHistoryMessages;
// key=userId, value=历史消息列表
private final Map<Integer, List<ChatRequest.Message>> store = new ConcurrentHashMap<>();
private final AtomicInteger idGen = new AtomicInteger(1);
/**
* 获取用户的历史消息
*/
public List<ChatRequest.Message> getHistory(Integer userId) {
if (userId == null) return Collections.emptyList();
List<ChatRequest.Message> history = store.get(userId);
return history == null ? Collections.emptyList() : new ArrayList<>(history);
}
/**
* 添加消息到历史
*/
public void addMessage(Integer userId, String role, String content) {
if (userId == null || content == null || content.trim().isEmpty()) return;
List<ChatRequest.Message> history = store.computeIfAbsent(
userId, k -> new ArrayList<>());
ChatRequest.Message msg = new ChatRequest.Message();
msg.setRole(role);
msg.setContent(content);
history.add(msg);
// 超过最大数量时,截断旧消息
if (history.size() > maxHistoryMessages) {
List<ChatRequest.Message> newHistory = new ArrayList<>(
history.subList(history.size() - maxHistoryMessages, history.size()));
store.put(userId, newHistory);
}
}
/**
* 清除用户历史
*/
public void clearHistory(Integer userId) {
if (userId != null) store.remove(userId);
}
/**
* 生成新用户 ID
*/
public Integer generateUserId() {
return idGen.getAndIncrement();
}
}
3.3 改造 ChatService,支持多轮
java
// service/impl/BailianChatServiceImpl.java(改造后)
@Autowired
private ConversationServiceImpl conversationService;
public String chatWithMemory(Integer userId, String userMessage) {
// 1. 获取历史消息
List<ChatRequest.Message> messages = conversationService.getHistory(userId);
// 2. 添加 system 消息(可选)
if (messages.isEmpty()) {
messages.add(0, ChatRequest.Message.system("你是一个智能助手"));
}
// 3. 添加当前用户消息
messages.add(ChatRequest.Message.user(userMessage));
// 4. 调用 API
String reply = doChat(messages);
// 5. 保存用户消息和 AI 回复到历史
conversationService.addMessage(userId, "user", userMessage);
conversationService.addMessage(userId, "assistant", reply);
return reply;
}
这样用户就能体验到"有记忆"的对话了。不过内存存储有个问题:服务重启后数据全丢。
四、会话 & 消息持久化到 MySQL
4.1 建表
先建两张表,一张存会话,一张存消息:
sql
-- 会话表
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 实体类
java
// 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 接口
java
// 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:
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 持久化服务
java
// service/impl/ChatPersistenceServiceImpl.java
@Service
@RequiredArgsConstructor
public class ChatPersistenceServiceImpl {
private final ChatSessionMapper sessionMapper;
private final ChatMessageMapper messageMapper;
/**
* 创建新会话
*/
public String createSession(Integer userId, String firstMessage) {
String sessionId = UUID.randomUUID().toString();
ChatSession session = new ChatSession();
session.setSessionId(sessionId);
session.setUserId(userId);
// 首条消息截取前50字作为标题
if (firstMessage != null && firstMessage.length() > 50) {
session.setTitle(firstMessage.substring(0, 50));
} else {
session.setTitle(firstMessage);
}
sessionMapper.insert(session);
return sessionId;
}
/**
* 保存消息
*/
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);
// 更新会话的 updated_at
LambdaQueryWrapper<ChatSession> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ChatSession::getSessionId, sessionId);
ChatSession session = sessionMapper.selectOne(wrapper);
if (session != null) {
session.setUpdatedAt(LocalDateTime.now());
sessionMapper.updateById(session);
}
}
/**
* 获取会话的所有消息(按时间升序)
*/
public List<ChatMessage> getSessionMessages(String sessionId) {
return messageMapper.selectBySessionIdOrderByTime(sessionId);
}
/**
* 获取 LLM 格式的历史消息(只取最近 maxHistory 条)
*/
public List<ChatRequest.Message> getHistoryForLLM(String sessionId, int maxHistory) {
List<ChatMessage> messages = getSessionMessages(sessionId);
List<ChatRequest.Message> history = new ArrayList<>();
int count = 0;
// 从后往前取,保证取到的是最近的消息
for (int i = messages.size() - 1; i >= 0; i--) {
ChatMessage cm = messages.get(i);
if ("user".equals(cm.getRole()) || "assistant".equals(cm.getRole())) {
if (count >= maxHistory) break;
ChatRequest.Message m = new ChatRequest.Message();
m.setRole(cm.getRole());
m.setContent(cm.getContent());
history.add(m);
count++;
}
}
Collections.reverse(history); // 恢复时间顺序
return history;
}
/**
* 删除会话(逻辑删除)
*/
public void deleteSession(String sessionId, Integer userId) {
LambdaQueryWrapper<ChatSession> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ChatSession::getSessionId, sessionId)
.eq(ChatSession::getUserId, userId);
ChatSession session = sessionMapper.selectOne(wrapper);
if (session != null) {
sessionMapper.deleteById(session.getId());
}
}
/**
* 更新会话标题
*/
public void updateSessionTitle(String sessionId, Integer userId, String title) {
LambdaQueryWrapper<ChatSession> wrapper = new LambdaQueryWrapper<>();
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);
}
}
/**
* 检查会话是否存在
*/
public boolean sessionExists(String sessionId, Integer userId) {
LambdaQueryWrapper<ChatSession> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ChatSession::getSessionId, sessionId)
.eq(ChatSession::getUserId, userId);
return sessionMapper.selectCount(wrapper) > 0;
}
}
4.5 持久化对话流程
java
public ChatResult chatWithPersistence(Integer userId, String sessionId,
String userMessage) {
// 1. 初始化用户ID
if (userId == null) userId = generateUserId();
// 2. 初始化会话ID
if (sessionId == null || sessionId.isEmpty()) {
sessionId = persistenceService.createSession(userId, userMessage);
} else {
if (!persistenceService.sessionExists(sessionId, userId)) {
sessionId = persistenceService.createSession(userId, userMessage);
}
}
// 3. 获取历史消息
List<ChatRequest.Message> history = persistenceService.getHistoryForLLM(
sessionId, maxHistoryMessages);
// 4. 构建消息列表
List<ChatRequest.Message> messages = new ArrayList<>();
messages.add(ChatRequest.Message.system("你是一个智能助手"));
messages.addAll(history);
messages.add(ChatRequest.Message.user(userMessage));
// 5. 调用 AI
String reply = doChat(messages);
// 6. 保存到数据库
persistenceService.saveMessage(sessionId, "user", userMessage, null);
persistenceService.saveMessage(sessionId, "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:
java
// service/impl/TextSplitterImpl.java
@Slf4j
@Service
public class TextSplitterImpl {
/**
* 解析 QA 格式的文档
*/
public List<Document> parseQADocuments(String content) {
List<Document> docs = new ArrayList<>();
String[] lines = content.split("\\n");
String currentQ = null;
StringBuilder currentA = new StringBuilder();
for (String line : lines) {
String trimmed = line.trim();
if (trimmed.startsWith("Q:") || trimmed.startsWith("Q:")) {
// 保存上一个 QA
if (currentQ != null && currentA.length() > 0) {
docs.add(createDoc(currentQ, currentA.toString().trim()));
}
currentQ = trimmed.substring(2).trim();
currentA = new StringBuilder();
} else if (trimmed.startsWith("A:") || trimmed.startsWith("A:")) {
currentA.append(trimmed.substring(2).trim()).append(" ");
} else if (currentA.length() > 0) {
currentA.append(trimmed).append(" ");
}
}
// 处理最后一个 QA
if (currentQ != null && currentA.length() > 0) {
docs.add(createDoc(currentQ, currentA.toString().trim()));
}
log.info("解析 QA 格式完成,共 {} 条", docs.size());
return docs;
}
/**
* 创建 Document 对象
*/
public Document createDoc(String question, String answer) {
Document doc = new Document();
doc.setContent(question); // content 存问题(用于向量检索)
doc.getMetadata().put("question", question);
doc.getMetadata().put("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"]}
java
// service/impl/EmbeddingServiceImpl.java
@Slf4j
@Service
public class EmbeddingServiceImpl {
@Value("${bailian.api.api-key}")
private String apiKey;
@Value("${bailian.api.workspace-id}")
private String workspaceId;
private OkHttpClient httpClient;
private ObjectMapper objectMapper;
private static final String MODEL = "text-embedding-v4";
private static final String BASE_URL =
"https://%s.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/embeddings";
private static final int BATCH_SIZE = 10; // 每批最多10条
@PostConstruct
public void init() {
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build();
this.objectMapper = new ObjectMapper();
}
/**
* 单条文本向量化
*/
public float[] embed(String text) throws IOException {
List<float[]> result = embedBatch(List.of(text));
return result.isEmpty() ? new float[0] : result.get(0);
}
/**
* 批量向量化(每批最多10条)
*/
public List<float[]> embedBatch(List<String> texts) throws IOException {
if (texts == null || texts.isEmpty()) return new ArrayList<>();
List<float[]> allVectors = new ArrayList<>();
String url = String.format(BASE_URL, workspaceId);
for (int i = 0; i < texts.size(); i += BATCH_SIZE) {
List<String> batch = texts.subList(i,
Math.min(i + BATCH_SIZE, texts.size()));
// 构造请求体
EmbeddingRequest req = new EmbeddingRequest();
req.setModel(MODEL);
req.setInput(batch);
String json = objectMapper.writeValueAsString(req);
// 发 HTTP 请求
Request httpReq = new Request.Builder()
.url(url)
.post(RequestBody.create(json,
MediaType.parse("application/json")))
.addHeader("Authorization", "Bearer " + apiKey)
.build();
try (Response response = httpClient.newCall(httpReq).execute()) {
String body = response.body().string();
if (!response.isSuccessful()) {
throw new IOException("Embedding 失败: " + body);
}
EmbeddingResponse embResp = objectMapper.readValue(
body, EmbeddingResponse.class);
for (EmbeddingResponse.EmbeddingData d : embResp.getData()) {
// Double 列表转 float[]
float[] vec = new float[d.getEmbedding().size()];
for (int j = 0; j < 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<Double> embedding;
}
}
5.4 内存向量库 + 余弦相似度检索
向量库就是个 List<VectorEntry>,每个 Entry 存文本内容和对应的向量。检索时用余弦相似度:
java
// service/MemoryVectorStore.java
@Slf4j
@Service
public class MemoryVectorStore implements ApplicationRunner {
@Autowired
private EmbeddingServiceImpl embeddingService;
@Autowired
private TextSplitterImpl textSplitter;
private final List<VectorEntry> vectorStore = new ArrayList<>();
private final AtomicBoolean ready = new AtomicBoolean(false);
private static final double THRESHOLD = 0.65; // 相似度阈值
private static final int TOP_K = 3; // 返回前3条
/**
* 应用启动时异步加载知识库
*/
@Async
@Override
public void run(ApplicationArguments args) {
try {
// 读取 FAQ 文档
ClassPathResource resource = new ClassPathResource("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("\n");
}
}
// 解析文档
List<Document> documents = textSplitter.parseQADocuments(content.toString());
// 向量化
List<String> texts = documents.stream()
.map(Document::getContent)
.collect(Collectors.toList());
List<float[]> vectors = embeddingService.embedBatch(texts);
// 存入向量库
for (int i = 0; i < 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("内存向量库初始化完成,共 {} 条记录", vectorStore.size());
} catch (Exception e) {
log.error("向量库初始化失败", e);
}
}
/**
* 检索与查询最相关的文档
*/
public List<VectorEntry> search(String query) {
if (!ready.get()) return Collections.emptyList();
try {
// 查询向量化
float[] qVec = embeddingService.embed(query);
// 计算余弦相似度
List<ScoredEntry> scored = new ArrayList<>();
for (VectorEntry entry : vectorStore) {
double sim = cosineSimilarity(qVec, entry.getVector());
scored.add(new ScoredEntry(entry, sim));
}
// 按相似度降序排列
scored.sort((a, b) -> Double.compare(b.similarity, a.similarity));
// 取 Top3 且相似度 >= 阈值
List<VectorEntry> results = new ArrayList<>();
for (ScoredEntry se : scored) {
if (se.similarity >= THRESHOLD && results.size() < TOP_K) {
results.add(se.entry);
}
}
return results;
} catch (Exception e) {
log.error("检索失败", e);
return Collections.emptyList();
}
}
/**
* 余弦相似度:cos(theta) = (A·B) / (|A| * |B|)
*/
private double cosineSimilarity(float[] a, float[] b) {
double dot = 0, na = 0, nb = 0;
for (int i = 0; i < 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<String, String> metadata = new HashMap<>();
}
@Data
@AllArgsConstructor
private static class ScoredEntry {
VectorEntry entry;
double similarity;
}
}
5.5 把检索结果拼入 System Prompt
java
// 在对话服务中
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 自定义注解
为了优雅地注册工具方法,我们定义两个注解:
java
// 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 的方法:
java
// register/ToolRegistry.java
@Slf4j
@Component
public class ToolRegistry implements BeanPostProcessor, ApplicationContextAware {
private ApplicationContext applicationContext;
private final Map<String, ToolRegistration> registry = new ConcurrentHashMap<>();
private final ObjectMapper mapper = new ObjectMapper();
@Override
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
this.applicationContext = ctx;
}
// Bean 初始化后自动扫描
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
scanBean(bean);
return bean;
}
private void scanBean(Object bean) {
Class<?> clazz = bean.getClass();
// 处理 CGLIB 代理
if (clazz.getName().contains("$$")) 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("注册工具: {}", 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<ToolParameter> params = new ArrayList<>();
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("参数 " + p.getName());
param.setRequired(true);
}
param.setType(p.getType());
param.setJsonType(mapType(p.getType()));
params.add(param);
}
reg.setParameters(params);
return reg;
}
// Java 类型 -> JSON Schema 类型
private String mapType(Class<?> type) {
if (type == String.class) return "string";
if (type == Integer.class || type == int.class) return "integer";
if (type == Long.class || type == long.class) return "integer";
if (type == Double.class || type == double.class ||
type == Float.class || type == float.class) return "number";
if (type == Boolean.class || type == boolean.class) return "boolean";
if (type == List.class || type.isArray()) return "array";
return "object";
}
/**
* 生成 Function Calling Schema(发给 AI 的 tools 定义)
*/
public List<ChatRequest.Tool> getToolDefinitions() {
List<ChatRequest.Tool> tools = new ArrayList<>();
for (ToolRegistration reg : registry.values()) {
ChatRequest.Tool tool = new ChatRequest.Tool();
tool.setType("function");
ChatRequest.FunctionDef func = new ChatRequest.FunctionDef();
func.setName(reg.getName());
func.setDescription(reg.getDescription());
// 构造 JSON Schema 参数定义
Map<String, Object> params = new LinkedHashMap<>();
params.put("type", "object");
Map<String, Object> props = new LinkedHashMap<>();
List<String> required = new ArrayList<>();
for (ToolParameter p : reg.getParameters()) {
Map<String, Object> prop = new LinkedHashMap<>();
prop.put("type", p.getJsonType());
prop.put("description", p.getDescription());
props.put(p.getName(), prop);
if (p.isRequired()) required.add(p.getName());
}
params.put("properties", props);
if (!required.isEmpty()) params.put("required", required);
func.setParameters(params);
tool.setFunction(func);
tools.add(tool);
}
return tools;
}
/**
* 执行工具(反射调用)
*/
public String execute(String toolName, String arguments) {
ToolRegistration reg = registry.get(toolName);
if (reg == null) return "未知工具: " + toolName;
try {
JsonNode argsNode = mapper.readTree(arguments);
Method method = reg.getMethod();
Object[] args = new Object[method.getParameterCount()];
for (int i = 0; i < 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() : "执行成功(无返回值)";
} catch (Exception e) {
log.error("执行工具失败: {}", toolName, e);
return "工具执行失败: " + e.getMessage();
}
}
private Object convert(JsonNode node, Class<?> 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<?> type;
private String jsonType;
}
}
6.4 写一个实际的工具方法
java
// tools/MathTool.java
@Slf4j
@Service
public class MathTool {
@ToolMethod(
name = "calculate_at_operation",
description = "计算两个数字之间的特殊运算(@运算)。" +
"当用户输入包含 '@' 符号的数学表达式时,必须调用此工具。" +
"例如 '2@4' 表示 a=2, b=4,返回运算结果。"
)
public String calculateAtOperation(
@ToolParam(name = "a", description = "@符号左边的数字") Integer a,
@ToolParam(name = "b", description = "@符号右边的数字") Integer b
) {
int result = a * b + 100;
return String.format("计算结果: %d @ %d = %d", a, b, result);
}
}
重点提醒: @ToolMethod 的 description 字段至关重要!它直接决定了 AI 什么时候会调用这个工具。描述越清晰准确,AI 就越能正确判断调用时机。
6.5 在 ChatService 中处理 Tool Calls
回到 BailianChatServiceImpl.java,改造 doChat 方法,支持 Tool Calls 的递归处理:
java
public String doChatWithTools(List<ChatRequest.Message> messages,
List<ChatRequest.Tool> tools) {
try {
if (tools == null) {
tools = toolRegistry.getToolDefinitions();
}
// 1. 构造请求体
ChatRequest request = new ChatRequest();
request.setModel(model);
request.setMessages(messages);
request.setStream(false);
request.setTools(tools);
request.setToolChoice("auto");
// 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("application/json")))
.addHeader("Authorization", "Bearer " + apiKey)
.addHeader("Content-Type", "application/json")
.build();
try (Response response = httpClient.newCall(httpRequest).execute()) {
String responseBody = response.body().string();
if (!response.isSuccessful()) {
return "AI服务暂时不可用 (状态码: " + response.code() + ")";
}
ChatResponse chatResponse = objectMapper.readValue(
responseBody, ChatResponse.class);
ChatResponse.Message msg = chatResponse.getChoices().get(0).getMessage();
// 3. 检查是否有工具调用
List<ChatRequest.ToolCall> toolCalls = msg.getToolCalls();
if (toolCalls != null && !toolCalls.isEmpty()) {
log.info("检测到 {} 个工具调用", toolCalls.size());
// 添加 assistant 消息(含 tool_calls)
ChatRequest.Message assistantMsg =
ChatRequest.Message.assistant(msg.getContent());
assistantMsg.setToolCalls(toolCalls);
messages.add(assistantMsg);
// 执行各个工具,添加 tool 消息
for (ChatRequest.ToolCall tc : toolCalls) {
String result = toolRegistry.execute(
tc.getFunction().getName(),
tc.getFunction().getArguments());
messages.add(ChatRequest.Message.tool(
tc.getId(), result));
}
// 4. 递归调用,让模型根据工具结果生成最终回答
return doChatWithTools(messages, tools);
}
return msg.getContent() != null ? msg.getContent() : "";
}
} catch (IOException e) {
return "AI服务异常: " + e.getMessage();
}
}
需要在 ChatRequest 和 ChatResponse 里补充 Tool 相关的内部类:
java
// 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("tool");
m.setContent(content);
m.setToolCallId(toolCallId);
return m;
}
}
6.6 完整调用流程
用户问 "2@4等于多少",整个流程是这样的:
1. 用户 → POST /api/chat/assistant {"message": "2@4等于多少"}
2. MemoryChatService 编排:
├── 构建 messages: [system, user("2@4等于多少")]
├── 获取 tools 定义: [{name: "calculate_at_operation", ...}]
└── 调用 BailianChatService.doChatWithTools()
3. 第一次调百炼 API:
请求: {messages: [...], tools: [...]}
响应: {finish_reason: "tool_calls", tool_calls: [{name: "calculate_at_operation", arguments: {"a":2,"b":4}}]}
4. 系统执行工具:
calculateAtOperation(2, 4) → "计算结果: 2 @ 4 = 108"
5. 第二次调百炼 API(递归):
请求: {messages: [system, user, assistant(tool_calls), tool("108")], tools: [...]}
响应: {finish_reason: "stop", content: "2@4 的计算结果是 108"}
6. 返回给用户:
{"reply": "2@4 的计算结果是 108"}
七、完整的 API 接口
把所有功能串起来,最终的 Controller 是这样的:
java
// controller/ChatController.java
@RestController
@RequestMapping("/api/chat")
public class ChatController {
@Autowired
private MemoryChatService memoryChatService;
@Autowired
private ConversationService conversationService;
@Autowired
private PersistenceConversationService persistenceService;
// 1. 全功能对话(RAG + 工具 + 内存记忆)
@PostMapping("/assistant")
public Result<ChatResult> assistant(@RequestBody AssistantRequest req) {
return Result.success(memoryChatService.chatWithMemory(
req.getUserId(), req.getMessage(), true, true));
}
// 2. 可控对话(可开关 RAG 和工具)
@PostMapping("/memory")
public Result<ChatResult> memory(@RequestBody MemoryRequest req) {
return Result.success(memoryChatService.chatWithMemory(
req.getUserId(), req.getMessage(),
req.isUseRag(), req.isUseTools()));
}
// 3. 清除内存历史
@DeleteMapping("/history/{userId}")
public Result<String> clearHistory(@PathVariable Integer userId) {
conversationService.clearHistory(userId);
return Result.success("历史已清除");
}
// 4. 持久化会话对话
@PostMapping("/session/chat")
public Result<ChatResult> sessionChat(@RequestBody AssistantRequest req) {
return Result.success(memoryChatService.chatWithPersistenceMemory(
req.getUserId(), req.getSessionId(), req.getMessage(), true, true));
}
// 5. 获取会话列表
@GetMapping("/sessions")
public Result<List<ChatSession>> getUserSessions(@RequestParam Integer userId) {
return Result.success(persistenceService.getUserSessions(userId));
}
// 6. 删除会话
@PostMapping("/sessions/{sessionId}")
public Result<Boolean> deleteSession(@PathVariable String sessionId,
@RequestParam Integer userId) {
persistenceService.deleteSession(sessionId, userId);
return Result.success(true);
}
// 7. 更新会话标题
@PostMapping("/sessions/title")
public Result<Boolean> updateTitle(@RequestBody AssistantUpdateSessionRequest req) {
persistenceService.updateSessionTitle(
req.getSessionId(), req.getUserId(), req.getTitle());
return Result.success(true);
}
// 8. 获取会话消息
@PostMapping("/messages")
public Result<List<ChatMessage>> 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 <T> Result<T> success(T data) {
return new Result<>(200, "Success", data);
}
public static <T> Result<T> error(String message) {
return new Result<>(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 的好处是:
- 透明:每一行 HTTP 请求都清清楚楚,出了问题好排查
- 轻量:不用引入一堆依赖
- 灵活:想加什么功能自己改,不受 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 就是降维打击。有问题欢迎交流~