资讯动态

Spring AI Alibaba 1.x Agent运行时配置RunnableConfig实战解析

发布时间:2026/9/8 2:34:03 来源:尧图企业网站定制
Spring AI Alibaba 1.x 系列的上一篇我讲了 ChatClient 的流式调用和结构化输出本来计划直接进入 Tool Calling 的细节但后来在实际项目里把一个查询型 Agent 调起来之后连续踩了好几个跟运行时配置相关的坑——不是模型能力的问题也不是 Prompt 写得不好而是 Agent 跑起来之后“怎么控制它的行为边界”这件事没想清楚。如果你也遇到过 Agent 突然死循环、无限调用工具、上下文被撑爆、或者日志里莫名出现一句Agent execution terminated due to error.那大概率是运行时配置没吃透。这篇文章就把 RunnableConfig 扒开结合 Spring AI Alibaba 1.x 的实际使用场景把 Agent 运行的配置参数一项一项讲清楚。1. RunnableConfig 是什么Agent 运行的“总控台”1.1 从一次线上事故说起先说一个我实际经历过的问题。项目里有个报表助手看起来功能很简单用户用自然语言提问Agent 判断需要查哪些表调用查询工具最后把结果翻译成回答。本地测试怎么调都正常一上生产就出事——同一个问题用户连续问了几次之后Agent 开始反复调用查询工具明明第一次已经拿到了结果它却在“要不要再查一次”之间来回横跳直到把我配置的调用次数上限打满然后整个流程直接抛异常退出界面上就显示一句干巴巴的“Agent execution terminated due to error.”。当时第一反应是模型太笨换了更强的模型也没什么改善。后来逐条日志追发现问题出在我对 Agent 的执行机制理解不够Agent 本质上是一个“感知-决策-行动”的循环每一轮都需要模型决定下一步做什么如果这个循环不加以约束或者上下文里累计的信息让模型产生了矛盾判断它就可能在原地打转。而控制这个循环的参数就是今天要讲的 RunnableConfig。1.2 RunnableConfig 在 Spring AI Alibaba 调用链中的位置在 Spring AI Alibaba 1.x 的体系里一个完整的 Agent 应用通常由这么几层组成最外层是业务代码负责接收用户请求、做参数校验、组织返回结果。中间层是 ChatClient 或 AgentExecutor负责编排 Prompt、调用模型、解析输出。再往下是 ToolCallback负责把本地方法、外部 HTTP 接口、MCP 服务等统一包装成模型可识别的工具。最底部是 ChatModel真正的模型推理入口。RunnableConfig 就夹在中间层和模型层之间。它的作用不是修改 Prompt也不是调整模型参数而是控制 Agent 循环本身的运行方式最多循环多少轮、如何标识一次请求、如何传递链路追踪信息、如何控制上下文的轮次范围。一个直觉的理解方式是把 Agent 想象成一辆自动驾驶的车ChatModel 是发动机Prompt 是导航路线工具是转向和刹车而 RunnableConfig 就是仪表盘上的那套限速和故障保护系统——它不会告诉你往哪开但是会在你失控的时候强制踩刹车。1.3 四个核心维度在 Spring AI 1.x 的 agent 模块里org.springframework.ai.agent.runnable.RunnableConfig这个类承担了运行控制职责。它实际上把配置分成了几个维度我平时最常用的有四个第一是递归限制recursionLimit这是最关键的兜底参数。它规定了 Agent 在一次请求中可以执行的模型调用轮数上限。每一轮模型调用算一次包括工具结果的返回和重新决策。一旦达到上限Agent 会终止执行并返回一个终止消息。第二是标记tags本质是一个字符串列表用于给一次执行打上业务维度的标签。比如某一次请求来自哪个业务线、属于哪个场景后续在日志和监控里可以按这些标签过滤。第三是元数据metadata是一个MapString, Object适合放结构化信息比如 requestId、userId、环境信息。如果项目里接入了链路追踪系统这些元数据可以携带到所有子调用中。第四是轮次上下文turnContext用于控制多轮对话场景下的上下文传递方式决定哪些历史消息保留、哪些丢弃。这四个维度看似简单但每一条在实际项目中都能引出不少坑。下面逐项展开。2. Agent 运行的配置参数逐项拆解2.1 递归限制防止 Agent 失控的第一道闸门先说 recursionLimit这是 RunnableConfig 里最容易被忽略又最致命的一个参数。很多第一次接触 Agent 开发的同事会问模型不是应该按我的 Prompt 乖乖执行吗为什么要限制循环次数原因在于工具调用型 Agent 的运行机制。一次提问进来之后模型先做一次推理如果它觉得需要查数据就会返回一个工具调用的请求应用层执行这个工具把结果拼回去再送给模型做第二次推理模型如果觉得信息还不够又发起新的工具调用……如此往复直到模型认为可以给出最终答案。这个循环的次数是完全由模型“自己觉得”决定的外部没有硬性约束的话模型完全可能陷入死循环。我在项目中遇到过的最极端情况是一个工具返回了空结果模型无法理解“为什么查出来是空”于是它决定换个参数再查一次换了三次都没有数据它居然开始尝试拼接出一些不存在的字段名去查询。如果没有递归限制这种错误会在一次请求里不断放大。那么 recursionLimit 应该设多少这取决于你的 Agent 需要几步才能完成一次典型任务。以一个简单的 NL2SQL 场景为例用户提问模型决定调用“获取表结构”工具第 1 轮。拿到表结构模型决定调用“执行查询”工具第 2 轮。拿到查询结果模型给出回答第 3 轮。这个流程至少需要 3 轮调用。如果查询失败需要重试就会变成 4-5 轮。所以给这类 Agent 设置 5-6 是比较合理的。如果你做的是多跳检索类的 Agent每跳一次都要做一次意图分析和一次检索那可能需要 8-10 轮。我的建议是先通过日志统计正常请求平均需要多少轮然后在此基础上加 2 作为兜底值不要凭感觉设一个很大的数。recursionLimit 过大会让失控请求白白消耗大量 token过小则会导致正常请求被误终止。2.2 标记与元数据可观测性的地基Agent 应用和传统 Web 应用最大的区别在于一次用户请求会引发多次模型调用。传统应用一次请求一条日志查起来很直接Agent 应用一次请求可能产生三五次模型调用、若干次工具调用如果没有统一的标识排查问题时会非常痛苦。tags 和 metadata 就是为解决这个问题设计的。我在所有 Agent 入口都强制要求传入两个东西一个是业务场景标签比如nl2sql、rag、customer-service一个是 requestId通过 metadata 传进去。这样在日志平台里我可以一键筛出某一次完整请求涉及的所有模型调用和工具调用记录。具体做法是在创建 RunnableConfig 的时候RunnableConfig config RunnableConfig.builder() .recursionLimit(6) .tag(nl2sql) .metadata(requestId, requestId) .metadata(userId, userId) .build();这些 tags 和 metadata 不只用于日志。如果你接入了阿里云或者其他 APM 平台它们可以直接映射到 trace 的 span attribute 上实现全链路分析。另外在做灰度发布或 A/B 测试时通过 metadata 传一个experimentGroup字段可以在统计报表时快速区分不同策略的效果差异。需要提醒一点不要把大对象放进 metadata。它是会被序列化传递的放一个几百 KB 的对象每次模型调用都会带着它序列化一次性能损耗非常明显。metadata 里只放字符串、数字这类轻量数据。2.3 模型推理参数ChatOptions 里的温度与采样RunnableConfig 管的是循环过程但在每一轮循环内部模型推理参数由 ChatOptions 控制。这两个东西经常被混淆。我见过有同事在 RunnableConfig 里找 temperature 找不到然后跑来吐槽 API 设计有问题——其实它们是不同层面的配置。在 Spring AI Alibaba 1.x 中ChatOptions 可以在多个层级设置全局的 application.yml 配置、ChatClient 创建时的默认配置、单次 Prompt 调用时的临时配置。优先级从高到低是单次调用 ChatClient 默认 全局配置。temperature 对 Agent 的影响比传统 Chat 应用大得多。模型在决定是否调用工具、选哪个工具、填什么参数时如果 temperature 太高它就倾向于“创造性发挥”可能出现幻觉式参数——比如明明工具要求传数字类型的 limit模型硬是传了个字符串10 条。对 Agent 场景我一般建议控制在 0.3 以下查库类场景直接 0。值得一提的还有 maxTokens。Agent 循环里每一轮的输出里都包含工具调用请求这部分会挤占输出 token。如果你的工具定义很多、参数说明很长maxTokens 设得太小会导致模型在调用工具时输出被截断出现工具名残缺、参数 JSON 不完整的问题。在工具密集型场景我一般把 maxTokens 设到 2000 以上。2.4 工具与 MCP 服务的接入配置Agent 的“行动”能力来自工具而工具在 Spring AI Alibaba 里来自两个渠道一种是通过Tool注解标注的本地方法另一种是外部的 MCP 服务。最近不少人在问“spring ai alibaba 如何使用别人提供的 MCP 服务”这里单独说一下。先说本地工具。用MethodToolCallbackProvider把 Spring Bean 里的方法包装成工具这一步比较直观Configuration public class AgentToolConfig { Bean public ToolCallbackProvider sqlServerTools(DatabaseQueryService queryService) { return MethodToolCallbackProvider.builder() .toolObjects(queryService) .build(); } }这里有一个特别值得注意的点工具的描述信息决定模型能不能正确触发它。我在工具描述上吃过亏——有一个获取用户信息的工具描述写的太笼统模型经常在应该调“订单查询”的时候去调它。后来把描述改成了“根据用户手机号获取基础信息当用户询问姓名、等级、注册时间时使用注意与订单查询区分”误调率明显下降。再说 MCP 服务。别人提供的 MCP 服务本质上就是一个暴露了标准协议接口的服务端Spring AI Alibaba 通过 MCP Client 接入接入后它的工具会被自动注册成 ToolCallbackAgent 就能像调用本地工具一样用。在 application.yml 里的配置大致是这样spring: ai: mcp: client: connections: external-biz-service: type: remote url: http://localhost:8081/mcp headers: Authorization: Bearer ${MCP_TOKEN}接入之后如果你用的是 ChatClient.Builder 的 defaultTools需要把 MCP 的工具也合并进来。遇到“MCP 服务注册了但 Agent 不调用”的问题时十个里有八个是工具描述太差或者模型根本没看到这个工具而不是协议层出了问题。MCP 服务接入还有一个常见的坑超时。外部 MCP 服务的响应速度不受你控制如果某个工具本身要执行十秒以上的查询模型这边可能已经等了很久。我给所有远程工具调用都设置了超时一旦超时就返回一个明确的中断信息让模型知道这个工具暂时不可用而不是让它傻等之后拿到一个异常。2.5 记忆与上下文管理最后一个维度和多轮对话有关。Agent 有状态这件事既是能力的来源也是配置的难点。你不想让 Agent 忘掉用户前面说的话但也不能让上下文无限膨胀。Spring AI Alibaba 的记忆机制主要体现在 ChatMemory 和消息窗口上。短期的消息窗口配置一般长这样MessageWindowChatMemory chatMemory MessageWindowChatMemory.builder() .maxMessages(20) .build();maxMessages 不是越大越好。语言模型的上下文窗口是有限的你塞进去 50 条历史消息模型能用来生成回答的空间就被挤占了而且对 token 成本的消耗几乎是线性增长的。实测下来20 条左右是大多数业务场景的甜点值。更精细的做法是把记忆分类型处理。普通寒暄历史保留最近 10 条而“用户已经确认的表结构”“已经查询到的关键数据”这种事实性信息单独存成业务记忆在每次请求时固定注入系统提示词。这样既不会上下文爆炸也能保证关键信息不丢。3. 实操封装一个带 RunnableConfig 的 NL2SQL Agent3.1 场景定义与工具设计理论和配置项拆完之后用一个完整案例串起来。假设要做一个销售数据问答机器人用户问“华东区上个月销售额排名前五的商品是什么”Agent 需要先了解表结构再生成查询最后把结果转成自然语言。这类场景我推荐最少依赖的方式两个工具一个是获取表结构的getTableSchema一个是执行只读查询的executeQuery。工具设计上有一个关键点不在工具里做任何业务判断工具只做执行判断交给模型。前一个工具返回的是完整的表结构 JSON模型基于它决定下一步查哪张表后一个工具执行 SQL 并返回结果集。这样的分层让模型可以灵活决策但也要求工具返回值足够结构化。3.2 工程落地依赖、配置与代码工程依赖方面我习惯在 Spring Boot 3.x 项目里加 Spring AI Alibaba 的 starter 依赖。核心依赖大致是dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId version1.x.x/version /dependency接入 MCP 客户端需要额外加dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client/artifactId /dependency注意具体版本号要以你当前使用的 Spring AI Alibaba 1.x 小版本的依赖管理为准。全局配置里把模型参数定在一个偏保守的范围spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus temperature: 0.1 max-tokens: 2000然后是工具类。我用 Spring JDBC 来执行查询这样一个 Demo 不需要引入复杂的 ORMComponent public class DatabaseQueryService { private final JdbcTemplate jdbcTemplate; public DatabaseQueryService(JdbcTemplate jdbcTemplate) { this.jdbcTemplate jdbcTemplate; } Tool(description 获取指定表的字段信息返回字段名、类型、注释列表。查询前请先调用此工具确认表结构。) public String getTableSchema(String tableName) { String sql SELECT column_name, data_type, COALESCE(column_comment, ) FROM information_schema.columns WHERE table_name ? ORDER BY ordinal_position ; ListMapString, Object rows jdbcTemplate.queryForList(sql, tableName); return rows.isEmpty() ? 表不存在或无权访问 : JSON.toJSONString(rows); } Tool(description 执行只读SQL查询仅支持SELECT语句返回结果集的JSON数组。) public String executeQuery(String sql) { if (sql null || !sql.trim().toLowerCase().startsWith(select)) { return 仅支持SELECT语句; } try { ListMapString, Object rows jdbcTemplate.queryForList(sql); if (rows.size() 200) { return JSON.toJSONString(rows.subList(0, 200)) 结果集过大仅返回前200条; } return JSON.toJSONString(rows); } catch (Exception e) { return SQL执行失败 e.getMessage(); } } }这里有个细节executeQuery 里对结果集做了截断。如果不截断一个表一百万行数据往模型上下文里灌一次请求就会把 token 打爆。对 Agent 而言“查到了什么”往往比“完整数据”更重要真要完整数据应该走文件导出通道而不是让模型在上下文里处理。接下来是 Agent 的执行入口。这里会把 RunnableConfig 用起来Service public class SqlAgentService { private final ChatClient chatClient; public SqlAgentService(ChatClient chatClient) { this.chatClient chatClient; } public String ask(String question, String requestId, String userId) { RunnableConfig config RunnableConfig.builder() .recursionLimit(6) .tag(nl2sql) .metadata(requestId, requestId) .metadata(userId, userId) .build(); // Spring AI Alibaba 1.x 中 RunnableConfig 最终会绑定到 ChatClient 的执行链路 return chatClient.prompt() .user(question) .options(config) .call() .content(); } }ChatClient 的初始化放在配置类里把工具注册进去再写一段系统提示词来约束模型行为Bean public ChatClient sqlAgentChatClient(ChatClient.Builder builder, ToolCallbackProvider sqlTools) { return builder .defaultSystem( 你是销售数据查询助手。你的工作流程 1. 根据用户问题判断涉及哪些表先调用 getTableSchema 获取表结构 2. 基于真实表结构编写 SQL调用 executeQuery 获取数据 3. 根据查询结果用简洁中文回答用户。 注意不允许编造字段名不允许执行非 SELECT 语句。 ) .defaultTools(sqlTools) .build(); }3.3 参数选择与效果验证参数方面我最初把 recursionLimit 设成了 4测下来发现不够。原因是模型在处理“上个月”这种相对时间时会先查当前日期或先确认时间口径然后再查表结构、再执行查询这就已经 4 轮了最后一轮生成答案就超限了。后来统计了 50 条测试请求平均需要 4.7 轮于是把 recursionLimit 定在 6既能覆盖绝大多数正常请求又不会给失控请求留太多空间。temperature 设了 0.1实测在 SQL 生成场景下这个值能保证大多数情况下每次生成的 SQL 结构一致不会出现“这次不加 limit下次加了 limit”这种不可控的波动。跑一个典型问题验证一下效果。输入“华东区上个月销售额前五的商品”Agent 的执行过程大致是第一轮模型判断需要查表结构发起了getTableSchema调用参数可能是orders和products。工具返回了表结构。第二轮模型看到表结构里有order_amount、region、product_name、order_date等字段决定编写 SQL发起executeQuery。第三轮工具返回查询结果模型组织自然语言回答。一次完美的流程用时大约 3 轮而如果第一步表名猜错了模型会通过工具结果修正自己额外消耗一两轮。这就是为什么工具返回信息里要包含“表不存在”这类反馈——模型能把失败转化为下一步的决策依据。4. 常见问题排查与避坑实录4.1 “Agent execution terminated due to error.”意味着什么这个报错在日志里出现时很多人的第一反应是模型出错了但实测下来绝大多数情况是 Agent 执行器捕获到了内部异常后主动终止了流程。也就是说它不是根因提示而是一个“熔断通知”。遇到这个报错我建议按这个顺序排查先看是否触发了 recursionLimit。如果日志里在报错之前出现多次工具调用的记录而且轮数和你的限制值一致基本就是递归超限导致的中断。解决方向是优化工具调用链或者适当提高限制值。再看工具本身是否抛了异常。比如 executeQuery 工具里如果没做 try-catchSQL 语法错误会直接把异常抛到 Agent 执行器里执行器捕获后就以“terminated due to error”收场。工具方法内部必须做好异常兜底保证任何情况下都返回一个可读的字符串而不是往上抛异常。最后看 MCP 远程服务是否超时。外部 MCP 服务连接不上或者响应超时同样会中断流程。4.2 递归超限与工具误判递归超限有一个隐蔽的诱因模型对工具参数的理解偏差。比如 getTableSchema 这个工具的参数是tableName模型如果传了一个带空格或带引号的值查询不到数据后它不会停下来而是会换个姿势再试几次之后就直接超限。解决这种问题有两个方向。一是优化工具描述把参数格式写清楚比如“tableName 为数据库中的原始表名不含引号例如 orders”。二是在工具内部做参数容错把常见的错误格式自动修正。这两个手段组合使用能显著减少因为参数误判导致的循环。另一个思路是把“失败信息”设计得更聪明。工具返回“无数据”时不妨带上提示词比如“没有找到该表可选表有orders, products, users”。这能让模型快速跳出错误路径而不是反复用错误参数重试。4.3 上下文爆炸与响应变慢Agent 每跑一轮工具返回的内容就会累积到上下文里。如果某个工具一次返回几十 KB 数据三轮之后上下文就已经非常臃肿模型推理速度明显下降token 成本直线上升。我的经验是在工具返回值上下功夫。第一是截断结果集超过一定行数就只返回摘要比如总数、前 N 条、聚合值。第二是结构化用紧凑的 JSON 格式返回不要用大段的自然语言描述。第三是清理无关字段查询结果只保留和回答问题相关的列不要每次把全表的字段都带回来。如果你发现上下文膨胀已经成了常态就要考虑引入记忆压缩机制。把老对话的核心事实总结成简短摘要替换掉原始对话历史比简单地调大 maxMessages 更有效。4.4 MCP 远程服务接入失败的排查最后单独说 MCP因为“如何使用别人提供的 MCP 服务”这个问题被问得最多。接入远程 MCP 服务时常见的坑按出现频率排名第一个是网络不通或者鉴权失败。很多人配置完 URL 就以为完事了忘了服务端可能需要 token。MCP 协议本身不规定鉴权方式常见的有 Header 里带 Bearer token、请求参数带签名。这部分要和提供方确认清楚。第二个是工具发现机制。MCP 服务可能暴露了十个工具但你只想用其中三个。Spring AI Alibaba 的 MCP 客户端允许你配置工具过滤只注册需要的工具。工具注册得越多模型的选择空间越大误选概率也越高。第三个是返回结果格式。MCP 服务返回的内容是纯文本还是结构化 JSON直接决定模型下一步能不能有效利用。如果你的下游 Agent 要基于 MCP 返回继续做事强烈建议要求提供方返回 JSON 格式并附上字段说明。模型处理结构化 JSON 的稳定性远高于处理散文式文本。如果你想确认 MCP 服务本身是否正常可以先跳过 Spring Boot单独写一个测试类直接用 MCP Client 拉取工具列表并调用一次这样能把“MCP 服务问题”和“Agent 编排问题”隔离开来。我在实际接入一个外部供应商的 MCP 服务时就是用这种方式发现对方返回的工具描述里缺了参数说明模型拿到工具后根本不知道怎么填参数。后来让对方在工具描述里补了参数示例问题立刻消失。回到最开始那个事故。当时我把 RunnableConfig 里的 recursionLimit 从默认值调小把 temperature 降到 0.1又给两个工具补了更严格的描述和异常兜底报表助手就再没有出现过无限循环的问题。事后复盘Agent 开发的难点从来不是写一个会调工具的模型而是给这个“会调工具的东西”划定清晰的边界。RunnableConfig 就是边界本身。参数不多但每一个都值得你在上线前认真过一遍。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价