比RAG更灵活?用SpringAI函数调用实现地铁查询系统:Qwen2.5+实时API对接指南
比RAG更灵活?用SpringAI函数调用实现地铁查询系统:Qwen2.5+实时API对接指南
当我们在构建一个需要与大模型深度集成的业务系统时,常常会面临一个核心选择:是让模型“记住”所有知识,还是教会它“调用”外部服务?对于静态的、历史的知识库,RAG(检索增强生成)技术无疑是强大的,它将文档切片、向量化后存入数据库,让模型在回答时能够“翻阅”这些资料。然而,当问题转向实时、动态的业务数据时,比如查询此刻的地铁拥挤度、下一班列车的到站时间,或者最新的票价政策,RAG就显得有些力不从心了。它的知识库是“冻结”的,更新需要重新走一遍ETL流程,无法应对瞬息万变的信息。
这时,函数调用(Function Calling) 的价值就凸显出来了。它让大模型从一个“博闻强识的学者”,转变为一个“懂得使用工具的专家”。模型本身不需要记住所有地铁线路的细节,它只需要理解用户的意图,并精准地调用我们预先定义好的业务API,获取最新的数据,再组织成自然语言回复给用户。这种模式,对于需要与实时业务系统交互的场景,提供了前所未有的灵活性和准确性。
本文将以一个地铁线路实时查询系统为具体案例,带你深入SpringAI的函数调用世界。我们将使用通义千问Qwen2.5作为本地大模型,一步步构建一个能够理解用户自然语言查询、动态调用模拟API、并返回结构化结果的智能助手。我们将重点剖析如何利用SpringAI的@Description注解实现意图识别与参数提取,对比RAG与函数调用的适用场景差异,并分享在模型选择、数据构造和集成调试中的关键要点与避坑指南。
1. 理解核心差异:RAG与函数调用的场景抉择
在深入代码之前,我们必须厘清RAG和函数调用各自最适合的战场。这并非一个“谁更好”的问题,而是“谁更合适”的选择。
RAG(检索增强生成) 的核心思想是知识内化。它通过一个离线的ETL(提取、转换、加载)流程,将你的私有文档(如PDF、Word、数据库表结构文档)转换成向量,存入向量数据库。当用户提问时,系统从向量库中检索出最相关的文档片段,作为“上下文”附加到用户的原始问题中,再一并提交给大模型。模型基于这份“参考资料”生成答案。
注意:RAG的优势在于处理非结构化、海量的静态知识。例如,查询公司内部的历史项目文档、产品规格说明书、法律法规条文等。它的知识库一旦建立,查询成本低,且能处理非常复杂的语义检索。
然而,RAG的短板也很明显:
- 数据实时性差:知识库更新滞后,需要定期重新运行ETL流程。
- 无法执行操作:它只能“告诉”你知识,不能“替”你执行任何操作,比如下单、查询账户余额、获取实时天气。
- 存在幻觉风险:如果检索到的上下文不相关或不足,模型可能基于错误信息“编造”答案。
相比之下,函数调用的核心是动作执行。我们预先定义好一系列工具函数(Tools),每个函数都有清晰的描述和参数格式。大模型的工作是理解用户请求,判断是否需要调用某个函数,如果需要,则严格按照我们定义的JSON Schema格式,提取出调用该函数所需的参数。我们的业务系统接收到这些参数后,执行真正的逻辑(如调用第三方API、查询数据库、执行计算),并将结果返回给模型,由模型最终组织成对用户的回复。
让我们用一个表格来直观对比:
| 特性维度 | RAG (检索增强生成) | Function Calling (函数调用) |
|---|---|---|
| 数据源 | 静态文档、历史数据 | 动态API、实时数据库、可执行服务 |
| 核心能力 | 知识检索与基于上下文的生成 | 意图识别、参数提取与动作执行 |
| 数据新鲜度 | 依赖ETL更新周期,延迟高 | 实时或近实时 |
| 适用场景 | 智能客服知识库、文档问答、历史数据分析 | 实时信息查询(天气、交通、股价)、业务操作(预订、支付、控制)、数据计算 |
| 系统复杂度 | 需要维护向量数据库和Embedding模型 | 需要定义清晰的函数接口和稳定的后端服务 |
| 响应流程 | 用户 -> 检索上下文 -> 大模型 -> 答案 | 用户 -> 大模型(识别意图&提取参数)-> 业务系统 -> 大模型(组织答案)-> 用户 |
对于我们的地铁查询系统,需求非常明确:用户问的是当前某个站点的线路信息。这些信息可能来自官方的实时API,或者一个动态更新的数据库。显然,函数调用是更自然、更高效的选择。我们不需要把全市所有地铁站点的信息都向量化存储,只需要教会模型:“当用户问‘XX站有什么线路’时,你就调用getSubwayStationDetails这个函数,并把站名参数传给我”。
2. 环境搭建与模型选择:Ollama与Qwen2.5
工欲善其事,必先利其器。我们选择在本地通过Ollama来运行大模型,这保证了数据的私密性和调用的低延迟。SpringAI对Ollama提供了开箱即用的支持。
首先,确保你的开发环境已经安装并运行了Ollama。然后,我们需要选择一个支持工具调用(Tool Calling) 功能的模型。并非所有模型都具备此能力。通义千问Qwen2.5系列模型在此方面表现优异,且对中文支持友好。
# 拉取Qwen2.5最新版本的模型(以7B参数为例,可根据机器配置选择)
ollama pull qwen2.5:7b
拉取完成后,你可以通过命令行测试一下模型的基础对话和工具调用理解能力。接下来,我们创建一个Spring Boot项目。在pom.xml中引入必要的依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
<version>0.8.1</version> <!-- 请使用当前最新稳定版 -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
在application.yml中配置Ollama连接和默认使用的模型:
spring:
ai:
ollama:
base-url: http://localhost:11434 # Ollama默认服务地址
chat:
options:
model: qwen2.5:7b # 指定使用我们拉取的模型
至此,基础环境就搭建完毕了。你可以写一个简单的@RestController来测试模型是否能正常对话,确保网络和配置无误。
3. 核心实现:使用@Description定义地铁查询函数
这是整个系统的灵魂所在。我们将利用SpringAI提供的@Description注解,以一种声明式的方式,向大模型“介绍”我们的业务函数。
我们计划实现一个函数:getSubwayStationDetails。它的作用是“根据地铁站名称,查询该站点经过的所有地铁线路信息”。我们需要明确告诉模型三件事:
- 函数是干什么的:用
@Description描述。 - 函数需要什么:定义一个请求记录(Record),其字段就是所需的参数。
- 函数返回什么:定义一个响应记录(Record),包含返回的数据结构。
下面是一个完整的工具类配置:
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;
import java.util.List;
@Configuration
public class SubwayToolConfiguration {
/**
* 定义请求参数结构。
* 大模型会尝试从用户输入中提取`stationName`字段。
*/
public record SubwayStationDetailsRequest(String stationName) {}
/**
* 定义响应结果结构。
* 包含站名、线路总数和线路名称列表。
*/
public record SubwayStationDetails(
String stationName,
Integer totalSubwayLine,
List<String> subwayLineNameList
) {}
/**
* 核心:注册一个名为“getSubwayStationDetails”的函数回调。
* @Description 注解中的文本至关重要,它直接指导大模型何时调用此函数。
*/
@Bean
@Description("根据提供的地铁站名称,获取该站点所有经过的地铁线路信息。例如,用户问'天府广场有哪些地铁线?',你就应调用此函数。")
public FunctionCallback getSubwayStationDetailsFunction() {
return FunctionCallbackWrapper.builder(new SubwayStationDetailsService()) // 绑定实际服务类
.withName("getSubwayStationDetails") // 函数唯一标识名
.withDescription("查询指定地铁站的线路详情")
.withResponseConverter((response) -> response) // 响应转换器,这里直接返回
.build();
}
/**
* 实际执行业务逻辑的服务类。
* 这里使用Mock数据模拟,真实场景应调用外部API或查询数据库。
*/
public static class SubwayStationDetailsService {
public SubwayStationDetails apply(SubwayStationDetailsRequest request) {
String stationName = request.stationName();
// 模拟数据查询逻辑
if (stationName.contains("天府广场")) {
return new SubwayStationDetails(stationName, 2, List.of("地铁1号线", "地铁2号线"));
} else if (stationName.contains("春熙路")) {
return new SubwayStationDetails(stationName, 3, List.of("地铁2号线", "地铁3号线", "地铁10号线"));
} else if (stationName.contains("火车南站")) {
return new SubwayStationDetails(stationName, 4, List.of("地铁1号线", "地铁7号线", "地铁18号线", "地铁33号线(在建)"));
} else {
// 模拟未找到的情况
return new SubwayStationDetails(stationName, 0, List.of("未找到该站点的线路信息"));
}
}
}
}
关键点解析:
@Description注解:这是与大模型沟通的桥梁。描述要清晰、具体,最好包含示例(如“用户问'天府广场有哪些地铁线?'”)。好的描述能极大提高模型识别意图的准确率。- 请求/响应记录(Record):使用Java Record定义数据结构,简洁明了。SpringAI会自动将其转换为JSON Schema供模型理解。字段名应语义清晰。
FunctionCallbackWrapper:这是SpringAI提供的构建器,用于将我们的普通Java方法包装成模型可以调用的“工具”。withName必须唯一。- Mock服务:在
SubwayStationDetailsService中,我们暂时用简单的条件判断返回模拟数据。这是开发初期的常见做法,便于快速验证函数调用链路是否通畅。后续可以轻松替换为真正的HTTP客户端调用或数据库查询。
4. 集成与对话:在ChatClient中调用工具
函数定义好了,接下来就需要在对话中启用它。SpringAI的ChatClient提供了流畅的API来集成工具调用。
我们创建一个ChatService,它负责处理用户的消息,并自动管理工具调用的流程。
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
@Service
public class SubwayChatService {
private final ChatClient chatClient;
// 通过构造器注入ChatClient,SpringAI会自动装配我们定义的FunctionCallback
public SubwayChatService(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
/**
* 单次调用,获取完整的ChatResponse,便于调试。
*/
public ChatResponse chatWithTools(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.call() // 触发对话,模型会决定是否以及如何调用工具
.chatResponse();
}
/**
* 流式调用,更适合前端展示,可以实时看到模型“思考”和调用工具的过程。
*/
public Flux<String> chatStreamWithTools(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.stream() // 改为流式调用
.content();
}
/**
* 更精细的控制:指定系统指令,影响模型行为。
*/
public String chatWithSystemInstruction(String userMessage) {
String systemInstruction = """
你是一个专业的地铁信息查询助手。你的回答应准确、简洁、友好。
当用户询问地铁站线路时,你必须使用可用的工具(函数)来获取实时信息,不得凭空编造。
如果工具返回的结果显示未找到信息,请如实告知用户。
""";
return chatClient.prompt()
.system(s -> s.text(systemInstruction)) // 设置系统指令
.user(userMessage)
.call()
.content();
}
}
然后,我们创建一个简单的REST控制器来暴露接口:
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
@RestController
@RequestMapping("/api/subway")
public class SubwayQueryController {
private final SubwayChatService chatService;
public SubwayQueryController(SubwayChatService chatService) {
this.chatService = chatService;
}
@PostMapping("/query")
public String query(@RequestBody QueryRequest request) {
// 调用非流式接口
return chatService.chatWithSystemInstruction(request.message());
}
@GetMapping(value = "/stream", produces = "text/event-stream")
public Flux<String> streamQuery(@RequestParam String message) {
// 调用流式接口,适合前端用EventSource连接
return chatService.chatStreamWithTools(message);
}
public record QueryRequest(String message) {}
}
现在,启动你的Spring Boot应用。使用Postman或curl进行测试:
curl -X POST http://localhost:8080/api/subway/query \
-H "Content-Type: application/json" \
-d '{"message": "请问天府广场通哪几条地铁?"}'
如果一切正常,你将看到类似以下的响应过程(在日志或流式响应中会更清晰):
- 模型理解问题,识别出需要调用
getSubwayStationDetails工具。 - 模型生成一个格式严格的JSON请求,如
{"stationName": "天府广场"}。 - SpringAI框架截获此请求,调用我们定义的
SubwayStationDetailsService.apply方法。 - 服务返回Mock数据:
{"stationName":"天府广场", "totalSubwayLine":2, "subwayLineNameList":["地铁1号线","地铁2号线"]}。 - 模型收到工具执行结果,组织成最终的自然语言回复:“天府广场站共有2条地铁线路经过,分别是地铁1号线和地铁2号线。”
5. 避坑要点与进阶优化
在实际开发中,你可能会遇到一些挑战。以下是一些关键要点和优化建议:
1. 模型选择与提示工程
- 工具调用支持:务必确认你使用的模型版本支持工具调用。Qwen2.5、GPT-4、Claude等主流模型都支持。
- 系统指令优化:
system指令是引导模型行为的有力工具。明确的指令可以减少模型“偷懒”(不调用工具直接回答)或错误调用的情况。例如,强调“必须使用工具查询实时信息”。 - 描述的精炼:
@Description和工具withDescription的文本要互补。前者告诉模型“什么时候用”,后者告诉模型“这是什么工具”。两者结合效果更好。
2. 从Mock到真实API的对接 Mock只是第一步。真实场景中,你需要调用地铁官方API或内部数据服务。
import org.springframework.web.client.RestTemplate;
import org.springframework.beans.factory.annotation.Value;
@Service
public class RealSubwayService {
private final RestTemplate restTemplate;
@Value("${subway.api.base-url}") private String apiBaseUrl;
public SubwayStationDetails findRealStationDetails(String stationName) {
// 1. 可能需要对站名进行清洗或编码
String cleanedName = cleanStationName(stationName);
// 2. 调用外部API
String url = String.format("%s/stations/%s/lines", apiBaseUrl, cleanedName);
ResponseEntity<ApiResponse> response = restTemplate.getForEntity(url, ApiResponse.class);
// 3. 将API响应映射到我们的SubwayStationDetails记录
return mapToDetails(stationName, response.getBody());
}
// ... 其他辅助方法
}
3. 错误处理与降级策略
- 网络超时与重试:调用外部API必须设置合理的超时和重试机制。
- 参数提取失败:模型可能无法从用户模糊的表述中提取出准确的
stationName。你需要设计降级策略,例如,让模型进行澄清追问(“您想查询哪个地铁站呢?”),这可以通过在函数调用链中处理null或非法参数来实现。 - 工具调用结果为空:当工具返回“未找到”时,模型应如何回复?这需要在系统指令或后续的提示中加以引导。
4. 复杂参数与多工具协作 一个复杂的查询可能涉及多个参数或多个工具。例如,“从天府广场坐地铁去火车南站怎么换乘最快?”。这可能需要:
- 单个函数,复杂参数:定义一个
calculateRoute函数,参数为startStation和endStation。 - 多个函数协作:先调用
getStationDetails获取两个站点的线路,再调用另一个findTransferPlan函数计算换乘方案。SpringAI支持在单次对话中依次调用多个工具。
5. 可观测性与调试 函数调用的过程比普通对话复杂。务必加强日志记录。
- 记录模型生成的原始工具调用请求JSON。
- 记录工具执行的结果。
- 记录模型的最终回复。这有助于在出现问题时,精准定位是模型意图识别错误、参数提取错误,还是后端服务逻辑错误。
函数调用技术为我们打开了一扇新的大门,让大模型能够安全、可控地与真实世界进行交互。它弥补了RAG在实时性和操作性上的不足,尤其适合构建需要查询动态数据或触发具体业务流程的AI应用。通过SpringAI简洁的注解和API,我们可以快速将这种能力集成到Java生态系统中。从地铁查询出发,这套模式可以轻松扩展到智能家居控制、电商订单查询、金融数据播报等无数场景。关键在于清晰地定义工具、编写准确的描述,并构建可靠的后端服务。剩下的,就交给大模型去理解和调度吧。
更多推荐



所有评论(0)