AI Agent 脚手架 —— Service层和Trigger层接口实现

一、前言

前面做了对于各节点的增强,其实现在还缺的只有对前端的透传了,因此在Service层,我们需要提供对话接口,这一节先做这个,后续再做trigger的透传和前端的对接。

这一节我认为更适合面向测试编程,先写出我们想实现的效果,可以看到我们希望包装一个命令实体,然后直接传参获得LLM的回复,其中适配了多模态,通过文本、文件、图片的参数传入直接给到LLM,将处理好的message传回。值得一提的是在我做这个的时候deepseek正好推出了v4.1的首个多模态模型,因此我们甚至不需要再去调api了。

java 复制代码
    @Test
    public void test_handelMessage_02_withImage() throws IOException {
        String agentId = "100002";
        String userId = "yds";
        String sessionId = chatService.createSession(agentId,userId);
        ChatCommandEntity chatCommandEntity = ChatCommandEntity.builder()
                .agentId(agentId)
                .userId(userId)
                .sessionId(sessionId)
                .texts(List.of(new ChatCommandEntity.Content.Text("告诉我这个图片是什么动物,一句话描述。")))
                .files(List.of())
                .inLineDates(List.of(new ChatCommandEntity.Content.InLineData(imageResource.getContentAsByteArray(), MimeTypeUtils.IMAGE_PNG_VALUE)))
                .build();

        List<String> message = chatService.handleMessage(chatCommandEntity);
        log.info("测试结果:{}", JSON.toJSONString(message));

    }

二、Service层

其实这里面很多方法都是复用的,根据参数不同分为不同的方法,比如像三个handleMessage,其实其实本质上最后传进去的参数都是一样,像sessionId这个参数,其实是可以通过其他两个参数创建的,所以只有其他两个参数也可以实现整个方法,而ChatCommandEntity也是一样的,本质上用的还是这几个参数。

java 复制代码
public interface IChatService {

    List<AiAgentConfigTableVO.Agent> queryAiAgentConfigList();

    String createSession(String agentId, String userId);

    List<String> handleMessage(String agentId, String userId, String sessionId, String message);

    List<String> handleMessage(String agentId, String userId, String message);

    Flowable<Event> handleMessageStream(String agentId, String userId, String sessionId, String message);

    List<String> handleMessage(ChatCommandEntity chatCommandEntity);

}

实现方法,其实主要步骤都差不多:

1.获取注册的bean值对象

2.获取Runner

3.启动器参数装填

4.返回LLM信息
这里着重说一下 Session ,首先它的目的就是为了存储短期记忆的,Session在这里同时干了两件事:对话历史(上下文) + 结构化状态(工作记忆)

查看ADK的源码,Session底层是用UUID生成的随机数,这个数是唯一的。

那么你可能会想,为啥都是唯一的了,我还需要把appName和userId传进去才能创建一个Session?

这里的原因在于ADK底层使用Map嵌套存储Session的,也就是appName和userId是作为键使用的,这样的好处就是便于快速定位想要的Session,不需要全表遍历,同时appName、userId提供了 应用/用户 隔离机制,便于校验权限和隔离。
另外这个 Runner 启动器其实有**三个阶段:**第一个阶段是启动器的参数装配,第二个阶段是启动器的运行参数装配,最后是启动器启动。

java 复制代码
@Slf4j
@Service
public class ChatService implements IChatService {

    @Resource
    private DefaultArmoryFactory defaultArmoryFactory;

    @Resource
    private AiAgentAutoConfigProperties aiAgentAutoConfigProperties;

    private final Map<String, String> userSessions = new ConcurrentHashMap<>();

    @Override
    public String createSession(String agentId, String userId) {
        AiAgentRegisterVO aiAgentRegisterVO = defaultArmoryFactory.getAiAgentRegisterVO(agentId);
        if (null == aiAgentRegisterVO) {
            throw new AppException(ResponseCode.E0001.getCode(), ResponseCode.E0002.getInfo());
        }

        String appName = aiAgentRegisterVO.getAppName();
        InMemoryRunner runner = aiAgentRegisterVO.getRunner();

        return userSessions.computeIfAbsent(userId, uid -> {
            Session session = runner.sessionService().createSession(appName, uid)
                    .blockingGet();
            return session.id();
        });
    }

    @Override
    public List<AiAgentConfigTableVO.Agent> queryAiAgentConfigList() {
        Map<String, AiAgentConfigTableVO> tables = aiAgentAutoConfigProperties.getTables();
        List<AiAgentConfigTableVO.Agent> agentList = new ArrayList<>();
        if (null != tables) {
            for (AiAgentConfigTableVO aiAgentConfigTableVO : tables.values()) {
                if (null != aiAgentConfigTableVO.getAgent()) {
                    agentList.add(aiAgentConfigTableVO.getAgent());
                }
            }
        }
        return agentList;
    }

    @Override
    public List<String> handleMessage(String agentId, String userId, String sessionId, String message) {

        //启动Runner-阻塞式
        AiAgentRegisterVO aiAgentRegisterVO = defaultArmoryFactory.getAiAgentRegisterVO(agentId);
        if (null == aiAgentRegisterVO) {
            throw new AppException(ResponseCode.E0001.getCode(), ResponseCode.E0002.getInfo());
        }
        InMemoryRunner runner = aiAgentRegisterVO.getRunner();

        Content userMessage = Content.fromParts(Part.fromText(message));
        Flowable<Event> events = runner.runAsync(userId, sessionId, userMessage);
        List<String> outputs = new ArrayList<>();
        events.blockingForEach(event -> outputs.add(event.stringifyContent()));

        return outputs;
    }

    @Override
    public List<String> handleMessage(String agentId, String userId, String message) {
        //获取sessionId
        AiAgentRegisterVO aiAgentRegisterVO = defaultArmoryFactory.getAiAgentRegisterVO(agentId);
        if (null == aiAgentRegisterVO) {
            throw new AppException(ResponseCode.E0001.getCode(), ResponseCode.E0002.getInfo());
        }
        String sessionId = createSession(agentId, userId);

        //路由到重载方法
        return handleMessage(agentId, userId, sessionId, message);
    }


    @Override
    public List<String> handleMessage(ChatCommandEntity chatCommandEntity) {
        AiAgentRegisterVO aiAgentRegisterVO = defaultArmoryFactory.getAiAgentRegisterVO(chatCommandEntity.getAgentId());
        if (null == aiAgentRegisterVO) {
            throw new AppException(ResponseCode.E0001.getCode(), ResponseCode.E0002.getInfo());
        }

        //多模态信息-Text-File-InLineData
        //Text
        List<Part> parts = new ArrayList<>();
        List<ChatCommandEntity.Content.Text> texts = chatCommandEntity.getTexts();
        if (null != texts && !texts.isEmpty()) {
            for (ChatCommandEntity.Content.Text text : texts) {
                parts.add(Part.fromText(text.getMessage()));
            }
        }
        //File
        List<ChatCommandEntity.Content.File> files = chatCommandEntity.getFiles();
        if (null != files && !files.isEmpty()) {
            for (ChatCommandEntity.Content.File file : files) {
                parts.add(Part.fromUri(file.getFileUri(),file.getMimeType()));
            }
        }
        //InLineData
        List<ChatCommandEntity.Content.InLineData> inLineDates = chatCommandEntity.getInLineDates();
        if (null != inLineDates && !inLineDates.isEmpty()) {
            for (ChatCommandEntity.Content.InLineData inLineData : inLineDates) {
                parts.add(Part.fromBytes(inLineData.getBytes(),inLineData.getMimeType()));
            }
        }

        Content content = Content.builder().role("user").parts(parts).build();

        InMemoryRunner runner = aiAgentRegisterVO.getRunner();
        Flowable<Event> events = runner.runAsync(chatCommandEntity.getUserId(), chatCommandEntity.getSessionId(), content);
        List<String> outputs = new ArrayList<>();
        events.blockingForEach(event -> outputs.add(event.stringifyContent()));

        return outputs;
    }

    @Override
    public Flowable<Event> handleMessageStream(String agentId, String userId, String sessionId, String message) {
        //启动Runner-流式
        AiAgentRegisterVO aiAgentRegisterVO = defaultArmoryFactory.getAiAgentRegisterVO(agentId);
        if (null == aiAgentRegisterVO) {
            throw new AppException(ResponseCode.E0001.getCode(), ResponseCode.E0002.getInfo());
        }
        InMemoryRunner runner = aiAgentRegisterVO.getRunner();

        Content userMessage = Content.fromParts(Part.fromText(message));
        return runner.runAsync(userId, sessionId, userMessage);

    }
}
java 复制代码
/**
 * @author 印东升
 * @description 对话命令,实体对象
 * @create 2026-09-11 11:19
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatCommandEntity {

    private String agentId;

    private String userId;

    private String sessionId;

    private List<Content.Text> texts;
    private List<Content.File> files;
    private List<Content.InLineData> inLineDates;

    @Data
    public static class Content {

        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        public static class Text {
            private String message;
        }

        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        public static class File {
            private String fileUri;
            private String mimeType;
        }

        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        public static class InLineData {
            private byte[] bytes;
            private String mimeType;
        }
    }

    public ChatCommandEntity buildSessionCommand(String agentId, String userId) {
        ChatCommandEntity chatCommandEntity = new ChatCommandEntity();
        chatCommandEntity.setAgentId(agentId);
        chatCommandEntity.setUserId(userId);
        return chatCommandEntity;
    }

    public ChatCommandEntity buildChatCommand(String agentId, String userId, String message) {
        ChatCommandEntity chatCommandEntity = new ChatCommandEntity();
        chatCommandEntity.setAgentId(agentId);
        chatCommandEntity.setUserId(userId);

        List<Content.Text> texts = new ArrayList<>();
        texts.add(new Content.Text(message));
        chatCommandEntity.setTexts(texts);

        return chatCommandEntity;
    }
}

三、Trigger层

其实没有太大难度,就是透传,然后定义一些DTO来传给前端。

java 复制代码
/**
 * @author 印东升
 * @description 智能体服务接口
 * @create 2026-09-15 13:41
 */
public interface IAgentService {

    Response<List<AiAgentConfigResponseDTO>> queryAiAgentConfigList();

    Response<CreateSessionResponseDTO> createSession(CreateSessionRequestDTO requestDTO);

    Response<ChatResponseDTO> chat(ChatRequestDTO requestDTO);

    ResponseBodyEmitter chatStream(ChatRequestDTO requestDTO);

}
java 复制代码
package cn.bugstack.ai.trigger.http;

/**
 * @author 印东升
 * @description
 * @create 2026-09-15 13:53
 */
@RestController
@Slf4j
@RequestMapping("/api/v1/")
@CrossOrigin(origins = "*")
public class AgentServiceController implements IAgentService {

    @Resource
    private IChatService chatService;


    @RequestMapping(value = "query_ai_agent_config_list",method = RequestMethod.GET)
    @Override
    public Response<List<AiAgentConfigResponseDTO>> queryAiAgentConfigList() {
        try {
            log.info("查询智能体配置表");

            List<AiAgentConfigTableVO.Agent> agentConfigs = chatService.queryAiAgentConfigList();
            List<AiAgentConfigResponseDTO> responseDTOS = agentConfigs.stream().map(agentConfig -> {
                AiAgentConfigResponseDTO responseDTO = new AiAgentConfigResponseDTO();
                responseDTO.setAgentId(agentConfig.getAgentId());
                responseDTO.setAgentName(agentConfig.getAgentName());
                responseDTO.setAgentDesc(agentConfig.getAgentDesc());
                return responseDTO;
            }).collect(Collectors.toList());

            return Response.<List<AiAgentConfigResponseDTO>>builder()
                    .code(ResponseCode.SUCCESS.getCode())
                    .info(ResponseCode.SUCCESS.getInfo())
                    .data(responseDTOS)
                    .build();

        } catch (AppException e) {
            log.info("查询智能体配置列表异常", e);
            return Response.<List<AiAgentConfigResponseDTO>>builder()
                    .code(e.getCode())
                    .info(e.getInfo())
                    .build();
        } catch (Exception e) {
            log.info("查询智能体配置列表失败", e);
            return Response.<List<AiAgentConfigResponseDTO>>builder()
                    .code(ResponseCode.UN_ERROR.getCode())
                    .info(ResponseCode.UN_ERROR.getInfo())
                    .build();
        }

    }

    @RequestMapping(value = "create_session",method = RequestMethod.GET)
    @Override
    public Response<CreateSessionResponseDTO> createSession(@RequestBody CreateSessionRequestDTO requestDTO) {
        try {
            log.info("创建会话开始");

            String agentId = requestDTO.getAgentId();
            String userId = requestDTO.getUserId();
            String sessionId = chatService.createSession(agentId, userId);

            CreateSessionResponseDTO createSessionResponseDTO = new CreateSessionResponseDTO();
            createSessionResponseDTO.setSessionId(sessionId);

            return Response.<CreateSessionResponseDTO>builder()
                    .code(ResponseCode.SUCCESS.getCode())
                    .info(ResponseCode.SUCCESS.getInfo())
                    .data(createSessionResponseDTO)
                    .build();

        } catch (AppException e) {
            log.info("创建会话异常", e);
            return Response.<CreateSessionResponseDTO>builder()
                    .code(e.getCode())
                    .info(e.getInfo())
                    .build();
        } catch (Exception e) {
            log.info("创建会话失败", e);
            return Response.<CreateSessionResponseDTO>builder()
                    .code(ResponseCode.UN_ERROR.getCode())
                    .info(ResponseCode.UN_ERROR.getInfo())
                    .build();
        }

    }

    @RequestMapping(value = "chat",method = RequestMethod.POST)
    @Override
    public Response<ChatResponseDTO> chat(@RequestBody ChatRequestDTO requestDTO) {

        try {
            log.info("对话开始");

            String userId = requestDTO.getUserId();
            String agentId = requestDTO.getAgentId();
            String sessionId = requestDTO.getSessionId();
            String message = requestDTO.getMessage();

            List<String> contents = chatService.handleMessage(agentId, userId, sessionId, message);

            ChatResponseDTO chatResponseDTO = new ChatResponseDTO();
            chatResponseDTO.setContent(String.join("/n", contents));

            return Response.<ChatResponseDTO>builder()
                    .code(ResponseCode.SUCCESS.getCode())
                    .info(ResponseCode.SUCCESS.getInfo())
                    .data(chatResponseDTO)
                    .build();

        } catch (AppException e) {
            log.info("对话异常", e);
            return Response.<ChatResponseDTO>builder()
                    .code(e.getCode())
                    .info(e.getInfo())
                    .build();
        } catch (Exception e) {
            log.info("对话失败", e);
            return Response.<ChatResponseDTO>builder()
                    .code(ResponseCode.UN_ERROR.getCode())
                    .info(ResponseCode.UN_ERROR.getInfo())
                    .build();
        }
    }

    @RequestMapping(value = "chat_stream",method = RequestMethod.POST)
    @Override
    public ResponseBodyEmitter chatStream(@RequestBody ChatRequestDTO requestDTO) {
        ResponseBodyEmitter emitter = new ResponseBodyEmitter();
        try {
            log.info("流式对话开始");

            String userId = requestDTO.getUserId();
            String agentId = requestDTO.getAgentId();
            String sessionId = requestDTO.getSessionId();
            String message = requestDTO.getMessage();

            Flowable<Event> eventFlowable = chatService.handleMessageStream(agentId, userId, sessionId, message);
            eventFlowable.subscribe(
                    event -> {
                        try {
                            emitter.send(event.stringifyContent());
                        } catch (Exception e) {
                            log.error("流式对话发送失败", e);
                            emitter.completeWithError(e);
                        }
                    },
                    emitter::completeWithError,
                    emitter::complete
            );

            ChatResponseDTO chatResponseDTO = new ChatResponseDTO();
            chatResponseDTO.setContent(String.join("/n", message));

        } catch (Exception e) {
            log.info("流式对话失败", e);
            emitter.completeWithError(e);
        }
        return emitter;
    }

}

四、对接前端

测试:

相关推荐
能不能静下心来看1 小时前
手搓三种 Agent 范式后,一次翻车让我看穿了它的本质
agent
Java的搬运工1 小时前
GitHub 克隆他人私有仓库:从授权到下载
ai
BD_Marathon2 小时前
测试invoke传递不同的参数类型
ai
小年糕是糕手2 小时前
【AI】中国 AI:从跟随,到并肩
ai·chatgpt·agent·codex·deepseek
DolphinScheduler社区2 小时前
Apache DolphinScheduler 3.4.3 发布!权限安全与稳定性全面增强,调度补火即将上线
开源·agent·海豚调度·大数据工作流调度
一切皆是因缘际会2 小时前
物质计算机:计算即物理,安全即拓扑
人工智能·ai·计算机架构·计算机系统架构
prog_61032 小时前
【笔记】用agent手搓agent(一)
人工智能·llm·大语言模型·agent
程序员无隅2 小时前
Harness Learn Engineering :用初始化、交接记录与恢复验证,让 Coding Agent 换个会话也能接着干
gpt·ai
三声三视2 小时前
一个卡片入场动画我返工 4 次:tri-lottie 规格单落地到 ArkTS 的踩坑记录
人工智能·ai·skillhub·tri-skills·tri-lottie