Spring AI工具已经调用成功,为什么最终回答仍为空?返回值、循环与上下文完整排查

文章摘要

有些Spring AI项目可以在日志中看到工具已经被调用,数据库查询或HTTP请求也成功执行,但客户端最终收到空字符串、模型重复调用同一工具,或者回答完全没有使用工具结果。这类问题与“模型没有选择工具”不同,通常发生在工具返回值序列化、异常处理、Tool Calling循环、流式事件拼接、结果过长、消息持久化和终止条件等环节。本文给出从工具执行结果到最终Assistant回答的完整排查路径。

一、先把链路分成五个阶段

一个完整工具调用不是只有“方法执行成功”:

模型选择工具
→ 参数解析
→ 工具执行
→ 结果写入Tool Response
→ 模型基于结果生成最终回答

日志只显示:

orderService.query()执行成功

只能证明第三阶段完成。

后面仍可能失败:

  • 返回对象无法序列化;
  • Tool Response为空;
  • 结果没有进入下一轮模型请求;
  • 模型再次调用工具;
  • 流式客户端漏掉最终事件;
  • 上下文超限;
  • Advisor提前返回;
  • 最终回答被安全策略拦截。

二、工具不要返回null

错误实现:

@Tool(description = "查询订单")
public OrderResult queryOrder(String orderId) {
    return repository.find(orderId)
            .orElse(null);
}

工具返回null时,模型可能只看到一个空结果,无法区分:

订单不存在
系统异常
没有权限
返回值丢失

推荐结构化返回:

public record ToolResult(
        boolean success,
        String code,
        String message,
        T data
) {
    public static  ToolResult success(T data) {
        return new ToolResult(
                true,
                "OK",
                "执行成功",
                data
        );
    }

    public static  ToolResult failure(
            String code,
            String message
    ) {
        return new ToolResult(
                false,
                code,
                message,
                null
        );
    }
}

不存在时返回:

{
  "success": false,
  "code": "ORDER_NOT_FOUND",
  "message": "订单不存在",
  "data": null
}

三、不要直接返回数据库Entity

数据库实体可能包含:

  • Hibernate代理;
  • 懒加载集合;
  • 双向关系;
  • 循环引用;
  • 内部字段;
  • 敏感字段;
  • 超大关联对象。

例如:

Order
→ Customer
→ Orders
→ Customer
→ ……

序列化可能失败或生成巨大结果。

推荐返回专用DTO:

public record OrderSummary(
        String orderId,
        String status,
        BigDecimal amount,
        String currency,
        Instant updatedAt
) {
}

只返回模型完成任务真正需要的字段。

四、返回值是否被异常转换成字符串

一些工具为了“方便”这样写:

catch (Exception exception) {
    return exception.getMessage();
}

模型会把错误字符串当成正常业务结果。

更危险的写法:

return "查询完成";

但没有返回真实数据,模型无法回答用户问题。

推荐区分:

业务成功
业务失败
技术异常
权限拒绝
等待确认

每类都使用稳定错误码。

五、检查工具结果的实际序列化内容

不要只打印Java对象:

log.info("result={}", result);

还应在安全脱敏后检查发送给模型的内容:

tool_result_json
result_size_bytes
serialization_status

可以在测试环境中显式序列化:

String json = objectMapper.writeValueAsString(result);

检查:

  • 是否为合法JSON;
  • 是否包含所需字段;
  • 是否出现空对象{}
  • 是否被截断;
  • 是否包含敏感信息;
  • 是否大到无法进入上下文。

六、工具结果过长会发生什么

如果工具返回:

5000条数据库记录
整个日志文件
完整网页HTML
几十万字文档

下一轮模型请求可能:

  • 超过上下文窗口;
  • 被Provider拒绝;
  • 成本骤增;
  • 模型忽略关键信息;
  • 最终回答变空;
  • 流式连接超时。

工具应该返回:

摘要
+分页信息
+少量关键记录
+可继续查询的游标

例如:

{
  "total": 2387,
  "returned": 20,
  "nextCursor": "eyJwYWdlIjoyfQ==",
  "items": []
}

七、Tool Calling循环是否继续进入下一轮

Spring AI 2.0通过ToolCallingAdvisor执行循环:

模型请求工具
→ 执行工具
→ 把结果加入对话
→ 再次调用模型
→ 得到最终回答

如果自动Tool Advisor被关闭:

AdvisorParams
    .toolCallingAdvisorAutoRegister(false)

应用必须自己完成后续循环。

否则你只能拿到:

工具调用请求

或工具执行结果,却没有最终自然语言回答。

八、是否错误地注册了多个ToolAdvisor

一个调用链中不应该同时存在多个负责工具执行循环的Advisor。

重复注册可能造成:

  • 工具执行两次;
  • 对话历史重复;
  • 循环顺序混乱;
  • 最终消息被覆盖;
  • 幂等冲突。

检查:

ChatClient自动注册的ToolCallingAdvisor
自定义ToolCallingAdvisor
ToolSearchToolCallingAdvisor

应该只有一个工具循环策略。

九、模型为什么重复调用同一个工具

常见原因:

1. 结果不包含完成信号

返回:

{
  "status": "PROCESSING"
}

模型可能继续查询。

2. 工具描述暗示需要再次确认

3. 返回值缺少用户要求的字段

用户问物流单号,工具只返回订单状态。

4. Tool Response没有进入下一轮上下文

5. Prompt要求“直到确认成功为止”

6. 工具调用失败却被包装为成功

建议在结果中加入:

terminal
retryable
nextAction

例如:

{
  "success": true,
  "terminal": true,
  "retryable": false,
  "data": {
    "trackingNo": "SF123456"
  }
}

十、必须为副作用工具设置幂等键

如果重复调用的是:

  • 创建订单;
  • 退款;
  • 发邮件;
  • 修改权限;
  • 提交审批;
  • 发布内容;

后果可能很严重。

幂等键建议由业务系统生成或验证:

tenant_id
+user_id
+conversation_id
+tool_name
+business_request_id

示例:

String idempotencyKey = String.join(
        ":",
        tenantId,
        conversationId,
        "cancel_order",
        orderId
);

数据库建立唯一约束,不能只依赖内存缓存。

十一、流式接口是否漏掉最终事件

流式工具调用可能包含:

模型文本增量
工具参数增量
工具调用开始
工具结果
下一轮模型文本
完成事件

如果前端只处理第一类文本事件,工具执行后生成的第二轮回答可能被忽略。

检查:

  • SSE事件类型;
  • 是否在工具轮次后继续订阅;
  • 是否过早调用takeUntil
  • 是否收到Complete;
  • Nginx是否断开长连接;
  • 客户端是否因空Chunk判定结束;
  • 取消信号是否传播到上游。

十二、Memory位置是否导致工具消息丢失

MessageChatMemoryAdvisor放在Tool Calling循环外部时,通常只持久化最终用户消息和Assistant消息。

如果业务需要完整工具轨迹,必须确认所用Memory Repository支持:

  • AI Tool Call Request;
  • Tool Response Message;
  • 多轮工具消息。

否则工具结果可能在当前请求可用,但下一轮对话无法恢复。

不要为了保存工具消息,盲目把Memory Advisor移入循环。还要同时处理:

  • 重复写入;
  • Repository序列化能力;
  • 上下文膨胀;
  • 敏感参数存储。

十三、最终回答是否被其他Advisor改变

调用链可能还有:

  • 内容审核;
  • 输出过滤;
  • 结构化输出验证;
  • 缓存;
  • 日志;
  • 自定义响应转换。

工具执行成功后,最终回答可能被:

  • 安全策略阻断;
  • Schema校验反复重试;
  • 缓存返回旧空结果;
  • 自定义Advisor提前替换;
  • 响应转换器解析失败。

建议记录每个Advisor的:

enter
exit
order
duration
response_present

十四、最终回答为空时的最小实验

第一步:固定工具返回值

@Tool(description = "返回测试订单")
public OrderSummary testOrder() {
    return new OrderSummary(
            "A1001",
            "SHIPPED",
            new BigDecimal("99.00"),
            "CNY",
            Instant.now()
    );
}

第二步:要求模型必须复述字段

调用testOrder,并返回orderId和status。

第三步:关闭其他Advisor

只保留Tool Calling。

第四步:分别测试call与stream

如果同步正常、流式失败,重点检查事件消费。

第五步:查看第二轮模型请求

确认Tool Response是否真的进入上下文。

十五、建议记录的观测字段

tool_call_id
tool_name
arguments_hash
execution_status
execution_duration_ms
result_serialization_status
result_size_bytes
tool_result_hash
loop_iteration
model_after_tool_called
final_response_present
stream_completed
advisor_chain

高风险业务还应记录:

idempotency_key
approval_id
operator
business_result_id

十六、完整排查清单

□ 工具没有返回null
□ 返回的是DTO而不是数据库Entity
□ 结果可以稳定序列化
□ 错误没有被伪装成成功字符串
□ 结果大小受到限制
□ ToolCallingAdvisor完成了第二轮模型调用
□ 没有重复注册ToolAdvisor
□ 结果包含terminal与retryable语义
□ 副作用工具使用数据库级幂等
□ 流式客户端没有漏掉工具后的回答
□ Memory与工具消息能力匹配
□ 其他Advisor没有替换最终响应
□ Trace中可以看到工具结果进入下一轮模型请求

总结

“工具调用成功但最终回答为空”说明问题已经越过工具选择阶段,应该重点检查:

返回值序列化
→ Tool Response写入
→ Tool Calling下一轮
→ 流式事件消费
→ 最终Advisor处理

只有把工具调用拆成完整阶段并逐段观测,才能判断结果究竟丢在了哪里。

延伸阅读

如果你正在关注企业级 AI 应用、Spring AI、RAG、Agent 与 MCP 工程化落地,欢迎访问 智元界

https://www.zyentor.com/

智元界将持续分享可运行的技术实战、架构设计、问题排查与企业应用案例。