在智慧社区项目中,我尝试用 Spring AI 集成了通义千问大模型,实现了一个 AI 导购助手。本文记录如何通过 Function Calling 让大模型“学会”调用商品查询等业务接口,并结合 Redis 二级缓存与 SSE 流式响应,在成本和体验之间取得平衡。

一、背景:AI 导购不能只会简单“聊天”

最开始接入大模型时,我让它直接回答用户的问题,比如“推荐一些低糖水果”。大模型会根据训练数据生成一段看起来很合理的回答,但问题在于——它推荐的“低糖水果”可能根本不在我的商品库里。(我是齐夏,我要开始说谎了)

要让 AI 真正成为导购助手,它必须能获取实时、真实的业务数据。这就需要让大模型在需要时,主动调用我们提供的业务接口。

Function Calling 正是为此而生。

二、什么是 Function Calling?

Function Calling 是各大模型厂商提供的一种能力:大模型根据用户输入,判断是否需要调用外部函数,如果需要,就返回一个结构化的 函数调用请求(JSON 格式),由应用层去执行真正的业务逻辑,最后将执行结果再喂给大模型,生成最终的自然语言回复。

流程如下:

用户: "推荐低糖水果"
   ↓
大模型: 判断需要调用商品查询函数
   ↓ 返回函数调用请求
   {
     "name": "recommendProducts",
     "arguments": { "requirement": "低糖水果" }
   }
   ↓
应用层: 执行 recommendProducts(requirement) 查询数据库/ES
   ↓ 返回商品列表
   [
     { "name": "圣女果", "price": 12.9, "sugar": "低" },
     { "name": "蓝莓", "price": 29.9, "sugar": "低" }
   ]
   ↓
大模型: 将商品列表组织成自然语言回复
   ↓
用户: "为您推荐以下低糖水果:圣女果(12.9元/盒)、蓝莓(29.9元/盒)……"

这样一来,大模型就不会回答不存在的东西,从“只会聊天的机器人”升级为“能调用业务系统的智能助手”。

哇,既然如此好用!那么到底该如何实现呢?会不会很复杂...?

下文会给出答案

三、Spring AI 中的实现

Spring AI 对 Function Calling 提供了非常简洁的封装,我们只需要定义一个普通的 Spring Bean,并在方法上标注 @Tool 注解即可。

3.1 定义工具函数

@Component
public class ProductTools {
    private final ProductService productService;

    public ProductTools(ProductService productService) {
        this.productService = productService;
    }
    @Tool(description = "根据用户需求推荐商品,返回商品名称、价格和糖分含量")
    public List<ProductDto> recommendProducts(String requirement) {
        // 调用业务层查询商品(可能是数据库或 Elasticsearch)
        List<Product> products = productService.searchByRequirement(requirement);
        return products.stream()
                .map(p -> new ProductDto(p.getName(), p.getPrice(), p.getSugarContent()))
                .toList();
    }

    public record ProductDto(String name, BigDecimal price, String sugarContent) {}
}

关键点:

  • @Tool 注解中的 description 非常重要,它会被发送给大模型,帮助模型理解这个函数何时该调用、参数含义是什么。

  • 方法签名会自动生成 Function Schema,无需手动拼接 JSON。

3.2 在对话中注册工具

@Service
public class AiAssistantService {

    private final ChatClient chatClient;
    private final ProductTools productTools;

    public AiAssistantService(ChatClient.Builder builder, ProductTools productTools) {
        this.chatClient = builder.build();
        this.productTools = productTools;
    }

    public String chat(String userMessage) {
        return chatClient.prompt()
                .user(userMessage)
                .tools(productTools)   // 注册工具函数
                .call()
                .content();
    }
}

当用户发送“推荐低糖水果”时,Spring AI 会自动:

  1. ProductTools 中所有 @Tool 方法的描述发送给大模型。

  2. 大模型若决定调用 recommendProducts,Spring AI 会反射执行该方法。

  3. 将执行结果自动回传大模型,并获取最终回答。

整个过程对开发者透明,只需关注业务逻辑的实现。

四、二级缓存:让高频问题不再“烧钱”

大模型 API 是按 Token 计费的。如果每个用户都问一遍“几点开门”,每次都调用大模型,token如流水,供不起啊。

于是我在 AI 模块中设计了一个 二级缓存策略

缓存层级

存储位置

内容

一级缓存

本地内存

预设高频问答模板(如“几点开门”→“7:00-22:00”)

二级缓存

Redis

用户历史提问的完整回答(Key 为问题内容的 MD5)

实现逻辑:

public String chatWithCache(String userMessage) {
    // 1. 一级缓存:预设模板匹配
    String preset = presetCache.match(userMessage);
    if (preset != null) {
        return preset;
    }
    // 2. 二级缓存:Redis 查询
    String cacheKey = "ai:qa:" + DigestUtils.md5Hex(userMessage);
    String cached = redisTemplate.opsForValue().get(cacheKey);
    if (cached != null) {
        return cached;
    }
    // 3. 缓存未命中,调用大模型
    String response = chatClient.prompt()
            .user(userMessage)
            .tools(productTools)
            .call()
            .content();
    // 4. 存入 Redis,过期时间 24 小时
    redisTemplate.opsForValue().set(cacheKey, response, 24, TimeUnit.HOURS);
    return response;
}

这样可以极大减少宝贵的token。(防御性编程:守护自己的钱袋子)

五、SSE 流式响应:让等待不再焦虑

大模型生成回答通常需要 2~5 秒,如果采用同步请求,用户会盯着空白页面等待,体验很差。

流式响应还是很常见的,平时用到的大模型都是如此,如果你喜欢神秘感自己可以不用,不过为了用户体验,加上!

Spring AI 支持 流式响应(Streaming),基于 SSE(Server-Sent Events)协议,可以逐字将生成内容推送到前端。

Controller 实现:

@GetMapping(value = "/api/ai/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(@RequestParam String message) {
    return chatClient.prompt()
            .user(message)
            .tools(productTools)
            .stream()               // 关键:使用 stream() 而非 call()
            .content()
            .map(chunk -> "data: " + chunk + "\n\n");  // SSE 格式
}

前端接收(JavaScript):

const eventSource = new EventSource('/api/ai/chat/stream?message=推荐水果');
eventSource.onmessage = (event) => {
    document.getElementById('output').innerHTML += event.data;
};
eventSource.addEventListener('done', () => eventSource.close());

流式响应的 首字延迟 通常在 500ms 以内,用户能立刻看到 AI 正在“打字”,心理等待时间大幅缩短。

六、存在的局限与后续计划

  • 目前仅集成了商品查询一个工具,运营端文案生成等工具还在开发中。

  • 大模型偶尔会出现“幻觉”,返回不在缓存也不在函数调用范围内的回答,后续计划引入 RAG(检索增强生成) 进一步约束回答范围。(请关注后续玩转AI内容)

  • 成本仍需控制,考虑对用户每日调用次数做限制。

七、总结

Function Calling 是连接大模型与业务系统的桥梁。通过 Spring AI 的简洁封装,我在社区项目中快速落地了一个可用的 AI 导购模块,并通过缓存和流式响应优化了成本与体验。

这让我深刻体会到:AI 落地的关键不在于模型本身有多强,而在于如何将它无缝嵌入现有的业务系统,并在性能、成本、体验之间找到平衡。

关于 AI 的一点随想:

AI 是一个强大的工具,但工具的价值在于使用它的人,重要的是你如何应用他,不要害怕会被他取代,业务才是Java!

欢迎交流讨论