Spring AI 从入门到精通:构建你的 AI 开发知识体系
Spring AI 从入门到精通:构建你的 AI 开发知识体系
前言:为什么需要 Spring AI?
在过去的两年里,大语言模型(LLM)以惊人的速度渗透到了软件开发的每一个角落。从 ChatGPT 的横空出世,到如今各类 AI 原生应用的遍地开花,开发者面临的核心挑战已经悄然发生了变化 —— 不再是"能不能调用一个 AI 模型",而是"如何将 AI 模型与企业的现有数据、业务系统、API 体系深度融合,构建出真正有价值的生产级应用"。
Java 生态在这股浪潮中曾经一度显得有些落后。Python 凭借 LangChain、LlamaIndex 等框架抢占了 AI 应用开发的先机,而 Java 开发者要么被迫切换到 Python 技术栈,要么只能通过简陋的 HTTP 客户端封装来调用大模型接口 —— 这两种方案都不够理想。
Spring AI 的出现彻底改变了这一局面。它将 Spring 生态二十年来积累的设计哲学 —— 依赖注入、面向接口编程、约定优于配置、自动装配、可移植性抽象 —— 完整地注入了 AI 应用开发领域。如果你熟悉 Spring Boot,你会发现 Spring AI 的使用体验几乎是"零学习成本"的:你不需要学习新的编程范式,不需要理解复杂的 Pipeline 概念,只需要像使用 Spring Data 或 Spring Security 一样,引入 Starter 依赖、配置几行属性、注入一个 Bean,就能开始构建强大的 AI 应用。
截至本文编写时,Spring AI 已经演进到了 1.1.x 和 2.0.0 里程碑版本,支持包括 OpenAI、Azure OpenAI、Anthropic Claude、Google Gemini、Amazon Bedrock、Ollama 在内的几乎所有主流 AI 服务提供商,并且提供了对数十种向量数据库的统一抽象。更重要的是,它不仅仅是一个"API 封装层",而是一个完整的 AI 应用开发框架 —— 它内置了 RAG(检索增强生成)、Function Calling(函数调用)、多模态支持、对话记忆管理、ETL 数据管道、MCP(模型上下文协议)等几乎所有现代 AI 应用开发所需的关键能力。
本文将按照"从浅到深,逐层拆解"的方式,带你全面理解 Spring AI 的每一个核心组件。我们会从最基础的 ChatClient 开始,逐步深入到 Prompt 工程、Embeddings 与向量存储、ETL 数据管道、RAG、Function Calling、多模态、对话记忆、MCP 协议以及可观测性。每一个组件我都会用代码示例配合讲解,让你不仅"知道是什么",更能"看懂怎么做"。
第一部分:概念铺垫 —— Spring AI 是什么,它的设计哲学是什么
1.1 Spring AI 的核心定位
在开始写代码之前,我们有必要先花一些篇幅理解 Spring AI 在整个 AI 技术栈中的位置。这个问题很重要,因为很多开发者第一次接触 Spring AI 时,会把它和 LangChain、LlamaIndex 等框架直接对标,而这种对标其实是不完全准确的。
Spring AI 的核心定位可以概括为:一个连接企业数据与 API 到 AI 模型的、符合 Spring 设计哲学的应用框架。注意这里的关键词是"连接"—— Spring AI 的目标不是重新发明一套 AI 的开发范式,而是让已经精通 Spring 生态的 Java 开发者,能够用最自然、最符合 Spring 习惯的方式来构建 AI 应用。
具体来说,Spring AI 提供了以下几个层面的能力:
-
可移植的 API 抽象层:你不需要关心底层是调用 OpenAI 的 GPT-4、Azure 的模型还是 Anthropic 的 Claude,统一的
ChatModel接口可以让你的代码在任何模型提供商之间平滑切换。 -
企业数据与 AI 的桥梁:通过 Embeddings、VectorStore、ETL Pipeline、RAG 等组件,Spring AI 解决了"如何让 AI 模型访问企业私有数据"这一核心问题。
-
AI 模型与外部工具的连接器:通过 Function Calling(Tool Calling)和 MCP 协议,Spring AI 让 AI 模型能够调用你的业务 API、数据库、外部服务。
-
生产级的工程实践:自动配置、可观测性(Metrics/Tracing)、对话记忆管理、流式响应等,这些是"能跑"和"能上生产"之间的差距。
1.2 Spring AI 的设计哲学
理解 Spring AI 的设计哲学至关重要,因为它决定了整个框架的使用体感。如果你是一个有 Spring 开发经验的工程师,以下三点会让你的学习曲线变得非常平坦:
第一,坚持 Spring 的"可移植性抽象"传统。 这和 Spring Data 的设计思路完全一致 —— 你操作的是 JpaRepository 接口,底层的具体实现可以是 MySQL、PostgreSQL、MongoDB,你切换数据库时业务代码完全不需要改动。Spring AI 也是如此:你操作的是 ChatModel 接口,底层可以是 OpenAI、Anthropic、Ollama 或者任何其他实现。
第二,拥抱 Spring Boot 的自动配置体系。 引入一个 Starter 依赖,配置 application.yml 中的几行属性,然后直接 @Autowired 注入你需要的 Bean,一切就绪。这是 Spring Boot 开发者最熟悉的体验,Spring AI 忠实地继承了下来。
第三,提供"简单场景简单做,复杂场景可以深度控制"的分层 API。 对于最常见的场景(比如一次性问答),ChatClient 可以让你在一行调用链中完成所有操作;而对于复杂的场景(比如多轮对话、RAG、Function Calling),你可以通过 Advisors 链逐步叠加能力,每一步都是可插拔的、可定制的。
第二部分:起步 —— 配置环境与第一个 AI 调用
2.1 添加依赖
Spring AI 使用独立的 BOM(Bill of Materials)来管理所有依赖版本。首先在你的 pom.xml 中添加 Spring AI 的 BOM:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.1.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
然后添加 OpenAI 的 Starter(你可以替换为其他模型提供商的 Starter):
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
如果你使用的是 Gradle,相应的配置如下:
dependencyManagement {
imports {
mavenBom("org.springframework.ai:spring-ai-bom:1.1.2")
}
}
dependencies {
implementation 'org.springframework.ai:spring-ai-starter-model-openai'
}
2.2 配置 API Key
在 application.yml 中配置你的 OpenAI API Key:
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o-mini
temperature: 0.7
Spring AI 支持通过环境变量、配置文件、命令行参数等多种方式注入 API Key,这种灵活性让你在不同环境(开发、测试、生产)之间切换变得非常容易。你还可以为不同的模型提供商配置各自的参数,所有配置项都有合理的默认值。
2.3 第一个 AI 调用:使用 ChatClient
现在我们来写第一个真正意义上的 AI 调用。Spring AI 提供了两个层面的 API:底层的 ChatModel 和更高层的 ChatClient。对于绝大多数场景,我推荐你直接从 ChatClient 开始。它是一个 Fluent API 风格的构建器,提供了链式调用的优雅体验。
@RestController
class AIController {
private final ChatClient chatClient;
AIController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
@GetMapping("/ai")
String chat(@RequestParam(defaultValue = "用三句话介绍 Java 语言的特点") String message) {
return this.chatClient.prompt()
.user(message)
.call()
.content();
}
}
启动 Spring Boot 应用,访问 http://localhost:8080/ai?message=什么是面向对象编程,你就会收到 AI 模型的回复。整个调用过程不到 20 行代码。
我来解释一下这段代码背后发生了什么:当你调用 chatClient.prompt().user(message).call().content() 时,Spring AI 在内部完成了以下步骤:
- 将
user(message)封装为一个UserMessage对象(Spring AI 的消息模型); - 创建一个
Prompt对象,包含这个消息; - 通过
ChatModel(根据你的配置自动装配了 OpenAI 的实现)调用远程 API; - 将 API 返回的
ChatResponse转换为Generation对象; - 最终通过
.content()提取出纯文本响应。
这个过程中,你完全不需要关心 HTTP 请求的构造、JSON 的序列化和反序列化、错误处理、重试策略等底层细节 —— 这些都被 Spring AI 封装好了。
第三部分:核心入口 —— ChatClient 与 ChatModel 深度解析
3.1 ChatModel:模型抽象层
ChatModel 是 Spring AI 中最重要的抽象接口之一。它类似于 JDBC 中的 DataSource 或者 Spring Data 中的 Repository —— 它定义了一套统一的 API 规范,而具体的实现由各个模型提供商提供。目前 Spring AI 支持的 ChatModel 实现包括:
| 模型提供商 | Maven Starter | ChatModel 实现类 |
|---|---|---|
| OpenAI | spring-ai-starter-model-openai |
OpenAiChatModel |
| Azure OpenAI | spring-ai-starter-model-azure-openai |
AzureOpenAiChatModel |
| Anthropic Claude | spring-ai-starter-model-anthropic |
AnthropicChatModel |
| Google Gemini | spring-ai-starter-model-google-genai |
GeminiChatModel |
| Amazon Bedrock | spring-ai-starter-model-bedrock-converse |
BedrockProxyChatModel |
| Ollama (本地) | spring-ai-starter-model-ollama |
OllamaChatModel |
| 智谱/DeepSeek 等 | spring-ai-starter-model-openai (兼容) |
OpenAiChatModel |
ChatModel 接口中最核心的方法是 call(Prompt prompt),它接收一个 Prompt 对象,返回一个 ChatResponse 对象。此外,它还提供了 stream(Prompt prompt) 方法用于流式响应。值得强调的是,由于 ChatModel 是一个接口,你的业务代码完全不会绑定到任何具体的模型提供商 —— 这是 Spring 面向接口编程思想在 AI 领域的典型体现。
在底层使用 ChatModel 的例子:
@RestController
public class LowLevelController {
private final ChatModel chatModel;
public LowLevelController(ChatModel chatModel) {
this.chatModel = chatModel;
}
@GetMapping("/chat/low-level")
public String chat(@RequestParam String message) {
Prompt prompt = new Prompt(new UserMessage(message));
ChatResponse response = chatModel.call(prompt);
return response.getResult().getOutput().getText();
}
}
你可以看到,使用 ChatModel 需要你手动构造 Prompt 和 UserMessage 对象,并且需要自己处理 ChatResponse 中的 Generation。这个 API 层级提供了最大的控制力,但也带来了更多的样板代码。
3.2 ChatClient:Fluent API 的优雅封装
ChatClient 是 Spring AI 推荐的默认入口。它在 ChatModel 之上提供了更加符合开发者直觉的 Fluent API,让你可以用链式调用的方式完成从 Prompt 构建到响应提取的整个过程。
ChatClient 的核心方法是 .prompt(),它返回一个 ChatClient.PromptSpec 对象,这个对象提供了一系列用于构建 Prompt 的方法:
.system(String text)/.system(SystemMessage message)—— 设置系统消息.user(String text)/.user(UserMessage message)—— 设置用户消息.messages(Message... messages)—— 设置多个消息.options(ChatOptions options)—— 覆盖默认的模型参数(温度、Top-P 等).advisors(Advisor... advisors)—— 注册 Advisor 链.tools(ToolCallback... tools)—— 注册可调用的工具.call()—— 执行同步调用,返回ChatClient.CallSpec.stream()—— 执行流式调用,返回Flux<String>
.call() 之后的 CallSpec 提供了几个提取响应的方法:
.content()—— 直接返回纯文本内容.chatResponse()—— 返回完整的ChatResponse对象.entity(Class<T> type)—— 将响应映射为指定类型的 Java 对象.entities(ParameterizedTypeReference<T> type)—— 映射为泛型集合类型
这种设计的美妙之处在于,你可以根据场景的复杂程度,自由选择在调用链的哪个层次"停下来"。如果只需要一段文本,.content() 就够了;如果需要完整的元数据(Token 用量、Finish Reason 等),再用 .chatResponse();如果需要结构化的 JSON 输出,则使用 .entity()。
第四部分:Prompt 工程 —— 与 AI 模型高效沟通的艺术
4.1 理解 Spring AI 的消息模型
在深入 Prompt 工程之前,我们必须先理解 Spring AI 的消息模型。所有的 Prompt 最终都是由一个或多个 Message 对象组成的,而 Message 接口有两个核心子类:
UserMessage:代表用户的输入,这是对话的主体内容。SystemMessage:代表系统级的指令,用于设定 AI 的角色、行为约束和输出格式。System Message 不会直接显示给用户,但它对模型的行为有着至关重要的影响。AssistantMessage:代表 AI 模型之前的回复,主要用于维护多轮对话的上下文。
这三类消息共同构成了 Prompt。一个典型的 Prompt 通常至少包含一个 SystemMessage 和一个 UserMessage:
String userText = """
请介绍三位黄金海盗时代中最著名的海盗,并说明他们的特点。
至少为每位海盗写一句话。
""";
Message userMessage = new UserMessage(userText);
String systemText = """
你是一个乐于助人的 AI 助手,帮助人们查找信息。
你的名字是 {name}
请以 {voice} 的风格回复用户的问题,并在回复中提及你的名字。
""";
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemText);
Message systemMessage = systemPromptTemplate.createMessage(
Map.of("name", "Jack", "voice", "海盗的口吻"));
Prompt prompt = new Prompt(List.of(userMessage, systemMessage));
List<Generation> response = chatModel.call(prompt).getResults();
4.2 System Prompt 的角色与最佳实践
System Prompt 是整个 Prompt 工程中最重要的一环。它扮演着三个关键角色:
-
角色设定:告诉模型它是谁(“你是一个资深 Java 架构师”)、它的知识范围(“你精通 Spring 全家桶”)、它的行为准则(“你总是给出可运行的完整代码”)。
-
格式约束:要求模型以特定格式输出(“返回合法 JSON”、“使用 Markdown 格式”、“代码注入使用 ```java 标记”)。
-
边界设定:限制模型的行为范围(“只回答 Java 相关的问题”、“如果不知道就如实说不知道”)。
Spring AI 通过 SystemPromptTemplate 支持在 System Prompt 中使用占位符,这使得你可以动态注入参数:
SystemPromptTemplate template = new SystemPromptTemplate("""
你是一个 {role},精通 {expertise}。
回答问题时请遵循以下准则:
- {guideline_1}
- {guideline_2}
当前对话的上下文是:{context}
""");
Message systemMessage = template.createMessage(Map.of(
"role", "Java 全栈架构师",
"expertise", "Spring AI、Spring Boot、微服务架构",
"guideline_1", "始终提供完整的、可直接运行的代码示例",
"guideline_2", "在解释概念时,优先使用类比和实际场景",
"context", "为一个 5 人团队设计 AI 聊天机器人后端"
));
4.3 结构化输出(Structured Output)
在实际的生产应用中,我们通常不希望 AI 返回一段自由格式的文本,而是希望它返回结构化的 JSON 数据,这样我们的程序才能可靠地解析和处理。Spring AI 通过 ChatClient 的 .entity() 方法原生支持这一点。
record MovieReviews(Movie[] movie_reviews) {
enum Sentiment {
POSITIVE, NEUTRAL, NEGATIVE
}
record Movie(Sentiment sentiment, String name) {}
}
MovieReviews reviews = chatClient.prompt()
.system("""
Classify movie reviews as positive, neutral or negative.
Return valid JSON.
""")
.user("""
Review: "Her" is a disturbing study revealing the direction
humanity is headed if AI is allowed to keep evolving, unchecked.
It's so disturbing I couldn't watch it.
JSON Response:
""")
.call()
.entity(MovieReviews.class);
这段代码的工作流程非常清晰:首先通过 System Prompt 告诉模型"你的输出必须是合法的 JSON",然后在 User Prompt 中提供待分类的文本并提示"JSON Response:",最后通过 .entity(MovieReviews.class) 告诉 Spring AI 要将模型返回的 JSON 自动反序列化为指定的 Java 记录类。
当你需要将格式化指令动态注入到 Prompt 中时(这在某些需要结构化的 Prompt 模板中非常有用),可以使用 StructuredOutputConverter:
StructuredOutputConverter outputConverter = ...;
String userInputTemplate = """
... user text input ....
{format}
""";
Prompt prompt = new Prompt(
PromptTemplate.builder()
.template(userInputTemplate)
.variables(Map.of("format", outputConverter.getFormat()))
.build()
.createMessage()
);
这种做法的底层原理是:StructuredOutputConverter.getFormat() 会返回具体的格式化指令(比如"返回一个 JSON 对象,包含字段 X、Y、Z"),然后将这段指令嵌入到 Prompt 的 {format} 占位符中,使得模型能够明确理解输出格式的要求。
第五部分:Embeddings —— 让 AI 理解你的数据
5.1 什么是 Embedding,为什么它至关重要?
如果说 ChatClient 解决的是"让 AI 和我们对话"的问题,那么 Embeddings 解决的就是"让 AI 理解我们的数据"的问题。这是整个 RAG(检索增强生成)体系的基石。
Embedding(嵌入向量)的核心思想是将文本(无论是单词、句子、段落还是整篇文档)转换为一个高维空间中的数值向量。在这个向量空间中,语义相近的文本会被映射到彼此靠近的位置。比如"今天天气真好"和"阳光明媚,万里无云"这两个句子虽然用词不同但语义相似,它们的向量距离会很近;而"今天天气真好"和"如何配置数据库连接池"语义完全不相关,它们的向量距离就会很远。
这个特性有什么实际价值呢?当你有一个包含大量文档的知识库时,你想找到"和用户当前问题最相关的那些文档片段",你不需要做关键词匹配(那会漏掉同义词、近义词),也不需要全文检索(那太慢),你只需要:
- 将用户的查询文本转换为一个向量;
- 在向量数据库中搜索与之距离最近的 N 个向量;
- 返回这些向量对应的文档片段。
这就是向量相似度检索,它是 RAG 的检索部分的核心机制。
5.2 使用 Spring AI 生成 Embedding
Spring AI 提供了 EmbeddingModel 接口来统一抽象文本到向量的转换过程。和 ChatModel 一样,EmbeddingModel 也有面向不同提供商的实现(OpenAiEmbeddingModel、AzureOpenAiEmbeddingModel 等)。
@RestController
public class EmbeddingController {
private final EmbeddingModel embeddingModel;
public EmbeddingController(EmbeddingModel embeddingModel) {
this.embeddingModel = embeddingModel;
}
@GetMapping("/ai/embedding")
public Map<String, Object> embed(@RequestParam String message) {
EmbeddingResponse embeddingResponse =
this.embeddingModel.embedForResponse(List.of(message));
return Map.of("embedding", embeddingResponse);
}
}
embedForResponse(List.of(message)) 方法接收一个文本列表,返回一个 EmbeddingResponse 对象,其中包含了每条文本对应的浮点数向量。你可以根据自己的需要调整输入文本的批处理大小,Spring AI 会负责与模型提供商的高效通信。
这里需要特别注意一点:用于生成 Embedding 的模型和用于对话的模型通常是不同的。比如 OpenAI 的 text-embedding-3-small 和 text-embedding-3-large 是专门优化的 Embedding 模型,它们的输出向量比 GPT 对话模型更适合做相似度计算。在 Spring AI 的配置中,你需要分别配置 Chat Model 和 Embedding Model:
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o-mini # 对话模型
embedding:
options:
model: text-embedding-3-small # Embedding 模型
第六部分:ETL 数据管道 —— 把原始文档变成 AI 可用的知识
6.1 ETL 管道的整体架构
ETL(Extract, Transform, Load)管道是 RAG 的数据准备阶段。它的任务是将各种格式的原始文档(PDF、Word、Markdown、JSON、网页等)转化为向量数据库中的、可以被高效检索的结构化数据。Spring AI 的 ETL 管道由三个核心组件组成:
- DocumentReader(提取):从不同的数据源读取原始文档。
- DocumentTransformer(转换):对文档进行分片、清洗、元数据增强等处理。
- DocumentWriter(加载):将处理后的文档写入向量数据库。
这三个组件的串联形成了一个完整的数据流水线:
原始文档 → DocumentReader → List<Document> → DocumentTransformer → List<Document> → DocumentWriter → 向量数据库
在 Spring AI 中,这三个组件被设计为函数式接口,既可以用最简洁的代码串联使用,也可以在每个阶段单独定制。
6.2 DocumentReader:从各种来源读取文档
Spring AI 提供了多种开箱即用的 DocumentReader 实现:
| Reader | 描述 |
|---|---|
TextReader |
读取纯文本文件 |
JsonReader |
读取 JSON 文件,可按字段提取内容 |
PagePdfDocumentReader |
按页读取 PDF 文件 |
ParagraphPdfDocumentReader |
按段落读取 PDF 文件 |
TikaDocumentReader |
通过 Apache Tika 支持几乎所有文档格式 |
以下是使用 ParagraphPdfDocumentReader 读取 PDF 的完整示例:
@Component
public class MyPagePdfDocumentReader {
List<Document> getDocsFromPdfWithCatalog() {
ParagraphPdfDocumentReader pdfReader = new ParagraphPdfDocumentReader(
"classpath:/sample1.pdf",
PdfDocumentReaderConfig.builder()
.withPageTopMargin(0)
.withPageExtractedTextFormatter(
ExtractedTextFormatter.builder()
.withNumberOfTopTextLinesToDelete(0)
.build())
.withPagesPerDocument(1)
.build()
);
return pdfReader.read();
}
}
PdfDocumentReaderConfig 提供了丰富的配置选项:你可以设置页边距、删除顶部行(比如页眉)、指定每份 Document 包含多少页。这种细粒度的控制对于处理格式复杂的 PDF 非常有价值。
下面是使用 JsonReader 读取 JSON 文件的示例:
@Component
class MyJsonReader {
private final Resource resource;
MyJsonReader(@Value("classpath:bikes.json") Resource resource) {
this.resource = resource;
}
List<Document> loadJsonAsDocuments() {
JsonReader jsonReader = new JsonReader(
this.resource, "description", "content");
return jsonReader.get();
}
}
JsonReader 的参数允许你指定 JSON 中哪些字段作为文档的内容、哪些作为元数据。比如你的 JSON 数据是这样的:
[
{
"id": 1,
"title": "山地自行车选购指南",
"description": "本指南覆盖入门级到专业级",
"content": "在选择山地自行车时,你需要考虑..."
}
]
你可以指定 content 字段作为向量化的正文内容,description 字段作为可检索的元数据。
6.3 DocumentTransformer:文本分片与清洗
DocumentTransformer 是将原始文档转换为适合向量检索的格式的关键步骤。它的核心任务是将过长的文档切分为适当大小的片段,这是一个非常微妙的工程问题,直接决定了你后续 RAG 的检索质量。
为什么需要分片?
- Token 限制:大多数 Embedding 模型都有输入长度限制(比如 OpenAI 的
text-embedding-3-small最大支持 8191 个 Token),超长文本无法直接向量化。 - 检索精度:如果一个"Document"包含了 20 页的内容,即使它和用户的查询高度相关,也很难从 20 页中找到最相关的那一段。而分段后的每个片段粒度更细,检索结果更精准。
- 上下文窗口:当检索到的文档片段被填入 LLM 的 Prompt 时,你希望填入的是最相关的那一小段,而不是一大篇。
Spring AI 提供了 TokenTextSplitter 作为默认的文本分片器:
List<Document> documents = textReader.get();
List<Document> splitDocuments = new TokenTextSplitter().apply(documents);
DocumentTransformer 的接口定义为:
public interface DocumentTransformer
extends Function<List<Document>, List<Document>> {
default List<Document> transform(List<Document> transform) {
return apply(transform);
}
}
它是一个标准的 Function<List<Document>, List<Document>>,这意味着你可以用 Java 的 andThen() 方法轻松组合多个 Transformer:
DocumentTransformer pipeline =
new KeywordMetadataEnricher(keywords)
.andThen(new SummaryMetadataEnricher(summaryModel))
.andThen(new TokenTextSplitter());
List<Document> processed = pipeline.apply(rawDocuments);
6.4 从读取到写入:一个完整的 ETL 流程
将三个组件串联起来,一个完整的 ETL 流程如下:
// Step 1: 读取 PDF
ParagraphPdfDocumentReader pdfReader = new ParagraphPdfDocumentReader(
"classpath:/knowledge-base.pdf",
PdfDocumentReaderConfig.builder()
.withPagesPerDocument(1)
.build()
);
List<Document> documents = pdfReader.read();
// Step 2: 分片
TokenTextSplitter splitter = new TokenTextSplitter();
List<Document> chunks = splitter.apply(documents);
// Step 3: 写入向量数据库
vectorStore.accept(chunks);
也可以使用更简洁的链式写法:
vectorStore.accept(
new TokenTextSplitter().apply(
new ParagraphPdfDocumentReader("classpath:/knowledge-base.pdf").read()
)
);
这里 vectorStore.accept(chunks) 的工作是:对每一个 Document 分片调用 Embedding 模型生成向量,然后将文本内容和对应的向量一起存入向量数据库。这一切都在 Spring AI 内部自动完成。
第七部分:Vector Store —— 向量数据库的抽象层
7.1 Spring AI 支持的向量数据库
如果说 Embeddings 是 RAG 的"引擎",那么 Vector Store 就是 RAG 的"仓储"。Spring AI 通过 VectorStore 接口提供了对数十种向量数据库的统一抽象,这其中包括:
- Milvus —— 高性能开源向量数据库,适合大规模场景
- Pinecone —— 全托管向量数据库服务
- Weaviate —— 自带向量化和模式管理的开源方案
- Qdrant —— Rust 编写的高性能向量搜索引擎
- Chroma —— 轻量级、适合开发和原型
- PGVector —— PostgreSQL 扩展,在关系数据库中实现向量搜索
- Redis Stack —— 基于 Redis 的向量搜索
- Elasticsearch —— 全文检索与向量搜索的融合
- MongoDB Atlas —— 文档数据库的向量搜索扩展
- Oracle、Cassandra、Neo4j 等
无论你选择哪个数据库,只需要引入对应的 Starter 依赖并配置连接信息,你的业务代码完全不需要改动。例如使用 Qdrant:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-qdrant</artifactId>
</dependency>
spring:
ai:
vectorstore:
qdrant:
host: localhost
port: 6334
collection-name: my-knowledge-base
7.2 相似度检索
将文档存入向量数据库之后,最核心的操作就是相似度检索。VectorStore 接口提供了 similaritySearch 方法:
List<Document> similarDocuments = vectorStore.similaritySearch(
SearchRequest.builder()
.query("如何配置 Spring Boot 的数据源?")
.topK(5)
.similarityThreshold(0.7)
.build()
);
三个关键参数:
query:用户的查询文本。Spring AI 会自动将其转换为 Embedding 向量,然后在数据库中搜索。topK:返回最相似的 K 个文档片段。这个值需要根据你的上下文窗口大小来设置,通常 3-5 个是最平衡的选择。similarityThreshold:相似度阈值(0.0-1.0)。只有相似度高于此阈值的文档才会被返回。这个参数非常重要 —— 如果没有阈值限制,向量数据库会返回"看起来最像但可能完全不相关"的结果,而 0.7 是一个经过大量实践验证的合理默认值。
第八部分:RAG —— 检索增强生成
8.1 RAG 的工作原理
RAG(Retrieval Augmented Generation,检索增强生成)是 Spring AI 中最引人注目的能力之一。它解决了一个 AI 应用中的根本性矛盾:大语言模型的知识截止于训练数据,它们不知道你企业内部的文档、最新的业务数据、今天刚发布的产品规格。
RAG 的核心思想非常优雅:在把用户的问题发给 AI 模型之前,先去你的知识库(向量数据库)中检索与问题最相关的文档片段,然后把"用户的问题"和"检索到的相关资料"一起打包放到 Prompt 里发给模型。这样,模型在生成回答时就有了"参考材料",可以基于你的数据给出准确的、有时效性的回答。
RAG 的完整流程可以分为两个阶段:
阶段一:数据准备(离线)
原始文档 → DocumentReader → DocumentTransformer → EmbeddingModel → VectorStore
阶段二:查询回答(在线)
用户提问 → EmbeddingModel → VectorStore.similaritySearch →
将检索结果注入 Prompt → ChatModel → 带上下文的回答
8.2 使用 Spring AI 实现 RAG
Spring AI 通过 RetrievalAugmentationAdvisor 将 RAG 能力封装为一个可插拔的 Advisor,可以轻松地挂载到 ChatClient 的调用链上:
Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(
VectorStoreDocumentRetriever.builder()
.similarityThreshold(0.50)
.vectorStore(vectorStore)
.build()
)
.build();
String answer = chatClient.prompt()
.advisors(retrievalAugmentationAdvisor)
.user(question)
.call()
.content();
这段代码背后发生了什么:
RetrievalAugmentationAdvisor拦截了用户的问题。- 它调用
VectorStoreDocumentRetriever,从 Vector Store 中检索与问题相关的文档片段(相似度阈值设为 0.50)。 - 检索到的文档片段被自动注入到 Prompt 的上下文中(通常放在 System Message 或作为额外消息插入)。
- 增强后的 Prompt 被发送给 ChatModel,模型基于这些参考资料生成回答。
- 返回给用户的回答中包含了来自知识库的准确信息。
这种设计的优雅之处在于:RAG 逻辑完全被隔离在 Advisor 中,你的业务代码不需要任何修改。你可以随时切换到不同的检索策略、调整相似度阈值、替换底层的向量数据库,而业务调用代码保持完全不变。
第九部分:Function Calling —— 让 AI 调用你的代码
9.1 什么场景需要 Function Calling?
Function Calling(在 Spring AI 中也称为 Tool Calling)是实现 AI Agent 的关键技术。它让 AI 模型能够"意识到"外部工具的存在,并在合适的时机选择调用这些工具来获取信息或执行操作。
考虑以下典型场景:
- 查询天气:“明天的北京天气怎么样?”—— 模型自身没有实时天气数据,但它可以调用你的
getCurrentWeather函数。 - 发送邮件:“帮我把会议纪要通过邮件发给张三”—— 模型可以调用你的
sendEmail函数。 - 数据库查询:“上个月销售额最高的产品是什么?”—— 模型可以调用你的
querySalesData函数。 - 调用企业内部 API:“帮我把这个订单的状态改为已发货”—— 模型可以调用你的
updateOrderStatus函数。
在这些场景中,AI 模型不需要自己"知道"天气、销售额、订单状态,它只需要知道有哪些工具可用、每个工具需要什么参数,然后像一个"调度中心"一样决定调用哪个工具、传什么参数。实际的执行由你的 Java 代码完成,AI 模型只是"决策者"。
9.2 使用 Spring AI 实现 Function Calling
Spring AI 提供了两种方式来定义工具函数,这里分别介绍。
方式一:使用 FunctionToolCallback(编程式定义)
ToolCallback weatherCallback = FunctionToolCallback
.builder("getCurrentWeather", new WeatherService())
.description("Get the weather in location")
.inputType(WeatherService.Request.class)
.build();
String response = ChatClient.create(chatModel)
.prompt()
.user("What's the weather in Paris, Tokyo, and New York?")
.tools(weatherCallback)
.call()
.content();
当用户问"What’s the weather in Paris, Tokyo, and New York?"时,模型会识别出需要调用 getCurrentWeather 函数,然后自动发起工具调用、获取天气数据,并基于这些数据生成最终的回复。注意,模型可能会对一个请求并发调用多个工具 —— 比如这个例子中,模型可能会同时查询三个城市的天气。
方式二:使用 @Tool 注解(声明式定义)
如果你的类已经封装了业务逻辑,可以使用 Spring AI 的注解方式让它变成 AI 可调用的工具:
public class WeatherService {
@Tool(description = "Get the weather in location")
public String weatherByLocation(
@ToolParam(description = "City or state name") String location) {
// 实际的天气查询逻辑
return "The weather in " + location + " is sunny, 25°C";
}
}
// 使用时直接传入实例,Spring AI 会自动解析 @Tool 注解
String response = ChatClient.create(chatModel)
.prompt()
.user("What's the weather like in Boston?")
.tools(new WeatherService())
.call()
.content();
当一个 ChatClient 调用链中注册了多个工具时,模型会根据用户的提问自动判断是否需要调用工具、调用哪个工具、传什么参数。这个过程被称为"工具调用循环"(Tool Call Loop),Spring AI 的 ToolCallingAdvisor 会自动管理这个循环 —— 你不需要写任何循环或判断逻辑。
Tool Calling 的强大之处在于它的组合性。你可以在一个调用链中注册多个工具,模型会根据用户的意图智能选择:
String response = ChatClient.create(chatModel)
.prompt()
.user("帮我查一下北京明天的天气,然后给张三发邮件告诉他明天要不要带伞")
.tools(
new WeatherService(), // 天气查询工具
new EmailService(), // 邮件发送工具
new CalendarService() // 日历查询工具
)
.call()
.content();
模型会自动先调用天气工具获取北京的天气预报,然后分析结果判断是否需要带伞,最后调用邮件工具向张三发送通知 —— 整个过程在 ToolCallingAdvisor 的管理下自动完成,开发者只需要定义工具的能力。
第十部分:多模态支持 —— 不止于文本
10.1 Spring AI 的多模态能力概述
多模态(Multimodal)是指 AI 模型理解和处理多种信息形式(文本、图像、音频、视频等)的能力。Spring AI 的 Message 接口通过 Media 类型来支持多模态数据:
// 从 Classpath 加载图片
var imageResource = new ClassPathResource("/multimodal.test.png");
var userMessage = new UserMessage(
"Explain what do you see on this picture?",
List.of(new Media(MimeTypeUtils.IMAGE_PNG, imageResource))
);
ChatResponse response = chatModel.call(
new Prompt(userMessage,
OpenAiChatOptions.builder()
.model("gpt-4o") // 必须使用支持视觉的模型
.build()
)
);
或者使用图片 URL:
var userMessage = new UserMessage(
"Explain what do you see on this picture?",
List.of(new Media(MimeTypeUtils.IMAGE_PNG,
URI.create("https://docs.spring.io/spring-ai/reference/_images/multimodal.test.png")))
);
ChatResponse response = chatModel.call(new Prompt(userMessage));
对于 PDF 文件,同样可以作为多模态输入发送给 AI 模型进行理解和总结:
var pdfResource = new ClassPathResource("/document.pdf");
var userMessage = UserMessage.builder()
.text("Please summarize this document.")
.media(List.of(new Media(new MimeType("application", "pdf"), pdfResource)))
.build();
ChatResponse response = chatModel.call(new Prompt(List.of(userMessage)));
10.2 多模态支持的模型
目前支持视觉多模态的主流模型包括:
| 模型 | 支持的模态 |
|---|---|
| GPT-4o / GPT-4o-mini | 文本、图像 |
| GPT-4 | 文本、图像 |
| Anthropic Claude 3 / 3.5 | 文本、图像、PDF |
| Google Gemini 系列 | 文本、图像、音频、视频 |
使用多模态功能时务必注意:你必须选择支持对应模态的模型,比如使用 gpt-4o 而不是 gpt-3.5-turbo,否则会收到错误。
第十一部分:对话记忆(Chat Memory)—— 让 AI 记住上下文
11.1 对话记忆的必要性
默认情况下,每一次 chatClient.prompt().user(message).call() 都是独立的、无状态的。模型不知道你上一轮说了什么,也不记得你之前让它扮演了什么角色。这对于"一次性问答"的场景没问题,但对于任何需要多轮交互的场景 —— 聊天助手、客服系统、代码助手 —— 都会造成体验上的断裂。
对话记忆(Chat Memory)就是来解决这个问题的。它的核心原理非常简单:将每一轮对话的消息(用户的输入和模型的回复)追加到一个消息列表中,在下一次对话时把这个列表作为历史消息传给模型。 这样一来,模型就有了"记忆"。
11.2 使用 Spring AI 的 Chat Memory
Spring AI 提供了 ChatMemory 接口来实现对话记忆,MessageWindowChatMemory 是其默认实现(使用滑动窗口策略):
// 创建一个记忆实例
ChatMemory chatMemory = MessageWindowChatMemory.builder().build();
String conversationId = "007";
// 第一轮对话
UserMessage userMessage1 = new UserMessage("My name is James Bond");
chatMemory.add(conversationId, userMessage1);
ChatResponse response1 = chatModel.call(new Prompt(chatMemory.get(conversationId)));
chatMemory.add(conversationId, response1.getResult().getOutput());
// 第二轮对话(不需要再次告知名字)
UserMessage userMessage2 = new UserMessage("What is my name?");
chatMemory.add(conversationId, userMessage2);
ChatResponse response2 = chatModel.call(new Prompt(chatMemory.get(conversationId)));
// 模型会回答 "James Bond"
如果你使用的是 ChatClient,记忆管理通过 MessageChatMemoryAdvisor 来实现,更加简洁:
chatClient.prompt()
.user("Do I have license to code?")
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
.call()
.content();
ChatMemory.CONVERSATION_ID 是一个关键参数,每次调用都必须提供,否则会抛出 IllegalArgumentException。这个 ID 用于区分不同的会话 —— 每个用户、每个对话窗口都应该有一个唯一的会话 ID。
11.3 多个 Advisor 的组合使用
MessageChatMemoryAdvisor 可以和 RetrievalAugmentationAdvisor、QuestionAnswerAdvisor 等其他 Advisor 组合使用,形成一个 Advisor 链:
chatClient.prompt()
.advisors(a -> a
.advisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(),
QuestionAnswerAdvisor.builder(vectorStore).build()
)
.param(ChatMemory.CONVERSATION_ID, conversationId))
.user("根据我们之前的讨论,帮我找到相关的技术文档")
.call()
.content();
在这个链中,MessageChatMemoryAdvisor 负责注入历史对话上下文,QuestionAnswerAdvisor 负责从向量数据库检索相关文档。两者协作,模型在生成回答时既有对话历史又有知识库的支撑。
第十二部分:Advisors —— Spring AI 的插件化架构
12.1 Advisors 的设计理念
Advisors 是 Spring AI 中最重要的架构概念之一,它是整个框架"可插拔、可组合"设计哲学的集中体现。你可以把 Advisor 理解为一个拦截器链(Interceptor Chain):每个 Advisor 在 Prompt 被发送到模型之前、模型返回响应之后,都可以对数据进行拦截和处理。
Spring AI 内置了多个 Advisors:
| Advisor | 功能 |
|---|---|
MessageChatMemoryAdvisor |
管理对话历史记忆 |
RetrievalAugmentationAdvisor |
从向量数据库检索相关文档注入 Prompt |
QuestionAnswerAdvisor |
基于知识库的问答 |
ToolCallingAdvisor |
管理 Function Calling 的调用循环 |
DynamicToolSearchAdvisor |
动态搜索并注册工具 |
12.2 构建 Advisor 链
Advisors 的真正威力在于它们可以链式组合:
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
String answer = chatClient.prompt()
.advisors(a -> a
.advisors(
RetrievalAugmentationAdvisor.builder()
.documentRetriever(VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.similarityThreshold(0.50)
.build())
.build()
)
.param(ChatMemory.CONVERSATION_ID, sessionId))
.user("Spring AI 中如何配置向量数据库?")
.tools(new DocumentationSearchTool())
.call()
.content();
在这个示例中,ChatClient 同时使用了三个能力:
- Chat Memory(通过默认 Advisor):记住这个会话之前的对话内容。
- RAG(通过按需注册的 Advisor):从向量数据库检索相关文档。
- Tool Calling(通过
.tools()注册):提供文档搜索工具给模型调用。
这三个能力完全独立、互不干扰,但它们可以被无缝组合到一个调用链中。这就是 Spring AI Advisors 架构的魅力 —— 你可以像搭积木一样逐步叠加 AI 应用的能力,每个"积木"都独立可测试、独立可替换。
第十三部分:MCP —— 模型上下文协议
13.1 什么是 MCP?
MCP(Model Context Protocol)是由 Anthropic 提出的一种开放协议,旨在标准化 AI 模型与外部工具、数据源之间的通信方式。Spring AI 完整实现了 MCP 协议,支持同时作为 MCP Client 和 MCP Server。
MCP 的核心价值在于标准化。在没有 MCP 之前,每个 AI 框架、每个工具都有自己的一套工具注册和调用机制,导致生态割裂。MCP 定义了一套统一的协议,使得:
- 任何支持 MCP 的工具都可以被任何支持 MCP 的 AI 应用调用;
- AI 应用可以将自己的内部能力通过 MCP 暴露给其他 AI 应用;
- 工具的发现、注册、调用、安全控制都有了统一的标准。
13.2 配置 MCP Client
Spring AI 通过 Spring Boot 的自动配置体系来管理 MCP 连接:
spring:
ai:
mcp:
client:
enabled: true
name: my-mcp-client
version: 1.0.0
request-timeout: 30s
type: SYNC # 或 ASYNC 用于响应式应用
streamable-http:
connections:
server1:
url: http://localhost:8083
endpoint: /mcp
stdio:
connections:
server1:
command: /path/to/server
args:
- --port=8080
- --mode=production
env:
API_KEY: your-api-key
DEBUG: "true"
Spring AI 支持三种 MCP 传输方式:
- SSE(Server-Sent Events):适用于服务端推送事件。
- Streamable HTTP:基于 HTTP 的流式传输,最常用的方式。
- Stdio:通过标准输入输出进行进程间通信,适用于本地工具。
13.3 构建 MCP Server
如果你想让自己的应用能够被其他 AI 应用通过 MCP 协议调用,你可以构建一个 MCP Server。Spring AI 通过 @McpTool 和 @McpResource 注解来声明式定义暴露的工具和资源:
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
}
@Component
public class MyMcpTools {
@McpTool(description = "查询指定用户的完整订单历史")
public List<Order> getUserOrders(
@McpToolParam(description = "用户唯一标识符") String userId,
@McpToolParam(description = "查询最近 N 天的订单") int days) {
// 实际的数据库查询逻辑
return orderRepository.findByUserIdAndDateRange(userId, days);
}
@McpTool(description = "生成指定时间范围内的销售报表")
public SalesReport generateSalesReport(
@McpToolParam(description = "报表开始日期(ISO 格式)") String startDate,
@McpToolParam(description = "报表结束日期(ISO 格式)") String endDate) {
// 实际的报表生成逻辑
return reportService.generate(startDate, endDate);
}
}
@Component
public class MyMcpResources {
@McpResource(description = "公司的产品目录信息")
public String getProductCatalog() {
return productService.getCatalog();
}
}
配置 MCP Server 的协议:
spring:
ai:
mcp:
server:
name: my-cool-mcp-server
protocol: STREAMABLE # 或 STATELESS
Spring Boot 的自动配置会自动扫描带有 @McpTool 和 @McpResource 注解的 Bean,将它们注册为 MCP Server 的工具和资源。任何支持 MCP 协议的 AI 客户端都可以发现并调用这些能力。
第十四部分:可观测性 —— 生产环境中的 AI 监控
14.1 为什么 AI 应用需要可观测性?
AI 应用的可观测性比传统应用更加重要,原因有三:
- 成本敏感:每次调用 AI 模型都有实际的费用支出(Token 计费),你需要精确追踪每次调用的 Token 消耗量。
- 延迟不可控:AI 模型的响应时间波动很大(受负载、Prompt 复杂度、Function Calling 往返次数等因素影响),你需要监控 P50/P95/P99 延迟。
- 行为不可预测:模型的输出质量可能因为 Prompt 的微小变化而产生显著差异,你需要追踪调用的成功率和错误类型。
14.2 Spring AI 的观测能力
Spring AI 无缝集成了 Spring 生态的可观测性体系(Micrometer + OpenTelemetry),提供了以下观测维度:
核心组件的指标和追踪覆盖:
- ChatClient:每次调用的延迟、Token 消耗(Input/Output)、成功率
- ChatModel:底层模型调用的延迟、Token 使用量
- EmbeddingModel:Embedding 生成的延迟和维度
- ImageModel:图片生成的延迟和尺寸
- VectorStore:向量检索的延迟和返回结果数量
数据的分类策略:
- 低基数键(Low-cardinality keys):如模型名称、提供商名称等,同时添加到 Metrics 和 Traces 中。
- 高基数键(High-cardinality keys):如具体的 Prompt 内容、会话 ID 等,仅添加到 Traces 中。
你只需要引入对应的 Spring Boot Actuator 和 Micrometer 依赖,Metrics 和 Tracing 就会自动启用。这种"零配置自动观测"的设计,让 AI 应用的运维也变得和传统 Spring Boot 应用一样简单。
第十五部分:总结 —— Spring AI 的学习路径与知识体系
15.1 知识体系全景图
经过了从"第一个 AI 调用"到"MCP 协议"的完整旅程,现在让我们把这些组件串联成一个有机的整体。Spring AI 的知识体系可以按照以下五个层次来理解:
┌─────────────────────────────────────────────────────────┐
│ 应用层 (Application) │
│ REST Controller / GraphQL / 消息队列 / 定时任务 │
├─────────────────────────────────────────────────────────┤
│ 编排层 (Orchestration) │
│ ChatClient · Advisors · Chat Memory · Tool Calling │
├─────────────────────────────────────────────────────────┤
│ 核心层 (Core) │
│ ChatModel · EmbeddingModel · Prompt · Messages │
│ Structured Output · Streaming · Multimodal │
├─────────────────────────────────────────────────────────┤
│ 数据层 (Data) │
│ ETL Pipeline · VectorStore · DocumentReader │
│ DocumentTransformer · DocumentWriter │
├─────────────────────────────────────────────────────────┤
│ 基础设施层 (Infrastructure) │
│ MCP Protocol · Observability · Auto-configuration │
└─────────────────────────────────────────────────────────┘
15.2 推荐学习路径
根据你的背景和需求,我推荐以下学习路径:
第一阶段:入门(1-2 天)
- 搭建 Spring Boot + Spring AI 项目
- 理解 ChatClient 的基本用法
- 完成第一个"发送消息、接收回复"的完整流程
- 理解
application.yml中模型参数的配置
第二阶段:对话与 Prompt(2-3 天)
- 深入理解 Message 模型(UserMessage、SystemMessage、AssistantMessage)
- 掌握 System Prompt 的编写技巧
- 学习结构化输出(
.entity()映射 Java 对象) - 理解 PromptTemplate 的占位符机制
第三阶段:RAG 体系(3-5 天)
- 理解 Embedding 的概念和 EmbeddingModel 的使用
- 学习 ETL Pipeline(DocumentReader → DocumentTransformer → DocumentWriter)
- 选一个向量数据库(推荐从 Chroma 或 PGVector 开始),理解 VectorStore 的基本操作
- 使用 RetrievalAugmentationAdvisor 实现端到端的 RAG
第四阶段:AI Agent 能力(3-5 天)
- 掌握 Function Calling / Tool Calling
- 使用 @Tool 和 @ToolParam 注解构建工具
- 理解 Chat Memory 和 MessageChatMemoryAdvisor
- 学习 Advisors 链的组合使用
第五阶段:生产级实践(持续)
- 配置可观测性(Metrics + Tracing)
- 理解 MCP 协议、搭建 MCP Server
- 流式响应的实现与优化
- 多模态支持的集成
15.3 最后的建议
学习 Spring AI 的过程,本质上是在学习"如何用工程化的方式构建 AI 应用"。Spring AI 的设计让这个学习过程变得非常平滑 —— 你不需要在一开始就理解 RAG、Function Calling 这些高级概念,你只需要从 ChatClient 开始,写几个简单的调用,逐渐积累体感,然后根据实际需求逐步向调用链上叠加能力。
记住 Spring AI 的核心设计哲学:简单场景简单做,复杂场景可以深度控制。当你只需要一段 AI 回复时,.call().content() 就够了;当你需要让 AI 访问企业内部数据时,加上 RetrievalAugmentationAdvisor;当你需要 AI 执行操作时,注册 ToolCallback。每一次能力的叠加都是可插拔的,每一个组件都是可替换的。
这就是 Spring 生态二十年来一直坚持的工程哲学 —— 当你掌握了核心抽象,你就掌握了整个生态。Spring AI 将这个哲学完整地带入了 AI 时代。
参考资料:本文内容基于 Spring AI 官方参考文档(
docs.spring.io/spring-ai/reference),代码示例来自官方文档中的实际使用场景,经过重新组织和补充说明。建议配合官方文档阅读以获取最新的 API 变更和版本差异信息。
更多推荐


所有评论(0)