Spring AI MCP实战(2)实现ChatMemory会话记忆以及删除 + 改进提示词 + 前端页面
根据上一篇,我们实现了mcp工具的调用,以及使用function calling去mysql中查询信息,这一节我们来实现ChatMemory会话记忆以及删除的功能。
前言
根据上一篇,我们实现了mcp工具的调用,以及使用function calling去mysql中查询信息,这一节我们来实现ChatMemory会话记忆以及删除的功能
会话记忆
实现ChatMemory接口
为了实现会话记忆,我们需要去实现ChatMemory接口:
package com.hyk.mcpclient.memory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Component
public class MyAgentChatMemory implements ChatMemory {
@Resource
private RedissonClient redissonClient;
@Resource
private ObjectMapper objectMapper;
@Override
public void add(String conversationId, List<Message> messages) {
RBucket<Object> bucket = redissonClient.getBucket("conversationId:" + conversationId);
String json;
try {
List<String> list = new ArrayList<>();
messages.forEach(message -> {
list.add(message.getText());
});
json = objectMapper.writeValueAsString(list);
} catch (JsonProcessingException e) {
log.error("json转换错误", e);
throw new RuntimeException(e);
}
bucket.set(json);
}
@Override
public List<Message> get(String conversationId) {
RBucket<Object> bucket = redissonClient.getBucket("conversationId:" + conversationId);
if(!bucket.isExists()){
log.info("conversationId:{} 不存在, 会话记忆不存在,创建新会话记忆", conversationId);
return new ArrayList<>();
}
String json = (String) bucket.get();
if(json != null){;
JavaType javaType = objectMapper.getTypeFactory().constructParametricType(List.class, String.class);
try {
List<String> list = objectMapper.readValue(json, javaType);
List<Message> messages = new ArrayList<>();
list.forEach(text -> {
messages.add(new UserMessage(text));
});
return messages;
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
return new ArrayList<>();
}
@Override
public void clear(String conversationId) {
RBucket<Object> bucket = redissonClient.getBucket("conversationId:" + conversationId);
if(!bucket.isExists()){
log.info("conversationId:{} 不存在,无法删除", conversationId);
return;
}
bucket.delete();
log.info("conversationId:{} 已删除", conversationId);
}
}
其中Message是一个接口,不能直接使用jackson转json,可以使用它的实现类UserMessage或者直接像我一样把message中的text提取出来
Redisson操作redis数据库
这里我的会话是存在redis中的
添加redisson依赖:
<!-- Redis 缓存 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.44.0</version>
</dependency>
新建redissonClient的配置类:
package com.hyk.mcpclient.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Value("${redis.host}")
private String redisHost;
@Value("${redis.port}")
private String redisPort;
@Value("${redis.database}")
private int redisDatabase;
@Bean // 添加这个注解
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer()
.setAddress("redis://" + redisHost + ":" + redisPort) // 修正地址格式
.setDatabase(redisDatabase);
return Redisson.create(config);
}
}
使用redisson操作redis数据库:
package com.hyk.mcpclient;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class RedissonTest {
@Resource
private RedissonClient redissonClient;
@Test
public void test() {
RBucket<Object> bucket = redissonClient.getBucket("test");
bucket.set("hello world");
System.out.println(bucket.get());
System.out.println(bucket.getAndDelete());
}
}
redisson操作redis就是去拿到一个bucket,然后对bucket进行增删改查的操作,不用手动提交,只要操作了就提交
配置类
把我们自己实现的chatmemory添加到chatClient的配置项中,我这里新建了一个配置项:
package com.hyk.mcpclient.client;
import com.hyk.mcpclient.common.prompt;
import com.hyk.mcpclient.memory.MyAgentChatMemory;
import com.hyk.mcpclient.tool.GetCityAdcodeTool;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.advisor.api.BaseChatMemoryAdvisor;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MemoryChatClientConfig {
@Resource
private OllamaChatModel ollamaChatModel;
@Resource
private OpenAiChatModel openAiChatModel;
@Resource
private ToolCallbackProvider toolCallbackProvider;
@Resource
private GetCityAdcodeTool getCityAdcodeTool;
@Resource
private MyAgentChatMemory myAgentChatMemory;
@Bean
public ChatClient memoryClient() {
return ChatClient.builder(openAiChatModel)
.defaultSystem(prompt.PROMPT_AGENT)
.defaultAdvisors(new SimpleLoggerAdvisor(),
MessageChatMemoryAdvisor.builder(myAgentChatMemory)
.build()
)
.defaultTools(getCityAdcodeTool)
.defaultToolCallbacks(toolCallbackProvider.getToolCallbacks())
.build();
}
}
Controller
然后因为每个会话需要有一个单独的会话id,我这里是作为路径参数传进来,controller代码如下:
@GetMapping(value = "/ai/chat", produces = "text/html; charset=utf-8")
public Flux<String> testMemory(@RequestParam String message, @RequestParam String conversationId) {
return memoryClient.prompt()
.user(message)
.advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, conversationId))
.stream()
.content();
}
在官方文档里我只查到了这一种指定conversationId的方法,就是
.advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, conversationId))
然后再给前端写一个删除会话的接口:
package com.hyk.mcpclient.controller;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@CrossOrigin(origins = "*") // 允许所有来源
public class DeleteConversationController {
@Resource
private RedissonClient redissonClient;
@GetMapping("/deleteConversation")
public String deleteConversation(@RequestParam String conversationId) {
RBucket<Object> bucket = redissonClient.getBucket("conversationId:" + conversationId);
if(!bucket.isExists()){
log.info("conversationId:{} 不存在,无法删除", conversationId);
return "conversationId:{} 不存在,无法删除";
}
bucket.delete();
log.info("conversationId:{} 已删除", conversationId);
return "conversationId:{} 已删除";
}
}
我这里前端页面和后端是不同源的,需要在controller添加跨域注解@CrossOrigin(origins = "*") // 允许所有来源

前端页面
让ai帮我生成的前端页面:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 聊天助手</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
display: flex;
height: 90vh;
}
.sidebar {
width: 300px;
background: #f8f9fa;
border-right: 1px solid #e9ecef;
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 20px;
border-bottom: 1px solid #e9ecef;
background: white;
}
.new-chat-btn {
width: 100%;
padding: 12px;
background: #007bff;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
}
.new-chat-btn:hover {
background: #0056b3;
}
.conversation-list {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.conversation-item {
padding: 12px;
margin: 5px 0;
background: white;
border-radius: 8px;
cursor: pointer;
border: 1px solid #e9ecef;
transition: all 0.3s;
display: flex;
justify-content: space-between;
align-items: center;
}
.conversation-item:hover {
background: #e9ecef;
}
.conversation-item.active {
background: #007bff;
color: white;
border-color: #007bff;
}
.delete-btn {
background: #dc3545;
color: white;
border: none;
border-radius: 4px;
padding: 4px 8px;
cursor: pointer;
font-size: 12px;
}
.delete-btn:hover {
background: #c82333;
}
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
}
.chat-header {
padding: 20px;
border-bottom: 1px solid #e9ecef;
background: white;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #f8f9fa;
}
.message {
margin-bottom: 15px;
max-width: 80%;
}
.user-message {
margin-left: auto;
}
.message-bubble {
padding: 12px 16px;
border-radius: 18px;
word-wrap: break-word;
}
.user-message .message-bubble {
background: #007bff;
color: white;
border-bottom-right-radius: 4px;
}
.ai-message .message-bubble {
background: white;
border: 1px solid #e9ecef;
border-bottom-left-radius: 4px;
}
.message-time {
font-size: 12px;
color: #6c757d;
margin-top: 5px;
text-align: right;
}
.ai-message .message-time {
text-align: left;
}
.chat-input-area {
padding: 20px;
border-top: 1px solid #e9ecef;
background: white;
}
.input-group {
display: flex;
gap: 10px;
}
.message-input {
flex: 1;
padding: 12px 16px;
border: 1px solid #e9ecef;
border-radius: 25px;
outline: none;
font-size: 14px;
resize: none;
height: 50px;
}
.message-input:focus {
border-color: #007bff;
}
.send-btn {
padding: 12px 24px;
background: #007bff;
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
}
.send-btn:hover:not(:disabled) {
background: #0056b3;
}
.send-btn:disabled {
background: #6c757d;
cursor: not-allowed;
}
.typing-indicator {
display: none;
padding: 12px 16px;
background: white;
border-radius: 18px;
border: 1px solid #e9ecef;
margin-bottom: 15px;
max-width: 80%;
}
.typing-dots {
display: flex;
gap: 4px;
}
.typing-dot {
width: 8px;
height: 8px;
background: #6c757d;
border-radius: 50%;
animation: typing 1.4s infinite;
}
.typing-dot:nth-child(2) {
animation-delay: 0.2s;
}
.typing-dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%, 60%, 100% {
transform: translateY(0);
}
30% {
transform: translateY(-10px);
}
}
.conversation-id {
font-size: 12px;
color: #6c757d;
margin-top: 5px;
}
.error-message {
color: #dc3545;
background: #f8d7da;
border: 1px solid #f5c6cb;
padding: 10px;
border-radius: 8px;
margin: 10px 0;
}
</style>
</head>
<body>
<div class="container">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar-header">
<button class="new-chat-btn" onclick="createNewConversation()">
+ 新建对话
</button>
</div>
<div class="conversation-list" id="conversationList">
<!-- 会话列表将通过 JavaScript 动态生成 -->
</div>
</div>
<!-- 主聊天区域 -->
<div class="chat-area">
<div class="chat-header">
<h2>AI 聊天助手</h2>
<div class="conversation-id" id="currentConversationId">
当前会话ID: 未选择
</div>
</div>
<div class="chat-messages" id="chatMessages">
<div class="welcome-message">
<div class="message ai-message">
<div class="message-bubble">
你好!我是 AI 助手,请选择一个会话或创建新会话来开始聊天。
</div>
</div>
</div>
</div>
<div class="typing-indicator" id="typingIndicator">
<div class="typing-dots">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
</div>
<div class="chat-input-area">
<div class="input-group">
<textarea
class="message-input"
id="messageInput"
placeholder="输入你的消息..."
rows="1"
onkeypress="handleKeyPress(event)"
></textarea>
<button class="send-btn" id="sendBtn" onclick="sendMessage()" disabled>
发送
</button>
</div>
</div>
</div>
</div>
<script>
// 全局变量
let currentConversationId = null;
let conversations = JSON.parse(localStorage.getItem('conversations') || '{}');
// 初始化页面
document.addEventListener('DOMContentLoaded', function() {
updateConversationList();
updateSendButton();
// 监听输入框变化
document.getElementById('messageInput').addEventListener('input', updateSendButton);
});
// 生成唯一的会话ID
function generateConversationId() {
return 'conv_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
// 创建新会话
function createNewConversation() {
const newId = generateConversationId();
conversations[newId] = {
id: newId,
title: '新对话',
createdAt: new Date().toISOString(),
messages: []
};
saveConversations();
switchConversation(newId);
updateConversationList();
}
// 切换会话
function switchConversation(conversationId) {
currentConversationId = conversationId;
document.getElementById('currentConversationId').textContent =
`当前会话ID: ${conversationId}`;
// 更新活跃状态
document.querySelectorAll('.conversation-item').forEach(item => {
item.classList.remove('active');
});
const activeItem = document.querySelector(`[data-conversation-id="${conversationId}"]`);
if (activeItem) {
activeItem.classList.add('active');
}
// 显示消息
displayMessages(conversations[conversationId].messages);
// 聚焦输入框
document.getElementById('messageInput').focus();
}
// 删除会话
function deleteConversation(conversationId, event) {
event.stopPropagation();
if (confirm('确定要删除这个会话吗?')) {
// 调用后端删除接口
fetch(`http://localhost:8081/deleteConversation?conversationId=${conversationId}`)
.then(response => response.text())
.then(result => {
console.log('删除结果:', result);
})
.catch(error => {
console.error('删除失败:', error);
});
// 从前端删除
delete conversations[conversationId];
if (currentConversationId === conversationId) {
currentConversationId = null;
document.getElementById('currentConversationId').textContent = '当前会话ID: 未选择';
document.getElementById('chatMessages').innerHTML = `
<div class="welcome-message">
<div class="message ai-message">
<div class="message-bubble">
你好!我是 AI 助手,请选择一个会话或创建新会话来开始聊天。
</div>
</div>
</div>
`;
}
saveConversations();
updateConversationList();
}
}
// 更新会话列表
function updateConversationList() {
const list = document.getElementById('conversationList');
const sortedConversations = Object.values(conversations)
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
if (sortedConversations.length === 0) {
list.innerHTML = '<div style="padding: 20px; text-align: center; color: #6c757d;">暂无会话</div>';
return;
}
list.innerHTML = sortedConversations.map(conv => `
<div class="conversation-item ${currentConversationId === conv.id ? 'active' : ''}"
data-conversation-id="${conv.id}"
onclick="switchConversation('${conv.id}')">
<span>${conv.title}</span>
<button class="delete-btn" onclick="deleteConversation('${conv.id}', event)">删除</button>
</div>
`).join('');
}
// 发送消息 - 修复版本
function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value.trim();
if (!message || !currentConversationId) return;
// 添加用户消息
addMessage('user', message);
input.value = '';
updateSendButton();
// 显示打字指示器
showTypingIndicator();
// 在流式输出开始前创建空的AI消息占位符
const aiMessageId = 'ai_msg_' + Date.now();
createEmptyAiMessage(aiMessageId);
// 调用后端接口
fetch(`http://localhost:8081/ai/chat?message=${encodeURIComponent(message)}&conversationId=${currentConversationId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let aiResponse = '';
function read() {
reader.read().then(({done, value}) => {
if (done) {
hideTypingIndicator();
// 保存完整的AI回复到历史记录
if (aiResponse) {
saveAiMessageToHistory(aiResponse);
updateConversationTitle(message);
} else {
// 如果没有收到回复,移除空的AI消息
removeEmptyAiMessage(aiMessageId);
addMessage('ai', '抱歉,没有收到回复。');
}
return;
}
const chunk = decoder.decode(value, {stream: true});
aiResponse += chunk;
// 实时更新AI消息内容
updateAiMessageContent(aiMessageId, aiResponse);
read();
}).catch(error => {
hideTypingIndicator();
console.error('读取流失败:', error);
removeEmptyAiMessage(aiMessageId);
addMessage('ai', '抱歉,响应读取失败。');
});
}
read();
})
.catch(error => {
hideTypingIndicator();
console.error('请求失败:', error);
removeEmptyAiMessage(aiMessageId);
let errorMessage = '抱歉,服务暂时不可用。';
if (error.message.includes('Failed to fetch')) {
errorMessage = '网络连接失败,请检查网络连接和后端服务状态。';
} else if (error.message.includes('HTTP error')) {
errorMessage = '服务器返回错误,请稍后重试。';
}
addMessage('ai', errorMessage);
});
}
// 创建空的AI消息占位符
function createEmptyAiMessage(messageId) {
const messagesContainer = document.getElementById('chatMessages');
const messageElement = document.createElement('div');
messageElement.className = 'message ai-message';
messageElement.id = messageId;
messageElement.innerHTML = `
<div class="message-bubble"></div>
<div class="message-time">${new Date().toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
})}</div>
`;
messagesContainer.appendChild(messageElement);
// 滚动到底部
messagesContainer.scrollTop = messagesContainer.scrollHeight;
return messageId;
}
// 更新AI消息内容
function updateAiMessageContent(messageId, content) {
const messageElement = document.getElementById(messageId);
if (messageElement) {
const bubble = messageElement.querySelector('.message-bubble');
if (bubble) {
bubble.textContent = content;
}
}
// 滚动到底部
const messagesContainer = document.getElementById('chatMessages');
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
// 移除空的AI消息(在出错时使用)
function removeEmptyAiMessage(messageId) {
const messageElement = document.getElementById(messageId);
if (messageElement) {
const bubble = messageElement.querySelector('.message-bubble');
if (bubble && !bubble.textContent.trim()) {
messageElement.remove();
}
}
}
// 保存AI消息到历史记录
function saveAiMessageToHistory(content) {
const messages = conversations[currentConversationId].messages;
const message = {
sender: 'ai',
content: content,
timestamp: new Date().toISOString()
};
messages.push(message);
saveConversations();
}
// 添加消息到界面
function addMessage(sender, content) {
const messages = conversations[currentConversationId].messages;
const message = {
sender: sender,
content: content,
timestamp: new Date().toISOString()
};
messages.push(message);
saveConversations();
// 只有用户消息才立即显示,AI消息通过流式输出处理
if (sender === 'user') {
displayMessages(messages);
}
}
// 显示消息
function displayMessages(messages) {
const container = document.getElementById('chatMessages');
// 过滤掉正在流式输出的消息
const displayedMessages = messages.filter(msg =>
msg.sender === 'user' || (msg.sender === 'ai' && msg.content)
);
container.innerHTML = displayedMessages.map(msg =>
createMessageElement(msg.sender, msg.content, msg.timestamp).outerHTML
).join('');
// 滚动到底部
container.scrollTop = container.scrollHeight;
}
// 创建消息元素
function createMessageElement(sender, content, timestamp = null) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${sender}-message`;
const time = timestamp ? new Date(timestamp) : new Date();
const timeString = time.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
messageDiv.innerHTML = `
<div class="message-bubble">${content}</div>
<div class="message-time">${timeString}</div>
`;
return messageDiv;
}
// 更新会话标题(使用第一条用户消息)
function updateConversationTitle(firstMessage) {
if (conversations[currentConversationId].title === '新对话') {
conversations[currentConversationId].title =
firstMessage.length > 20 ? firstMessage.substring(0, 20) + '...' : firstMessage;
saveConversations();
updateConversationList();
}
}
// 显示/隐藏打字指示器
function showTypingIndicator() {
document.getElementById('typingIndicator').style.display = 'block';
document.getElementById('chatMessages').scrollTop = document.getElementById('chatMessages').scrollHeight;
}
function hideTypingIndicator() {
document.getElementById('typingIndicator').style.display = 'none';
}
// 更新发送按钮状态
function updateSendButton() {
const input = document.getElementById('messageInput');
const button = document.getElementById('sendBtn');
button.disabled = !input.value.trim() || !currentConversationId;
}
// 处理回车键发送
function handleKeyPress(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
}
// 保存会话到本地存储
function saveConversations() {
localStorage.setItem('conversations', JSON.stringify(conversations));
}
</script>
</body>
</html>
然后修改了一下提示词,让我们有一个真正的agent:
public static final String PROMPT_AGENT = """
你是一个由hyk设计的ai智能体,你具有多种功能,
你现在拥有的功能如下:
功能1. 获取天气信息:使用getWeather工具获取指定城市的天气信息
查询流程:
1. 首先使用 getCityAdcode 工具确认城市的adcode
2. 然后使用 getWeatherByAdcode 工具查询具体天气 需要传递步骤1中查询到的城市adcode
3. 如果城市不存在,直接告知用户
回复要求:
- 用友好的中文回复
- 包含完整的天气信息
- 如果查询失败,给出友好的错误提示
示例回复:
"北京当前天气:晴,温度25°C,湿度60%,东南风3级,祝您有愉快的一天!🌞"
如果查询失败,请告诉我失败的具体原因
功能2. 普通问答助手
作为ai和用户进行正常对话,无需进行工具调用
功能3. 城市adcode查询
查询流程:
1. 使用 getCityAdcode 工具确认城市的adcode
2. 如果城市不存在,直接告知用户
功能4. 查询今日新闻
查询流程:
1. 使用getNews工具查询今日新闻
2. 将得到的结果返回给用户
3. 如果出现问题,请告诉用户失败的原因
""";
最后的运行结果如下:

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)