Initial commit: Fintec AI Framework with Agent, RAG, and MCP modules
This commit is contained in:
BIN
fintec-framework-agent-spring-boot-autoconfigure/.DS_Store
vendored
Normal file
BIN
fintec-framework-agent-spring-boot-autoconfigure/.DS_Store
vendored
Normal file
Binary file not shown.
93
fintec-framework-agent-spring-boot-autoconfigure/pom.xml
Normal file
93
fintec-framework-agent-spring-boot-autoconfigure/pom.xml
Normal file
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.ccb.fintec</groupId>
|
||||
<artifactId>fintec-framework-parent</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>fintec-framework-agent-spring-boot-autoconfigure</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<!-- 核心协议层 -->
|
||||
<dependency>
|
||||
<groupId>com.ccb.fintec</groupId>
|
||||
<artifactId>fintec-framework-ai-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot AutoConfigure -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot AOP -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Retry 支持 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.retry</groupId>
|
||||
<artifactId>spring-retry</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-aspects</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Configuration Processor -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- ChatClient / Function Calling 支持 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring AI Chat Client 支持 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-client-chat</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring AI Advisors(包含 CallAroundAdvisor 等) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-advisors-vector-store</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MCP Client 支持 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-mcp-client</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- WebFlux for SSE transport -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Test Dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.ccb.fintec.agent.autoconfigure.aspect;
|
||||
|
||||
import com.ccb.fintec.agent.autoconfigure.config.AiCoreConfigProperties;
|
||||
import com.ccb.fintec.core.exception.AiException;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* AI 调用限流切面
|
||||
* 基于滑动窗口算法实现简单的限流
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@ConditionalOnProperty(prefix = "fintec.ai.rate-limit", name = "enabled", havingValue = "true", matchIfMissing = false)
|
||||
public class AiRateLimitAspect {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AiRateLimitAspect.class);
|
||||
|
||||
private final AiCoreConfigProperties properties;
|
||||
private final ConcurrentHashMap<String, RequestCounter> counters = new ConcurrentHashMap<>();
|
||||
|
||||
public AiRateLimitAspect(AiCoreConfigProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Around("execution(* org.springframework.ai.chat.client.ChatClient.*(..))")
|
||||
public Object limitRate(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
int maxRequests = properties.getRateLimit().getMaxRequestsPerMinute();
|
||||
|
||||
// 简单实现:基于方法签名的限流
|
||||
String key = joinPoint.getSignature().toShortString();
|
||||
RequestCounter counter = counters.computeIfAbsent(key, k -> new RequestCounter());
|
||||
|
||||
if (!counter.allowRequest(maxRequests)) {
|
||||
log.warn("Rate limit exceeded for {}", key);
|
||||
throw new AiException("RATE_LIMIT_EXCEEDED",
|
||||
"AI 调用频率超过限制: " + maxRequests + " 次/分钟");
|
||||
}
|
||||
|
||||
try {
|
||||
return joinPoint.proceed();
|
||||
} catch (Exception e) {
|
||||
log.error("AI call failed", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的请求计数器
|
||||
*/
|
||||
private static class RequestCounter {
|
||||
private final AtomicInteger count = new AtomicInteger(0);
|
||||
private volatile long windowStart = System.currentTimeMillis();
|
||||
|
||||
public synchronized boolean allowRequest(int maxRequests) {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
// 重置窗口(每分钟)
|
||||
if (now - windowStart > 60000) {
|
||||
count.set(0);
|
||||
windowStart = now;
|
||||
}
|
||||
|
||||
return count.incrementAndGet() <= maxRequests;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.ccb.fintec.agent.autoconfigure.aspect;
|
||||
|
||||
import com.ccb.fintec.agent.autoconfigure.config.AiCoreConfigProperties;
|
||||
import com.ccb.fintec.core.exception.AiException;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI 安全过滤切面
|
||||
*
|
||||
* 功能:
|
||||
* 1. 检查用户输入是否包含敏感词
|
||||
* 2. 检查 AI 响应是否包含敏感词
|
||||
* 3. 发现敏感内容时抛出异常,阻止调用
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@ConditionalOnProperty(prefix = "fintec.ai.safety", name = "enabled", havingValue = "true", matchIfMissing = false)
|
||||
public class SafetyAspect {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SafetyAspect.class);
|
||||
|
||||
private final List<String> blockKeywords;
|
||||
|
||||
public SafetyAspect(AiCoreConfigProperties properties) {
|
||||
this.blockKeywords = properties.getSafety().getBlockKeywords();
|
||||
log.info("🔒 安全过滤已启用,拦截关键词: {}", blockKeywords);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截 ChatModel 的 call 方法
|
||||
*/
|
||||
@Around("execution(* org.springframework.ai.chat.model.ChatModel.call(..))")
|
||||
public Object checkSafety(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
// 1. 检查请求参数中的用户输入
|
||||
Object[] args = joinPoint.getArgs();
|
||||
for (Object arg : args) {
|
||||
if (arg != null) {
|
||||
String argStr = arg.toString();
|
||||
if (containsSensitiveWord(argStr)) {
|
||||
throw new AiException("SAFETY_VIOLATION", "❌ 请求包含敏感内容,已被拦截");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 执行 AI 调用
|
||||
Object result = joinPoint.proceed();
|
||||
|
||||
// 3. 检查 AI 响应
|
||||
if (result instanceof ChatResponse chatResponse) {
|
||||
String responseText = chatResponse.getResult().getOutput().getText();
|
||||
if (containsSensitiveWord(responseText)) {
|
||||
log.warn("⚠️ AI 响应包含敏感内容,已拦截");
|
||||
throw new AiException("SAFETY_VIOLATION", "❌ 响应包含敏感内容,已被拦截");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文本是否包含敏感词
|
||||
*/
|
||||
private boolean containsSensitiveWord(String text) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String lowerText = text.toLowerCase();
|
||||
for (String keyword : blockKeywords) {
|
||||
if (lowerText.contains(keyword.toLowerCase())) {
|
||||
log.warn("🚫 检测到敏感词: {}", keyword);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.ccb.fintec.agent.autoconfigure.config;
|
||||
|
||||
import com.ccb.fintec.agent.autoconfigure.multimodal.MultimodalTemplate;
|
||||
import com.ccb.fintec.agent.autoconfigure.template.AgentTemplate;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
|
||||
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
|
||||
import org.springframework.ai.chat.memory.ChatMemory;
|
||||
import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository;
|
||||
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
|
||||
/**
|
||||
* Agent 自动配置类
|
||||
*
|
||||
* 提供简化的 Agent 开发体验:
|
||||
* 1. 自动创建 ChatClient,内置对话记忆和日志 Advisor
|
||||
* 2. 提供 AgentTemplate,封装常用调用模式
|
||||
* 3. 默认使用内存记忆,业务系统可自行配置 JDBC 或其他持久化方案
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass(ChatModel.class)
|
||||
@EnableConfigurationProperties(AiCoreConfigProperties.class)
|
||||
public class AgentAutoConfiguration {
|
||||
|
||||
/**
|
||||
* 默认对话记忆:内存实现
|
||||
* 如果业务系统需要持久化记忆,可以声明自己的 ChatMemory Bean 覆盖此配置
|
||||
*
|
||||
* 示例:JDBC 持久化记忆
|
||||
* <pre>{@code
|
||||
* @Bean
|
||||
* public ChatMemory jdbcChatMemory(DataSource dataSource) {
|
||||
* return MessageWindowChatMemory.builder()
|
||||
* .chatMemoryRepository(new JdbcChatMemoryRepository(dataSource))
|
||||
* .maxMessages(20)
|
||||
* .build();
|
||||
* }
|
||||
* }</pre>
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ChatMemory.class)
|
||||
public ChatMemory chatMemory(AiCoreConfigProperties properties) {
|
||||
return MessageWindowChatMemory.builder()
|
||||
.chatMemoryRepository(new InMemoryChatMemoryRepository())
|
||||
.maxMessages(properties.getAgent().getMemoryWindowSize())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心:ChatClient 统一构造
|
||||
* 内置:记忆 Advisor + 日志 Advisor
|
||||
* 安全过滤通过 SafetyAspect AOP 切面实现
|
||||
* 业务系统可以通过声明自己的 ChatClient Bean 完全覆盖
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ChatClient.class)
|
||||
public ChatClient chatClient(ChatModel chatModel,
|
||||
ChatMemory chatMemory,
|
||||
AiCoreConfigProperties properties) {
|
||||
return ChatClient.builder(chatModel)
|
||||
.defaultAdvisors(
|
||||
// 对话记忆(自动管理上下文)
|
||||
MessageChatMemoryAdvisor.builder(chatMemory).build(),
|
||||
// 请求/响应日志,方便调试
|
||||
new SimpleLoggerAdvisor()
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* AgentTemplate:业务系统的唯一入口
|
||||
* 封装了所有常用 AI 调用模式,内置自动重试能力
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(AgentTemplate.class)
|
||||
public AgentTemplate agentTemplate(ChatClient chatClient,
|
||||
ChatModel chatModel,
|
||||
AiCoreConfigProperties properties,
|
||||
RetryTemplate retryTemplate) {
|
||||
return new AgentTemplate(chatClient, chatModel, properties, retryTemplate);
|
||||
}
|
||||
|
||||
/**
|
||||
* MultimodalTemplate:多模态能力(图片/文档理解)
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(MultimodalTemplate.class)
|
||||
public MultimodalTemplate multimodalTemplate(ChatClient chatClient) {
|
||||
return new MultimodalTemplate(chatClient);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.ccb.fintec.agent.autoconfigure.config;
|
||||
|
||||
import com.ccb.fintec.core.properties.AgentProperties;
|
||||
import com.ccb.fintec.core.properties.ObservabilityProperties;
|
||||
import com.ccb.fintec.core.properties.RateLimitProperties;
|
||||
import com.ccb.fintec.core.properties.RetryProperties;
|
||||
import com.ccb.fintec.core.properties.SafetyProperties;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
|
||||
/**
|
||||
* AI 核心配置属性绑定类(Agent 模块)
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "fintec.ai")
|
||||
public class AiCoreConfigProperties {
|
||||
|
||||
private AgentProperties agent = new AgentProperties();
|
||||
private SafetyProperties safety = new SafetyProperties();
|
||||
private RateLimitProperties rateLimit = new RateLimitProperties();
|
||||
private RetryProperties retry = new RetryProperties();
|
||||
private ObservabilityProperties observability = new ObservabilityProperties();
|
||||
|
||||
public AgentProperties getAgent() {
|
||||
return agent;
|
||||
}
|
||||
|
||||
public void setAgent(AgentProperties agent) {
|
||||
this.agent = agent;
|
||||
}
|
||||
|
||||
public SafetyProperties getSafety() {
|
||||
return safety;
|
||||
}
|
||||
|
||||
public void setSafety(SafetyProperties safety) {
|
||||
this.safety = safety;
|
||||
}
|
||||
|
||||
public RateLimitProperties getRateLimit() {
|
||||
return rateLimit;
|
||||
}
|
||||
|
||||
public void setRateLimit(RateLimitProperties rateLimit) {
|
||||
this.rateLimit = rateLimit;
|
||||
}
|
||||
|
||||
public RetryProperties getRetry() {
|
||||
return retry;
|
||||
}
|
||||
|
||||
public void setRetry(RetryProperties retry) {
|
||||
this.retry = retry;
|
||||
}
|
||||
|
||||
public ObservabilityProperties getObservability() {
|
||||
return observability;
|
||||
}
|
||||
|
||||
public void setObservability(ObservabilityProperties observability) {
|
||||
this.observability = observability;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.ccb.fintec.agent.autoconfigure.config;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.retry.RetryCallback;
|
||||
import org.springframework.retry.RetryContext;
|
||||
import org.springframework.retry.RetryListener;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
|
||||
/**
|
||||
* Retry 重试配置
|
||||
*
|
||||
* 为 AI 调用提供自动重试能力,处理网络波动、临时故障等场景
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "fintec.ai.retry", name = "enabled", havingValue = "true", matchIfMissing = false)
|
||||
public class RetryAutoConfiguration {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(RetryAutoConfiguration.class);
|
||||
|
||||
@Bean
|
||||
public RetryTemplate retryTemplate(AiCoreConfigProperties properties) {
|
||||
RetryTemplate template = RetryTemplate.builder()
|
||||
.maxAttempts(properties.getRetry().getMaxAttempts())
|
||||
.fixedBackoff(properties.getRetry().getBackoffMs())
|
||||
.build();
|
||||
|
||||
// 注册监听器,记录重试日志
|
||||
template.registerListener(new RetryListener() {
|
||||
@Override
|
||||
public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
|
||||
if (context.getRetryCount() > 0) {
|
||||
log.info("AI 调用重试完成,总重试次数: {}, 最终结果: {}",
|
||||
context.getRetryCount(),
|
||||
throwable == null ? "成功" : "失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
|
||||
log.warn("AI 调用第 {} 次重试,原因: {}",
|
||||
context.getRetryCount(),
|
||||
throwable.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ccb.fintec.agent.autoconfigure.graph.config;
|
||||
|
||||
import com.ccb.fintec.agent.autoconfigure.graph.template.GraphTemplate;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* Graph 工作流自动配置
|
||||
*/
|
||||
@AutoConfiguration
|
||||
public class GraphAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public GraphTemplate graphTemplate() {
|
||||
return new GraphTemplate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.ccb.fintec.agent.autoconfigure.graph.example;
|
||||
|
||||
import com.ccb.fintec.core.graph.Condition;
|
||||
import com.ccb.fintec.core.graph.Node;
|
||||
import com.ccb.fintec.agent.autoconfigure.graph.template.GraphTemplate;
|
||||
import com.ccb.fintec.agent.autoconfigure.graph.workflow.*;
|
||||
|
||||
/**
|
||||
* Graph 工作流使用示例
|
||||
*/
|
||||
public class GraphWorkflowExample {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
// 示例1: 顺序工作流 - 数据处理流水线
|
||||
sequentialExample();
|
||||
|
||||
// 示例2: 并行工作流 - 同时调用多个AI模型
|
||||
parallelExample();
|
||||
|
||||
// 示例3: 路由工作流 - 根据问题类型选择不同处理逻辑
|
||||
routingExample();
|
||||
|
||||
// 示例4: 循环工作流 - 迭代优化直到满足条件
|
||||
loopExample();
|
||||
}
|
||||
|
||||
/**
|
||||
* 顺序工作流示例: 文本处理流水线
|
||||
*/
|
||||
private static void sequentialExample() {
|
||||
System.out.println("=== 顺序工作流示例 ===");
|
||||
|
||||
Node<String, String> cleanNode = GraphTemplate.node("清理文本",
|
||||
text -> text.trim().replaceAll("\\s+", " "));
|
||||
|
||||
Node<String, String> translateNode = GraphTemplate.node("翻译",
|
||||
text -> "[翻译] " + text);
|
||||
|
||||
Node<String, String> summarizeNode = GraphTemplate.node("总结",
|
||||
text -> "[总结] " + text.substring(0, Math.min(50, text.length())));
|
||||
|
||||
SequentialWorkflow workflow = GraphTemplate.sequential(
|
||||
cleanNode, translateNode, summarizeNode
|
||||
);
|
||||
|
||||
String result = workflow.execute(" 这是一段需要处理的 文本内容 ");
|
||||
System.out.println("结果: " + result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 并行工作流示例: 同时分析文本的多个维度
|
||||
*/
|
||||
private static void parallelExample() {
|
||||
System.out.println("\n=== 并行工作流示例 ===");
|
||||
|
||||
Node<String, String> sentimentNode = GraphTemplate.node("情感分析",
|
||||
text -> "情感: 积极");
|
||||
|
||||
Node<String, String> keywordNode = GraphTemplate.node("关键词提取",
|
||||
text -> "关键词: AI, Spring, 工作流");
|
||||
|
||||
Node<String, String> categoryNode = GraphTemplate.node("分类",
|
||||
text -> "分类: 技术文档");
|
||||
|
||||
ParallelWorkflow workflow = GraphTemplate.parallel(
|
||||
sentimentNode, keywordNode, categoryNode
|
||||
);
|
||||
|
||||
var results = workflow.execute("Spring AI 是一个强大的框架");
|
||||
results.forEach(r -> System.out.println("结果: " + r));
|
||||
|
||||
workflow.shutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* 路由工作流示例: 根据问题类型选择处理逻辑
|
||||
*/
|
||||
private static void routingExample() {
|
||||
System.out.println("\n=== 路由工作流示例 ===");
|
||||
|
||||
Node<String, String> techNode = GraphTemplate.node("技术问题处理",
|
||||
q -> "技术回答: " + q);
|
||||
|
||||
Node<String, String> businessNode = GraphTemplate.node("业务问题处理",
|
||||
q -> "业务回答: " + q);
|
||||
|
||||
Node<String, String> defaultNode = GraphTemplate.node("通用回答",
|
||||
q -> "通用回答: " + q);
|
||||
|
||||
RoutingWorkflow workflow = GraphTemplate.routing()
|
||||
.addBranch("技术", ctx -> ((String) ctx).contains("技术"), techNode)
|
||||
.addBranch("业务", ctx -> ((String) ctx).contains("业务"), businessNode)
|
||||
.setDefault(defaultNode);
|
||||
|
||||
String result1 = workflow.execute("这是一个技术问题");
|
||||
System.out.println("结果1: " + result1);
|
||||
|
||||
String result2 = workflow.execute("这是一个业务问题");
|
||||
System.out.println("结果2: " + result2);
|
||||
|
||||
String result3 = workflow.execute("这是一个普通问题");
|
||||
System.out.println("结果3: " + result3);
|
||||
}
|
||||
|
||||
/**
|
||||
* 循环工作流示例: 迭代优化文本直到满意
|
||||
*/
|
||||
private static void loopExample() {
|
||||
System.out.println("\n=== 循环工作流示例 ===");
|
||||
|
||||
int[] iteration = {0};
|
||||
|
||||
Node<String, String> optimizeNode = GraphTemplate.node("优化文本",
|
||||
text -> {
|
||||
iteration[0]++;
|
||||
return text + " [优化版本" + iteration[0] + "]";
|
||||
});
|
||||
|
||||
// 最多迭代3次,或者当文本长度超过50时停止
|
||||
LoopWorkflow workflow = GraphTemplate.loop(
|
||||
optimizeNode,
|
||||
ctx -> ((String) ctx).length() < 50,
|
||||
3
|
||||
);
|
||||
|
||||
String result = workflow.execute("初始文本");
|
||||
System.out.println("结果: " + result);
|
||||
System.out.println("迭代次数: " + iteration[0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.ccb.fintec.agent.autoconfigure.graph.node;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* 简单节点实现
|
||||
*/
|
||||
public class SimpleNode<I, O> implements com.ccb.fintec.core.graph.Node<I, O> {
|
||||
|
||||
private final String name;
|
||||
private final Function<I, O> function;
|
||||
|
||||
public SimpleNode(String name, Function<I, O> function) {
|
||||
this.name = name;
|
||||
this.function = function;
|
||||
}
|
||||
|
||||
@Override
|
||||
public O execute(I input) {
|
||||
return function.apply(input);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.ccb.fintec.agent.autoconfigure.graph.template;
|
||||
|
||||
import com.ccb.fintec.core.graph.Condition;
|
||||
import com.ccb.fintec.core.graph.Node;
|
||||
import com.ccb.fintec.agent.autoconfigure.graph.node.SimpleNode;
|
||||
import com.ccb.fintec.agent.autoconfigure.graph.workflow.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Graph 工作流模板 - 提供便捷的工作流创建方法
|
||||
*/
|
||||
public class GraphTemplate {
|
||||
|
||||
/**
|
||||
* 创建顺序工作流
|
||||
*/
|
||||
@SafeVarargs
|
||||
public static SequentialWorkflow sequential(Node<?, ?>... nodes) {
|
||||
return new SequentialWorkflow(nodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并行工作流
|
||||
*/
|
||||
@SafeVarargs
|
||||
public static ParallelWorkflow parallel(Node<?, ?>... nodes) {
|
||||
return new ParallelWorkflow(nodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建路由工作流
|
||||
*/
|
||||
public static RoutingWorkflow routing() {
|
||||
return new RoutingWorkflow();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建循环工作流
|
||||
* @param node 要循环执行的节点
|
||||
* @param condition 继续循环的条件
|
||||
* @param maxIterations 最大迭代次数
|
||||
*/
|
||||
public static LoopWorkflow loop(Node<?, ?> node, Condition condition, int maxIterations) {
|
||||
return new LoopWorkflow(node, condition, maxIterations);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建简单节点
|
||||
*/
|
||||
public static <I, O> Node<I, O> node(String name, java.util.function.Function<I, O> function) {
|
||||
return new SimpleNode<>(name, function);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.ccb.fintec.agent.autoconfigure.graph.workflow;
|
||||
|
||||
import com.ccb.fintec.core.graph.Condition;
|
||||
import com.ccb.fintec.core.graph.Node;
|
||||
|
||||
/**
|
||||
* 循环工作流 - 根据条件重复执行节点
|
||||
*/
|
||||
public class LoopWorkflow {
|
||||
|
||||
private final Node<?, ?> node;
|
||||
private final Condition continueCondition;
|
||||
private final int maxIterations;
|
||||
|
||||
/**
|
||||
* @param node 要循环执行的节点
|
||||
* @param continueCondition 继续循环的条件(返回true则继续)
|
||||
* @param maxIterations 最大迭代次数(防止无限循环)
|
||||
*/
|
||||
public LoopWorkflow(Node<?, ?> node, Condition continueCondition, int maxIterations) {
|
||||
this.node = node;
|
||||
this.continueCondition = continueCondition;
|
||||
this.maxIterations = maxIterations;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行循环工作流
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <I, O> O execute(I input) {
|
||||
Object result = input;
|
||||
int iteration = 0;
|
||||
|
||||
while (iteration < maxIterations && continueCondition.evaluate(result)) {
|
||||
result = ((Node<Object, Object>) node).execute(result);
|
||||
iteration++;
|
||||
}
|
||||
|
||||
if (iteration >= maxIterations) {
|
||||
throw new IllegalStateException("Loop exceeded maximum iterations: " + maxIterations);
|
||||
}
|
||||
|
||||
return (O) result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.ccb.fintec.agent.autoconfigure.graph.workflow;
|
||||
|
||||
import com.ccb.fintec.core.graph.Node;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 并行工作流 - 并行执行多个节点
|
||||
*/
|
||||
public class ParallelWorkflow {
|
||||
|
||||
private final List<Node<?, ?>> nodes;
|
||||
private final ExecutorService executor;
|
||||
|
||||
@SafeVarargs
|
||||
public ParallelWorkflow(Node<?, ?>... nodes) {
|
||||
this(Arrays.asList(nodes), Executors.newCachedThreadPool());
|
||||
}
|
||||
|
||||
public ParallelWorkflow(List<Node<?, ?>> nodes, ExecutorService executor) {
|
||||
this.nodes = nodes;
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 并行执行所有节点
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <I, O> List<O> execute(I input) {
|
||||
List<Callable<Object>> tasks = nodes.stream()
|
||||
.map(node -> (Callable<Object>) () -> ((Node<Object, Object>) node).execute(input))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
try {
|
||||
List<Future<Object>> futures = executor.invokeAll(tasks);
|
||||
return futures.stream()
|
||||
.map(future -> {
|
||||
try {
|
||||
return (O) future.get();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Parallel workflow execution failed", e);
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException("Parallel workflow interrupted", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭线程池
|
||||
*/
|
||||
public void shutdown() {
|
||||
executor.shutdown();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.ccb.fintec.agent.autoconfigure.graph.workflow;
|
||||
|
||||
import com.ccb.fintec.core.graph.Condition;
|
||||
import com.ccb.fintec.core.graph.Node;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 路由工作流 - 根据条件选择不同的节点执行
|
||||
*/
|
||||
public class RoutingWorkflow {
|
||||
|
||||
private final Map<String, Branch> branches = new LinkedHashMap<>();
|
||||
private Node<Object, Object> defaultNode;
|
||||
|
||||
/**
|
||||
* 添加分支
|
||||
* @param name 分支名称
|
||||
* @param condition 条件
|
||||
* @param node 节点
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public RoutingWorkflow addBranch(String name, Condition condition, Node<?, ?> node) {
|
||||
branches.put(name, new Branch(condition, (Node<Object, Object>) node));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认节点(当所有条件都不满足时执行)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public RoutingWorkflow setDefault(Node<?, ?> node) {
|
||||
this.defaultNode = (Node<Object, Object>) node;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行路由工作流
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <I, O> O execute(I input) {
|
||||
for (Map.Entry<String, Branch> entry : branches.entrySet()) {
|
||||
if (entry.getValue().condition.evaluate(input)) {
|
||||
return (O) entry.getValue().node.execute(input);
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultNode != null) {
|
||||
return (O) defaultNode.execute(input);
|
||||
}
|
||||
|
||||
throw new IllegalStateException("No matching branch found and no default node set");
|
||||
}
|
||||
|
||||
private static class Branch {
|
||||
private final Condition condition;
|
||||
private final Node<Object, Object> node;
|
||||
|
||||
public Branch(Condition condition, Node<Object, Object> node) {
|
||||
this.condition = condition;
|
||||
this.node = node;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.ccb.fintec.agent.autoconfigure.graph.workflow;
|
||||
|
||||
import com.ccb.fintec.core.graph.Node;
|
||||
|
||||
/**
|
||||
* 顺序工作流 - 按顺序执行多个节点
|
||||
*/
|
||||
public class SequentialWorkflow {
|
||||
|
||||
private final Node<?, ?>[] nodes;
|
||||
|
||||
@SafeVarargs
|
||||
public SequentialWorkflow(Node<?, ?>... nodes) {
|
||||
this.nodes = nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行顺序工作流
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <I, O> O execute(I input) {
|
||||
Object result = input;
|
||||
for (Node<?, ?> node : nodes) {
|
||||
result = ((Node<Object, Object>) node).execute(result);
|
||||
}
|
||||
return (O) result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.ccb.fintec.agent.autoconfigure.mcp;
|
||||
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* MCP Client 自动配置 - 支持连接到外部 MCP Server
|
||||
*
|
||||
* 注意:MCP Client 的具体连接配置通过 application.yml 完成:
|
||||
* spring:
|
||||
* ai:
|
||||
* mcp:
|
||||
* client:
|
||||
* enabled: true
|
||||
* sse:
|
||||
* connections:
|
||||
* server1:
|
||||
* url: http://localhost:9090
|
||||
* stdio:
|
||||
* connections:
|
||||
* server2:
|
||||
* command: java
|
||||
* args: [-jar, path/to/server.jar]
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass(ToolCallbackProvider.class)
|
||||
public class McpClientAutoConfiguration {
|
||||
// MCP Client 由 spring-ai-starter-mcp-client 自动配置
|
||||
// 这里无需额外 Bean 定义
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.ccb.fintec.agent.autoconfigure.multimodal;
|
||||
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.content.Media;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 多模态模板 - 支持图片理解、文档理解等多模态输入
|
||||
*/
|
||||
public class MultimodalTemplate {
|
||||
|
||||
private final ChatClient chatClient;
|
||||
|
||||
public MultimodalTemplate(ChatClient chatClient) {
|
||||
this.chatClient = chatClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片理解 - 单图问答
|
||||
* @param image 图片资源
|
||||
* @param question 问题
|
||||
* @return AI 回答
|
||||
*/
|
||||
public String askWithImage(Resource image, String question) {
|
||||
return chatClient.prompt()
|
||||
.user(userSpec -> userSpec
|
||||
.text(question)
|
||||
.media(new Media(MimeTypeUtils.IMAGE_PNG, image))
|
||||
)
|
||||
.call()
|
||||
.content();
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片理解 - 多图问答
|
||||
* @param images 图片列表
|
||||
* @param question 问题
|
||||
* @return AI 回答
|
||||
*/
|
||||
public String askWithImages(List<Resource> images, String question) {
|
||||
return chatClient.prompt()
|
||||
.user(userSpec -> {
|
||||
userSpec.text(question);
|
||||
images.forEach(image -> userSpec.media(new Media(MimeTypeUtils.IMAGE_PNG, image)));
|
||||
})
|
||||
.call()
|
||||
.content();
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片理解 - 流式输出
|
||||
* @param image 图片资源
|
||||
* @param question 问题
|
||||
* @return 流式响应
|
||||
*/
|
||||
public Flux<String> streamWithImage(Resource image, String question) {
|
||||
return chatClient.prompt()
|
||||
.user(userSpec -> userSpec
|
||||
.text(question)
|
||||
.media(new Media(MimeTypeUtils.IMAGE_PNG, image))
|
||||
)
|
||||
.stream()
|
||||
.content();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package com.ccb.fintec.agent.autoconfigure.template;
|
||||
|
||||
import com.ccb.fintec.agent.autoconfigure.config.AiCoreConfigProperties;
|
||||
import com.ccb.fintec.core.dto.AiResponse;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.memory.ChatMemory;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Agent 核心入口
|
||||
* 设计原则:
|
||||
* 1. 简单用法 3 行搞定
|
||||
* 2. 复杂用法通过 getChatClient() 拿底层,不被锁死
|
||||
* 3. 内置公司级策略:默认提示词、会话记忆、元数据收集
|
||||
*/
|
||||
public class AgentTemplate {
|
||||
|
||||
private final ChatClient chatClient;
|
||||
private final ChatModel chatModel;
|
||||
private final String defaultSystemPrompt;
|
||||
private final RetryTemplate retryTemplate;
|
||||
|
||||
public AgentTemplate(ChatClient chatClient, ChatModel chatModel,
|
||||
AiCoreConfigProperties properties, RetryTemplate retryTemplate) {
|
||||
this.chatClient = chatClient;
|
||||
this.chatModel = chatModel;
|
||||
this.defaultSystemPrompt = properties.getAgent().getDefaultSystemPrompt();
|
||||
this.retryTemplate = retryTemplate;
|
||||
}
|
||||
|
||||
// ========== 基础问答 ==========
|
||||
|
||||
/** 最简单用法:直接问答(带自动重试) */
|
||||
public String ask(String question) {
|
||||
return retryTemplate.execute(context ->
|
||||
chatClient.prompt()
|
||||
.system(defaultSystemPrompt)
|
||||
.user(question)
|
||||
.call()
|
||||
.content()
|
||||
);
|
||||
}
|
||||
|
||||
/** 带会话ID的多轮对话(自动记忆上下文,带自动重试) */
|
||||
public String ask(String question, String conversationId) {
|
||||
return retryTemplate.execute(context ->
|
||||
chatClient.prompt()
|
||||
.system(defaultSystemPrompt)
|
||||
.user(question)
|
||||
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
|
||||
.call()
|
||||
.content()
|
||||
);
|
||||
}
|
||||
|
||||
/** 自定义系统提示词的问答(带自动重试) */
|
||||
public String ask(String systemPrompt, String question, String conversationId) {
|
||||
return retryTemplate.execute(context ->
|
||||
chatClient.prompt()
|
||||
.system(systemPrompt)
|
||||
.user(question)
|
||||
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
|
||||
.call()
|
||||
.content()
|
||||
);
|
||||
}
|
||||
|
||||
// ========== 流式输出 ==========
|
||||
|
||||
/** 流式问答,适合前端打字机效果 */
|
||||
public Flux<String> stream(String question) {
|
||||
return chatClient.prompt()
|
||||
.system(defaultSystemPrompt)
|
||||
.user(question)
|
||||
.stream()
|
||||
.content();
|
||||
}
|
||||
|
||||
/** 带会话ID的流式多轮对话 */
|
||||
public Flux<String> stream(String question, String conversationId) {
|
||||
return chatClient.prompt()
|
||||
.system(defaultSystemPrompt)
|
||||
.user(question)
|
||||
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
|
||||
.stream()
|
||||
.content();
|
||||
}
|
||||
|
||||
// ========== Tool Calling ==========
|
||||
|
||||
/** 带工具的问答,AI 自动决定是否调用工具(带自动重试) */
|
||||
public String askWithTools(String question, Object... toolObjects) {
|
||||
return retryTemplate.execute(context ->
|
||||
chatClient.prompt()
|
||||
.system(defaultSystemPrompt)
|
||||
.user(question)
|
||||
.tools(toolObjects)
|
||||
.call()
|
||||
.content()
|
||||
);
|
||||
}
|
||||
|
||||
/** 带工具 + 会话记忆(带自动重试) */
|
||||
public String askWithTools(String question, String conversationId, Object... toolObjects) {
|
||||
return retryTemplate.execute(context ->
|
||||
chatClient.prompt()
|
||||
.system(defaultSystemPrompt)
|
||||
.user(question)
|
||||
.tools(toolObjects)
|
||||
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
|
||||
.call()
|
||||
.content()
|
||||
);
|
||||
}
|
||||
|
||||
// ========== 结构化输出 ==========
|
||||
|
||||
/** 直接返回 Java 对象,AI 自动填充字段(带自动重试) */
|
||||
public <T> T askForObject(String question, Class<T> responseType) {
|
||||
return retryTemplate.execute(context ->
|
||||
chatClient.prompt()
|
||||
.system(defaultSystemPrompt)
|
||||
.user(question)
|
||||
.call()
|
||||
.entity(responseType)
|
||||
);
|
||||
}
|
||||
|
||||
// ========== 多模态 ==========
|
||||
|
||||
/**
|
||||
* 图片理解:传入图片 URL,让 AI 分析
|
||||
* 注意:需要引入支持多模态的模型(如 GPT-4 Vision)
|
||||
*/
|
||||
public String askWithImage(String question, String imageUrl) {
|
||||
// 使用 ChatClient 的多模态支持
|
||||
// 具体实现取决于使用的模型和 Spring AI 版本
|
||||
return chatClient.prompt()
|
||||
.system(defaultSystemPrompt + "\n用户提供了图片: " + imageUrl)
|
||||
.user(question)
|
||||
.call()
|
||||
.content();
|
||||
}
|
||||
|
||||
// ========== 元数据收集 ==========
|
||||
|
||||
/** 返回统一的 AiResponse(包含 Token 使用量、耗时等元数据,带自动重试) */
|
||||
public AiResponse askWithMetadata(String question) {
|
||||
return retryTemplate.execute(context -> {
|
||||
String requestId = UUID.randomUUID().toString();
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
ChatResponse response = chatClient.prompt()
|
||||
.system(defaultSystemPrompt)
|
||||
.user(question)
|
||||
.call()
|
||||
.chatResponse();
|
||||
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
|
||||
AiResponse aiResponse = new AiResponse();
|
||||
aiResponse.setRequestId(requestId);
|
||||
aiResponse.setContent(response.getResult().getOutput().getText());
|
||||
aiResponse.setDuration(duration);
|
||||
|
||||
// 设置 Token 使用量
|
||||
if (response.getMetadata() != null && response.getMetadata().getUsage() != null) {
|
||||
var usage = response.getMetadata().getUsage();
|
||||
AiResponse.TokenUsage tokenUsage = new AiResponse.TokenUsage(
|
||||
usage.getPromptTokens(),
|
||||
usage.getCompletionTokens(),
|
||||
usage.getTotalTokens()
|
||||
);
|
||||
aiResponse.setTokenUsage(tokenUsage);
|
||||
}
|
||||
|
||||
return aiResponse;
|
||||
} catch (Exception e) {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
AiResponse errorResponse = new AiResponse();
|
||||
errorResponse.setRequestId(requestId);
|
||||
errorResponse.setDuration(duration);
|
||||
|
||||
AiResponse.ErrorInfo errorInfo = new AiResponse.ErrorInfo(
|
||||
"AI_CALL_FAILED",
|
||||
e.getMessage(),
|
||||
e.getClass().getSimpleName()
|
||||
);
|
||||
errorResponse.setError(errorInfo);
|
||||
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ========== 逃生通道:拿底层对象做高级定制 ==========
|
||||
|
||||
/**
|
||||
* 获取底层 ChatClient,用于 starter 未封装的高级场景
|
||||
* 示例:chatClient.prompt().user(...).advisors(...).call()
|
||||
*/
|
||||
public ChatClient getChatClient() {
|
||||
return chatClient;
|
||||
}
|
||||
|
||||
/** 获取底层 ChatModel,用于直接操作模型 */
|
||||
public ChatModel getChatModel() {
|
||||
return chatModel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
com.ccb.fintec.agent.autoconfigure.config.AgentAutoConfiguration
|
||||
com.ccb.fintec.agent.autoconfigure.config.RetryAutoConfiguration
|
||||
com.ccb.fintec.agent.autoconfigure.graph.config.GraphAutoConfiguration
|
||||
com.ccb.fintec.agent.autoconfigure.mcp.McpClientAutoConfiguration
|
||||
@@ -0,0 +1,184 @@
|
||||
package com.ccb.fintec.agent.autoconfigure.aspect;
|
||||
|
||||
import com.ccb.fintec.agent.autoconfigure.config.AiCoreConfigProperties;
|
||||
import com.ccb.fintec.core.exception.AiException;
|
||||
import com.ccb.fintec.core.properties.SafetyProperties;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.Signature;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* SafetyAspect 单元测试
|
||||
*/
|
||||
class SafetyAspectTest {
|
||||
|
||||
private SafetyAspect safetyAspect;
|
||||
|
||||
@Mock
|
||||
private AiCoreConfigProperties properties;
|
||||
|
||||
@Mock
|
||||
private ProceedingJoinPoint joinPoint;
|
||||
|
||||
@Mock
|
||||
private Signature signature;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSafeRequest_ShouldPass() throws Throwable {
|
||||
// 准备测试数据
|
||||
List<String> blockKeywords = Arrays.asList("暴力", "色情", "赌博");
|
||||
AiCoreConfigProperties properties = createProperties(true, blockKeywords);
|
||||
|
||||
safetyAspect = new SafetyAspect(properties);
|
||||
|
||||
when(joinPoint.getArgs()).thenReturn(new Object[]{"这是一个正常的请求"});
|
||||
when(joinPoint.proceed()).thenReturn("正常的响应");
|
||||
when(joinPoint.getSignature()).thenReturn(signature);
|
||||
when(signature.toShortString()).thenReturn("ChatModel.call()");
|
||||
|
||||
// 执行测试
|
||||
Object result = safetyAspect.checkSafety(joinPoint);
|
||||
|
||||
// 验证结果
|
||||
assertEquals("正常的响应", result);
|
||||
verify(joinPoint, times(1)).proceed();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRequestWithBlockedKeyword_ShouldThrowException() throws Throwable {
|
||||
// 准备测试数据
|
||||
List<String> blockKeywords = Arrays.asList("暴力", "色情", "赌博");
|
||||
AiCoreConfigProperties properties = createProperties(true, blockKeywords);
|
||||
|
||||
safetyAspect = new SafetyAspect(properties);
|
||||
|
||||
when(joinPoint.getArgs()).thenReturn(new Object[]{"这个请求包含暴力内容"});
|
||||
when(joinPoint.getSignature()).thenReturn(signature);
|
||||
when(signature.toShortString()).thenReturn("ChatModel.call()");
|
||||
|
||||
// 执行测试并验证异常
|
||||
AiException exception = assertThrows(AiException.class, () -> {
|
||||
safetyAspect.checkSafety(joinPoint);
|
||||
});
|
||||
|
||||
// 验证异常信息
|
||||
assertEquals("SAFETY_VIOLATION", exception.getErrorCode());
|
||||
assertTrue(exception.getMessage().contains("敏感内容"));
|
||||
verify(joinPoint, never()).proceed();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResponseWithBlockedKeyword_ShouldThrowException() throws Throwable {
|
||||
// 准备测试数据
|
||||
List<String> blockKeywords = Arrays.asList("暴力", "色情", "赌博");
|
||||
AiCoreConfigProperties properties = createProperties(true, blockKeywords);
|
||||
|
||||
safetyAspect = new SafetyAspect(properties);
|
||||
|
||||
// 模拟正常的请求
|
||||
when(joinPoint.getArgs()).thenReturn(new Object[]{"正常的请求"});
|
||||
|
||||
// 模拟包含敏感词的响应
|
||||
ChatResponse mockResponse = mock(ChatResponse.class);
|
||||
org.springframework.ai.chat.model.Generation generation = mock(org.springframework.ai.chat.model.Generation.class);
|
||||
org.springframework.ai.chat.messages.AssistantMessage assistantMessage = mock(org.springframework.ai.chat.messages.AssistantMessage.class);
|
||||
|
||||
when(generation.getOutput()).thenReturn(assistantMessage);
|
||||
when(assistantMessage.getText()).thenReturn("这个响应包含色情内容");
|
||||
when(mockResponse.getResult()).thenReturn(generation);
|
||||
when(joinPoint.proceed()).thenReturn(mockResponse);
|
||||
when(joinPoint.getSignature()).thenReturn(signature);
|
||||
when(signature.toShortString()).thenReturn("ChatModel.call()");
|
||||
|
||||
// 执行测试并验证异常
|
||||
AiException exception = assertThrows(AiException.class, () -> {
|
||||
safetyAspect.checkSafety(joinPoint);
|
||||
});
|
||||
|
||||
// 验证异常信息
|
||||
assertEquals("SAFETY_VIOLATION", exception.getErrorCode());
|
||||
assertTrue(exception.getMessage().contains("响应包含敏感内容"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSafetyDisabled_ShouldNotCheck() throws Throwable {
|
||||
// 准备测试数据 - 安全功能禁用
|
||||
List<String> blockKeywords = Arrays.asList("暴力", "色情", "赌博");
|
||||
AiCoreConfigProperties properties = createProperties(false, blockKeywords);
|
||||
|
||||
safetyAspect = new SafetyAspect(properties);
|
||||
|
||||
// 即使包含敏感词,也不应该拦截(因为功能已禁用)
|
||||
// 注意:由于 @ConditionalOnProperty,当 enabled=false 时,bean 不会被创建
|
||||
// 这个测试主要验证配置逻辑
|
||||
assertNotNull(safetyAspect);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCaseInsensitiveMatching() throws Throwable {
|
||||
// 准备测试数据 - 测试大小写不敏感
|
||||
List<String> blockKeywords = Arrays.asList("VIOLENCE", "TEST");
|
||||
AiCoreConfigProperties properties = createProperties(true, blockKeywords);
|
||||
|
||||
safetyAspect = new SafetyAspect(properties);
|
||||
|
||||
when(joinPoint.getArgs()).thenReturn(new Object[]{"这个请求包含violence内容"});
|
||||
when(joinPoint.getSignature()).thenReturn(signature);
|
||||
when(signature.toShortString()).thenReturn("ChatModel.call()");
|
||||
|
||||
// 执行测试并验证异常(应该拦截,因为大小写不敏感)
|
||||
AiException exception = assertThrows(AiException.class, () -> {
|
||||
safetyAspect.checkSafety(joinPoint);
|
||||
});
|
||||
|
||||
assertEquals("SAFETY_VIOLATION", exception.getErrorCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyBlockKeywords_ShouldNotBlock() throws Throwable {
|
||||
// 准备测试数据 - 空的敏感词列表
|
||||
AiCoreConfigProperties properties = createProperties(true, Collections.emptyList());
|
||||
|
||||
safetyAspect = new SafetyAspect(properties);
|
||||
|
||||
when(joinPoint.getArgs()).thenReturn(new Object[]{"包含暴力内容的请求"});
|
||||
when(joinPoint.proceed()).thenReturn("响应内容");
|
||||
when(joinPoint.getSignature()).thenReturn(signature);
|
||||
when(signature.toShortString()).thenReturn("ChatModel.call()");
|
||||
|
||||
// 执行测试
|
||||
Object result = safetyAspect.checkSafety(joinPoint);
|
||||
|
||||
// 验证结果 - 不应该被拦截
|
||||
assertEquals("响应内容", result);
|
||||
verify(joinPoint, times(1)).proceed();
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助方法:创建 AiCoreConfigProperties 对象
|
||||
*/
|
||||
private AiCoreConfigProperties createProperties(boolean safetyEnabled, List<String> blockKeywords) {
|
||||
AiCoreConfigProperties properties = new AiCoreConfigProperties();
|
||||
SafetyProperties safetyProperties = new SafetyProperties();
|
||||
safetyProperties.setEnabled(safetyEnabled);
|
||||
safetyProperties.setBlockKeywords(blockKeywords);
|
||||
properties.setSafety(safetyProperties);
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user