一、前言
至此,对于动态决策的基本逻辑我们都写完了,接下来就是完善和写对接前端的接口了。
这里我们要加上流式响应,所以稍微还要修改一下前面的代码。
二、自动装配
首先,我们前面的装配是要自己写代码实现的,这个很麻烦,因为我们知道,既然都要去使用Agent了,那我肯定要装配一个客户端啊,所以这个应该是一个标准的前置动作,即在使用Agent前必须要做的,因此我们将这个写到配置类中,启动服务时自动装配,当然,也不能完全写死,我们允许在配置文件中修改对应的配置(包括但不限于是否自动装配 、装配客户端的ids)。
java
@Data
@ConfigurationProperties(prefix = "spring.ai.agent.auto-config")
public class AiAgentAutoConfigProperties {
/**
* 是否启用AI Agent自动装配
*/
private boolean enabled = false;
/**
* 需要自动装配的客户端ID列表
*/
private List<String> clientIds;
}
java
@Slf4j
@Configuration
@EnableConfigurationProperties(AiAgentAutoConfigProperties.class)
@ConditionalOnProperty(prefix = "spring.ai.agent.auto-config", name = "enabled", havingValue = "true")
public class AiAgentAutoConfiguration implements ApplicationListener<ApplicationReadyEvent> {
@Resource
private AiAgentAutoConfigProperties aiAgentAutoConfigProperties;
@Resource
private DefaultArmoryStrategyFactory defaultArmoryStrategyFactory;
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
try {
log.info("AI Agent 自动装配开始,配置: {}", aiAgentAutoConfigProperties);
// 检查配置是否有效
if (!aiAgentAutoConfigProperties.isEnabled()) {
log.info("AI Agent 自动装配未启用");
return;
}
List<String> clientIds = aiAgentAutoConfigProperties.getClientIds();
if (CollectionUtils.isEmpty(clientIds)) {
log.warn("AI Agent 自动装配配置的客户端ID列表为空");
return;
}
// 解析客户端ID列表(支持逗号分隔的字符串)
List<String> commandIdList;
if (clientIds.size() == 1 && clientIds.get(0).contains(Constants.SPLIT)) {
// 处理逗号分隔的字符串
commandIdList = Arrays.stream(clientIds.get(0).split(Constants.SPLIT))
.map(String::trim)
.filter(id -> !id.isEmpty())
.collect(Collectors.toList());
} else {
commandIdList = clientIds;
}
log.info("开始自动装配AI客户端,客户端ID列表: {}", commandIdList);
// 执行自动装配
StrategyHandler<ArmoryCommandEntity, DefaultArmoryStrategyFactory.DynamicContext, String> armoryStrategyHandler =
defaultArmoryStrategyFactory.armoryStrategyHandler();
String result = armoryStrategyHandler.apply(
ArmoryCommandEntity.builder()
.commandType(AiAgentEnumVO.AI_CLIENT.getCode())
.commandIdList(commandIdList)
.build(),
new DefaultArmoryStrategyFactory.DynamicContext());
log.info("AI Agent 自动装配完成,结果: {}", result);
} catch (Exception e) {
log.error("AI Agent 自动装配失败", e);
}
}
}
bash
ai:
openai:
base-url: https://apis.itedus.cn
api-key: sk-xxxxxxxxxxx
agent:
auto-config:
enabled: true
client-ids: 3101,3102,3103,3104
三、接口对接
1.服务层
请求体:
java
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AutoAgentRequestDTO {
@Serial
private static final long serialVersionUID = 1L;
/**
* Ai智能提ID
*/
private String aiAgentId;
/**
* 用户消息
*/
private String message;
/**
* 会话ID
*/
private String sessionId;
/**
* 最大执行步数
*/
private Integer maxStep;
}
服务接口:在这里去选择策略
java
public interface IAiAgentService {
ResponseBodyEmitter autoAgent(AutoAgentRequestDTO requestDTO, HttpServletResponse response);
}
Controller:
java
@Slf4j
@RestController
@RequestMapping("/api/v1/agent")
@CrossOrigin(origins = "*", allowedHeaders = "*", methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.OPTIONS})
public class AiAgentController implements IAiAgentService {
@Resource(name = "autoAgentExecuteStrategy")
private IExecuteStrategy autoAgentExecuteStrategy;
@Resource
private ThreadPoolExecutor threadPoolExecutor;
@RequestMapping(value = "auto_agent", method = RequestMethod.POST)
public ResponseBodyEmitter autoAgent(@RequestBody AutoAgentRequestDTO request, HttpServletResponse response) {
log.info("AutoAgent流式执行请求开始,请求信息:{}", JSON.toJSONString(request));
try {
// 设置SSE响应头
response.setContentType("text/event-stream");
response.setCharacterEncoding("UTF-8");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Connection", "keep-alive");
// 1. 创建流式输出对象
ResponseBodyEmitter emitter = new ResponseBodyEmitter(Long.MAX_VALUE);
// 2. 构建执行命令实体
ExecuteCommandEntity executeCommandEntity = ExecuteCommandEntity.builder()
.aiAgentId(request.getAiAgentId())
.message(request.getMessage())
.sessionId(request.getSessionId())
.maxStep(request.getMaxStep())
.build();
// 3. 异步执行AutoAgent
threadPoolExecutor.execute(() -> {
try {
autoAgentExecuteStrategy.execute(executeCommandEntity, emitter);
} catch (Exception e) {
log.error("AutoAgent执行异常:{}", e.getMessage(), e);
try {
emitter.send("执行异常:" + e.getMessage());
} catch (Exception ex) {
log.error("发送异常信息失败:{}", ex.getMessage(), ex);
}
} finally {
try {
emitter.complete();
} catch (Exception e) {
log.error("完成流式输出失败:{}", e.getMessage(), e);
}
}
});
return emitter;
} catch (Exception e) {
log.error("AutoAgent请求处理异常:{}", e.getMessage(), e);
ResponseBodyEmitter errorEmitter = new ResponseBodyEmitter();
try {
errorEmitter.send("请求处理异常:" + e.getMessage());
errorEmitter.complete();
} catch (Exception ex) {
log.error("发送错误信息失败:{}", ex.getMessage(), ex);
}
return errorEmitter;
}
}
}
这里单独把这一步拿出来说一下,ResponseBodyEmitter 相当于是一个后端往前端持续写数据的通道。是SpringMVC里面提供的一个响应对象,可以允许响应长连接,进而实现流式响应的能力,说白了就是保持连接,后端传一段消息过来就在前端展示一段,类似TCP但是单向。
java
// 1. 创建流式输出对象
ResponseBodyEmitter emitter = new ResponseBodyEmitter(Long.MAX_VALUE);
在finally模块中关闭连接:
java
emitter.complete();
同时要配置SSE响应头:是为了让前端和中间网络层知道:这个接口要保持连接,并实时接收服务端一段一段推送的数据。
就相当于:
以前的流程:请求 -> 处理 -> 响应
现在的流程:请求 -> 处理一部分 -> 响应一部分 -> 处理一部分 -> 响应一部分 ... -> 结束连接
2.节点层
首先要引入流式响应,因此要在AbstractExecuteSupport中新增一个方法,用于获取在自动配置阶段装填的流式响应发射器对象(因为我们希望每一个分析的步骤都流式响应回前端)。
java
/**
* 通用的SSE结果发送方法
* @param dynamicContext 动态上下文
* @param result 要发送的结果实体
*/
protected void sendSseResult(DefaultAutoAgentExecuteStrategyFactory.DynamicContext dynamicContext,
AutoAgentExecuteResultEntity result) {
try {
ResponseBodyEmitter emitter = dynamicContext.getValue("emitter");
if (emitter != null) {
// 发送SSE格式的数据
String sseData = "data: " + JSON.toJSONString(result) + "\n\n";
emitter.send(sseData);
}
} catch (IOException e) {
log.error("发送SSE结果失败:{}", e.getMessage(), e);
}
}
接下来就是在每个节点的每个步骤都添加上发射器,所有都要添加,这里只展示一部分:
java
if (line.contains("任务状态分析:")) {
sendAnalysisSubResult(dynamicContext, currentSection, sectionContext.toString(), sessionId);
currentSection = "analysis_status";
log.info("\n🎯 任务状态分析:");
continue;
} else if (line.contains("执行历史评估:")) {
sendAnalysisSubResult(dynamicContext, currentSection, sectionContext.toString(), sessionId);
currentSection = "analysis_history";
log.info("\n📈 执行历史评估:");
continue;
}
......
四、测试
