Spring AI 概念速通:把 Prompt / 结构化输出 / RAG / Tool Calling 串成一条"可落地"的开发链路
- 前置知识:Java/Spring Boot 后端开发;会写 Controller/配置文件;了解一点点"提示词/大模型"即可。
- 本文基于:原文链接 https://docs.spring.io/spring-ai/reference/concepts.html(Spring AI Reference 1.1.2 ;章节范围:Models→Evaluating AI responses) (Home)
1. 这节解决的真实问题是什么(场景化)
你在业务里接入大模型,真正卡你的通常不是"能不能调通",而是这些问题:
- 接口要稳定:AI 调用超时、抖动、限流、偶发 429/5xx,你怎么重试/降级?
- 输出要可用 :你要的是
Ticket{type, priority, summary},但模型回你一段散文;"让它输出 JSON"也不一定靠谱 - 知识要新且可控 :模型训练数据会"过期",你需要把私有文档/数据库塞进它的上下文(RAG / prompt stuffing)
- 要能办事:不仅回答,还要能查订单/改库存/拉工单------这就进入 Tool Calling(工具调用)
- 要能评估:上线后你得知道回答是否贴合意图、是否"胡说八道",要有可自动化的评测入口
2. 官方在讲什么(你自己的话讲清楚)
下面把 concepts 这一页的"概念"翻译成工程语言(每条后面标注原文小节):
- 模型不止 Chat :Spring AI 讨论的是一类"模型家族"(语言/图像/音频/Embedding 等),你要先选输入输出形态再谈落地(Models) (Home)
- Prompt 不是一段字符串 :在很多模型里,prompt 由多条消息组成,并且有 role(system / user 等),system 用来"定规矩/设上下文"(Prompts) (Home)
- Prompt Template 是工程能力 :提示词要参数化,把用户输入、安全约束、业务上下文拼成可复用模板;Spring AI 使用 StringTemplate 作为模板引擎(Prompt Templates) (Home)
- Embedding = 可计算的语义坐标 :把文本/图像等变成向量,用"距离"做相似度,最典型用途就是语义检索与 RAG(Embeddings) (Home)
- Tokens 是成本与上限 :token 既决定"能塞多少上下文"(context window),也决定"要花多少钱"(Tokens = Money)------输入输出都会计费(Tokens) (Home)
- 结构化输出是"字符串→对象"的转换问题 :模型输出天然是
String;就算你要求 JSON,它也可能不稳定;因此需要"结构化输出转换"(Structured Output) (Home) - 给模型"补知识"三条路 :Fine-tuning(重且贵)、Prompt stuffing(把相关数据塞进 prompt)、Tool Calling(让模型调用你的 API);Spring AI 明确把 prompt stuffing 视为 RAG 的一种实现路径(Bringing Your Data & APIs...) (Home)
- RAG 的工程核心在 ETL + 分块 :先把文档拆成语义边界合理的小块,再控制每块大小占 token 上限的一小部分;查询时把"问题 + 相似块"塞回 prompt(Retrieval Augmented Generation) (Home)
- Tool Calling 是一条对话链路 :你要把工具定义(名称/描述/入参 schema)随请求发给模型;模型返回要调用的工具与参数;应用执行后再把结果回传给模型生成最终答案(Tool Calling) (Home)
- 评估不是玄学 :Spring AI 提供
EvaluatorAPI 来做基础评测策略(Evaluating AI responses) (Home)
必要的精确锚定:
- 原文在 Tokens 里给了一个非常工程化的提醒:"Tokens = Money" (Home)
3. 最小可跑:10--20 分钟跑通
下面用一个很典型的落地任务:工单分诊(结构化输出)
输入:用户描述;输出:type/priority/summary 三字段 JSON(映射成 Java record)。
3.1 依赖/配置
依赖(Maven/Gradle 任选) :先用 OpenAI Chat 做例子(你也可以换成其他模型 starter)。官方给的 starter 坐标是 spring-ai-starter-model-openai (Home)
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
application.yml(最小)(示例字段名按 Spring AI OpenAI 模块的属性体系组织;你至少要配置 key 和 model):
yaml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o-mini # 示例:按你实际可用模型改
实战建议(不是原文事实):开发环境用环境变量注入 key,生产用 Vault/KMS/Secret 管理,避免 key 落盘。
3.2 关键代码(Controller + record)
Spring AI 的 ChatClient 是最顺手的"落地入口":它用 fluent API 组装 prompt,支持同步/流式调用,并且能把输出映射成实体(entity())。 (Home)
java
package com.example.ai;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/ticket")
public class TicketController {
private final ChatClient chatClient;
public TicketController(ChatClient.Builder builder) {
this.chatClient = builder.build(); // Spring Boot 自动注入 builder :contentReference[oaicite:18]{index=18}
}
@PostMapping("/triage")
public TicketTriage triage(@RequestBody TicketRequest req) {
// system:定规则;user:放业务输入(roles 的概念来自 "Prompts"):contentReference[oaicite:19]{index=19}
return this.chatClient.prompt()
.system("你是资深客服分诊助手,只输出结构化结果,不要输出多余解释。")
.user(u -> u.text("""
下面是用户的工单描述:
{text}
输出字段要求:
- type: {bug|question|billing|feature}
- priority: {P0|P1|P2|P3}
- summary: 20字以内中文摘要
""").param("text", req.text()))
.call()
.entity(TicketTriage.class); // 把模型输出映射为 Java record :contentReference[oaicite:20]{index=20}
}
public record TicketRequest(String text) {}
public record TicketTriage(String type, String priority, String summary) {}
}
3.3 运行与验证步骤
- 启动应用(确保
OPENAI_API_KEY已设置) - 调用:
bash
curl -X POST http://localhost:8080/ticket/triage \
-H "Content-Type: application/json" \
-d '{"text":"升级后无法登录,提示token无效,影响所有用户"}'
- 预期输出(示例):
json
{"type":"bug","priority":"P0","summary":"升级后登录失败,提示token无效"}
3.4 常见报错 1--2 个及修复
-
报错:返回无法映射为对象 / JSON 解析失败
原因:模型输出不是稳定 JSON(官方在 concepts 里明确说"即使你要求 JSON,本质仍是 String,而且不保证 100% 准确") (Home)
修复:见下文"坑位 1:结构化输出别只靠嘴"。
-
报错:请求无响应/看起来没真正调用模型
原因:
call()只是选择同步模式,真正触发执行是在你调用content()/chatResponse()/entity()等终结方法时(这点非常容易误判) (Home)修复:确保你链路末尾真的调用了终结方法;需要打点就围绕终结方法做。
4. 生产化版本:真正能上线的改造
下面我按"这篇文章的主题链路"挑 4 个工程点,给你能直接套的改造方向(含哪些是官方概念,哪些是实践建议)。
4.1 超时/重试:把抖动当常态
OpenAI chat 模块提供了统一的重试属性前缀 spring.ai.retry,并列出了 max-attempts、指数退避等参数 (Home)
yaml
spring:
ai:
retry:
max-attempts: 3
backoff:
initial-interval: 1s
multiplier: 2
max-interval: 10s
on-client-errors: false
实战建议(推断):
- 只对"可恢复"的错误重试(429/5xx),4xx 通常直接失败更合理;
- 外面再包一层你自己的熔断/限流(Resilience4j / gateway),避免雪崩。
4.2 成本控制:用 token 计费思维做预算
官方把"Tokens = Money"写得非常直白,并强调 token 限制(context window)决定你能塞多少上下文 (Home)
你可以这样工程化:
- 每次请求拿
ChatResponse读 token 使用量(metadata 里有 token 数;并解释"托管模型按 token 计费") (Home) - 结合模型单价做实时成本统计(实践建议)
- RAG 场景下严格控制"召回块数量 + 每块长度",否则成本会线性上涨(与 RAG 分块建议对齐) (Home)
4.3 结构化输出:从"让它输出 JSON"升级到"可验证的对象"
官方在 concepts 里指出:结构化输出往往需要专门的提示 + 转换 ,甚至可能需要多次交互来满足格式 (Home)
而在 ChatClient API 里,你可以:
- 用
.entity()直接映射 record (Home) - 对支持原生结构化输出的模型,通过
AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT打开(并说明有些模型对"对象数组"不原生支持) (Home)
4.4 Tool Calling:让 AI "能办事",但责任边界在你这
官方把 Tool Calling 的链路说得很清楚:定义工具 schema → 模型决定调用 → 你执行 → 结果回填 → 模型生成最终回答 (Home)
上线时你必须补的"工程护栏"(实践建议):
- 对工具入参做强校验(JSON schema 只是起点)
- 每个工具都要审计日志(谁触发、传了什么、改了什么)
- 对高风险工具加权限与二次确认(例如"退款/删除数据")
5. 你一定会踩的坑(至少 3 条)
坑 1:你以为"输出 JSON"就等于"结构化"
-
现象:接口偶发返回多余字段、中文解释、或 JSON 不闭合,导致反序列化失败
-
原因:官方明确说模型输出传统上是
String,要求 JSON 也不保证 100% 准确 (Home) -
排查顺序:
- 先把原始模型输出完整打到日志(注意脱敏)
- 统计失败样本:是多了文本?还是字段错?还是数组/嵌套不稳定?
-
修复方案:
-
小测试验证:
- 写一个参数化测试,喂 20 条典型工单描述,断言
type/priority/summary非空且枚举合法;失败就把原文输出落盘回归。
- 写一个参数化测试,喂 20 条典型工单描述,断言
坑 2:Prompt 模板占位符和 JSON 大括号打架
-
现象:你在 prompt 里放 JSON 示例,结果
{}被当成模板变量,渲染时报错或内容被替换 -
原因:
ChatClient内部使用PromptTemplate+TemplateRenderer,默认变量语法是{}(StringTemplate) (Home) -
排查顺序:
- 定位是模板渲染阶段报错,还是模型输出阶段异常
- 搜索 prompt 内是否出现了未绑定的
{xxx}
-
修复方案:
- 改分隔符,例如用
<>作为占位符(官方在 ChatClient 文档里给了示例) (Home)
- 改分隔符,例如用
-
小测试验证:
- 对包含 JSON 示例的 prompt 做单测:渲染结果必须包含原始 JSON 字符串不变。
坑 3:RAG 一上来就"塞一堆文档",token 爆炸 + 成本爆炸
-
现象:请求变慢、429、账单飙升,回答还不一定更准
-
原因:官方强调 token 有上限(context window)且直接影响计费;RAG 分块还要求每块大小只是 token 上限的一小部分 (Home)
-
排查顺序:
- 打印每次召回的文档块数量、每块字符数/估算 token
- 观察
ChatResponse的 token 使用量(用它做仪表盘) (Home)
-
修复方案:
- 限制 topK、限制 chunk 大小、优先语义边界切分(段落/表格/代码方法不要拦腰切) (Home)
-
小测试验证:
- 固定一组问题,比较 topK=3/5/8 的正确率与 token 成本,选"性价比拐点"。
坑 4:你以为 call() 就已经发请求了
- 现象:你加了埋点/计时,发现"call 了但没耗时",或链路里没看到外部请求
- 原因:官方写得很明确:
call()不触发执行,真正调用发生在content()/chatResponse()/responseEntity()等终结方法 (Home) - 排查顺序:把埋点放到终结方法附近;或者直接返回
ChatResponse看 metadata - 修复方案:统一封装一个
AiGateway,只暴露"终结后的结果",把时延/异常/成本统计放里面(实践建议) - 小测试验证:mock 外部模型(或用 stub server),断言终结方法被调用时才触发一次请求。
6. Checklist(10 条以内)
- 明确本次能力目标:纯聊天 / 结构化输出 / RAG / Tool Calling / 评测
- 用 system message 固定"输出边界/安全边界"(role 概念) (Home)
- Prompt 模板参数化,避免字符串拼接地狱(并注意
{}分隔符冲突) (Home) - 结构化输出别只靠"请输出 JSON",用
.entity()或 structured output 能力 (Home) - 记录 token 使用量,做成本仪表盘(Tokens = Money) (Home)
- RAG:先 ETL/分块/入库,再检索回填;chunk 切分遵守语义边界与 token 比例 (Home)
- Tool Calling:工具入参强校验 + 审计日志 + 权限控制 (Home)
- 配置重试退避(429/5xx),并准备熔断/限流 (Home)
- 关键链路可回归:用固定样本集做自动化评测(Evaluator API) (Home)
- 所有原始输入/输出日志都要脱敏与分级存储(实践建议)
7. 参考
-
原文链接:https://docs.spring.io/spring-ai/reference/concepts.html
- 对应小节:Models / Prompts / Prompt Templates / Embeddings / Tokens / Structured Output / Bringing Your Data & APIs to the AI Model / Retrieval Augmented Generation / Tool Calling / Evaluating AI responses (Home)
-
ChatClient API(用于本文最小可跑示例的关键依据):https://docs.spring.io/spring-ai/reference/api/chatclient.html (Home)
-
OpenAI Chat 模块(starter 与重试属性):https://docs.spring.io/spring-ai/reference/api/chat/openai-chat.html (Home)