一站式讲清Spring AI Alibaba的OverAllState和RunnableConfig

引言

在 Spring AI Alibaba(以及更广泛的 Spring AI 和 LangChain 生态系统)中,RunnableConfigOverallState是构建Agent(智能体)或Workflow(工作流)时的两个核心概念,那它们到底有什么区别呢?

RunnableConfig (运行时配置)

RunnableConfig 是一个配置对象,用于在执行链(Chain)或 Agent 的各个步骤之间传递运行时的元数据和控制参数。它类似于一个"背包",伴随着请求在整个系统中流动,但它不包含业务数据,只包含"如何执行"的指令与参数。

核心作用:

  1. 控制递归与循环 (Recursion Limit)

    • 在 Agent 循环(例如:思考 -> 工具 -> 思考)中,为了防止死循环,RunnableConfig 可以设置最大递归次数(例如 25 次)。如果超过这个次数,系统会强制停止并报错。
  2. 回调机制 (Callbacks)

    • 它携带了 CallbackManager。你可以通过它挂载监听器,监控 Token 消耗、流式输出(Streaming)、或者在某个步骤开始/结束时触发日志。
  3. 元数据与追踪 (Metadata & Tags)

    • 你可以给当前的执行请求打上 tags(标签)或 metadata(元数据)。这对于链路追踪(如接入 LangSmith 或 Zipkin)非常重要。例如,标记 userId: 123environment: production
  4. 并发控制与超时

    • 在某些实现中,它可以携带取消信号(Cancellation Signals)或超时设置

关键理解

你可以将RunnableConfig理解为"显式的、跨线程安全的 ThreadLocal"。

在 Spring AI / LangChain 的架构中,RunnableConfig 起到了同样的作用:

  • 隐式传递:虽然它作为参数传递,但在 Agent 的内部组件(如各个 Tool、各个 Chain 节点)之间,它往往是作为一个"环境上下文"存在的。
  • 存放"带外数据" (Out-of-band Data) :它专门设计了一个 metadata 字段(是一个 Map),允许你往里面塞一些非业务逻辑核心、但全链路都需要的数据。

异步与跨线程安全性 (这是最大的区别)

  • ThreadLocal : 它是绑定在特定线程 上的。Spring AI 底层(特别是在流式输出 Streaming 或调用大模型 API 时)经常是异步的 (使用 WebFluxCompletableFuture)。一旦代码切换了线程(例如从 Tomcat 线程切换到 Netty I/O 线程),ThreadLocal 里的数据就会丢失,除非你做非常复杂的 Context 复制。
  • RunnableConfig : 它是一个普通的 Java 对象 。当 AI 任务在不同的线程、不同的 Reactor 流之间传递时,这个对象会作为一个参数一直跟着走。无论线程怎么切,数据都在那里,不会丢。

作用域清晰 (Scope)

  • ThreadLocal : 如果忘记 remove(),容易造成内存泄漏(Memory Leak),或者在线程池复用时造成数据污染(上一个请求的数据留到了下一个请求)。
  • RunnableConfig : 它的生命周期严格绑定在单次执行链(Run)上。也就是 agent.invoke() 结束,这个 Config 对象也就完成了使命,被 GC 回收,非常干净

示例

下面是Spring AI Alibaba的一段示例

  1. toolContext.getContext() 中取出 RunnableConfig

  2. 从中读取 metadata("user_id")

  3. 根据user_id

    返回不同位置:

    • 用户 ID 为 "1""Florida"
    • 其他 → "San Francisco"
java 复制代码
// 用户位置工具 - 使用上下文
public class UserLocationTool implements BiFunction<String, ToolContext, String> {
    @Override
    public String apply(
        @ToolParam(description = "User query") String query,
        ToolContext toolContext) {
        // 从上下文中获取用户信息
        String userId = "";
        if (toolContext != null && toolContext.getContext() != null) {
			RunnableConfig runnableConfig = (RunnableConfig) toolContext.getContext().get(AGENT_CONFIG_CONTEXT_KEY);
			Optional<Object> userIdObjOptional = runnableConfig.metadata("user_id");
			if (userIdObjOptional.isPresent()) {
				userId = (String) userIdObjOptional.get();
			}
		}
		if (userId == null) {
			userId = "1";
		}
        return "1".equals(userId) ? "Florida" : "San Francisco";
    }
}

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

适用场景

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

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

java 复制代码
@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());  
}  
}

OverallState (全局状态/上下文)

OverallState 通常不是 Spring AI 框架内的一个固定类名(不像 RunnableConfig 属于核心接口),它通常是开发者在构建 基于图(Graph-based)状态机(Stateful) 的 Agent 时自定义的一个类。

在 Spring AI Alibaba 的高级应用(如仿照 LangGraph 的模式)中,Agent 的执行过程被视为一系列节点的流转。OverallState 就是在这些节点之间传递的共享内存对象

它一般包含的就是应用的业务数据。

核心作用:

  1. 统一的数据总线

    • 所有的节点(Node)都从 OverallState 读取数据,处理完后将结果写回 OverallState
  2. 记忆管理 (Memory)

    • 它通常包含:

      • input:用户的原始问题。
      • chat_history:对话的历史消息列表。
      • intermediate_steps:Agent 的中间思考过程或工具调用结果。
      • output:最终生成的答案。
  3. 状态流转

    • 在 Spring AI Alibaba 对接 Qwen(通义千问)时,模型生成的回复会更新到 State 中,下一个节点(比如工具执行节点)会读取 State 中的模型指令去执行工具。

工作流

下面我给出一个示例工作流。

  • Start -> 传入初始 OverallState(包含用户问题)。

  • LLM Node -> 读取 State,调用 Qwen 模型,将模型的回复(比如"去调用天气API")写入 State。

  • Tool Node -> 检测到 State 里有调用请求,执行天气 API,将结果"25度"写入 State。

  • LLM Node -> 再次读取 State(包含问题+工具结果),生成最终回答"今天25度",写入 State。

  • End -> 返回 State 中的最终回答。

总结对比

特性 RunnableConfig OverallState (自定义状态)
关注点 过程控制 (Process Control) 业务数据 (Business Data)
内容示例 递归限制、回调函数、元数据、标签 用户问题、LLM 回复、工具结果、数据库查询值
可变性 通常在运行开始时设定,传递过程中较少修改 在每个步骤(Node)中不断被读取和修改(累加信息)
类比 交通规则与信号灯(红绿灯、限速) 货车上的货物(每一站可能会装卸货物)
相关推荐
生成论实验室1 天前
生成论之基:“阴阳”作为元规则的重构与证成——基于《易经》与《道德经》的古典重诠与现代显象
人工智能·科技·神经网络·算法·架构
数据分享者1 天前
对话对齐反馈数据集:12000+高质量人类-助手多轮对话用于RLHF模型训练与评估-人工智能-大语言模型对齐-人类反馈强化学习-训练符合人类期望的对话模型
人工智能·语言模型·自然语言处理
Java后端的Ai之路1 天前
【人工智能领域】- 卷积神经网络(CNN)深度解析
人工智能·神经网络·cnn
_清欢l1 天前
Dify+test2data实现自然语言查询数据库
数据库·人工智能·openai
咕噜签名-铁蛋1 天前
云服务器GPU:释放AI时代的算力引擎
运维·服务器·人工智能
Niuguangshuo1 天前
变分推断:用简单分布逼近复杂世界的艺术
人工智能·机器学习
enjoy编程1 天前
Spring-AI 大模型未来:从“学会世界”到“进入世界”的范式跃迁
人工智能·领域大模型·替换工种·中后训练·长尾场景
沛沛老爹1 天前
深入理解Agent Skills——AI助手的“专业工具箱“实战入门
java·人工智能·交互·rag·企业开发·web转型ai
俊哥V1 天前
AI一周事件(2026年01月01日-01月06日)
人工智能·ai