中文资讯 掘金 AI Sun, 04 Oct 2026 正文 ok 447 words

把 AgentScope Harness 装进 RuoYi-Vue-Plus:纯 Java AI 平台的集成实践

英文标题:把 AgentScope Harness 装进 RuoYi-Vue-Plus:纯 Java AI 平台的集成实践

装配期白名单裁剪、Redis 状态存储、技能仓库只读接入、MCP 双向与工具名归一化、会话级资源集,以及四个真实踩坑与规避。
打开原文相关 3 · 机会 1
media3.2 KB media589 B media8.7 KB

中文

前两篇讲了「是什么」和「权限怎么落地」。这一篇讲工程:智能体内核怎么与一个成熟的 Java 中台对接,以及我们踩过的坑。

一、目标:让中台「长出」智能体内核

我们不想要一个独立的 Agent 服务,再让业务系统去调它。目标是把智能体能力做成中台的一个模块 :

  • 一个进程、一个 jar、一套构建;
  • 复用底座的权限、事务、缓存、审计;
  • 前端仍是一个 Vue 工程,通过 SSE 拿流式回答。

落点就是 ruoyi-modules/ruoyi-ai,包 org.dromara.ai。

二、依赖与版本

分类 组件 版本
语言 / 运行时 Java 21
业务框架 Spring Boot 4.1.1(Jetty)
智能体内核 AgentScope Harness 2.0.3
MCP 官方 MCP Java SDK 0.17.2
状态存储 Redis(经 Redisson) AgentScope RedisAgentStateStore
技能仓库 PostgreSQL AgentScope PostgresSkillRepository
向量库 Milvus v2.6.13
权限 Sa-Token 1.46.0

三、装配一个 HarnessAgent

装配集中在 AgentRegistry。它的职责是:按「智能体定义 × 会话资源集」把模型、工具、权限、中间件、策略拼成一个可复用的 HarnessAgent 实例,并按指纹缓存——定义或资源组合变化时自动重建,避免每轮对话都新建模型 HTTP 客户端 。

装配主干大致是这样:

HarnessAgent.Builder builder = HarnessAgent.builder()
    .name(definition.getAgentCode())
    .description(...)
    .sysPrompt(...)                     
    .model(model)                       
    .toolkit(toolkit)                   
    .stateStore(stateStore)             
    .workspace(workspace)               
    .maxIters(maxIters)                 
    .toolsConfig(buildToolsConfig(...)) 
    .permissionContext(...)             
    .middlewares(buildMiddlewares(definition));


builder.disableFilesystemTools().disableShellTool().disableMemoryTools();

builder.skillRepositories(List.of(aiSkillRepository))
       .skillFilter(SkillFilter.only(skillNames));
builder.disableDefaultWorkspaceSkills();

几个值得展开的点。

3.1 工具白名单裁剪(allow / deny)

框架的默认工具箱会附带联网检索、文件、命令等内置工具。企业平台没有沙箱 ,所以必须双重收敛:

private ToolsConfig buildToolsConfig(List<String> registeredCodes, List<String> skillNames,
                                     McpClientAssembly mcpAssembly) {
    List<String> allow = new ArrayList<>(registeredCodes);
    if (!skillNames.isEmpty()) {
        allow.addAll(SKILL_BUILTIN_TOOLS);        
    }
    if (!mcpAssembly.toolNames().isEmpty()) {
        allow.addAll(mcpAssembly.toolNames());    
    }
    ToolsConfig config = new ToolsConfig();
    config.setAllow(allow);                       
    config.setDeny(List.of("web_search", "web_fetch"));  
    return config;
}

教训:技能内置工具、MCP 工具都必须显式放进 allow 名单 ,否则会被框架的 ToolFilter 直接裁掉——模型看不到,你还找不到原因。

3.2 中间件注入

平台级中间件对所有智能体自动生效:

  • CurrentTimeMiddleware:每轮现算当前时间(日期 + 星期 + 时分 + 时区)注入 system prompt。模型自己不知道「今天几号」,框架原生注入的又是英文日期。
  • ExpertRosterMiddleware:只对入口智能体注入「本轮候选专家清单」。
  • SlotInheritanceMiddleware:只对入口智能体注入「槽位继承」。

中间件的好处是每轮现算、不进装配指纹 ——清单变了不必重建 Agent 实例。

四、状态与持久化

4.1 会话状态:Redis

多轮记忆、暂停恢复都依赖状态存储。我们直接复用项目已有的 RedissonClient:

@Bean
public AgentStateStore aiAgentStateStore(RedissonClient redissonClient) {
    return RedisAgentStateStore.builder()
        .redissonClient(redissonClient)
        .keyPrefix("bizbuddy:ai:agentscope")
        .build();
}

用的是 RedisAgentStateStore 而非已 @Deprecated 的 RedissonAgentStateStore,两者键布局一致,切换无需数据迁移 。

4.2 技能仓库:复用业务表

技能的建表与写入由项目自己的 SQL 与服务负责(保留审计列与生效范围治理),框架侧只读:

return PostgresSkillRepository.builder(dataSource)
    .schemaName("public")
    .skillsTableName("ai_skill")
    .resourcesTableName("ai_skill_resource")
    .createIfNotExist(false)   
    .writeable(false)          
    .build();

五、工具接入:业务工具工厂

平台的工具注册表 ai_tool 只存元数据 (编码、名称、描述、参数 Schema、类型、实现标识),真正的实现必须存在于代码侧:

toolFactory.create(definition, kbIds).ifPresent(toolkit::registerAgentTool);

这道设计有意为之:避免出现「裸 SQL / 裸 Shell / 裸 HTTP」的万能工具 。目前代码侧登记了 25 个工具实现,覆盖部门 / 用户 / 公告 / 角色 / 菜单权限 / 知识库检索等,其中写入类工具统一走 HITL。

六、MCP 双向集成

6.1 出口:把只读工具暴露出去

平台自建 /mcp 服务端(不依赖 spring-ai),可把标记为「对外暴露」的只读 工具提供给外部调用方,并支持访问口令校验(Authorization: Bearer <token> 或 X-MCP-Token)。

6.2 接入:外部 MCP 工具

接入外部 MCP Server 时,我们没有 走框架的 ToolsConfig.mcpServers,而是自行注册「归一化名装饰后」的客户端:

toolkit.registerMcpClient(client).block(Duration.ofSeconds(20));

原因:远端工具名可能不满足 LLM 的 function name 规范(例如 weather.search_local 带点号),在严格校验的模型上会整轮 400 。平台因此在装配期做工具名归一化 :合法名原样保留,非法字符替换为下划线,撞名时追加原名哈希后缀。这样 weather.search_local 会以 weather_search_local 注册,模型即可正常调用。

另外,框架的 McpClientManager 不负责关闭客户端 ,所以注册失败时必须由装配方 close(),否则连接泄漏。

七、装配单元:从「智能体 × 场景包」到「智能体 × 资源集签名」

入口智能体「小Z」不绑定业务工具 ,它的工具来自会话级资源集 (对话底栏 + 选择:专家 / 场景包 / 工具 / 技能 / MCP 工具)。

由于工具集是装配期 决定的(allow 名单在装配时固定),运行期没有等价的「工具可见面覆盖」通道。所以资源集被压成确定性签名 ,直接进装配缓存键:

小Z   : agentId 
专家  : agentId 

好处是完全复用既有的装配与权限机制 ,改选择后下一轮即重新装配生效;代价是装配实例数随「资源组合种类」增长(同组合的多个会话共享实例)。

八、踩坑记录

坑 1:状态版本 CAS 与实例复用冲突

框架的 AgentState 走版本化 CAS (saveIfVersion → getVersioned)。当某个实例先服务过某会话、随后该会话状态被另一实例推进时,再复用这个实例会反序列化失败(Failed to get versioned state: agent_state)。

规避方式:为每个会话记录「最近一次装配用的资源集签名」,会话内切换资源组合时主动丢弃目标实例 ,下次新建即可。

坑 2:MCP 工具名不合规导致整轮 400

见 §6.2。归一化是同款问题的通用解法。

坑 3:框架不释放 MCP 连接

见 §6.2。注册失败路径必须自行关闭。

坑 4:「我配了 MCP 客户端,为什么小Z 还是不知道天气?」

这是一个被真实用户报上来的问题,很典型。用户已经在「MCP 客户端」页配好了天气工具,于是认为小Z 应该会用。排查后发现:

  • 那两个报「不知道」的会话,ai_session_resource 里一行资源都没有 ;
  • 装配日志显示 mcpTools=[]、空集签名;
  • 而更早一个会话(当时通过底栏 + 选中了该 MCP 工具)确实成功调用过天气工具。

结论:MCP 工具是「会话级」的。 「MCP 客户端」页的配置只是让工具可被选中 ,新会话默认是空集,必须在底栏 + 里勾选。这是平台既定的会话级设计,不是缺陷。

这个坑值得所有做企业 Agent 的团队注意:「配置好了」和「对本轮可用」是两件事 ,产品上要用 UI 把这个区别讲清楚,否则用户会认为工具坏了。

九、小结

把 Harness 装进中台,核心就三件事:

  1. 装配要可控 :allow / deny 白名单 + 装配期权限规则,工具可见面在装配时定死;
  2. 状态要持久 :Redis 状态存储承载多轮记忆与 HITL 暂停恢复;
  3. 边界要显式 :技能工具、MCP 工具、技能仓库都要显式接入,框架不会替你猜。

再叠加前两篇讲的「三层权限」,一个纯 Java 的企业级 Agent 平台就立起来了。


相关仓库

作者:AI架构师张磊

English / 原文

前两篇讲了「是什么」和「权限怎么落地」。这一篇讲工程:智能体内核怎么与一个成熟的 Java 中台对接,以及我们踩过的坑。

一、目标:让中台「长出」智能体内核

我们不想要一个独立的 Agent 服务,再让业务系统去调它。目标是把智能体能力做成中台的一个模块 :

  • 一个进程、一个 jar、一套构建;
  • 复用底座的权限、事务、缓存、审计;
  • 前端仍是一个 Vue 工程,通过 SSE 拿流式回答。

落点就是 ruoyi-modules/ruoyi-ai,包 org.dromara.ai。

二、依赖与版本

分类 组件 版本
语言 / 运行时 Java 21
业务框架 Spring Boot 4.1.1(Jetty)
智能体内核 AgentScope Harness 2.0.3
MCP 官方 MCP Java SDK 0.17.2
状态存储 Redis(经 Redisson) AgentScope RedisAgentStateStore
技能仓库 PostgreSQL AgentScope PostgresSkillRepository
向量库 Milvus v2.6.13
权限 Sa-Token 1.46.0

三、装配一个 HarnessAgent

装配集中在 AgentRegistry。它的职责是:按「智能体定义 × 会话资源集」把模型、工具、权限、中间件、策略拼成一个可复用的 HarnessAgent 实例,并按指纹缓存——定义或资源组合变化时自动重建,避免每轮对话都新建模型 HTTP 客户端 。

装配主干大致是这样:

HarnessAgent.Builder builder = HarnessAgent.builder()
    .name(definition.getAgentCode())
    .description(...)
    .sysPrompt(...)                     
    .model(model)                       
    .toolkit(toolkit)                   
    .stateStore(stateStore)             
    .workspace(workspace)               
    .maxIters(maxIters)                 
    .toolsConfig(buildToolsConfig(...)) 
    .permissionContext(...)             
    .middlewares(buildMiddlewares(definition));


builder.disableFilesystemTools().disableShellTool().disableMemoryTools();

builder.skillRepositories(List.of(aiSkillRepository))
       .skillFilter(SkillFilter.only(skillNames));
builder.disableDefaultWorkspaceSkills();

几个值得展开的点。

3.1 工具白名单裁剪(allow / deny)

框架的默认工具箱会附带联网检索、文件、命令等内置工具。企业平台没有沙箱 ,所以必须双重收敛:

private ToolsConfig buildToolsConfig(List<String> registeredCodes, List<String> skillNames,
                                     McpClientAssembly mcpAssembly) {
    List<String> allow = new ArrayList<>(registeredCodes);
    if (!skillNames.isEmpty()) {
        allow.addAll(SKILL_BUILTIN_TOOLS);        
    }
    if (!mcpAssembly.toolNames().isEmpty()) {
        allow.addAll(mcpAssembly.toolNames());    
    }
    ToolsConfig config = new ToolsConfig();
    config.setAllow(allow);                       
    config.setDeny(List.of("web_search", "web_fetch"));  
    return config;
}

教训:技能内置工具、MCP 工具都必须显式放进 allow 名单 ,否则会被框架的 ToolFilter 直接裁掉——模型看不到,你还找不到原因。

3.2 中间件注入

平台级中间件对所有智能体自动生效:

  • CurrentTimeMiddleware:每轮现算当前时间(日期 + 星期 + 时分 + 时区)注入 system prompt。模型自己不知道「今天几号」,框架原生注入的又是英文日期。
  • ExpertRosterMiddleware:只对入口智能体注入「本轮候选专家清单」。
  • SlotInheritanceMiddleware:只对入口智能体注入「槽位继承」。

中间件的好处是每轮现算、不进装配指纹 ——清单变了不必重建 Agent 实例。

四、状态与持久化

4.1 会话状态:Redis

多轮记忆、暂停恢复都依赖状态存储。我们直接复用项目已有的 RedissonClient:

@Bean
public AgentStateStore aiAgentStateStore(RedissonClient redissonClient) {
    return RedisAgentStateStore.builder()
        .redissonClient(redissonClient)
        .keyPrefix("bizbuddy:ai:agentscope")
        .build();
}

用的是 RedisAgentStateStore 而非已 @Deprecated 的 RedissonAgentStateStore,两者键布局一致,切换无需数据迁移 。

4.2 技能仓库:复用业务表

技能的建表与写入由项目自己的 SQL 与服务负责(保留审计列与生效范围治理),框架侧只读:

return PostgresSkillRepository.builder(dataSource)
    .schemaName("public")
    .skillsTableName("ai_skill")
    .resourcesTableName("ai_skill_resource")
    .createIfNotExist(false)   
    .writeable(false)          
    .build();

五、工具接入:业务工具工厂

平台的工具注册表 ai_tool 只存元数据 (编码、名称、描述、参数 Schema、类型、实现标识),真正的实现必须存在于代码侧:

toolFactory.create(definition, kbIds).ifPresent(toolkit::registerAgentTool);

这道设计有意为之:避免出现「裸 SQL / 裸 Shell / 裸 HTTP」的万能工具 。目前代码侧登记了 25 个工具实现,覆盖部门 / 用户 / 公告 / 角色 / 菜单权限 / 知识库检索等,其中写入类工具统一走 HITL。

六、MCP 双向集成

6.1 出口:把只读工具暴露出去

平台自建 /mcp 服务端(不依赖 spring-ai),可把标记为「对外暴露」的只读 工具提供给外部调用方,并支持访问口令校验(Authorization: Bearer <token> 或 X-MCP-Token)。

6.2 接入:外部 MCP 工具

接入外部 MCP Server 时,我们没有 走框架的 ToolsConfig.mcpServers,而是自行注册「归一化名装饰后」的客户端:

toolkit.registerMcpClient(client).block(Duration.ofSeconds(20));

原因:远端工具名可能不满足 LLM 的 function name 规范(例如 weather.search_local 带点号),在严格校验的模型上会整轮 400 。平台因此在装配期做工具名归一化 :合法名原样保留,非法字符替换为下划线,撞名时追加原名哈希后缀。这样 weather.search_local 会以 weather_search_local 注册,模型即可正常调用。

另外,框架的 McpClientManager 不负责关闭客户端 ,所以注册失败时必须由装配方 close(),否则连接泄漏。

七、装配单元:从「智能体 × 场景包」到「智能体 × 资源集签名」

入口智能体「小Z」不绑定业务工具 ,它的工具来自会话级资源集 (对话底栏 + 选择:专家 / 场景包 / 工具 / 技能 / MCP 工具)。

由于工具集是装配期 决定的(allow 名单在装配时固定),运行期没有等价的「工具可见面覆盖」通道。所以资源集被压成确定性签名 ,直接进装配缓存键:

小Z   : agentId 
专家  : agentId 

好处是完全复用既有的装配与权限机制 ,改选择后下一轮即重新装配生效;代价是装配实例数随「资源组合种类」增长(同组合的多个会话共享实例)。

八、踩坑记录

坑 1:状态版本 CAS 与实例复用冲突

框架的 AgentState 走版本化 CAS (saveIfVersion → getVersioned)。当某个实例先服务过某会话、随后该会话状态被另一实例推进时,再复用这个实例会反序列化失败(Failed to get versioned state: agent_state)。

规避方式:为每个会话记录「最近一次装配用的资源集签名」,会话内切换资源组合时主动丢弃目标实例 ,下次新建即可。

坑 2:MCP 工具名不合规导致整轮 400

见 §6.2。归一化是同款问题的通用解法。

坑 3:框架不释放 MCP 连接

见 §6.2。注册失败路径必须自行关闭。

坑 4:「我配了 MCP 客户端,为什么小Z 还是不知道天气?」

这是一个被真实用户报上来的问题,很典型。用户已经在「MCP 客户端」页配好了天气工具,于是认为小Z 应该会用。排查后发现:

  • 那两个报「不知道」的会话,ai_session_resource 里一行资源都没有 ;
  • 装配日志显示 mcpTools=[]、空集签名;
  • 而更早一个会话(当时通过底栏 + 选中了该 MCP 工具)确实成功调用过天气工具。

结论:MCP 工具是「会话级」的。 「MCP 客户端」页的配置只是让工具可被选中 ,新会话默认是空集,必须在底栏 + 里勾选。这是平台既定的会话级设计,不是缺陷。

这个坑值得所有做企业 Agent 的团队注意:「配置好了」和「对本轮可用」是两件事 ,产品上要用 UI 把这个区别讲清楚,否则用户会认为工具坏了。

九、小结

把 Harness 装进中台,核心就三件事:

  1. 装配要可控 :allow / deny 白名单 + 装配期权限规则,工具可见面在装配时定死;
  2. 状态要持久 :Redis 状态存储承载多轮记忆与 HITL 暂停恢复;
  3. 边界要显式 :技能工具、MCP 工具、技能仓库都要显式接入,框架不会替你猜。

再叠加前两篇讲的「三层权限」,一个纯 Java 的企业级 Agent 平台就立起来了。


相关仓库

作者:AI架构师张磊