后端转 AI 应用开发,我先把这 12 个名词理清了:一张图看懂架构、RAG、Tool Calling 与 MCP

34 阅读12分钟

适合人群:会 Spring Boot、想上手 AI 应用开发,但被一堆新名词劝退的后端同学。
读完你会得到:一张可背的架构图、一份概念对照表、一段能跑的最小代码、以及 5 个我真实踩过的坑。


一、先做个自测:这 5 个问题你能答上来几个?

不用查资料,凭直觉答:

  1. 大模型接入的「2 种途径、3 种方式、4 种调用」,分别指什么?
  2. 「百炼」和「灵积 DashScope」是什么关系?写代码时该用哪个?
  3. RAG 的 ETL 和 检索,哪个是离线跑的、哪个是每次请求都跑的?
  4. Tool Calling 和 MCP 有什么区别?为什么有了前者还要有后者?
  5. 模型调用工具时,为什么日志里会出现两轮 Request/Response?

如果 5 题里有 2 题答不上来,这篇就是给你写的。

我第一次看这些名词时最大的感受是:每个词单看都懂,连起来就乱了。 后来发现乱的根源不在技术,在于概念分层没理清——同一个东西,从"部署在哪""怎么接触""代码怎么写"三个角度各有一套叫法,混着记必然打结。

所以这篇不按教程章节讲,按认知顺序重排:先看清整体架构,再拆四个能力块,最后落到代码。


二、一张图看懂 AI 应用的分层架构

任何 AI 应用,骨架都是这五层:

fig1-架构.png

四个关键点,记住就能自己画:

1)Advisor 就是 AOP。
前置 Advisor 在请求进模型前动手(敏感词过滤、拼接 Prompt、注入检索结果),后置 Advisor 在响应出模型后动手(落库记忆、打日志、脱敏)。多个 Advisor 用 @Order 控制顺序。

这是 Spring AI 相比其他框架最有价值的抽象——你要加的横切逻辑,全都能挂在 Advisior 上,业务代码不用改。

2)中间四个能力块不是平级调用,触发方式完全不同。

能力谁决定用不用代码里怎么配
Tools模型自己决定(被动触发).tools(new MyTools())
MCP模型自己决定(远程进程)配 MCP Client
RAG框架主动塞进 Prompt挂 QuestionAnswerAdvisor
Memory框架自动注入历史挂 MessageChatMemoryAdvisor

搞混这四者,就会写出"我明明配了知识库,怎么模型不查"这种问题——因为你以为它是自动的,其实它是被动的。

3)一次提问的真实链路是 9 步,其中第 7 步是循环。

fig2-链路.png

sequenceDiagram
    participant U as 用户
    participant S as 服务端
    participant M as 大模型
    participant T as 工具/向量库

    U->>S: 提问
    S->>S: 前置 Advisor → 取记忆 → RAG 检索
    S->>M: 带 tools 定义的请求
    M-->>S: 我要调 getWeather(上海)
    S->>T: 反射执行 @Tool 方法
    T-->>S: 返回结果
    S->>M: 把结果回灌,再问一次
    M-->>S: 最终答案
    S->>U: SSE 流式吐字

划重点:模型返回 tool_calls 后,Spring AI 会反射调用你的方法,然后带着结果再请求一次模型。所以工具调用不是一次请求,是"模型 → 工具 → 模型"的来回,日志里出现多轮 Request/Response 是正常的,不是 bug。

4)外部依赖清单 = 你要准备的环境。
向量库(PGvector)、模型 API 或本地 Ollama、文件系统(记忆持久化 / 生成 PDF)、以及——终端。给模型终端权限等于给了它执行命令的能力,这是安全红线,后面 MCP 章节会反复提。


三、RAG:两张图说的是同一件事

RAG 是全篇最该背下来的东西,但网上讲法很乱:业务文章画一张流程图,Spring AI 文档画一张 ETL 图,看起来像两个东西。

其实是同一件事的两种命名:

Spring AI 组件阶段干什么业务视角的说法
Reader离线读数据源,产出 Document文档收集
Transformer离线切分、清洗成 chunks文档切片
Writer离线向量化后写入库向量转换与存储
VectorStore共用向量库(PGvector)向量数据库
Retriever在线用问题检索 chunks相似度搜索 + TopK
Augmenter在线问题 + 切片拼成新 Prompt查询增强
ChatModel在线生成最终回答大模型

离线 / 在线这条线必须分清。
ETL 是建库时跑一次,检索是每次提问都跑。
分不清的人会写出"每问一句就把文档重新切一遍"的代码——我见过不止一个。

另外一个高频误解:向量召回和条件过滤不是先后的两件事,而是并行的两路(混合检索 Hybrid Search),最后才融合排序。业务图里那两个箭头分叉,说的就是这个。

最后是 RAG 的"防幻觉三件套",写 Prompt 时可以直接抄:

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、带上历史对话、答不出就明说答不出。第三句最容易被忽略,但它恰恰是压幻觉最有效的一句。


四、三个最容易混的辨析(建议截图保存)

辨析 1:2 种途径 / 3 种方式 / 4 种调用

这三个数字不是同一维度,混着记必乱:

说法回答什么问题具体是
2 种途径模型跑在谁的机器上云服务 / 自部署
3 种方式你用什么形态接触它AI 应用平台 / AI 软件客户端 / 写代码
4 种调用代码里怎么写平台 SDK / HTTP 直连 / Spring AI / LangChain4j

一句话串起来:你用"写代码"这种方式,通过"云服务"这条途径,拿 Spring AI 去调模型。

途径怎么选?个人开发者选云服务就够了,自部署的算力和维护成本会劝退你;企业看重数据不出网再考虑自部署。

辨析 2:百炼 vs 灵积 DashScope

这俩名字都带"阿里云",很多人以为是同一个东西:

百炼灵积 DashScope
定位可视化平台模型 API 接口
面向技术 + 非技术,点界面就能建应用技术开发同学
层次更上层,更简单更底层,更灵活
一句话点出来的写出来的

结论:写代码对接的是灵积。 Spring AI 的 spring.ai.dashscope 配的就是灵积,百炼那边建好的应用另有调用方式(走 appId)。

辨析 3:Tool Calling vs MCP

  • Tool Calling 解决的是:模型怎么调用一个函数(协议、参数、结果回灌);
  • MCP 解决的是:这些函数从哪来(统一标准,像 USB-C,工具和数据源即插即拔)。

有了 Tool Calling 还要 MCP,就像有了"手"还要"标准接口"——不然每接一个工具,你都要为不同框架重写一遍适配。


五、能跑起来的最小骨架(Spring AI 1.0)

理论说完,上代码。注意:1.0 GA 把 starter 全部改名了,很多人第一步就栽在这儿(坑 1 详述)。

1. 依赖

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-bom</artifactId>
      <version>1.0.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  
  <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
  </dependency>
  
  <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
  </dependency>
</dependencies>

2. 配置

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。这个坑我踩过。

3. ChatClient:把记忆和日志挂上去

@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, VectorStore vectorStore) {
        return builder
                .defaultSystem("你是一位耐心的情感顾问,回答要具体、可执行。")
                .defaultAdvisors(
                        MessageChatMemoryAdvisor.builder(memory).build(),  // 自动记忆
                        new SimpleLoggerAdvisor()                          // 打印请求/响应
                )
                .build();
    }
}

注意 MessageChatMemoryAdvisor.builder(...) 是 builder 形式,不是 new(...)——老教程里的写法在 GA 上会编译不过。

4. 一次带记忆的对话

@RestController
@RequestMapping("/ai")
@RequiredArgsConstructor
public class ChatController {

    private final ChatClient chatClient;

    @GetMapping("/chat")
    public Flux<String> chat(@RequestParam String message,
                             @RequestParam String conversationId) {
        return chatClient.prompt()
                .user(message)
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
                .stream()          // 流式,配合前端 SSE 出打字机效果
                .content();
    }
}

5. 挂上 RAG

@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();
}

入库(离线,跑一次就行):

@Component
@RequiredArgsConstructor
public class DocIngestService {

    private final VectorStore vectorStore;

    public void ingest(Resource resource) {
        List<Document> raw = new TextReader(resource).get();
        List<Document> chunks = new TokenTextSplitter().apply(raw);  // 切片
        vectorStore.add(chunks);                                     // 向量化 + 入库
    }
}

6. 加一个工具(Tool Calling)

public class WeatherTools {

    @Tool(description = "查询指定城市的实时天气,参数为城市中文名,如:上海")
    public String getWeather(@ToolParam(description = "城市名") String city) {
        return city + ":晴,26℃,东南风 3 级";
    }
}
String answer = chatClient.prompt()
        .user("上海今天适合出门吗?")
        .tools(new WeatherTools())   // 挂上去,模型自己决定调不调
        .call()
        .content();

关键在 @Tool 的 description。 它不是注释,是给模型看的"说明书"——写得含糊,模型就不会调、或者乱调。这一点比很多人想的更重要:工具调用的准确率,八成取决于描述写得好不好。

7. 结构化输出

public record LoveReport(String score, List<String> problems, List<String> suggestions) {}

LoveReport report = chatClient.prompt()
        .user("帮我分析这段对话:" + dialog)
        .call()
        .entity(LoveReport.class);   // 自动转 JSON 并反序列化

六、5 个我真实踩过的坑

坑 1:Spring AI 1.0 把 starter 全改名了

网上 90% 的教程还是老坐标,直接抄会报"找不到依赖":

老坐标(1.0 之前)新坐标(1.0 GA)
spring-ai-openai-spring-boot-starterspring-ai-starter-model-openai
spring-ai-pgvector-store-spring-boot-starterspring-ai-starter-vector-store-pgvector
spring-ai-mcp-server-spring-boot-starterspring-ai-starter-mcp-server

规律记一下:spring-ai-starter-{类型}-{具体}。模型是 model,向量库是 vector-store,MCP 是 mcp。

同理,InMemoryChatMemory 已被 MessageWindowChatMemory 取代,多个 Advisor 的构造方式也从 new 变成了 builder。编译不过先怀疑版本,别怀疑自己。

坑 2:以为工具调用只请求一次模型

第一次看到日志里刷出两组 Request/Response,我以为是死循环。其实这就是设计:模型先要工具,工具跑完,再带着结果问一次。

要防的是真死循环——agent 里必须设 maxSteps 熔断:

if (currentStep >= maxSteps) {
    state = AgentState.FINISHED;
    results.add("Terminated: Reached max steps (" + maxSteps + ")");
    return String.join("\n", results);
}

自己写 Agent,第一件事就是加它,不然一次跑飞能烧掉你半个月额度。

坑 3:把 ETL 写进了请求链路

RAG 的切片 + 向量化是离线的。见过有人在每次对话接口里重新读文档、重新切、重新入库——单次响应从 1 秒变 8 秒,还查不出原因。

判断方法:这段代码是不是每次请求都会执行?如果是,且涉及"读文件 / 切分 / embedding",那就是放错地方了。

坑 4:Spring AI 官方能力表里的 ❌ 不是"模型不会"

官方文档有张 Chat Models 能力对比表,DeepSeek 那行 Tools 标的是 ❌。但 DeepSeek 官方 API 明明支持 function calling。

原因是:这张表说的是「Spring AI 这个集成有没有实现」,不是「模型本身会不会」。DeepSeek 走 OpenAI 兼容代理接入时,集成层没把工具调用打通。

所以工具死活不触发时,先换一个原生集成的模型(比如通义)验证,别急着改自己的 @Tool。

坑 5:通义兼容地址多写了 /v1

base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 → 直接 404。

正确写法结尾停在 /compatible-mode。这种坑没有技术含量,但能卡你半小时。


七、模型怎么选:别看参数,看四件事

各类榜单眼花缭乱,实际选型就看四个维度:

维度你要问自己的问题
准确性你的场景容错率多高?能不能接受 5% 的胡说
功能支持需不需要看图(多模态)、调工具(function calling)、长文档(上下文窗口)
性能与成本单价 × 日调用量 = 月账单,算完再决定用 Max 还是 Plus
数据合规数据能不能出网?不能就只能自部署开源模型

官方对比入口,选型时直接查:

  • Spring AI:docs.spring.io/spring-ai/reference/1.0/api/chat/comparison.html
  • LangChain4j:docs.langchain4j.dev/integrations/language-models/

一个经验:做 Demo 用最强模型(省时间),做产品先算成本再选(不然上线就亏)。


八、附:12 个名词速查表(建议截图)

标题说的 12 个名词,压成一张表。忘了随时回来看:

#名词一句话解释易错点
1大模型参数规模数十亿到数万亿的深度网络,能理解生成语言、处理多模态数据参数不是越大越好,成本要算
2Token模型处理文本的最小单位,中文约 1 字 ≈ 1~2 token计费和上下文长度都按 token 算
3Prompt给模型的输入指令,含 system / user / assistant 三种角色system 定人设,user 提需求
4temperature / top_p前者控随机性(高=发散),后者核采样(低=保守)两个一起调容易打架,先调一个
5Embedding把文本转成向量(一串浮点数),语义相近则距离相近换 Embedding 模型要重建库
6向量数据库专门存向量并做相似度检索的库,如 PGvector索引不是必须一开始就建
7切片 Chunk把长文档切成小段,是检索的最小单位切太大召回不准,切太小丢上下文
8RAG先检索资料、再让模型基于资料回答,即"开卷考试"离线建库 ≠ 在线检索
9Rerank对初步召回的 TopK 结果做精排,提升准确率它是精排,不是第二次召回
10Tool Calling模型自主决定调用你写的函数,并把结果带回对话description 写不好就调不准
11MCP工具与数据源的标准化协议,AI 界的 USB-C和 Tool Calling 不是替代关系
12Agent / ReAct会自己拆任务、循环"推理-行动"直到完成的智能体必须设 maxSteps 熔断

再加三个工程概念,日常写代码最常碰:

名词一句话
AdvisorSpring AI 的切面,请求前/响应后插逻辑(安全、记忆、RAG、日志)
ChatMemory对话历史存储,按 conversationId 隔离,可持久化
SSE服务端单向流式推送,前端"打字机效果"靠它

九、写在最后

回头看,AI 应用开发真正难的不是模型,而是把新概念挂到你已有的知识体系上:

  • Advisor ≈ AOP 切面
  • RAG ≈ 开卷考试(先翻书再答题)
  • Tool Calling ≈ 给模型一双手
  • MCP ≈ 手的标准接口(USB-C)
  • Agent ≈ 会自己列待办的实习生

一旦这么映射,剩下的都是工程问题——而工程问题,后端本来就擅长。

三个最容易记住的结论:

  1. Advisor 是入口:所有横切逻辑(安全、记忆、RAG、日志)都挂在它上面,业务代码不侵入。
  2. 离线归离线:ETL 建库一次,检索每次跑,别混。
  3. 描述即接口:@Tool 的 description 和 Prompt 一样,是"给模型看的代码"。

如果这篇帮你省了几个小时,点个赞 + 收藏,后面我会继续更 RAG 调优(切片参数、TopK、Rerank 实测对比)和 Agent 自研的踩坑记录——那才是真正花时间的地方。

有问题评论区聊,看到都会回。