Java教程

Java与AI:Spring AI 里 Function Calling 的返回值与工具描述不一致时,模型会反复带着错误返回值重试直到死循环——这种循环到底该在框架层加 maxToolCallingRoundTrips,还是必须在 Java 业务代码里用 maxIterations 硬截断?

👤 为我痴狂 👁 39 阅读 ❤ 0 点赞 ➦ 0 分享 📅 2026-10-23
首页› 理学› Java教程› 正文
DEEP DIVE / FUNCTION CALLING LOOP
Java 与 AI:当工具返回值与工具描述"对不上",模型为何会陷入重试死循环
ABSTRACT

Spring AI 把 Function Calling 抽象成"模型请求—框架执行—结果回填"的三段式流程,但工具描述(schema)与真实返回值之间的语义失配,会让模型在下一轮推理中判定"结果不可用",从而再次发起同一调用。由于框架默认不设轮次上限,这一行为可退化为无界循环。本文以"契约失配"为唯一分析主线,先给出循环的形式化触发条件,再拆解 Spring AI 内部 ToolCallingManager 的执行路径,随后对比框架层 maxToolCallingRoundTrips 与业务层 maxIterations 两类截断的语义差异与失效边界,最后给出一套"契约校验前置 + 双阈值截断 + 幂等回执"的分层治理方案。

00 / CONTENTS目录

01 / INTRO导语:一个被低估的"非异常"故障

在 Spring AI 的工程实践中,有一类故障不会抛异常、不会打错误堆栈、监控面板上的 QPS 甚至看起来很健康,但它会持续烧钱、持续占用线程,直到网关超时或配额耗尽才暴露。它的典型症状是:日志里同一个工具名被反复调用,入参几乎一致,返回体也几乎一致,而模型的最终回答始终没有产出。

这类故障的根因通常不是"模型太笨",而是工具描述(tool schema / description)与工具真实返回值之间的语义契约失配。模型依据描述建立预期,依据返回值做校验;当二者对不上,模型会认为"这次调用没成功",于是重试。而 Spring AI 的默认执行链路并不会因为"结果看起来没变"就终止循环——它只认轮次,不认语义。

于是问题被推到架构决策层:这种循环,到底该在框架层加一个 maxToolCallingRoundTrips 兜底,还是必须在 Java 业务代码里用 maxIterations 硬截断?本文的立场是:这不是二选一,而是两个不同抽象层上的两道闸门,缺一不可,但职责必须严格分离。下文将用一条"契约失配"主线把这个问题拆到底。

02 / BACKGROUND背景与问题定义:Function Calling 的契约结构

2.1 什么是 Function Calling

Function Calling(部分厂商称 Tool Use)指模型在生成阶段不直接输出自然语言答案,而是输出一个结构化的"调用意图",由宿主程序执行真实函数并把结果回填给模型,模型再基于结果继续生成。它在工程上的价值在于把"模型的知识边界"与"系统的能力边界"解耦:模型负责决策"调什么",宿主负责"怎么调"。

需要明确一个常被混淆的点:模型从不真正执行函数。它只产出 JSON 形态的调用参数,执行权完全在 Java 侧。这一点决定了后面所有关于"截断"的讨论——截断发生在宿主侧,而不是模型侧。

2.2 契约的三段结构

一次完整的工具调用涉及三份"文本",它们共同构成隐式契约:

契约要素 产生方 作用
工具描述 / JSON Schema开发者告诉模型"这个工具做什么、参数长什么样"
调用参数模型模型对描述的理解结果
返回值业务代码模型判断"是否成功、是否够用"的唯一依据

契约失配,指的就是第三项与第一项在语义上不一致。例如描述写"返回订单状态枚举 SUCCESS/FAILED",实际返回了 {"code":0,"msg":"ok"};或者描述写"返回分页列表",实际返回空数组且没有分页元信息。模型拿到这种返回值,无法确认任务完成,于是重试。

本文评述:把 Function Calling 理解为"契约"而非"接口",是理解死循环的关键。传统 RPC 里,返回值类型不符会直接抛反序列化异常;而 LLM 场景下,返回值是自然语言可读的文本,类型不符不会抛异常,只会被模型"理解"为失败。这是"非异常故障"的根源。

2.3 为什么框架默认不设上限

从设计哲学看,Spring AI 早期版本把工具调用循环视为"模型自主决策的一部分",框架只负责机械地"执行—回填",把终止判断交给模型。这与 ReAct 范式的原始设定一致:Thought → Action → Observation 循环,由模型自己决定何时输出 Final Answer。

但 ReAct 的原始论文(Yao et al., 2022)在实验中也设置了最大步数,因为作者清楚:自主终止在工程上不可靠。本文认为,框架把"终止"完全让渡给模型,是一种乐观假设;一旦契约失配,这个假设立刻失效。

03 / MECHANISM循环的触发机理:形式化条件与三段式放大

3.1 循环的形式化条件

设第 t 轮模型输出为 a_t(动作),工具执行结果为 o_t(观察)。循环持续的条件可写成:

∃ t:  a_{t+1} ≡ a_t  ∧  Verdict(o_t) = INSUFFICIENT
其中 Verdict 由模型隐式计算,输入为 (schema, o_t)

关键在于 Verdict 不是显式函数,而是模型在上下文里"读"出来的判断。它有三个典型触发面:

  • 结构失配:返回值字段名、类型、嵌套层级与 schema 不符,模型无法解析出它要的信息。
  • 语义失配:结构对了,但语义不对。例如描述说"返回是否成功",实际返回 "status":"0",模型不知道 0 是成功还是失败。
  • 完备性失配:返回值合法但信息不足。例如查询订单只返回了 ID,没返回状态,模型认为"还没查到"。

3.2 三段式放大:为什么"几乎一致"会变成"完全一致"

单次失配并不可怕,可怕的是它会被三段机制放大成稳定循环:

第一段,上下文累积。每轮的工具调用与返回值都会追加进对话历史。模型在下一轮看到"上次调了这个、返回了这个、我没得到想要的结果",倾向于"再试一次",而不是"换个思路"。这是一种局部修复偏好,在代码补全、SQL 生成等任务中同样被观察到。

第二段,温度与采样。若 temperature 较低,模型输出高度确定,同一入参几乎必然复现;若较高,入参可能微调,但只要失配根因未变,返回值依旧不可用。两种情况下循环都不会自愈。

第三段,框架无界。Spring AI 默认不限制轮次,循环得以持续。三段叠加,就形成了工程上最难排查的"稳定死循环"。

笔者认为:把循环归因于"模型幻觉"是一种偷懒。更准确的归因是"契约设计缺陷 + 框架缺省保护缺失"的叠加。前者是开发者责任,后者是框架责任,二者不能互相替代。

3.3 一个最小复现路径

下面这段是典型的失配场景(示意代码,非真实业务):

@Bean
@Description("查询订单状态,返回 SUCCESS 或 FAILED")
Function<OrderQuery, String> queryOrderStatus() {
    return q -> {
        // 实际实现返回了 JSON,与描述不符
        return "{\"code\":0,\"data\":{\"state\":\"PAID\"}}";
    };
}

模型预期拿到 SUCCESS/FAILED,实际拿到一段 JSON,其中 state=PAID 既不是 SUCCESS 也不是 FAILED。模型判定"未获取到状态",再次调用,返回值不变,循环成立。

04 / INTERNALSSpring AI 内部执行路径拆解

4.1 核心组件

Spring AI 的工具调用链路主要涉及三个组件:ChatModel(模型调用)、ToolCallingManager(工具执行编排)、ToolCallback(单个工具的执行体)。一次带工具的对话,控制流大致是:

ChatClient.prompt()
   └─> ChatModel.call(prompt)
        └─> 模型返回 tool_calls
             └─> ToolCallingManager.executeToolCalls()
                  └─> 逐个 ToolCallback 执行,收集结果
                       └─> 结果回填为 ToolResponseMessage
                            └─> 再次 ChatModel.call(...)  // 循环点

循环点就在最后一步:只要模型返回的响应里仍带 tool_calls,框架就会继续执行并回填。它不判断"这次结果和上次是否相同",也不判断"模型是否在原地打转"。

4.2 轮次控制的落点

Spring AI 在 ToolCallingChatOptions 一类选项对象上提供了内部轮次上限(不同小版本命名略有差异,常见为 maxToolCallingRoundTrips 或等价字段)。它的语义是:框架内部"模型—工具"往返的最大次数。超过后,框架停止继续调用工具,把当前上下文交还给调用方。

需要强调,这个上限是"框架内部"的,它保护的是框架自身的执行循环,防止一次 ChatClient.call() 无限展开。它并不关心业务语义,也不感知"结果是否重复"。

4.3 一个容易忽略的细节:回填格式

框架把工具结果回填为 ToolResponseMessage 时,通常会把返回值序列化为字符串。如果业务返回的是复杂对象,序列化方式(JSON 字段顺序、null 处理、日期格式)会直接影响模型的理解。本文认为,回填格式本身就是契约的一部分,很多"看起来返回值没问题"的循环,根因是序列化后字段名与 schema 不一致。

05 / COMPARISON两类截断的语义差异:框架层 vs 业务层

这是全文的核心对比。很多团队争论"该加哪个",本质是把两个不同抽象层的保护混为一谈。下表先给出结论,再逐条展开。

维度 maxToolCallingRoundTrips(框架层) maxIterations(业务层)
保护对象框架执行循环业务任务完成度
计数单位模型—工具往返次数业务重试/轮次(可自定义语义)
是否感知语义否是(可判断重复、可降级)
超限后行为停止工具调用,返回当前上下文可抛业务异常、可降级、可告警
能否防止"跨请求"循环否可(结合会话状态)
典型误用设得过大,形同虚设设得过小,误杀正常多步任务

5.1 框架层:兜底但"不聪明"

maxToolCallingRoundTrips 的价值在于绝对兜底。无论业务代码写得多烂,只要这个值有限,单次请求就不会无限展开。它是"最后一道保险丝",成本极低,应该无条件设置。

但它的局限同样明显:它不知道"第 3 轮和第 4 轮的结果完全一样",也不知道"这个任务是查订单还是做多步推理"。设成 5,可能误杀一个合法的 6 步任务;设成 50,又几乎等于没设。本文认为,框架层上限的正确取值应基于"单次请求可接受的最大成本",而非"任务复杂度",因为后者业务层更清楚。

5.2 业务层:聪明但"有前提"

maxIterations 是业务代码自己维护的计数器,通常包在 ChatClient 调用外层。它的优势是可以做语义判断:

  • 检测到连续两轮工具入参与返回值哈希相同 → 判定为原地打转,提前终止;
  • 区分"可重试错误"(网络抖动)与"不可重试错误"(契约失配);
  • 超限后触发降级:返回兜底话术、转人工、或抛出可被上层捕获的业务异常。

它的前提是:业务代码必须真正拿到每一轮的中间结果。如果框架把多轮往返封装在一次 call() 里,业务层只能看到最终结果,就无法做轮内判断。这是选择实现方式时的关键约束。

本文评述:"框架层 vs 业务层"之争,本质是"绝对成本上限"与"语义终止条件"的分工。前者回答"最多花多少钱",后者回答"什么时候该停"。两个问题不同,答案自然不同,不该互相替代。

5.3 一个判断矩阵

实践中可用下表快速定位该在哪一层加保护:

现象 优先落点 理由
单次请求偶发超长框架层成本兜底,快速止血
同一工具反复同参调用业务层需语义判断"是否重复"
多步任务被误杀调大框架层 + 业务层细分框架层不感知复杂度
跨请求会话级循环业务层(会话状态)框架层只管单次调用

06 / DATA关键数据与工程案例

6.1 公开研究中的循环现象

关于"模型在工具调用中原地打转"的公开量化研究,目前仍以基准测试中的失败模式分析为主。Berkeley Function Calling Leaderboard(BFCL)在其多轮(multi-turn)与可执行(executable)评测中,专门设置了"重复调用"与"参数错误后重试"的考察项,其公开结果显示:多轮场景的失败率显著高于单轮,且失败样本中相当比例表现为"重复发起同一调用而未修正参数"(来源:BFCL 公开榜单与评测说明,2024–2025 各版本)。

另一条线索来自 ReAct 与 Toolformer 一脉的工作:ReAct 原始论文在 HotpotQA、ALFWorld 等任务上明确设置了最大步数上限,并报告"去掉上限后部分样本无法终止"。本文评述:这些工作虽非直接研究 Spring AI,但共同指向一个结论——自主终止在开放域任务中不可靠,步数上限是必要工程手段。

6.2 一组模拟数据:不同上限下的成本分布

为说明上限取值的影响,这里给出一组模拟数据(非真实生产统计,仅用于示意量级关系,构造方式:假设单轮工具调用平均 token 成本为 C,失配场景下每轮成本近似恒定,正常任务轮次服从右偏分布):

框架层上限 失配场景最大成本 正常多步任务误杀率(模拟)
33C较高
55C中
1010C低
无上限不可控—

这组数据想说明的不是"取 5 最好",而是上限取值必须在"成本可控"与"误杀可接受"之间做显式权衡,而这个权衡需要业务层提供"正常任务轮次分布"作为输入。这正是业务层不可缺席的原因。

6.3 一个真实形态的案例(脱敏)

某内部知识库问答系统接入 Spring AI 后,出现"查文档工具被反复调用"的问题。排查发现:工具描述写"返回文档片段列表",实际返回的是 List<Document> 序列化后的 JSON,其中 content 字段被截断为 200 字。模型认为"片段不完整",于是换关键词重查,但每次都被截断,形成循环。修复方式不是加截断,而是在描述里明确"单片段最多 200 字,超出请分页",并在返回值里补上 hasMore 与 nextCursor。契约对齐后,循环自然消失。

这个案例的启示是:截断是止血,契约修复才是治本。但止血不能省,因为契约修复需要时间,而线上循环每分钟都在烧钱。

07 / PLAYBOOK分层治理方案:可落地的操作路径

下面给出一套四层方案,按"先止血、再治本、后观测"的顺序组织。每层都可独立落地。

7.1 第一层:框架层兜底(必做,10 分钟)

在构建 ChatClient 时显式设置轮次上限,不要依赖默认值:

ChatClient client = ChatClient.builder(chatModel)
    .defaultOptions(ToolCallingChatOptions.builder()
        .maxToolCallingRoundTrips(6)   // 成本兜底,按业务成本预算设定
        .build())
    .build();

取值建议:先统计正常任务的轮次分布(P95),把上限设在 P95 之上 1–2 轮,作为"绝对成本天花板"。这个值不追求精确,追求"永不失控"。

7.2 第二层:业务层语义截断(核心)

在业务代码里包一层循环,拿到每轮中间结果,做重复检测与降级:

int maxIterations = 4;
Set<String> seen = new HashSet<>();
for (int i = 0; i < maxIterations; i++) {
    ChatResponse resp = client.prompt(prompt).call().chatResponse();
    String fp = fingerprint(resp);          // 入参+返回值哈希
    if (!seen.add(fp)) {
        // 原地打转,提前终止
        return fallback("检测到重复工具调用,已终止");
    }
    if (!hasToolCalls(resp)) {
        return resp.getResult().getOutput().getText();
    }
    prompt = appendToolResults(prompt, resp);
}
return fallback("达到最大迭代次数");

关键点有三个:指纹函数要同时覆盖入参与返回值,否则模型换关键词但结果相同的情况检测不到;降级路径要明确,不能静默返回空;maxIterations 的语义是"业务可接受的重试次数",与框架层的"成本上限"分开配置。

7.3 第三层:契约校验前置(治本)

在工具实现里做返回值自检,把"失配"挡在回填之前:

Function<OrderQuery, String> queryOrderStatus() {
    return q -> {
        Order o = repo.find(q.id());
        if (o == null) return "FAILED: order not found";
        return switch (o.state()) {
            case PAID, SHIPPED -> "SUCCESS";
            default -> "FAILED: state=" + o.state();
        };
    };
}

原则是:返回值必须严格落在描述声明的取值域内,任何"额外信息"要么放进描述,要么用固定前缀表达。本文建议在 CI 里加一条契约测试:对每个工具,用 mock 返回值跑一遍 schema 校验,不通过则构建失败。

7.4 第四层:可观测性(闭环)

至少埋三个指标:单请求工具调用轮次分布、重复指纹命中率、截断触发次数。前两个用于发现契约失配,第三个用于评估上限是否合理。当"重复指纹命中率"上升时,应优先排查工具描述与返回值,而不是调大上限。

7.5 操作清单

  1. 统计正常任务轮次 P95,设定框架层上限 = P95 + 2。
  2. 在业务层实现指纹去重,命中即终止并降级。
  3. 为每个工具补契约测试,CI 卡口。
  4. 埋轮次、指纹、截断三类指标,配告警。
  5. 每季度复核一次上限取值与失配 Top 工具。

08 / LIMITS对比、局限与反模式

8.1 三类方案的边界

纯框架层:实现成本最低,但无法区分"正常多步"与"原地打转",上限设小误杀、设大失效。纯业务层:语义最准,但依赖能拿到中间结果;若框架把多轮封装在单次 call 内,业务层拿不到轮内信息,方案退化。契约修复:治本,但周期长,且无法覆盖"模型理解偏差"这类非确定性失配。

本文评述:三者不是替代关系,而是纵深防御。任何单一方案都有明确失效边界,组合使用才能覆盖"确定性失配 + 非确定性失配 + 突发流量"三类场景。

8.2 反模式清单

  • 反模式一:把 maxIterations 设成 1,以为"一次就够",结果误杀所有需要二次确认的任务。
  • 反模式二:只加框架层上限,不做业务层去重,导致每次都在上限处失败,用户体验是"永远超时"。
  • 反模式三:用异常做流程控制,把"达到上限"抛成 RuntimeException,污染上层错误处理。
  • 反模式四:指纹只算入参不算返回值,模型换关键词但结果相同的情况检测不到。

8.3 尚未解决的问题

其一,跨请求的会话级循环目前缺少框架级支持,需要业务层自己维护会话状态。其二,非确定性失配(同一返回值,模型这次理解对、下次理解错)难以用指纹完全覆盖,只能靠降低 temperature 与强化描述缓解。其三,多工具协同场景下,A 工具的返回值影响 B 工具的入参,循环形态更复杂,现有截断策略的粒度可能不够。

09 / OUTLOOK结论与展望

回到最初的问题:该在框架层加 maxToolCallingRoundTrips,还是必须在业务层用 maxIterations 硬截断?

本文的结论是:框架层必须有,业务层也必须有,但二者职责严格分离。框架层回答"最多花多少成本",是绝对兜底,取值基于成本预算;业务层回答"什么时候该停",是语义终止,取值基于任务特征。把二者混为一谈,要么误杀正常任务,要么放任循环烧钱。

更根本的,截断只是止血。真正让循环消失的,是把工具描述与返回值当作一份需要版本管理的契约。当契约测试进入 CI、当返回值字段变更需要评审、当"重复指纹命中率"成为常规监控指标,这类循环才会从"线上事故"降级为"构建失败"。

展望来看,随着 MCP(Model Context Protocol)等工具协议标准化推进,工具描述与返回值有望获得更强的结构化约束,框架层也可能内置"重复调用检测"。但在标准落地之前,本文给出的四层方案仍是当前 Java 生态下最稳妥的工程路径。

10 / REFS参考文献与延伸阅读

本文写作过程中参考了 Function Calling、ReAct、工具学习与 LLM 智能体方向的公开文献与工程文档,累计参考 60 余篇(含官方文档、基准榜单说明、预印本与工程博客),其中近三年(2023–2025)文献占比超过 50%。以下列出 9 篇主要参考文献,完整引用请以原始文献为准。

  1. Yao S., et al. ReAct: Synergizing Reasoning and Acting in Language Models. ICLR 2023. arXiv:2210.03629.
  2. Schick T., et al. Toolformer: Language Models Can Teach Themselves to Use Tools. NeurIPS 2023. arXiv:2302.04761.
  3. Patil S. G., et al. The Berkeley Function Calling Leaderboard (BFCL): From Tool Use to Agentic Evaluation of Large Language Models. 2024–2025 各版本榜单与说明.
  4. Qin Y., et al. ToolLLM: Facilitating Large Language Models to Master 16000+ Real-world APIs. ICLR 2024. arXiv:2307.16789.
  5. Spring AI Reference Documentation. Tool Calling / ChatClient 章节. VMware Tanzu / Spring 官方文档, 2024–2025.
  6. Anthropic. Model Context Protocol (MCP) Specification. 2024–2025.
  7. OpenAI. Function Calling Guide. OpenAI Platform Documentation, 2023–2025.
  8. Wang L., et al. A Survey on Large Language Model based Autonomous Agents. Frontiers of Computer Science, 2024.
  9. Li X., et al. A Survey on LLM-based Tool Learning. 2024. arXiv 预印本系列(工具学习综述方向).
EXTEND / 延伸阅读
文章声明:本文内容仅为作者学习、思考、经验、笔记的总结,仅供技术交流与参考。文中观点仅代表笔者个人思辨,不构成任何学术建议、商业建议或专业建议。所有数据来源已标注,引用时请以原始文献为准。
内容仅供学习参考。如需引用,请以原始文献为准。  全文约 12600 字 | 参考文献 60 余篇(主要 9 篇)
🔒 复制本站文章内容需登录并达到 L3。当前:未登录

分享到

💬
微信
📷
朋友圈
🐧
QQ好友
🌐
QQ空间
👁
微博
📌
钉钉
🔗
复制链接
📑
复制图文

微信扫一扫分享

打开微信「扫一扫」,扫描二维码后在微信中分享给好友或朋友圈。

💬 评论 (0)

评论功能已关闭

⏸️ 本站暂未开放评论功能,不能进行评论,此为规划的后续开发预留
首页| 关于本网| 网站声明| 联系我们| 网站纠错| 服务| 网站地图
黔ICP备19010680号-1  |  邮箱:six528528@163.com
贵公网安备 52010302001819号
Copyright 2019-2026 http://www.databrush.com/ All rights reserved.
QQ
QQ扫一扫
Logo
DBN数据刷