Initial commit: Fintec AI Framework with Agent, RAG, and MCP modules

This commit is contained in:
limqsh
2026-04-27 17:23:58 +08:00
parent a9a1441537
commit 69c5aacdc8
85 changed files with 7143 additions and 0 deletions

Binary file not shown.

View 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>

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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]);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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 定义
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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;
}
}