大模型本质上只是一个预测下一个 Token 的函数,它天生有四个缺陷:无状态、知识冻结、与世隔绝、输出不可控。智能体的出现,就是为了从外部弥补这些缺陷。本文从大模型的本质出发,逐个拆解每个缺陷及对应的解决方案——上下文记忆、RAG、Tool Calling、MCP、提示词工程与结构化输出,重点讲"为什么需要"和"解决思路",帮助读者理解什么是智能体以及它的构建思路。Spring AI Alibaba 的实现作为附带展示,最后将所有模块拼装成一个完整的简单智能体,并附上核心类速查表。

一、大模型的本质:一个预测下一个词的函数

在聊智能体之前,必须先理解大模型到底是什么。

抛开所有包装,大语言模型(LLM)的核心能力只有一件事:给你一段文本,预测下一个 Token 是什么。但在此之前,模型会先对输入文本进行分词(Tokenization)——把"今天天气"切分成一个个 Token(如"今天"“天气”),然后基于这些 Token 预测下一个 Token 是什么。你输入"今天天气",它预测"很好",然后把"今天天气很好"作为新的输入,继续预测下一个 Token,如此循环,直到预测出一个结束符号。

这就是所谓的"自回归生成"——本质上就是一个函数 f(Token序列) → 下一个Token,不断调用自身,把一个个 Token 串起来,形成看起来像人话的输出。

这个机制很强大,GPT、Qwen、DeepSeek 都是基于此。但它也带来了几个天生的、无法从模型内部解决的缺陷

  • 无状态:每次调用这个函数,它只看你这次传进来的文本。上一次调用传了什么?它不知道。你昨天跟它说了什么?它不知道。函数调用结束,一切归零。
  • 知识冻结:模型的"知识"来自训练数据。训练完成的那一刻,它的知识就冻结了。2024年训练的模型,不知道2025年发生的事。它无法获取任何训练截止日期之后的信息。
  • 与世隔绝:模型只能处理文本输入、产出文本输出。它不能查数据库、不能调 API、不能读文件、不能执行代码。它像一个被关在房间里的学者,只有一扇窗户(你传进来的文本)和一支笔(它输出的文本)。
  • 输出不可控:模型生成的是自然语言,格式随意。你想要一个 JSON,它可能给你一段散文;你想要一个数字,它可能给你一段解释。它没有义务按你的格式来。

这四个缺陷,不是某个模型的问题,而是"预测下一个词"这个机制本身决定的。智能体(Agent)的出现,就是为了从外部弥补这些缺陷——既然模型内部解决不了,那就在模型外面加一层"壳",给它配上记忆、工具、知识库,让它不再只是一个函数。

本文聚焦的是简单智能体的搭建——通过上下文堆叠的方式逐一弥补缺陷,不具备自主推理循环。目的是帮助读者理解什么是智能体、它大概的构建思路是什么。关于复杂智能体(ReAct 模式)和多智能体协作的搭建实现,会在后面的博客中详细讲解。

下面逐个拆解:每个缺陷是什么,智能体怎么修缮,Spring AI Alibaba 怎么实现。


二、前置了解:Spring AI Alibaba 核心角色

内容来自官网 : https://java2ai.com/

在进入具体模块之前,先对 Spring AI Alibaba 的几个核心角色有个印象,这样后面看到各种类名不会懵。如果你已经熟悉,可以直接跳到下一节。

  • ChatModel:最底层的模型调用接口,纯粹的"发请求、拿响应"通道。你给它一个 Prompt,它返回一个 ChatResponse。类似 HttpClient
  • ChatClient:更高层的流畅 API,在 ChatModel 之上封装,把 Prompt 构建、Advisor 拦截、工具注册这些事统一编排起来。类似 RestTemplate。你可以链式调用:.prompt().system().user().advisors().tools().call()
  • Advisor:AOP 风格的拦截器,请求前修改 Prompt,响应后修改响应。记忆和 RAG 都是通过 Advisor 实现的。

调用链路:你的代码 → ChatClient → [Advisor 链] → ChatModel → 大模型 API → ChatResponse → 你的代码

搞清楚这三个角色,后面所有模块的拼装逻辑就通了。


三、缺陷一:无状态 → 上下文记忆

问题:模型记不住你说过什么

因为大模型就是一个函数,函数调用结束,状态就没了。你第一句话说"我叫张三",第二句话问"我叫什么",模型不知道——因为第二次调用函数时,它只看到了"我叫什么"这五个字,第一次的"我叫张三"已经不在了。

这不是 bug,这是函数的本质。纯函数不应该有副作用,不应该有状态。

智能体怎么补:把历史消息塞进 Prompt

既然模型自己记不住,那就由外部来记。思路很直接:

  1. 找个地方把历史消息存起来(内存、数据库、Redis……)。
  2. 每次调用模型前,把历史消息取出来,拼到当前 Prompt 里一起传给模型。
  3. 模型调用后,把新的用户消息和模型回复存回去。

这样,模型每次看到的就不只是当前这一句话,而是一段完整的对话历史。它"记住"了,但不是自己记住的——是你帮它记住的。这就是"上下文堆叠":记忆不是模型的能力,而是外部注入的上下文。

当然,历史消息不能无限塞——模型有上下文长度限制(4K、8K、128K……),塞太多会超限。所以需要一个"窗口"策略,只保留最近 N 条消息。另外,默认的内存存储也有上限——服务重启后数据就丢了,生产环境需要换成 JDBC 或 Redis 等持久化方案。

Spring AI Alibaba 的实现

Spring AI 把记忆机制分了三层,类不多,但一开始看到容易晕,其实逻辑很清晰:

  • ChatMemoryRepository:存储层,负责存和取。InMemoryChatMemoryRepository 是默认实现(内存中的 Map,注意:服务重启后数据丢失,且内存容量有限),生产环境可以换成 JDBC 或 Redis 实现。你就把它当成一个 Map<会话ID, 消息列表>
  • MessageWindowChatMemory:管理层,维护消息窗口。为什么叫"Window"?因为它只保留最近 N 条消息,老的自动丢弃。这是为了不超出模型的上下文长度限制。
  • MessageChatMemoryAdvisor:Advisor 层,干两件事——请求前从 ChatMemory 取历史消息注入 Prompt,响应后把新消息存入 ChatMemory。你不需要手动做这两步,注册这个 Advisor 就行。
// 1. 存储库(生产环境换 JDBC/Redis,默认内存实现重启后数据丢失)
ChatMemoryRepository repository = new InMemoryChatMemoryRepository();

// 2. ChatMemory,窗口大小 20
ChatMemory chatMemory = MessageWindowChatMemory.builder()
        .chatMemoryRepository(repository)
        .maxMessages(20)
        .build();

// 3. Advisor,绑定会话 ID
MessageChatMemoryAdvisor memoryAdvisor = MessageChatMemoryAdvisor.builder()
        .chatMemory(chatMemory)
        .conversationId("user-123")
        .build();

// 4. 注册到 ChatClient
ChatClient chatClient = ChatClient.builder(chatModel)
        .defaultAdvisors(memoryAdvisor)
        .build();

// 之后每次调用,历史消息会自动注入 Prompt,无需手动处理
chatClient.prompt().user("我叫张三").call().content();

四、缺陷二:知识冻结 → RAG 检索增强

问题:模型只知道训练数据里的事

模型的"知识"来自训练数据。训练完成的那一刻,它的知识就定格了。它不知道训练截止日期之后发生的新闻事件,不知道你公司的内部制度,不知道你个人的偏好和习惯——它只知道训练数据里有过的事情。

而且不只是新闻事件。你公司的内部文档、产品手册、客户数据……这些模型训练时根本不可能见过,它怎么可能知道?

智能体怎么补:先检索,再生成

RAG(Retrieval-Augmented Generation,检索增强生成)的思路是:既然模型自己不知道,那就把相关资料找出来塞给它,让它基于资料回答

类比一下:大模型就像一个参加闭卷考试的学生,只能靠脑子里的知识答题。RAG 就是给他换成了开卷考试——你不用把所有知识背下来,我给你一本参考书,你翻一翻再答。

具体流程分四步,每一步都有其存在的必要性:

1. 文档加载(ETL)

原始资料可能是 PDF、Word、网页、数据库记录……格式各异。第一步需要把这些异构数据统一解析成纯文本。Spring AI 提供了 DocumentReader 接口,PdfReaderTextReader 等是具体实现,负责把文件变成 Document 对象。

2. 文档切分(Split)

为什么需要切分?两个原因:一是大文档太长,不能整篇塞进 Prompt(模型有上下文长度限制);二是检索需要粒度足够细,如果一篇文章 5000 字作为一个检索单元,用户问一个具体问题时,检索出来的内容会包含大量无关信息,既浪费 Prompt 空间又干扰模型回答。

切分策略通常按 Token 数量切,并设置重叠(Overlap)——前后片段有部分重叠,避免一句话被切断导致语义断裂。Spring AI 的 TokenTextSplitter 就是做这件事的。

3. 向量化与存储(Embed & Store)

为什么需要向量化?因为我们需要根据用户输入的问题去查找语义相似的知识。比如用户问"年假怎么休",公司内部制度里写的是"带薪休假制度"——字面上没有重叠,但语义上是相关的。如果用传统的 MySQL 这类结构化数据库,只能做关键词精确匹配,"年假"和"带薪休假"匹配不上,就查不到。向量化就是用 Embedding 模型把文本映射到高维空间中的一个点(向量),语义相近的文本在这个空间中的距离也相近。这样,"年假怎么休"和"带薪休假制度"的向量距离就会很近,检索时就能匹配上。

向量存入专门的向量数据库(VectorStore),它支持基于向量距离的相似度检索。Spring AI 的 VectorStore 是统一抽象接口,支持 Milvus、Redis、PGVector、Chroma 等多种实现。

4. 检索与生成(Retrieve & Generate)

用户提问时,把问题也转成向量,在向量数据库中找到语义最相近的文档片段,把这些片段塞进 Prompt,让模型基于它们回答。关键理解:RAG 没有改变模型本身,它改变的是模型的输入。你往 Prompt 里塞了真实资料,模型的输出自然就更靠谱。这依然是"上下文堆叠"的思路。

Spring AI Alibaba 的实现

涉及的类各司其职:

  • DocumentReader:文档读取接口,PdfReaderTextReader 等是具体实现。
  • TokenTextSplitter:切分器,按 Token 数量切分,支持重叠。
  • EmbeddingModel:向量化模型接口,Spring AI Alibaba 默认集成了通义向量化模型。
  • VectorStore:向量数据库的统一抽象,add() 存入,similaritySearch() 检索。
  • QuestionAnswerAdvisor:一个 Advisor,自动完成"用户问题 → 向量检索 → 注入 Prompt"的流程。
# application.yml - 以 Milvus 为例的 VectorStore 配置
spring:
  ai:
    vectorstore:
      milvus:
        client:
          host: "127.0.0.1"
          port: 19530
        database-name: "default"
        collection-name: "knowledge_base"
        initialize-schema: true
// 1. 加载文档并切分
DocumentReader reader = new PdfReader(new FileSystemResource("company-faq.pdf"));
List<Document> documents = reader.get();
TokenTextSplitter splitter = new TokenTextSplitter();
List<Document> chunks = splitter.apply(documents);

// 2. 向量化存入 VectorStore(EmbeddingModel 自动调用)
vectorStore.add(chunks);

// 3. 注册 QuestionAnswerAdvisor
QuestionAnswerAdvisor qaAdvisor = QuestionAnswerAdvisor.builder()
        .vectorStore(vectorStore)
        .build();

ChatClient chatClient = ChatClient.builder(chatModel)
        .defaultAdvisors(qaAdvisor)
        .build();

// 4. 提问——自动检索相关文档并注入 Prompt
chatClient.prompt().user("公司的年假制度是什么?").call().content();
// 模型基于检索到的公司 FAQ 片段回答,而不是凭空编造

五、缺陷三:与世隔绝 → 工具调用与 MCP

问题:模型只能处理文本,不能"做事"

大模型被关在文本的世界里。它不能查数据库、不能调 API、不能读文件、不能执行代码。你问它"北京今天天气怎么样",它只能根据训练数据中关于北京天气的一般性描述来回答,而不是真的去查实时天气。

这不是偷懒,是它真的做不到——它只是一个预测下一个词的函数,没有手、没有脚、没有网络连接。

智能体怎么补:给模型装上"手"

工具调用的思路是:既然模型自己不能做事,那就让它告诉你"我需要做什么",然后你帮它做

流程是这样的,每一步都值得细看:

1. 注册工具

你预先定义一组工具(Java 方法),用 @Tool 注解标记,并写清楚 description——这个描述非常关键,因为模型就是根据描述来判断是否需要调用这个工具的。描述写得模糊,模型可能判断错误;描述写得清楚,模型就能精准决策。

2. 模型决策

模型在生成回答时,如果判断需要调用工具,不会直接返回文本,而是返回一个特殊结构——工具调用请求(包含工具名称和参数)。比如用户问"北京天气",模型会返回类似 {"tool": "getWeather", "arguments": {"city": "北京"}} 的结构,而不是一段文字。

3. 框架执行

Spring AI 框架拦截到工具调用请求后,自动执行对应的 Java 方法。这一步是框架帮你做的,你不需要手动解析工具调用请求、手动调用方法、手动把结果塞回去。框架会把工具的返回值作为一条 Tool 角色的消息追加到对话中,然后再次调用模型。

4. 模型整合

模型拿到工具返回的结果后,基于这些真实数据生成最终的自然语言回答。比如拿到"北京:晴天,25°C"后,模型会输出"北京今天天气晴朗,气温25度"。

注意:这个过程是单轮的——模型最多调用一次工具就给出最终回答。它不会像 ReAct 那样"调用工具 → 看结果 → 再决定要不要调另一个工具"。这是当前这种"简单智能体"的局限,后面会再提到。

MCP:标准化的工具生态

上面的工具调用是"本地工具"——代码在你项目里,你写好 Java 方法注册上去就行。但现实更复杂:

  • 你可能想用别人写好的工具(天气查询、地理位置查询等实时数据服务)。
  • 工具可能是 Python 写的、Node.js 写的,不一定是 Java。
  • 你可能想让多个智能体共享同一套工具。

MCP(Model Context Protocol)就是为解决这些问题而生的标准化协议。它定义了工具提供方(MCP Server)和使用方(MCP Client)之间的通信规范,让两者解耦。类比一下:Tool Calling 是你自己在家做饭(食材厨具都在手边),MCP 是点外卖(你不需要知道厨房在哪,看菜单下单就行)。

关于 MCP 的更多细节(Server 端开发、Stdio/SSE 两种通信模式、与 Spring AI 的深度集成等),我会在后面的博客中详细讲解,这里只做粗略说明。

Spring AI Alibaba 的实现

工具调用涉及的类:

  • @Tool 注解:标记一个方法为可被模型调用的工具,description 参数告诉模型这个工具是干什么的。模型根据描述判断是否需要调用。
  • MethodToolCallbackProvider:工具注册容器,自动扫描对象上的 @Tool 方法,在请求时告诉模型有哪些工具可用。
// 定义工具
public class WeatherTools {

    @Tool(description = "查询指定城市的当前天气信息")
    public String getWeather(String city) {
        // 实际场景中这里会调天气 API
        return city + ":晴天,温度 25°C,湿度 60%";
    }

    @Tool(description = "查询指定城市的未来三天天气预报")
    public String getForecast(String city) {
        return city + ":明天多云 22°C,后天小雨 18°C,大后天晴 26°C";
    }
}

// 注册到 ChatClient
ChatClient chatClient = ChatClient.builder(chatModel)
        .defaultTools(new WeatherTools())
        .build();

// 模型会自动判断是否需要调用工具
chatClient.prompt().user("北京今天天气怎么样?").call().content();
// 模型调用 getWeather("北京"),整合结果返回

MCP 涉及的类和配置:

  • McpFunctionCallbackProvider:将 MCP Server 提供的工具转换为 Spring AI 的 FunctionCallback。引入 starter 后自动装配,不需要手动创建。
  • spring-ai-starter-mcp-client-webflux:SSE 模式的 MCP Client starter。
  • spring-ai-starter-mcp-client:Stdio 模式的 MCP Client starter。
# application.yml - 配置 MCP Server 连接(SSE 模式)
spring:
  ai:
    mcp:
      client:
        sse:
          connections:
            weather-server:
              url: http://localhost:3001/sse
// MCP 工具自动注册,直接使用
ChatClient chatClient = ChatClient.builder(chatModel)
        .defaultTools(mcpFunctionCallbackProvider)  // 自动注入
        .build();

chatClient.prompt().user("帮我查一下上海的天气").call().content();
// 模型通过 MCP 协议调用远程天气工具

六、缺陷四:输出不可控 → 提示词工程与结构化输出

问题:模型想说什么就说什么

大模型生成的是自然语言,格式随意。你问"给我一个用户列表",它可能给你一段文字描述,也可能给你一个 JSON,也可能给你一个 Markdown 表格——全看它"心情"(实际上是概率分布的采样结果)。

对于聊天场景,这无所谓。但如果你要把模型的输出接入业务代码——比如把结果存数据库、传给下一个服务——你就需要模型按指定格式输出,最好是直接输出一个 Java 对象。

另外,模型的行为也不可控。你让它做法律咨询,它可能跑去聊哲学。你让它用中文回答,它可能蹦英文。没有约束,模型就是一个"自由发挥"的文本生成器。

智能体怎么补:用 Prompt 约束行为,用 Schema 约束输出

两件事:

提示词工程——通过系统提示词(System Prompt)给模型设定"人设"和"行为准则"。比如"你是一个法律顾问,只回答法律相关问题,使用中文"。这不是硬约束,模型有可能不遵守,但实践中效果很好——大模型有很强的"指令遵循"能力,只要你明确说了,它大概率会照做。

结构化输出——在 Prompt 中追加 JSON Schema 描述,要求模型按指定格式输出。框架会自动完成"追加 Schema 指令 → 模型返回 JSON → 反序列化为 Java 对象"的流程。这是硬约束,如果模型支持 JSON Mode,输出格式有保证。

四种消息角色

在讲提示词工程的具体实现之前,需要先了解 Spring AI 中消息的四种角色。这也是 OpenAI Chat Completions API 的标准定义,Spring AI 遵循了同样的规范:

角色 说明 什么时候出现
System 设定模型的行为规则、角色人设、回答风格。对整个对话生效,优先级最高 通常在对话开始时设置一次
User 用户输入的内容,即你问模型的问题或指令 每次用户发言时
Assistant 模型的回复。它不仅仅是一个答案——在多轮对话中,历史 Assistant 消息会被塞回 Prompt,让模型"记住"自己说过什么 每次模型回复时
Tool 工具调用的返回结果。当模型决定调用工具时,工具的执行结果以 Tool 角色的消息回传给模型 模型调用工具后

理解这四种角色,你就明白了 Prompt 的完整结构:System 设定规则 → User 提问 → Assistant 回答(可能包含工具调用请求)→ Tool 返回工具结果 → Assistant 基于工具结果生成最终回答。记忆机制就是把历史的 User 和 Assistant 消息循环塞入,工具调用就是在 Assistant 和 Tool 之间多走一轮。

Spring AI Alibaba 的实现

提示词工程

  • ChatClient.defaultSystem():设置默认的系统提示词,对整个对话生效。
  • ChatClient.system() / .user():分别设置系统消息和用户消息。
  • PromptTemplate:支持 {placeholder} 占位符的提示词模板,运行时替换参数。
// 设置系统提示词
ChatClient chatClient = ChatClient.builder(chatModel)
        .defaultSystem("你是一个专业的Java开发助手,回答要简洁准确,使用中文。")
        .build();

chatClient.prompt().user("Spring Boot怎么配置数据源?").call().content();

// PromptTemplate 动态参数(通过 ChatClient 调用)
String answer = chatClient.prompt()
        .system("你是一个{role},请用{style}的风格回答问题。")
        .user("微服务怎么拆分?")
        .call()
        .content();

结构化输出

  • ChatClient.entity():指定输出类型,框架自动处理 Schema 追加和反序列化。
  • BeanOutputConverter:底层实现,把 Java 类转成 JSON Schema 描述,并负责反序列化。你通常不需要直接用它。
// 定义输出结构
public record MovieReview(
    String title,
    int rating,
    String summary,
    List<String> pros,
    List<String> cons
) {}

// 使用结构化输出
MovieReview review = chatClient.prompt()
        .user("请评价电影《星际穿越》")
        .call()
        .entity(MovieReview.class);

// review 是类型安全的 Java 对象,直接用
review.title();    // 星际穿越
review.rating();   // 9

七、拼装:所有能力如何组合

到这里,四个缺陷和对应的解决方案都讲完了。关键问题是:这些能力怎么组合成一个完整的智能体?

答案藏在前面反复出现的那个词里——上下文堆叠。每个模块做的事情,本质上都是在请求发给大模型之前,往 Prompt 里塞入额外的上下文信息:

  • 记忆 → 塞历史消息
  • RAG → 塞检索到的文档片段
  • 工具 → 塞函数定义和调用结果
  • 提示词 → 塞角色设定和行为规则
  • 结构化输出 → 塞输出格式约束

最终发给大模型的,就是一个包含了所有上下文的超长 Prompt。模型一次调用就给出回答,没有循环推理。

在 Spring AI Alibaba 中,所有模块通过 ChatClient 的链式 API 统一组装:

ChatClient agent = ChatClient.builder(chatModel)
        // 提示词:人设
        .defaultSystem("你是一个智能客服助手,基于公司知识库回答问题,必要时查询外部信息。")
        // 记忆 + RAG:通过 Advisor 注入
        .defaultAdvisors(
            MessageChatMemoryAdvisor.builder()
                .chatMemory(chatMemory)
                .conversationId("user-123")
                .build(),
            QuestionAnswerAdvisor.builder()
                .vectorStore(vectorStore)
                .build()
        )
        // 工具:本地 + MCP
        .defaultTools(new WeatherTools())
        .defaultTools(mcpFunctionCallbackProvider)
        .build();

// 使用
String answer = agent.prompt()
        .user("帮我查一下公司年假制度,顺便看看北京今天天气")
        .call()
        .content();

八、这个智能体的局限

坦率地说,这个"简单智能体"有明显短板,而这些短板恰恰源于大模型的本质——它只是一个预测下一个词的函数:

  • 没有自主推理:模型不会"想一想再决定怎么做",它根据当前 Prompt 一次性给出回答。如果需要多步操作(“先查数据库,再根据结果调 API”),它做不到。
  • 工具调用是单轮的:模型最多调用一次工具就给出最终回答,不会根据工具返回的结果决定是否继续调用其他工具。
  • 错误恢复能力弱:如果工具调用失败或返回了意外结果,模型没有机会"重试"或"换一个方案"。

这些问题的根源是一样的:当前的模式是"一次 Prompt 一次回答",没有循环。模型拿到所有上下文后一次性输出,没有"先试试,不行再调整"的机会。

解决方案是引入 ReAct 模式(Reasoning + Acting),让模型进入"思考 → 行动 → 观察"的循环。在 ReAct 中,模型可以先调用一个工具,看结果,再决定下一步——这才是一个真正意义上的智能体。关于 ReAct 模式和 Spring AI Alibaba 中更上层的 Agent 模块,我会在后面的博客中详细说明,这里只是展示一个简单智能体的搭建流程和操作。

但对于很多场景来说,这个简单智能体已经够用了。 一个能记住上下文、能查知识库、能调工具的对话系统,覆盖了大部分企业级 AI 应用的需求。先把简单智能体理解透,再考虑要不要升级到 ReAct,这是更务实的学习路径。


九、核心类速查表

模块 核心类 做了什么
上下文记忆 MessageChatMemoryAdvisor + MessageWindowChatMemory + ChatMemoryRepository 每次请求自动注入历史消息,响应后自动存储新消息
提示词工程 ChatClient.defaultSystem() + PromptTemplate 设定角色人设,动态参数化提示词
结构化输出 ChatClient.entity() + BeanOutputConverter 约束模型输出为 Java 对象
工具调用 @Tool + MethodToolCallbackProvider 让模型调用本地 Java 方法
MCP 协议 McpFunctionCallbackProvider + starter 标准化地调用远程工具服务
RAG QuestionAnswerAdvisor + VectorStore + TokenTextSplitter + EmbeddingModel 检索知识库文档注入 Prompt

每个模块的思路都是同一个模式:在请求发给大模型之前,往 Prompt 里塞入额外的上下文信息。记忆塞历史消息,RAG 塞文档片段,工具塞函数定义,提示词塞角色设定。理解了这个统一模式,Spring AI 的各种接口就不再是一堆零散的 API,而是一套有逻辑的拼装体系。

简单智能体不是终点,但它是最扎实的起点。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐