【Spring Boot 接通义千问,5 分钟跑通第一个 AI 对话 】
Spring AI Alibaba 实战(一):5 个概念 + 第一次跟 AI 对话
这是我学习 Spring AI Alibaba 的第一篇记录。
目标很简单:搞清楚几个必须知道的 AI 概念,然后用 Spring Boot 跑通第一个对话程序。
理论篇
一、Java 工程师为什么能搞 AI Agent
开始学之前,我最大的顾虑是:“AI 不都是 Python 的吗?我一个 Java 开发,能搞得了?”
后来想明白了。AI Agent 的核心不是训练模型,而是工程化——把大模型的能力串起来,做成一个能干活的系统。这恰恰是 Java 工程师擅长的:
- 工具注册和调度 → 你写了多年的 Spring 依赖注入
- 多步编排和状态管理 → 你熟悉的工作流引擎
- 高可用、可观测、分布式 → Java 生态的强项
- 企业级落地 → Spring Boot 就是事实标准
所以问题不是"Java 能不能搞 AI",而是"有没有好用的框架"。Spring AI Alibaba 就是答案——Spring 团队和阿里一起做的,API 风格跟 Spring 全家桶一脉相承。
二、开始写代码之前,你得搞懂这 5 个概念
不需要懂 Transformer 和 Attention 机制。但下面 5 个概念直接影响你写代码时的每一个决策,跳过任何一个后面都会踩坑。
2.1 Token——LLM 的计费单位
Token 不是"字"也不是"词",是模型处理文本的最小单位。中文大约 1 个字 ≈ 1-2 个 Token。
"我想查北京到上海的机票"
→ 大约 12-15 个 Token
为什么要关心这个?因为它影响三件事:
| 影响 | 说明 |
|---|---|
| 钱 | API 按 Token 数计费,输入和输出分别算 |
| 容量 | 一次对话能装的信息有上限(Context Window) |
| 速度 | Token 越多,响应越慢 |
通义千问的价格参考(2025):
| 模型 | 输入(每百万 Token) | 输出(每百万 Token) | 上下文长度 |
|---|---|---|---|
| qwen-turbo | ¥2 | ¥6 | 128K |
| qwen-plus | ¥4 | ¥12 | 128K |
| qwen-max | ¥20 | ¥60 | 32K |
💡 开发建议:调试用 qwen-turbo(便宜),上线用 qwen-plus(均衡),核心场景用 qwen-max(最强)。
2.2 Prompt——你给 LLM 的指令
Prompt 是你发给大模型的文本。它不是简单的"输入",而是有结构的:
| 角色 | 作用 | 示例 |
|---|---|---|
| System | 设定 AI 的"人格"和规则 | “你是一个专业机票分析师,只回答机票相关问题” |
| User | 用户的实际问题 | “帮我查北京到上海明天的机票” |
| Assistant | 模型之前的回答(用于多轮对话) | “已为您查到以下航班…” |
System Prompt 是 Agent 的灵魂。后面你会发现,很多时候调 System Prompt 比改代码有用 10 倍。
2.3 Temperature——控制输出的"稳定性"
Temperature 是一个 0 到 1 之间的参数,决定了 LLM 回答的随机程度。用 Java 来类比:temperature=0 就像一个确定性的 switch-case,给同样的输入永远走同一个分支;temperature=1 更像 Random.nextInt(),每次执行结果都不一样。
| Temperature | 效果 | 适用场景 |
|---|---|---|
| 0 | 每次回答几乎相同 | Agent 工具调用、数据分析 |
| 0.3 | 轻微变化 | Agent 推荐值 |
| 0.7 | 默认值,有一定随机性 | 普通对话 |
| 1.0 | 每次都不一样 | 创意写作 |
怎么选一个合理的值? 看你的业务对"一致性"的要求:
| 场景 | 推荐值 | 原因 |
|---|---|---|
| Function Calling / 工具调用 | 0 | 参数必须精确,随机性会导致 JSON 格式错误或传错参数 |
| Agent 决策与推理 | 0 - 0.3 | 推理链路要稳定,但允许微小灵活性 |
| 客服 / FAQ 问答 | 0.3 - 0.5 | 回答要准确,但措辞可以自然一点,不要像机器人 |
| 普通对话 / 聊天 | 0.7 | 平衡准确性和趣味性,大多数模型的默认值 |
| 营销文案 / 创意写作 | 0.8 - 1.0 | 需要发散思维,每次生成不同的表达 |
一个实用的定值策略:先从 0.7(默认值)开始,发现输出太"飘"就往下调,太"死板"就往上调。 线上环境建议把 temperature 写进配置文件而不是硬编码,方便随时调整。
⚠️ 重点:Agent 中涉及工具调用、结构化输出的环节,建议用较低的 temperature(0-0.3)。但 Agent 不只有工具调用——如果某个环节需要生成面向用户的自然语言回复(比如总结、推荐理由),适当提高到 0.5-0.7 反而更自然。关键是按环节分别设置,而不是整个 Agent 一刀切。
2.4 Context Window——对话的"内存大小"
Context Window 是一次对话能容纳的总 Token 数。所有东西都要塞进去:System Prompt、历史对话、当前问题、工具描述、模型回答。

超出上限怎么办?旧的对话会被截断,模型会"忘掉"之前聊过的内容。这就是为什么后面要专门学 Memory 管理(第 4 章)。
2.5 Function Calling——Agent 的核心引擎
这是最重要的概念。没有 Function Calling,就没有 Agent。
传统 LLM 只能输出文本。Function Calling 让 LLM 能够"调用函数"——准确说,是 LLM 告诉你它想调用什么函数、传什么参数,真正的执行由你的代码完成。

关键点:
- LLM 不直接调用 API——它只输出 JSON 格式的调用意图
- 你的代码负责执行——接收意图,调真正的 API
- 结果喂回给 LLM——LLM 拿到结果后,组织成自然语言回复
用 Java 类比:LLM 就像一个"智能路由器",根据用户请求决定调用哪个 Service 方法,但它自己不执行方法。你写的 @Service 才是干活的。
三、Spring AI 的分层架构
Spring AI 的设计跟 Spring Data 一个思路——统一抽象,多种实现:

面向 ChatModel 接口编程,底层换成任何 LLM 都不需要改业务代码。跟你用 JPA 换数据库一个道理。
而 ChatClient 是 ChatModel 上面的高层封装,关系类似 JdbcTemplate 和 DataSource。日常开发用 ChatClient 就够了,链式调用、拦截器、流式输出都支持。
Spring AI Alibaba 在这套抽象之上,提供了通义千问的完整实现,加上百炼平台集成、增强 RAG 等额外能力。
四、幻觉——LLM 一本正经地胡说
这个必须单独拿出来说,因为它是 Agent 开发中最容易踩的坑。
LLM 会非常自信地给出完全编造的答案:
用户: "国航 CA1234 航班什么时候起飞?"
LLM: "国航 CA1234 航班每天 08:30 从北京首都机场起飞" ← 完全编造的
因为 LLM 本质是"文本接龙"——预测最可能的下一个 Token,不是查事实。
这也是 Agent 存在的核心原因之一:Agent 通过 Function Calling 获取真实数据,而不是让 LLM 瞎编。我们的机票比价 Agent 不会"编造"票价,而是调用真实 API 查询。
实战篇
五、跑通第一个对话程序
5.1 申请通义千问 API Key
- 访问阿里云百炼平台
- 开通"模型服务灵积"
- 在"API-KEY 管理"中创建 Key
- 记住这个 Key,后面要用
💡 新账号有免费额度,够学完这个系列。
5.2 项目结构
整个系列用 Maven 父子工程管理,每章一个子模块:
spring-ai-alibaba-course/
├── pom.xml ← 父 POM(BOM 版本管理)
├── quick-start/ ← 本章代码
│ ├── pom.xml
│ └── src/main/java/com/ai/course/quickstart/
│ ├── QuickStartApplication.java
│ └── controller/
│ └── ChatController.java
├── chat-client/ ← 第 2 章(后续添加)
├── function-calling/ ← 第 3 章(后续添加)
└── ... ← 每章一个子模块
父 POM 的核心配置(完整代码见仓库):
<properties>
<java.version>21</java.version>
<spring-boot.version>3.4.3</spring-boot.version>
<spring-ai-alibaba.version>1.0.0.2</spring-ai-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Spring Boot BOM -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring AI Alibaba BOM -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-bom</artifactId>
<version>${spring-ai-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
为什么不用
spring-boot-starter-parent做 parent?因为它会锁死 parent 位置。通过 BOM 导入,既统一了版本,又保留了自定义父 POM 的自由度。
子模块 POM(quick-start/pom.xml)——注意依赖不写版本号,全部由父 POM 管理:
<parent>
<groupId>com.ai.course</groupId>
<artifactId>spring-ai-alibaba-course</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>quick-start</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
</dependencies>
5.3 配置文件
# quick-start/src/main/resources/application.yml
spring:
application:
name: quick-start
ai:
dashscope:
api-key: ${AI_DASHSCOPE_API_KEY} # 通过环境变量注入,别硬编码!
chat:
options:
model: qwen-plus
temperature: 0.7
server:
port: 8080
⚠️ 安全提醒:API Key 永远不要写在代码里。设置环境变量:
- Linux/Mac:
export AI_DASHSCOPE_API_KEY=sk-xxxxx- Windows:
set AI_DASHSCOPE_API_KEY=sk-xxxxx- IDEA:Run Configuration → Environment variables
5.4 启动类
package com.ai.course.quickstart;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class QuickStartApplication {
public static void main(String[] args) {
SpringApplication.run(QuickStartApplication.class, args);
}
}
5.5 第一个对话接口
Spring AI 推荐用 ChatClient(高层 API)而不是底层的 ChatModel。关系就像 JdbcTemplate 和 DataSource:
package com.ai.course.quickstart.controller;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
@RestController
@RequestMapping("/api/chat")
public class ChatController {
// 用 ChatClient 而不是 ChatModel —— 链式 API 更简洁,支持 Advisor 拦截
private final ChatClient chatClient;
public ChatController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.defaultSystem("你是一个友好的AI助手。")
.build();
}
/**
* 同步对话
* 试试:GET /api/chat?message=你好
*/
@GetMapping
public String chat(@RequestParam(defaultValue = "你好") String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
/**
* 流式对话 —— 打字机效果
* 试试:GET /api/chat/stream?message=介绍一下春季热门航线
*/
@GetMapping(value = "/stream", produces = "text/event-stream;charset=UTF-8")
public Flux<String> chatStream(
@RequestParam(defaultValue = "你好") String message) {
return chatClient.prompt()
.user(message)
.stream()
.content();
}
}
启动项目,打开浏览器访问 http://localhost:8080/api/chat?message=你好,你应该能看到通义千问的回复。
流式接口 /api/chat/stream 会一个字一个字地吐出来,体验明显好很多。LLM 生成完整回答可能要 5-10 秒,流式让用户边生成边看到,不用干等。
5.6 Temperature 对比实验
理论篇讲了 Temperature 影响输出稳定性,现在亲眼看看。加一个实验接口:
package com.ai.course.quickstart.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.concurrent.CompletableFuture;
@RestController
@RequestMapping("/api/experiment")
public class ExperimentController {
private static final Logger log = LoggerFactory.getLogger(ExperimentController.class);
private final ChatClient chatClient;
public ExperimentController(ChatClient.Builder builder) {
// 通过 System Prompt 限制回答长度,让对比更直观
this.chatClient = builder
.defaultSystem("你是一个简洁的助手,回答限制在20字以内。")
.build();
}
/**
* Temperature 对比实验
* GET /api/experiment/temperature?message=用一个词形容春天
*
* 同一问题,分别用 temperature 0 和 1.0 各调用 3 次
* temperature=0 的回答几乎相同,temperature=1.0 每次都不一样
*/
@GetMapping("/temperature")
public Map<String, List<String>> temperatureExperiment(
@RequestParam(value = "message", defaultValue = "用一个词形容春天") String message) {
double[] temperatures = {0.0, 1.0};
int rounds = 3;
log.info("开始 Temperature 对比实验,消息:{}", message);
Map<String, CompletableFuture<List<String>>> futures = new LinkedHashMap<>();
for (double temp : temperatures) {
String key = "temperature_" + temp;
futures.put(key, CompletableFuture.supplyAsync(() -> {
List<String> responses = new ArrayList<>();
for (int i = 0; i < rounds; i++) {
log.info("[{}] 第 {} 次调用开始...", key, i + 1);
long start = System.currentTimeMillis();
String content = chatClient.prompt()
.user(message)
.options(ChatOptions.builder()
.temperature(temp)
.build())
.call()
.content();
long cost = System.currentTimeMillis() - start;
log.info("[{}] 第 {} 次调用完成,耗时 {}ms,回答:{}", key, i + 1, cost, content);
responses.add(content);
}
return responses;
}));
}
Map<String, List<String>> results = new LinkedHashMap<>();
futures.forEach((key, future) -> results.put(key, future.join()));
log.info("Temperature 对比实验完成");
return results;
}
}
直接访问 GET /api/experiment/temperature(默认问题"用一个词形容春天"),你会看到类似结果:
{
"temperature_0.0": ["温暖", "温暖", "温暖"],
"temperature_1.0": ["生机", "温柔", "绚烂"]
}
- temperature=0:三次回答完全一样——输出稳定可预测
- temperature=1.0:每次回答都不同——充满随机性和创意
这就是为什么 Agent 场景要用低 temperature——你需要 LLM 的输出稳定可预测
六、与机票比价 Agent 的集成
本章搭建的 quick-start 是整个系列的起点。后面的章节会在这个基础上逐步加能力:
- 第 2 章:用 ChatClient 的 Advisor 和 Prompt 模板,让对话更可控
- 第 3 章:通过 Function Calling 接入机票查询工具,从"编造数据"进化为"查真实数据"
- 第 4 章:加 Memory,实现多轮对话(“北京飞上海” → “明天的” → “最便宜的”)
当前模块的配置(模型、温度)和父 POM 的依赖管理,会被所有后续模块复用。
七、FAQ 与踩坑记录
Q1:启动报错 No qualifying bean of type 'ChatModel'
最常见的原因是缺依赖或者环境变量没设:
- 确认 pom.xml 里有
spring-ai-alibaba-starter-dashscope - 确认环境变量
AI_DASHSCOPE_API_KEY已设置且值正确 - 确认 application.yml 里用的是
${AI_DASHSCOPE_API_KEY}而不是写死的 Key
Q2:调用报 401 Unauthorized 或 InvalidApiKey
Key 无效或过期。到百炼平台检查 Key 状态。新账号第一次使用需要先开通百炼服务并创建 API Key。另外看看环境变量里的 Key 有没有多余的空格或换行。
Q3:流式接口返回乱码
三个排查方向:
@GetMapping的produces必须是text/event-stream;charset=UTF-8- 如果有 Nginx 反代,加
proxy_buffering off; - 浏览器直接访问 SSE 显示可能不友好,用
curl或前端EventSource测
Q4:第一次调用特别慢(10 秒以上)
正常。第一次请求涉及到底层 HTTP 连接建立、模型加载等。后续请求会快很多。如果持续很慢,检查网络是否需要代理。
本章小结
| 理论篇 | 实战篇 |
|---|---|
| Token 与计费 | 环境搭建 |
| Prompt 三种角色 | 同步对话 |
| Temperature 稳定性 | 流式输出 |
| Context Window 容量 | Temperature 实验 |
| Function Calling 核心 |
下一章:ChatClient 的高级用法和 Prompt 工程。会用到 Advisor 拦截器、Prompt 模板、结构化输出——让 LLM 的返回直接变成 Java 对象。
如果这篇文章对你有帮助,欢迎点赞收藏。有问题欢迎评论区交流。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)