一、本章介绍
为 InMemoryRunner 运行器扩展 Plugin 插件机制,使用户能够在智能体执行流程的不同阶段获取运行上下文,并根据实际需求实现监控、干预、治理等扩展能力。
借助插件机制,可以在关键节点进行拦截处理、日志记录、结果输出、性能监控等操作,从而增强系统的可观测性和可扩展性。同时,这种设计模式也广泛应用于各类框架和中间件之中,本质上都是通过扩展点与回调机制实现能力解耦。
二、流程设计
如图,增强运行体插件配置,可以在各个节点埋入钩子;

Runner 的 Plugin 机制通过回调钩子(Hook)贯穿 AI Agent 运行生命周期的各个阶段,在用户输入、模型调用、工具调用、智能体执行等关键节点触发相应逻辑。这些扩展点为开发者提供了灵活的介入能力,可用于日志记录、性能分析、执行链路调试、权限控制、监控系统对接(如 Prometheus)、请求与响应内容调整(如敏感词过滤)等场景。
从执行流程来看,图中展示的各个阶段节点都能够被 Plugin 感知和采集。也就是说,无论配置的是哪一种 Agent,都可以获取其完整的运行过程信息,包括输入内容、推理过程、工具调用情况以及最终输出结果。更进一步,开发者还可以基于上下文数据实现自定义处理逻辑,例如拦截不符合规范的请求、动态修改参数、记录关键指标,甚至终止后续执行流程,从而实现更精细化的治理与控制能力。
这种设计本质上是一种典型的“事件驱动 + 插件扩展”模式。核心流程保持稳定,通过开放扩展点让业务能力自由生长,既保证了框架的统一性,也赋予了开发者足够的灵活性。
三、工程实现
1. 工程结构
首先,在 agent 智能体模块的 armory 装配目录下,新增 matter 物料模块。随后,将原有的 mcp 以及新扩展的 plugin 统一迁移到该模块中,便于后续集中管理。其中,MyLogPlugin、MyTestPlugin 作为插件示例类,主要用于功能测试和演示。
接着,为了支持 Runner 运行体的插件配置,需要在 AiAgentConfigTableVO 中补充对应的参数信息。之后在 RunnerNode 中读取插件配置,并将其加载到运行体实例化过程中。这样,InMemoryRunner 在执行时就能够具备 Plugin 插件扩展能力。
2. 核心模块
2.1 新增配置
@Data
public class AiAgentConfigTableVO {
.............
@Data
public static class Runner {
private String agentName;
private List<String> pluginNameList;
}
}
}- 在 Model 的 Runner 下,新增加插件配置
pluginNameList。
2.2 加载插件
protected AiAgentRegisterVO doApply(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception {
log.info("Ai Agent 装配操作 - RunnerNode");
AiAgentConfigTableVO aiAgentConfigTableVO = requestParameter.getAiAgentConfigTableVO();
String appName = aiAgentConfigTableVO.getAppName();
AiAgentConfigTableVO.Agent agent = aiAgentConfigTableVO.getAgent();
String agentId = agent.getAgentId();
String agentName = agent.getAgentName();
String agentDesc = agent.getAgentDesc();
InMemoryRunner runner = getRunner(dynamicContext, aiAgentConfigTableVO, appName);
AiAgentRegisterVO aiAgentRegisterVO = AiAgentRegisterVO.builder()
.appName(appName)
.agentId(agentId)
.agentName(agentName)
.agentDesc(agentDesc)
.runner(runner)
.build();
// 注册到 Spring 容器
registerBean(agentId, AiAgentRegisterVO.class, aiAgentRegisterVO);
return aiAgentRegisterVO;
}
@NotNull
private InMemoryRunner getRunner(DefaultArmoryFactory.DynamicContext dynamicContext, AiAgentConfigTableVO aiAgentConfigTableVO, String appName) {
AiAgentConfigTableVO.Module.Runner runnerConfig = aiAgentConfigTableVO.getModule().getRunner();
String agentName = runnerConfig.getAgentName();
if (StringUtils.isBlank(agentName)) {
log.error("runner.agentName is null");
throw new AppException(ResponseCode.ILLEGAL_PARAMETER.getCode(), ResponseCode.ILLEGAL_PARAMETER.getInfo());
}
BaseAgent baseAgent = dynamicContext.getAgentGroup().get(agentName);
List<BasePlugin> plugins;
List<String> pluginNameList = runnerConfig.getPluginNameList();
if (null != pluginNameList && !pluginNameList.isEmpty()) {
plugins = new ArrayList<>();
for (String pluginName : pluginNameList) {
BasePlugin plugin = getBean(pluginName);
plugins.add(plugin);
}
} else {
plugins = ImmutableList.of();
}
return new InMemoryRunner(baseAgent, appName, plugins);
}
@Override
public StrategyHandler<ArmoryCommandEntity, DefaultArmoryFactory.DynamicContext, AiAgentRegisterVO> get(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception {
return defaultStrategyHandler;
}在构建
InMemoryRunner运行体时,需要先判断runnerConfig中是否配置了plugins。如果未配置插件,则使用ImmutableList.of()创建一个空集合,避免后续出现空指针问题。当存在
pluginNameList配置时,则根据配置的 Bean 名称,从 Spring 容器中获取对应的 Plugin 实例对象,并组装到插件集合中。最后,在实例化InMemoryRunner时,将这些插件统一注入,使其能够在运行过程中参与各个生命周期阶段的处理,实现日志记录、监控埋点、权限校验、结果拦截等扩展能力。
2.3 插件开发
2.3.1 默认插件
@Slf4j
@Service("myLogPlugin")
public class MyLogPlugin extends LoggingPlugin {
}MyLogPlugin继承自LoggingPlugin,其主要作用是通过添加@Service("myLogPlugin")注解,将插件注册到 Spring 容器中,方便后续通过 Bean 名称进行获取和装配。查看
LoggingPlugin的源码实现,了解其内部的配置方式和执行逻辑。通过阅读源码,可以更加清晰地理解 Plugin 机制在各个生命周期阶段是如何进行日志记录、事件监听以及运行状态追踪的。
2.3.2 扩展插件
@Slf4j
@Service("myTestPlugin")
public class MyTestPlugin extends BasePlugin {
public MyTestPlugin(String name) {
super(name);
}
public MyTestPlugin() {
super("MyTestPlugin");
}
@Override
public Maybe<Content> onUserMessageCallback(InvocationContext invocationContext, Content userMessage) {
log.info("用户输入信息:{}", userMessage.text());
return super.onUserMessageCallback(invocationContext, userMessage);
}
@Override
public Maybe<Content> beforeAgentCallback(BaseAgent agent, CallbackContext callbackContext) {
String agentName = agent.name();
log.info("智能体名称:{}", agentName);
return super.beforeAgentCallback(agent, callbackContext);
}
@Override
public Maybe<LlmResponse> beforeModelCallback(CallbackContext callbackContext, LlmRequest llmRequest) {
Optional<String> model = llmRequest.model();
log.info("ai 模型:{}", model.orElse(""));
return super.beforeModelCallback(callbackContext, llmRequest);
}
}MyTestPlugin是一个用于测试的自定义插件实现。在开发时需要注意配置super("MyTestPlugin"),用于指定当前插件的名称,便于在运行过程中识别和区分不同插件。在插件中实现了几个回调节点,并在对应位置打印了日志信息。读者可以通过 Debug 调试的方式观察这些回调方法的触发时机,以及上下文中携带的具体数据内容,从而更直观地理解 Plugin 在 Runner 生命周期中的执行过程。
四、测试验证
1. yml 配置
ai:
agent:
config:
tables:
testAgent03:
app-name: testAgent03
agent:
agent-id: 100003
agent-name: 单一智能体
agent-desc: 单一智能体
module:
ai-api:
base-url: https://api1.oaipro.com
api-key: sk-FtZfc3Lyr2RjMkT1TDMTuY73t2NDvyU5dDgqjt4ZqwdMAdNk3xAW
completions-path: v1/chat/completions
embeddings-path: v1/embeddings
chat-model:
model: gpt-4.1
tool-mcp-list:
- sse:
name: baidu-search
base-uri: http://appbuilder.baidu.com/v2/ai_search/mcp/
sse-endpoint: sse?api_key=bce-v3/ALTAK-6SyOtHHftyPsWLoIpz3WL/e7a0d8c2e39e9596b0f1e183cd985da1c32a0d67
request-timeout: 50000
- local:
name: myToolCallbackProvider
agents:
- name: onlyAgent
description: Spring Boot重磅学习计划
instruction: |
通过百度检索Spring Boot编程实战项目,并根据检索内容,针对初学用户给出学习计划。
runner:
agent-name: onlyAgent
plugin-name-list:
- myTestPlugin
- myLogPlugin- 在 runner 运行体中,配置插件列表,这里配置了2个插件的 bean 的名称。
2. 单测验证
@Test
public void test_handlerMessage_04(){
AiAgentRegisterVO aiAgentRegisterVO = applicationContext.getBean("100003", AiAgentRegisterVO.class);
String appName = aiAgentRegisterVO.getAppName();
InMemoryRunner runner = aiAgentRegisterVO.getRunner();
Session session = runner.sessionService()
.createSession(appName, "cactusli")
.blockingGet();
Content userMsg = Content.fromParts(Part.fromText("把cactusli转换为大写"));
Flowable<Event> events = runner.runAsync("cactusli", session.id(), userMsg);
List<String> outputs = new ArrayList<>();
events.blockingForEach(event -> outputs.add(event.stringifyContent()));
log.info("测试结果:{}", JSON.toJSONString(outputs));
}26-06-05.17:10:30.347 [main ] INFO MyTestPlugin - 用户输入信息:把cactusli转换为大写
26-06-05.17:10:30.348 [main ] INFO LoggingPlugin - [logging_plugin] 🚀 USER MESSAGE RECEIVED
26-06-05.17:10:30.349 [main ] INFO LoggingPlugin - [logging_plugin] Invocation ID: e-8009855e-fe5e-4dfa-8b32-f1d7dfe37a71
26-06-05.17:10:30.349 [main ] INFO LoggingPlugin - [logging_plugin] Session ID: 16d287e3-85db-4eca-a053-be8a5478ca46
26-06-05.17:10:30.349 [main ] INFO LoggingPlugin - [logging_plugin] User ID: cactusli
26-06-05.17:10:30.349 [main ] INFO LoggingPlugin - [logging_plugin] App Name: testAgent03
26-06-05.17:10:30.349 [main ] INFO LoggingPlugin - [logging_plugin] Root Agent: onlyAgent
26-06-05.17:10:30.349 [main ] INFO LoggingPlugin - [logging_plugin] User Content: 把cactusli转换为大写
26-06-05.17:10:30.354 [main ] INFO LoggingPlugin - [logging_plugin] 🏃 INVOCATION STARTING
26-06-05.17:10:30.354 [main ] INFO LoggingPlugin - [logging_plugin] Invocation ID: e-8009855e-fe5e-4dfa-8b32-f1d7dfe37a71
26-06-05.17:10:30.354 [main ] INFO LoggingPlugin - [logging_plugin] Starting Agent: onlyAgent
26-06-05.17:10:30.358 [main ] INFO MyTestPlugin - 智能体名称:onlyAgent
26-06-05.17:10:30.358 [main ] INFO LoggingPlugin - [logging_plugin] 🤖 AGENT STARTING
26-06-05.17:10:30.358 [main ] INFO LoggingPlugin - [logging_plugin] Agent Name: onlyAgent
26-06-05.17:10:30.358 [main ] INFO LoggingPlugin - [logging_plugin] Invocation ID: e-8009855e-fe5e-4dfa-8b32-f1d7dfe37a71
26-06-05.17:10:30.380 [main ] INFO MyTestPlugin - ai 模型:openai
26-06-05.17:10:30.380 [main ] INFO LoggingPlugin - [logging_plugin] 🧠 LLM REQUEST
26-06-05.17:10:30.380 [main ] INFO LoggingPlugin - [logging_plugin] Model: openai
26-06-05.17:10:30.380 [main ] INFO LoggingPlugin - [logging_plugin] Agent: onlyAgent
26-06-05.17:10:30.381 [main ] INFO LoggingPlugin - [logging_plugin] System Instruction: '通过百度检索Spring Boot编程实战项目,并根据检索内容,针对初学用户给出学习计划。
You are an agent. Your internal name is "onlyAgent". The description about you is "Spring Boot重磅学习计划".'
26-06-05.17:10:35.124 [main ] INFO SpringAIObservabilityHandler - Request completed successfully: model=openai, type=chat, duration=4735ms, tokens=2254
26-06-05.17:10:35.127 [main ] INFO LoggingPlugin - [logging_plugin] 🧠 LLM RESPONSE
26-06-05.17:10:35.127 [main ] INFO LoggingPlugin - [logging_plugin] Agent: onlyAgent
26-06-05.17:10:35.127 [main ] INFO LoggingPlugin - [logging_plugin] Content: cactusli 转换为大写是:CACTUSLI
26-06-05.17:10:35.128 [main ] INFO LoggingPlugin - [logging_plugin] Partial: false
26-06-05.17:10:35.128 [main ] INFO LoggingPlugin - [logging_plugin] Turn Complete: true
26-06-05.17:10:35.131 [main ] INFO LoggingPlugin - [logging_plugin] 📢 EVENT YIELDED
26-06-05.17:10:35.131 [main ] INFO LoggingPlugin - [logging_plugin] Event ID: 0dd97dd7-4574-40ff-b50f-c901b80e180f
26-06-05.17:10:35.131 [main ] INFO LoggingPlugin - [logging_plugin] Author: onlyAgent
26-06-05.17:10:35.131 [main ] INFO LoggingPlugin - [logging_plugin] Content: cactusli 转换为大写是:CACTUSLI
26-06-05.17:10:35.131 [main ] INFO LoggingPlugin - [logging_plugin] Final Response: true
26-06-05.17:10:35.133 [main ] INFO LoggingPlugin - [logging_plugin] 🤖 AGENT COMPLETED
26-06-05.17:10:35.133 [main ] INFO LoggingPlugin - [logging_plugin] Agent Name: onlyAgent
26-06-05.17:10:35.133 [main ] INFO LoggingPlugin - [logging_plugin] Invocation ID: e-8009855e-fe5e-4dfa-8b32-f1d7dfe37a71
26-06-05.17:10:35.135 [main ] INFO LoggingPlugin - [logging_plugin] ✅ INVOCATION COMPLETED
26-06-05.17:10:35.135 [main ] INFO LoggingPlugin - [logging_plugin] Invocation ID: e-8009855e-fe5e-4dfa-8b32-f1d7dfe37a71
26-06-05.17:10:35.135 [main ] INFO LoggingPlugin - [logging_plugin] Final Agent: onlyAgent
26-06-05.17:10:35.136 [main ] INFO AiAgentAutoConfigTest - 测试结果:["cactusli 转换为大写是:CACTUSLI"]