DeepSeek RAGMCP + Agent智能体项目 —— Agent项目初始化和数据加载模型设计
一、前言从这一节开始就正式进入Agent部分的学习了这一节的内容其实本质上还是后端前端依旧采用的是DDD架构的微服务其中也用到了很多设计模式我大概率会按照前面讲大营销的方式来讲这部分的内容这部分内容的难度还是有的要沉下心来慢慢学。二、库表设计不要小看这一部分拿着sql文件直接创建表确实很爽但是一定要搞清楚每张表在干什么为啥这样设计。但是直接看表结构来搞清楚每张表是很困难的因为这里有总共11张表其中有些表又是关联表直接去看每张表会很混乱所以应该结合系统架构图来看表的设计首先这是系统架构图下面是我自己画的库表和架构的关系图很重要只有搞清楚每张表设计的思路后续写代码才知道自己在干什么否则就是抄代码没有意义。三、项目初始化1.多数据源配置与以往的项目不同我们这次使用了两个数据源一个是mysql一个是向量库pgvector这种多数据源的情况应该在配置类中配置因为Spring Boot默认只自动配置一个数据源第二个需要手动配置这是因为依赖注入数据源会有歧义所以SpringBoot选择这个策略。基于SpringBoot的这个特性在多数据源情况下我们不如直接在配置类中配置所有数据源。Configuration public class DataSourceConfig { Bean(mysqlDataSource) Primary public DataSource mysqlDataSource(Value(${spring.datasource.mysql.driver-class-name}) String driverClassName, Value(${spring.datasource.mysql.url}) String url, Value(${spring.datasource.mysql.username}) String username, Value(${spring.datasource.mysql.password}) String password, Value(${spring.datasource.mysql.hikari.maximum-pool-size:10}) int maximumPoolSize, Value(${spring.datasource.mysql.hikari.minimum-idle:5}) int minimumIdle, Value(${spring.datasource.mysql.hikari.idle-timeout:30000}) long idleTimeout, Value(${spring.datasource.mysql.hikari.connection-timeout:30000}) long connectionTimeout, Value(${spring.datasource.mysql.hikari.max-lifetime:1800000}) long maxLifetime) { // 连接池配置 HikariDataSource dataSource new HikariDataSource(); dataSource.setDriverClassName(driverClassName); dataSource.setJdbcUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); dataSource.setMaximumPoolSize(maximumPoolSize); dataSource.setMinimumIdle(minimumIdle); dataSource.setIdleTimeout(idleTimeout); dataSource.setConnectionTimeout(connectionTimeout); dataSource.setMaxLifetime(maxLifetime); dataSource.setPoolName(MainHikariPool); return dataSource; } Bean(sqlSessionFactory) public SqlSessionFactoryBean sqlSessionFactory(Qualifier(mysqlDataSource) DataSource mysqlDataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(mysqlDataSource); // 设置MyBatis配置文件位置 PathMatchingResourcePatternResolver resolver new PathMatchingResourcePatternResolver(); sqlSessionFactoryBean.setConfigLocation(resolver.getResource(classpath:/mybatis/config/mybatis-config.xml)); // 设置Mapper XML文件位置 sqlSessionFactoryBean.setMapperLocations(resolver.getResources(classpath:/mybatis/mapper/*.xml)); return sqlSessionFactoryBean; } Bean(sqlSessionTemplate) public SqlSessionTemplate sqlSessionTemplate(Qualifier(sqlSessionFactory) SqlSessionFactoryBean sqlSessionFactory) throws Exception { return new SqlSessionTemplate(Objects.requireNonNull(sqlSessionFactory.getObject())); } Bean(pgVectorDataSource) public DataSource pgVectorDataSource(Value(${spring.datasource.pgvector.driver-class-name}) String driverClassName, Value(${spring.datasource.pgvector.url}) String url, Value(${spring.datasource.pgvector.username}) String username, Value(${spring.datasource.pgvector.password}) String password, Value(${spring.datasource.pgvector.hikari.maximum-pool-size:5}) int maximumPoolSize, Value(${spring.datasource.pgvector.hikari.minimum-idle:2}) int minimumIdle, Value(${spring.datasource.pgvector.hikari.idle-timeout:30000}) long idleTimeout, Value(${spring.datasource.pgvector.hikari.connection-timeout:30000}) long connectionTimeout) { // 连接池配置 HikariDataSource dataSource new HikariDataSource(); dataSource.setDriverClassName(driverClassName); dataSource.setJdbcUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); dataSource.setMaximumPoolSize(maximumPoolSize); dataSource.setMinimumIdle(minimumIdle); dataSource.setIdleTimeout(idleTimeout); dataSource.setConnectionTimeout(connectionTimeout); // 确保在启动时连接数据库 dataSource.setInitializationFailTimeout(1); // 设置为1ms如果连接失败则快速失败 dataSource.setConnectionTestQuery(SELECT 1); // 简单的连接测试查询 dataSource.setAutoCommit(true); dataSource.setPoolName(PgVectorHikariPool); return dataSource; } Bean(pgVectorJdbcTemplate) public JdbcTemplate pgVectorJdbcTemplate(Qualifier(pgVectorDataSource) DataSource dataSource) { return new JdbcTemplate(dataSource); } }spring: datasource: mysql: username: root password: 123456 url: jdbc:mysql://192.168.xxx.xxx:13306/ai-agent-station-study?useUnicodetruecharacterEncodingutf8autoReconnecttruezeroDateTimeBehaviorconvertToNullserverTimezoneAsia/ShanghaiuseSSLtrue driver-class-name: com.mysql.cj.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource hikari: pool-name: Retail_HikariCP minimum-idle: 15 #最小空闲连接数量 idle-timeout: 180000 #空闲连接存活最大时间默认60000010分钟 maximum-pool-size: 25 #连接池最大连接数默认是10 auto-commit: true #此属性控制从池返回的连接的默认自动提交行为,默认值true max-lifetime: 1800000 #此属性控制池中连接的最长生命周期值0表示无限生命周期默认1800000即30分钟 connection-timeout: 30000 #数据库连接超时时间,默认30秒即30000 connection-test-query: SELECT 1 pgvector: driver-class-name: org.postgresql.Driver username: postgres password: postgres url: jdbc:postgresql://192.168.xxx.xxx:15432/ai-rag-knowledge type: com.zaxxer.hikari.HikariDataSource hikari: maximum-pool-size: 5 minimum-idle: 2 idle-timeout: 30000 connection-timeout: 300002.补全基础设施层就是把每个表的基本增删改查写出来所以要补充实体类、Dao层Mapper、Mapper的XML文件、仓储四、数据加载模型设计1.需求这里就开始正式写业务代码了这里我们的目标是去实例化用户配置API、对话模型、MCP、顾问角色、提示词具体要实现的流程如下同时回顾一下库表和架构的关系便于后续编写仓储部分2.业务1RootNode类接下来开始正式写代码了。这里我们先装配api因此下面的代码的唯一需求就是装配api。要从整体架构开始看根据架构图装配是要从根节点开始的这里我们使了扳手工程包的用于规范设计模式的代码所以根节点继承了这个扳手工程框架类Service Slf4j public class RootNode extends AbstractArmorySupport { private final MapString, ILoadDataStrategy loadDataStrategyMap; public RootNode(MapString, ILoadDataStrategy loadDataStrategyMap) { this.loadDataStrategyMap loadDataStrategyMap; } Override protected void multiThread(ArmoryCommandEntity requestParameter, DefaultArmoryStrategyFactory.DynamicContext dynamicContext) throws ExecutionException, InterruptedException, TimeoutException { String commandType requestParameter.getCommandType(); ILoadDataStrategy loadDataStrategy loadDataStrategyMap.get(commandType);//装载指定配置客户端、对话模型... loadDataStrategy.loadData(requestParameter,dynamicContext); } Override protected String doApply(ArmoryCommandEntity requestParameter, DefaultArmoryStrategyFactory.DynamicContext dynamicContext) throws Exception { return router(requestParameter, dynamicContext); } Override public StrategyHandlerArmoryCommandEntity, DefaultArmoryStrategyFactory.DynamicContext, String get(ArmoryCommandEntity armoryCommandEntity, DefaultArmoryStrategyFactory.DynamicContext dynamicContext) throws Exception { return null; } }这里不用太在意这个根节点类只需要知道这个类在目前的作用只有一个——装载根据前端传来的装载类型和一个动态上下文这个动态上下文就是一个扩展的数据由于这里是根节点自然应该给上下文一个初始值后续上下文将在调用链中动态修改通过节点传递的方式另外这里依旧采用的Map存储策略实现节点的Bean装载数据策略接口的所有实现类节点都被存在这个Map中key是Bean名val是Bean。Spring的特性详见大营销正因为存储了这些实现节点我们才得以找到前端想让我们加载的内容是想加载客户端还是想加载对话模型。2实现节点类自然的接下来就是去编写加载数据的实现节点当然有两个了一个加载客户端一个加载对话模型这里我们只讲加载客户端Service Slf4j public class AIClientLoadDataStrategy implements ILoadDataStrategy { Resource private IAgentRepository agentRepository; Resource protected ThreadPoolExecutor threadPoolExecutor; Override public void loadData(ArmoryCommandEntity requestParam, DefaultArmoryStrategyFactory.DynamicContext DynamicContext) { ListString clientIdList requestParam.getCommandIdList(); CompletableFuture.supplyAsync(() - { log.info(xx); return agentRepository.queryAiClientApiVOListByClientIds(clientIdList); }, threadPoolExecutor); } }显然的我们要从数据库中去找客户端的apiai_client_api表但是目前我们的参数是希望加载的客户端列表ids所以就要去让仓储处理这个事情了而仓储中就应该按照我画的库表-架构关系图来调用Dao层了。因此代码如下看着有点复杂其实就只是调用了三次Dao每次判空最终实现由ai_client找到ai_client_apiOverride public ListAiClientApiVO queryAiClientApiVOListByClientIds(ListString clientIdList) { if (clientIdList null || clientIdList.isEmpty()) { return Collections.emptyList(); } ListAiClientApiVO result new ArrayList(); for (String clientId : clientIdList) { ListAiClientConfig configs aiClientConfigDao.queryBySourceTypeAndId(AI_CLIENT.getCode(), clientId); for (AiClientConfig config : configs) { if (AI_CLIENT_MODEL.getCode().equals(config.getTargetType()) config.getStatus() 1) { String modelId config.getTargetId(); AiClientModel model aiClientModelDao.queryByModelId(modelId); if (model ! null model.getStatus() 1) { String apiId model.getApiId(); AiClientApi apiConfig aiClientApiDao.queryByApiId(apiId); if (apiConfig ! null apiConfig.getStatus() 1) { AiClientApiVO apiVo AiClientApiVO.builder() .apiId(apiConfig.getApiId()) .baseUrl(apiConfig.getBaseUrl()) .apiKey(apiConfig.getApiKey()) .completionsPath(apiConfig.getCompletionsPath()) .embeddingsPath(apiConfig.getEmbeddingsPath()) .build(); if (result.stream().noneMatch(vo - vo.getApiId().equals(apiVo.getApiId()))) { result.add(apiVo); } } } } } } return result; }当然在load方法中肯定不止要加载api这一个数据还有很多只是这里不写了3动态上下文还记得我们刚刚提到了dynamicContext这个上下文静态类我们放在了工厂中这是常见的实现类分类的手段区分是什么的上下文Service public class DefaultArmoryStrategyFactory { Data Builder AllArgsConstructor NoArgsConstructor public static class DynamicContext{ private MapString,Object dataObject new HashMap(); public T void setValue(String key,T value){ dataObject.put(key,value); } public T T getValue(String key){ return (T)dataObject.get(key); } } }五、补充显然的尽管刚刚我提到了RootNode这个类在目前的作用只有一个——装载根据前端传来的装载类型和一个上下文但是我们知道节点嘛肯定是要传递的根节点后面肯定要继续传递请求所以RootNode类中的doApply()以及get()其实是doapply()调用了get()都是用来进入下一个节点的但是由于我们还没有写其他逻辑所以这里get()中返回的是null自然也就暂时没有用。