文章目录
-
- [1. 先自测,五道题,不许查资料](#1. 先自测,五道题,不许查资料)
-
- [1.1 凭直觉答](#1.1 凭直觉答)
- [1.2 单看都懂,连起来就乱](#1.2 单看都懂,连起来就乱)
- [2. 骨架就五层,多一层都没有](#2. 骨架就五层,多一层都没有)
-
- [2.1 一条请求从哪儿走到哪儿](#2.1 一条请求从哪儿走到哪儿)
- [2.2 Advisor 不是顾问,是切面](#2.2 Advisor 不是顾问,是切面)
- [2.3 中间四块,脾气完全不一样](#2.3 中间四块,脾气完全不一样)
- [2.4 九步链路,第七步会回头](#2.4 九步链路,第七步会回头)
- [2.5 外部依赖 = 你要提前认的亲戚](#2.5 外部依赖 = 你要提前认的亲戚)
- [3. RAG:两套黑话,说的是同一件事](#3. RAG:两套黑话,说的是同一件事)
-
- [3.1 先在脑子里翻译一遍](#3.1 先在脑子里翻译一遍)
- [3.2 离线一次,在线每次,这条线必须画清](#3.2 离线一次,在线每次,这条线必须画清)
- [3.3 两路并行,不是先来后到](#3.3 两路并行,不是先来后到)
- [3.4 防幻觉三件套,可以直接抄](#3.4 防幻觉三件套,可以直接抄)
- [4. 三组最容易混的概念](#4. 三组最容易混的概念)
-
- [4.1 二、三、四,三个数字不在一个维度](#4.1 二、三、四,三个数字不在一个维度)
- [4.2 百炼和灵积,不是一家药铺的两个柜台](#4.2 百炼和灵积,不是一家药铺的两个柜台)
- [4.3 Tool Calling 和 MCP,一个是手,一个是插座](#4.3 Tool Calling 和 MCP,一个是手,一个是插座)
- [5. 能跑起来的最小骨架](#5. 能跑起来的最小骨架)
-
- [5.1 依赖](#5.1 依赖)
- [5.2 配置](#5.2 配置)
- [5.3 ChatClient:把记忆和日志挂上去](#5.3 ChatClient:把记忆和日志挂上去)
- [5.4 一次带记忆的对话](#5.4 一次带记忆的对话)
- [5.5 挂上 RAG](#5.5 挂上 RAG)
- [5.6 加一个工具](#5.6 加一个工具)
- [5.7 结构化输出](#5.7 结构化输出)
- [6. 五个坑,都是我自己挖的](#6. 五个坑,都是我自己挖的)
-
- [6.1 坑一:starter 全改名了](#6.1 坑一:starter 全改名了)
- [6.2 坑二:以为工具调用只请求一次模型](#6.2 坑二:以为工具调用只请求一次模型)
- [6.3 坑三:把 ETL 写进了请求链路](#6.3 坑三:把 ETL 写进了请求链路)
- [6.4 坑四:能力表里的 ❌ 不是"模型不会"](#6.4 坑四:能力表里的 ❌ 不是"模型不会")
- [6.5 坑五:多写了一个 /v1](#6.5 坑五:多写了一个 /v1)
- [7. 选模型:别看参数,看四件事](#7. 选模型:别看参数,看四件事)
- [8. 十二个名词速查表](#8. 十二个名词速查表)
- [9. 最后三句](#9. 最后三句)
P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看,传送门https://blog.csdn.net/HHX_01
先说个结论:这些名词没有一个是难的,难的是它们挤在一起。就像一个人同时叫张三、张总、张老师、张哥------你知道都是他,但你不知道该递烟还是递简历。
1. 先自测,五道题,不许查资料
1.1 凭直觉答
- 大模型接入的「两种途径、三种方式、四种调用」,分别指什么?
- 「百炼」和「灵积 DashScope」是什么关系,写代码该用哪个?
- RAG 的 ETL 和检索,哪个是离线跑的,哪个是每次请求都跑的?
- Tool Calling 和 MCP 有什么区别,为什么有了前者还要有后者?
- 模型调用工具时,日志里为什么会冒出两轮 Request / Response?
答对零到一题:正常。我第一次全错,唯一的收获是确认了自己不属于天才那一档。
答对两题:你已经超过群里大部分发"AI 好难"的人,虽然那些人里有三个是你的同事。
答对三题以上:这篇你可以只收藏不看。收藏这个动作本身能带来安全感,我懂。
1.2 单看都懂,连起来就乱
我拿第一题去问一个写了八年后端的兄弟。他沉默了大概四十秒,然后问我:中午吃啥。
这是我见过最体面的认输。
后来我想明白了,乱的根源不在技术,在分层。同一个东西,从"部署在谁的机器上""你用什么形态接触它""代码里怎么写"三个角度,各有一套叫法。混着记,必然打结。
就像你妈叫你全名、你爸叫你小名、你领导叫你工号,三个称呼一个人,你非要当三个人记,那当然记不住。
所以下面不按教程章节顺序讲,按我脑子里真实的顺序:先看清整体,再拆四块,最后上代码。你要跳着看我不拦,反正我当年也是跳着的。
2. 骨架就五层,多一层都没有
2.1 一条请求从哪儿走到哪儿
客户端 → 服务端 → 应用层 → Spring AI → 外部依赖。完。
画图这事我一开始很抗拒,觉得那是给领导看的。后来调一个流式接口,前端一直说"字不出来",我盯着日志看了两个小时,最后发现我把返回类型写成了普通对象,不是流。图要是早画一遍,那一行我能少看四十分钟。
从那以后我就信了一句话:图画出来是给自己省时间的,不是给别人看的。虽然最后还是要给别人看。
2.2 Advisor 不是顾问,是切面
第一次看到这个词,我脑子里出现的是一个穿西装、在会议室里点头的人。
实际上它是 AOP。前置 Advisor 在请求进模型之前动手:过滤敏感词、拼 Prompt、把检索结果塞进去。后置 Advisor 在响应出来之后动手:落库、打日志、脱敏。多个 Advisor 用 @Order 排队。
顺序挺重要。你要是先打日志再脱敏,日志里就躺着一片不该出现的用户原话,然后你在复盘会上解释了很久,解释到一半还有人举手补充。
一句话:所有横切逻辑挂这上面,业务代码一行不动。这是我见过最像"白捡"的抽象,白捡的东西不多,这个算一个。
2.3 中间四块,脾气完全不一样
| 能力 | 谁决定用不用 | 代码里怎么配 |
|---|---|---|
| Tools | 模型自己决定,被动触发 | .tools(new MyTools()) |
| MCP | 模型自己决定,远程进程 | 配 MCP Client |
| RAG | 框架主动塞进 Prompt | 挂 QuestionAnswerAdvisor |
| Memory | 框架自动注入历史 | 挂 MessageChatMemoryAdvisor |
先记住谁按的按钮,再记怎么配。顺序反了,你配得再对也不知道为什么它不动。
所以"我明明配了知识库,怎么它不查"这个问题,八成不是模型懒,是你把被动的东西当成了自动的。
这就像家里的扫地机器人。你不按它,它就在那儿待着,姿势还挺端正。不是它不会扫,是没人下指令。
2.4 九步链路,第七步会回头
客户端请求 → Controller → 前置 Advisor → 注入记忆 → RAG 检索 → 调模型 → 工具循环 → 后置 Advisor → SSE 流式返回。
渲染错误: Mermaid 渲染失败: Parse error on line 1: sequenceDiagramDiagr... ^ Expecting 'SPACE', 'NEWLINE', 'SD', got 'ACTOR'
重点只有一句:模型返回 tool_calls 之后,框架会反射执行你的方法,然后带着结果再请求模型一次。
所以日志里出现两组 Request / Response 是正常的。我第一次看到,以为死循环,重启了三次服务。第四次没重启,是因为我手抖点错了按钮,结果它自己好了。
那一晚我学到两件事。一,这不是 bug。二,我点错按钮的时候运气最好。
2.5 外部依赖 = 你要提前认的亲戚
向量库 PGvector、模型 API 或本地 Ollama、文件系统(记忆落盘、生成 PDF),还有终端。
终端这条得单独说。给模型终端权限,等于把家门钥匙给它,还附赠一句"随便用"。后面讲 MCP 还会提,这里先记着:能不给就不给。
这话不是吓人。真出事的时候,你不会怪模型,你只会怪自己当初为什么点了那个"允许"。
3. RAG:两套黑话,说的是同一件事
3.1 先在脑子里翻译一遍
业务文章画一张流程图,框架文档画一张 ETL 图,看着像两个东西。我第一次对照着看,怀疑自己开错了标签页。
其实就是同一件事的两种命名。
| 框架组件 | 阶段 | 干什么 | 业务视角的说法 |
|---|---|---|---|
Reader |
离线 | 读数据源,产出 Document |
文档收集 |
Transformer |
离线 | 切分、清洗成 chunks | 文档切片 |
Writer |
离线 | 向量化后写进库 | 向量存储 |
VectorStore |
共用 | 向量库,比如 PGvector | 向量数据库 |
Retriever |
在线 | 拿问题去检索 chunks | 相似度搜索 + TopK |
Augmenter |
在线 | 问题 + 切片拼成新 Prompt | 查询增强 |
ChatModel |
在线 | 生成最终回答 | 大模型 |
英文记不住没关系,记动作:Reader 是把资料搬进来,Transformer 是切小块,Writer 是灌进库,Retriever 是拿问题去捞,Augmenter 是把捞到的和问题拌一起,ChatModel 是开口说话。
这么一翻,你会发现它就是个厨房:进货、切菜、装盘、点单、出餐。唯一区别是这个厨房不会在高峰期骂人。
3.2 离线一次,在线每次,这条线必须画清
ETL 是建库时跑一次的,检索是每次提问都跑的。
分不清的人会写出"每问一句就把文档重新切一遍"的代码。我见过,而且不止一个。
表现是:单次响应从一秒变八秒。接口不报错,日志很干净,监控曲线漂亮得像教程配图,只有用户在评论区问一句"你们这个是不是卡了"。
判断方法很土,但有效:这段代码是不是每次请求都会执行?如果是,里面还有"读文件、切分、embedding"三个动作之一,那就是放错楼层了。
3.3 两路并行,不是先来后到
向量召回和条件过滤不是先做 A 再做 B,是两路同时跑,最后融合排序,也就是混合检索。
业务图里那两个分叉的箭头,说的就是这个。我当初以为是画图的人手滑,多画了一条线,还认真地想帮他改。
3.4 防幻觉三件套,可以直接抄
Context information is below:
---------------------
{context}
---------------------
Given the context and provided history information and not prior knowledge,
reply to the user comment. If the answer is not in the context,
inform the user that you can't answer the question.
三句话三个作用:只依据 context、带上历史对话、答不出就明说答不出。
第三句最常被删,因为它看起来像在示弱。实际上它最省钱。模型硬编一个答案,你还得花半小时写道歉话术,写完还要发出去。
4. 三组最容易混的概念
4.1 二、三、四,三个数字不在一个维度
| 说法 | 回答什么问题 | 具体是 |
|---|---|---|
| 两种途径 | 模型跑在谁的机器上 | 云服务 / 自部署 |
| 三种方式 | 你用什么形态接触它 | AI 应用平台 / AI 客户端 / 写代码 |
| 四种调用 | 代码里怎么写 | 平台 SDK / HTTP 直连 / Spring AI / LangChain4j |
一句话串起来:你用"写代码"这个方式,通过"云服务"这条途径,拿 Spring AI 去调模型。
我第一次把这仨当递进关系记。结果在分享会上被问"那四种调用是不是对应四种途径",我说是,然后我讲了十分钟。讲完我自己都不知道我讲了啥,但台下有人点头,点得还挺认真。
途径怎么选:个人开发者云服务就够了,自部署的算力和运维会先把你劝退;企业数据不能出网,再谈自部署。
4.2 百炼和灵积,不是一家药铺的两个柜台
两个名字都挂着阿里云,很多人以为是同一个东西,包括我。
"百炼"听着像老字号,"灵积"听着像快递公司。实际上一个是可视化平台,一个是模型 API 接口。
| 百炼 | 灵积 DashScope | |
|---|---|---|
| 定位 | 可视化平台 | 模型 API 接口 |
| 面向 | 技术 + 非技术,点界面就能建应用 | 写代码的同学 |
| 层次 | 更上层,更简单 | 更底层,更灵活 |
| 一句话 | 点出来的 | 写出来的 |
结论:写代码对接的是灵积。Spring AI 里配 spring.ai.dashscope 配的就是它。百炼那边建好的应用另有调用方式,走 appId。
我以前一直以为"我在百炼建了个应用,代码里就该连百炼",折腾了半天,最后发现我连的是另一个门。
4.3 Tool Calling 和 MCP,一个是手,一个是插座
Tool Calling 解决的是"模型怎么调用一个函数"------协议、参数、结果怎么回灌。
MCP 解决的是"这些函数从哪来"------统一标准,像 USB-C,工具和数据源即插即拔。
有了手为什么还要插座?因为没有插座,每接一个工具你都要为不同框架重写一遍适配。写着写着你就会发现自己在复制粘贴,而且粘错了行,还找不出错在哪。
5. 能跑起来的最小骨架
先上一句保命的:1.0 GA 把 starter 全改名了。网上大部分教程还是老坐标,直接抄会"找不到依赖"。
我第一次抄的时候,以为自己没配镜像源,换了三个源,清了两次缓存,最后发现是坐标过期。那些教程的年纪,够申请文物保护了。
5.1 依赖
xml
org.springframework.ai
spring-ai-bom
1.0.0
pom
import
org.springframework.ai
spring-ai-starter-model-openai
org.springframework.ai
spring-ai-starter-vector-store-pgvector
5.2 配置
yaml
spring:
ai:
openai:
api-key: ${AI_API_KEY} # 别硬编码,走环境变量
base-url: https://dashscope.aliyuncs.com/compatible-mode
chat:
options:
model: qwen-plus # 换模型只改这一行
temperature: 0.7
通义的兼容地址结尾停在 /compatible-mode,不要再加 /v1,加了直接 404。
这个坑我踩了。我盯着 404 看了半小时,从网络想到权限,从权限想到账号,最后发现是我多打了一个斜杠。它甚至不是全角的。
半小时,就为了一个斜杠。这个斜杠值不值钱不好说,但它确实让我在工位上安静了很久。
5.3 ChatClient:把记忆和日志挂上去
java
@Configuration
public class AiConfig {
@Bean
public ChatMemory chatMemory() {
// 1.0 GA 没有 InMemoryChatMemory 了,用 MessageWindowChatMemory
return MessageWindowChatMemory.builder()
.maxMessages(20) // 滑动窗口,防止 token 爆炸
.build();
}
@Bean
public ChatClient chatClient(ChatClient.Builder builder,
ChatMemory memory) {
return builder
.defaultSystem("你是一位耐心的情感顾问,回答要具体、可执行。")
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(memory).build(),
new SimpleLoggerAdvisor()
)
.build();
}
}
注意 MessageChatMemoryAdvisor.builder(...) 是 builder 形式,不是 new(...)。老教程里的写法在 GA 上编译不过。
编译不过的时候先怀疑版本,别怀疑自己。这句话我用了很多年,它救过的不止是我的自尊。
5.4 一次带记忆的对话
java
@RestController
@RequestMapping("/ai")
@RequiredArgsConstructor
public class ChatController {
private final ChatClient chatClient;
@GetMapping("/chat")
public Flux chat(@RequestParam String message,
@RequestParam String conversationId) {
return chatClient.prompt()
.user(message)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
.stream() // 流式,配合前端 SSE 出打字机效果
.content();
}
}
conversationId 这个参数别省。省了,所有人的聊天记录会挤在一起,张三的失恋会出现在李四的对话框里,然后李四以为你在暗示他。
5.5 挂上 RAG
java
@Bean
public ChatClient ragChatClient(ChatClient.Builder builder, VectorStore vectorStore) {
return builder
.defaultAdvisors(
QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.topK(5) // 取最相关的 5 条
.similarityThreshold(0.6) // 低于 0.6 直接丢
.build())
.build())
.build();
}
入库是离线动作,跑一次就行。
java
@Component
@RequiredArgsConstructor
public class DocIngestService {
private final VectorStore vectorStore;
public void ingest(Resource resource) {
List raw = new TextReader(resource).get();
List chunks = new TextTokenSplitter().apply(raw);
vectorStore.add(chunks);
}
}
topK 和阈值这两个数,别一上来就调到很保守。取太少,模型答得理直气壮但没依据;取太多,context 塞满,回答慢,token 还贵。
5.6 加一个工具
java
public class WeatherTools {
@Tool(description = "查询指定城市的实时天气,参数为城市中文名,如:上海")
public String getWeather(@ToolParam(description = "城市名") String city) {
return city + ":晴,26℃,东南风 3 级";
}
}
java
String answer = chatClient.prompt()
.user("上海今天适合出门吗?")
.tools(new WeatherTools())
.call()
.content();
关键在 @Tool 的 description。它不是注释,是给模型看的说明书。写得含糊,模型就不调,或者乱调。
我写过一个含糊的"查询天气",结果模型拿它去判断情感问题。用户问"他是不是不爱我了",它回了一个上海的天气。严格来说那天确实晴,26 度,东南风三级,但那不是重点。
工具调用的准确率,八成取决于这段描述。剩下两成,取决于你有没有把参数名写清楚。别指望模型猜,它猜得比你自信。
5.7 结构化输出
java
public record LoveReport(String score,
List problems,
List suggestions) {}
LoveReport report = chatClient.prompt()
.user("帮我分析这段对话:" + dialog)
.call()
.entity(LoveReport.class);
有了这个,你就不用自己在返回的字符串里找 JSON 了。手抠 JSON 这事我干过,抠到第三个字段的时候,我理解了什么叫做"程序员的中年危机"。
6. 五个坑,都是我自己挖的
6.1 坑一:starter 全改名了
| 老坐标(1.0 之前) | 新坐标(1.0 GA) |
|---|---|
spring-ai-openai-spring-boot-starter |
spring-ai-starter-model-openai |
spring-ai-pgvector-store-spring-boot-starter |
spring-ai-starter-vector-store-pgvector |
spring-ai-mcp-server-spring-boot-starter |
spring-ai-starter-mcp-server |
规律记一下:spring-ai-starter-{类型}-{具体}。模型是 model,向量库是 vector-store,MCP 是 mcp。
同理,InMemoryChatMemory 被 MessageWindowChatMemory 取代,多个 Advisor 的构造方式从 new 变成了 builder。
6.2 坑二:以为工具调用只请求一次模型
第一次看到日志里刷出两组 Request / Response,我以为是死循环。重启了三次服务。
其实这就是设计:模型先要工具,工具跑完,再带着结果问一次。
但真死循环是要防的,agent 里必须设熔断。
java
if (currentStep >= maxSteps) {
state = AgentState.FINISHED;
results.add("Terminated: Reached max steps (" + maxSteps + ")");
return String.join("\n", results);
}
自己写 Agent,第一件事就是加它。不加,一次跑飞能烧掉你半个月额度,然后财务在群里 @ 你,语气很客气,内容很致命。
6.3 坑三:把 ETL 写进了请求链路
切片加向量化是离线的。见过有人在每次对话接口里重新读文档、重新切、重新入库,单次响应从一秒变八秒,还查不出原因。
判断方法还是那句:这段代码是不是每次请求都执行?是,而且涉及"读文件、切分、embedding",那就是放错地方了。
这事儿最难受的地方在于它不报错。它只是慢。慢到用户以为你在思考,其实你在切文档。
6.4 坑四:能力表里的 ❌ 不是"模型不会"
官方有一张 Chat Models 能力对比表,DeepSeek 那行 Tools 标的是 ❌。但 DeepSeek 官方 API 明明支持 function calling。
原因是,这张表说的是"这个集成有没有实现",不是"模型本身会不会"。
就像简历上写"精通 Java",面试一问三不知。不是 Java 不会,是他没实现。
所以工具死活不触发时,先换一个原生集成的模型验证,别急着改自己的 @Tool。改到最后你会发现代码没问题,问题在你信错了表。
6.5 坑五:多写了一个 /v1
没有技术含量,但能卡你半小时。上面说过了,这里再提一次,因为它值得。
有些错误就是这样,它不难,它只是很会等。等你把这半小时花完,它才允许你走出去。
7. 选模型:别看参数,看四件事
| 维度 | 你要问自己的问题 |
|---|---|
| 准确性 | 你的场景容错率多高,能不能接受 5% 的胡说 |
| 功能支持 | 需不需要看图、调工具、吃长文档 |
| 性能与成本 | 单价 × 日调用量 = 月账单,算完再决定用 Max 还是 Plus |
| 数据合规 | 数据能不能出网,不能就只能自部署开源模型 |
参数大不等于适合你。参数大只说明账单大。
一个经验:做 Demo 用最强的,省时间;做产品先算成本再选,不然上线就亏。
"上线就亏"这四个字,我见过太多人用真金白银验证过。第一周账单出来,老板的表情就像你把他的猫弄丢了,而你还站在那儿,手里拿着一份技术选型报告。
8. 十二个名词速查表
| # | 名词 | 一句话 | 易错点 |
|---|---|---|---|
| 1 | 大模型 | 参数规模巨大的深度网络,能理解生成语言、处理多模态 | 参数不是越大越好,成本要算 |
| 2 | Token | 模型处理文本的最小单位,中文约 1 字 ≈ 1~2 token | 计费和上下文长度都按它算 |
| 3 | Prompt | 给模型的输入指令,含 system / user / assistant 三种角色 | system 定人设,user 提需求 |
| 4 | temperature / top_p | 前者控随机性,后者核采样 | 两个一起调容易打架,先调一个 |
| 5 | Embedding | 把文本转成向量,语义相近则距离相近 | 换 Embedding 模型要重建库 |
| 6 | 向量数据库 | 专存向量并做相似度检索的库,如 PGvector | 索引不必一开始就建 |
| 7 | 切片 Chunk | 把长文档切成小段,是检索的最小单位 | 切太大召回不准,切太小丢上下文 |
| 8 | RAG | 先检索资料再让模型回答,即开卷考试 | 离线建库 ≠ 在线检索 |
| 9 | Rerank | 对初步召回的 TopK 做精排 | 它是精排,不是第二次召回 |
| 10 | Tool Calling | 模型自主决定调用你写的函数,把结果带回对话 | description 写不好就调不准 |
| 11 | MCP | 工具与数据源的标准化协议,AI 界的 USB-C | 和 Tool Calling 不是替代关系 |
| 12 | Agent / ReAct | 会自己拆任务、循环"推理-行动"直到完成 | 必须设 maxSteps 熔断 |
再加三个日常最常碰的:
| 名词 | 一句话 |
|---|---|
| Advisor | 框架里的切面,请求前 / 响应后插逻辑 |
| ChatMemory | 对话历史存储,按 conversationId 隔离,可持久化 |
| SSE | 服务端单向流式推送,前端打字机效果靠它 |
9. 最后三句
回头看,真正难的不是模型,是把新概念挂到你已有的知识体系上。
Advisor 约等于 AOP 切面。RAG 约等于开卷考试。Tool Calling 约等于给模型一双手。MCP 约等于手的标准接口。Agent 约等于一个会自己列待办、但必须有人盯着别跑飞的实习生。
一旦这么映射,剩下的都是工程问题。工程问题,后端本来就擅长。你写过的那些烂接口、那些凌晨三点的线上问题,都比模型听话。
三句结论:一,Advisor 是入口,所有横切逻辑挂它上面,业务代码不侵入。二,离线归离线,ETL 建库一次,检索每次跑,别混。三,描述即接口,@Tool 的 description 和 Prompt 一样,是给模型看的代码。
要是这篇帮你省了几个小时,点个赞,收藏一下。收藏了不看也行------收藏这个动作本身能带来安全感,我真的懂。
有问题评论区聊。看到都会回,回得慢的时候通常是在盯着一个 404,而且那个 404 八成是我自己多打的斜杠。
P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看,传送门https://blog.csdn.net/HHX_01