AI项目博客
记录 AI 工程实践与大模型应用

Spring AI Alibaba 27|Hooks 与 Interceptors · 自定义 Hooks 和 Interceptors:使用 RunnableConfig 跨调用共享数据

RunnableConfig 提供了一个 context() 方法,允许你在同一个执行流程中的多个 Hook 调用、多轮模型或工具调用之间共享数据。这对于实现计数器、累积统计信息或跨多次调用维护状态非常有用。

适用场景:

  • 跟踪模型或工具调用次数;
  • 累积性能指标(总耗时、平均响应时间等);
  • 在 before/after Hook 之间传递临时数据;
  • 实现基于计数的限流或断路器。

示例:使用 RunnableConfig.context() 实现调用计数器

import com.alibaba.cloud.ai.graph.agent.hook.ModelHook;
import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.OverAllState;
import java.util.concurrent.CompletableFuture;
import java.util.Map;

@HookPositions({HookPosition.BEFORE_MODEL, HookPosition.AFTER_MODEL})
public class ModelCallCounterHook extends ModelHook {

  private static final String CALL_COUNT_KEY = "__model_call_count__";
  private static final String TOTAL_TIME_KEY = "__total_model_time__";
  private static final String START_TIME_KEY = "__call_start_time__";

  @Override
  public String getName() {
      return "model_call_counter";
  }

  @Override
  public CompletableFuture<Map<String, Object>> beforeModel(OverAllState state, RunnableConfig config) {
      // 从 context 读取当前计数(如果不存在则默认为 0)
      int currentCount = config.context().containsKey(CALL_COUNT_KEY)
              ? (int) config.context().get(CALL_COUNT_KEY) : 0;

      System.out.println("模型调用 #" + (currentCount + 1));

      // 记录开始时间
      config.context().put(START_TIME_KEY, System.currentTimeMillis());

      return CompletableFuture.completedFuture(Map.of());
  }

  @Override
  public CompletableFuture<Map<String, Object>> afterModel(OverAllState state, RunnableConfig config) {
      // 读取当前计数并递增
      int currentCount = config.context().containsKey(CALL_COUNT_KEY)
              ? (int) config.context().get(CALL_COUNT_KEY) : 0;
      config.context().put(CALL_COUNT_KEY, currentCount + 1);

      // 计算本次调用耗时并累加到总耗时
      if (config.context().containsKey(START_TIME_KEY)) {
          long startTime = (long) config.context().get(START_TIME_KEY);
          long duration = System.currentTimeMillis() - startTime;

          long totalTime = config.context().containsKey(TOTAL_TIME_KEY)
                  ? (long) config.context().get(TOTAL_TIME_KEY) : 0L;
          config.context().put(TOTAL_TIME_KEY, totalTime + duration);

          // 输出统计信息
          int newCount = currentCount + 1;
          long newTotalTime = totalTime + duration;
          System.out.println("模型调用完成: " + duration + "ms");
          System.out.println("累计统计 - 调用次数: " + newCount + ", 总耗时: " + newTotalTime + "ms, 平均: " + (newTotalTime / newCount) + "ms");
      }

      return CompletableFuture.completedFuture(Map.of());
  }
}

示例:基于 context 实现调用次数限制

import com.alibaba.cloud.ai.graph.agent.hook.ModelHook;
import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;
import com.alibaba.cloud.ai.graph.agent.hook.JumpTo;
import org.springframework.ai.chat.messages.AssistantMessage;
import java.util.List;
import java.util.ArrayList;

@HookPositions({HookPosition.BEFORE_MODEL, HookPosition.AFTER_MODEL})
public class ModelCallLimiterHook extends ModelHook {

  private static final String CALL_COUNT_KEY = "__model_call_count__";
  private final int maxCalls;

  public ModelCallLimiterHook(int maxCalls) {
      this.maxCalls = maxCalls;
  }

  @Override
  public String getName() {
      return "model_call_limiter";
  }

  @Override
  public CompletableFuture<Map<String, Object>> beforeModel(OverAllState state, RunnableConfig config) {
      // 读取当前调用次数
      int callCount = config.context().containsKey(CALL_COUNT_KEY)
              ? (int) config.context().get(CALL_COUNT_KEY) : 0;

      // 检查是否超过限制
      if (callCount >= maxCalls) {
          System.out.println("达到模型调用次数限制: " + maxCalls);

          // 添加终止消息
          List<Message> messages = new ArrayList<>(
              (List<Message>) state.value("messages").orElse(new ArrayList<>())
          );
          messages.add(new AssistantMessage(
              "已达到模型调用次数限制 (" + callCount + "/" + maxCalls + "),Agent 执行终止。"
          ));

          // 返回更新并跳转到结束
          return CompletableFuture.completedFuture(Map.of("messages", messages));
      }

      return CompletableFuture.completedFuture(Map.of());
  }

  @Override
  public CompletableFuture<Map<String, Object>> afterModel(OverAllState state, RunnableConfig config) {
      // 递增计数器
      int callCount = config.context().containsKey(CALL_COUNT_KEY)
              ? (int) config.context().get(CALL_COUNT_KEY) : 0;
      config.context().put(CALL_COUNT_KEY, callCount + 1);

      return CompletableFuture.completedFuture(Map.of());
  }

  @Override
  public List<JumpTo> canJumpTo() {
      return List.of(JumpTo.end);
  }
}

使用示例:

ReactAgent agent = ReactAgent.builder()
  .name("limited_agent")
  .model(chatModel)
  .tools(tools)
  .hooks(new ModelCallCounterHook())  // 监控调用统计
  .hooks(new ModelCallLimiterHook(5)) // 限制最多调用 5 次
  .build();

关键要点:

  • context() 是共享的: 同一个执行流程中的所有 Hook 共享同一个 context;
  • 数据持久性: context 中的数据在整个 Agent 执行期间保持有效;
  • 类型安全: 需要自己管理 context 中数据的类型转换;
  • 命名约定: 建议使用双下划线前缀命名 context key(如 __model_call_count__)以避免与用户数据冲突。

使用多个 Hooks 和 Interceptors 时,理解执行顺序很重要:

ReactAgent agent = ReactAgent.builder()
  .name("my_agent")
  .model(chatModel)
  .hooks(hook1, hook2, hook3)
  .interceptors(interceptor1, interceptor2)
  .interceptors(toolInterceptor1, toolInterceptor2)
  .build();

执行流程:

  1. Before Agent Hooks(按顺序):

    hook1.beforeAgent()
    hook2.beforeAgent()
    hook3.beforeAgent()

  2. Agent 循环开始
  3. Before Model Hooks(按顺序):

    hook1.beforeModel()
    hook2.beforeModel()
    hook3.beforeModel()

  4. Model Interceptors(嵌套调用):

    interceptor1 → interceptor2 → 模型调用

  5. After Model Hooks(逆序):

    hook3.afterModel()
    hook2.afterModel()
    hook1.afterModel()

  6. Tool Interceptors(如果有工具调用,嵌套调用):

    toolInterceptor1 → toolInterceptor2 → 工具执行

  7. Agent 循环结束
  8. After Agent Hooks(逆序):

    hook3.afterAgent()
    hook2.afterAgent()
    hook1.afterAgent()

关键规则:

  • before_* hooks: 从第一个到最后一个
  • after_* hooks: 从最后一个到第一个(逆序)
  • Interceptors: 嵌套调用(第一个拦截器包装所有其他的)

本文整理自 Spring AI Alibaba 官方文档《Hooks 与 Interceptors》:https://java2ai.com/docs/frameworks/agent-framework/tutorials/hooks

赞(0)
未经允许不得转载:AI项目博客 » Spring AI Alibaba 27|Hooks 与 Interceptors · 自定义 Hooks 和 Interceptors:使用 RunnableConfig 跨调用共享数据

评论 抢沙发

AI项目博客

专注 AI 工程化落地 —— 覆盖智能体、大模型、Java AI 与 Python 的学习笔记,以及 AI 平台的项目实战记录。

关于本站文章归档

登录

找回密码

注册