让Spring AI学会查天气:函数调用实战与智能代理设计

想象一下,你的AI助手不仅能回答"北京今天天气如何",还能直接调用后台服务返回实时数据——这正是Spring AI的函数调用能力带来的变革。对于已经熟悉基础对话开发的Java工程师而言,掌握这项技能意味着能将大语言模型真正转化为业务场景中的智能代理。本文将手把手带你实现一个天气查询函数,并深入探讨这种模式在复杂系统中的设计哲学。

1. 为什么函数调用是AI集成的分水岭

传统API调用就像让盲人转述天气预报——开发者需要预先定义所有可能的查询参数和返回格式。而函数调用则是给AI装上了"感官系统",让它能自主决定何时调用、如何解析外部服务。这种能力转变带来了三个维度的提升:

  • 动态决策 :模型根据对话上下文自主触发函数,无需硬编码条件判断
  • 服务编排 :单个查询可串联多个函数调用(如先查天气再推荐穿搭)
  • 认知增强 :突破训练数据的时间限制,获取实时信息

在Spring AI中,这一切通过 FunctionCallback 机制实现。与直接调用REST API不同,你需要先教会AI两件事:

  1. 什么情况下应该调用这个函数(通过自然语言描述)
  2. 如何理解函数的输入输出(通过Schema定义)

2. 构建天气服务函数

我们先从核心的 MockWeatherService 实现开始。这个示例类展示了函数调用的标准契约:

public class WeatherFunction implements Function<WeatherFunction.Request, WeatherFunction.Response> {
    public record Request(String location, Unit unit) {}
    public record Response(double temp, Unit unit, String condition) {}
    
    public enum Unit { C, F }
    
    @Override
    public Response apply(Request request) {
        // 实际项目这里替换为真实API调用
        return new Response(
            ThreadLocalRandom.current().nextDouble(-10, 35),
            request.unit(),
            List.of("晴","多云","小雨").get(ThreadLocalRandom.current().nextInt(3))
        );
    }
}

关键设计要点:

  • 使用Java 16+的record类型定义DTO,减少样板代码
  • 温度单位采用枚举值,避免魔法字符串
  • 响应中增加天气状况描述,增强实用性

3. 函数注册与提示工程

注册函数时需要精心设计元数据,这直接影响AI的调用决策质量。下面是进阶配置示例:

FunctionCallbackWrapper.builder(new WeatherFunction())
    .withName("GetCurrentWeather")
    .withDescription("获取指定地点的实时天气数据,包括温度值和天气状况")
    .withInputType(WeatherFunction.Request.class)
    .withResponseConverter(response -> {
        // 自定义响应转换逻辑
        var temp = response.temp();
        return String.format("地点:%s 温度:%.1f°%s 天气:%s", 
            request.location(), temp, 
            response.unit().name(), 
            response.condition());
    })
    .build()

提示词设计技巧

  • 在用户问题中暗示需要实时数据(如"当前"、"最新"等关键词)
  • 使用 withFunction() 明确指定允许调用的函数
  • 示例对话:
    Prompt prompt = new Prompt(
        "对比上海和深圳当前天气,用摄氏度显示,分析哪个城市更适合户外活动",
        OpenAiChatOptions.builder()
            .withFunction("GetCurrentWeather")
            .withTemperature(0.2) // 降低随机性
            .build()
    );
    

4. 解析与错误处理实战

函数调用的响应需要特殊处理,完整流程应包含以下环节:

ChatResponse response = chatClient.call(prompt);
List<FunctionCall> functionCalls = response.getResults()
    .stream()
    .flatMap(r -> r.getMetadata().getFunctionCalls().stream())
    .toList();

if (!functionCalls.isEmpty()) {
    functionCalls.forEach(call -> {
        try {
            Object result = functionRegistry.get(call.getName())
                .apply(call.getArguments());
            // 将结果重新注入对话上下文
        } catch (Exception e) {
            log.error("函数调用失败: {}", call.getName(), e);
        }
    });
}

常见问题解决方案:

问题现象 可能原因 调试方法
函数未被调用 描述信息不清晰 检查withDescription是否准确
参数解析失败 Schema类型不匹配 验证record字段命名
响应格式错误 转换器逻辑异常 添加响应日志

5. 生产级扩展方案

基础实现后,还需要考虑以下工程化问题:

性能优化

  • 为函数调用添加缓存层(Caffeine+注解方案):
    @Cacheable(value = "weather", key = "#request.location()")
    public Response apply(Request request) {
        // 真实API调用
    }
    

熔断降级

  • 集成Resilience4j处理第三方服务超时:
    @CircuitBreaker(name = "weatherService", fallbackMethod = "fallbackWeather")
    public Response apply(Request request) {
        // ...
    }
    
    private Response fallbackWeather(Request request, Exception e) {
        return new Response(25.0, Unit.C, "数据暂不可用");
    }
    

监控指标

  • 通过Micrometer暴露关键指标:
    MeterRegistry.counter("ai.function.calls", "name", "weather")
        .increment();
    

6. 智能代理设计模式

将单一函数扩展为智能代理系统时,推荐采用以下架构:

用户请求 → 路由决策器 → 功能模块集
            ↑               ↓
        上下文记忆 ← 结果聚合器

具体实现策略:

  1. 按领域划分功能模块(天气、日历、电商等)
  2. 使用向量数据库维护对话历史
  3. 实现优先级调度算法处理并发调用

在Spring生态中,可以结合Spring StateMachine管理复杂对话状态:

StateMachine<State, Event> stateMachine = ...;
stateMachine.sendEvent(new FunctionCallEvent("weather"));

实际项目中,我们发现最影响体验的不是技术实现,而是函数描述的精确度。曾经因为将"获取天气"描述为"查询气象数据",导致AI在用户问"会下雨吗"时没有触发函数。这提醒我们:设计函数时要用最终用户的自然语言习惯,而不是开发者的技术术语。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐