17-SpringAI请求链与上下文
从模型 API 到 Spring AI:一次请求的上下文怎样形成?
直接调用模型接口能得到一句回答,但业务应用还需要默认规则、会话、检索、工具和用量记录。Spring AI 的价值在于组织这些能力;理解实际调用链,才能知道某条上下文是在哪一步加入的。
Spring AI 接入片段使用 Java 17、Spring Boot 3.5.3 与 Spring AI 1.1.8;协议与第三方平台示例另行标明适用范围。
1. ChatModel 与 ChatClient 分别负责什么
先把模型访问与应用组织分开。
两个 API 分别负责什么?
ChatModel 是模型调用抽象,接收 Prompt,返回 ChatResponse;不同实现负责对接不同提供方。ChatClient 提供更方便的链式调用,还可以组织默认提示词、Advisor、工具和结果转换。
| 对象 | 主要职责 |
|---|---|
| Message | 系统、用户、助手等消息 |
| Prompt | 消息集合与调用选项 |
| ChatModel | 执行具体模型请求 |
| ChatResponse | 内容、结束信息和提供方元数据 |
| ChatClient | 构建请求、编排增强和选择返回形式 |
ChatClient 不是另一个模型。它可以建立在 ChatModel 之上,方便业务表达调用意图。Spring AI 1.1 ChatClient
一个完整的同步接口
以下使用 Java 17、Spring Boot 3.5.3、Spring AI 1.1.8。Spring AI 1.1.x 对应 Boot 3.5.x;2.x 的依赖与部分 API 不应直接混入这个示例。官方版本兼容说明
pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.3</version><relativePath/>
</parent>
<groupId>demo</groupId><artifactId>spring-ai-demo</artifactId><version>1.0.0</version>
<properties><java.version>17</java.version></properties>
<dependencyManagement><dependencies><dependency>
<groupId>org.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId>
<version>1.1.8</version><type>pom</type><scope>import</scope>
</dependency></dependencies></dependencyManagement>
<dependencies>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-model-openai</artifactId></dependency>
</dependencies>
<build><plugins><plugin>
<groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId>
</plugin></plugins></build>
</project>
src/main/resources/application.yml:
spring:
ai:
openai:
api-key: ${AI_API_KEY}
chat:
options:
model: ${AI_MODEL}
src/main/java/demo/Main.java:
package demo;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
public record Question(@NotBlank @Size(max = 4000) String message) {}
public record Answer(String text) {}
@RestController
public static class ChatController {
private final ChatClient client;
public ChatController(ChatClient.Builder builder) {
client = builder.defaultSystem("你是技术学习助手。无法确定的内容应说明不确定。")
.build();
}
@PostMapping("/chat")
public Answer chat(@Valid @RequestBody Question request) {
String text = client.prompt().user(request.message()).call().content();
return new Answer(text);
}
}
}
在启动进程中设置 AI_API_KEY 和该凭证可访问的 AI_MODEL,再执行 mvn spring-boot:run。接口请求体为 {"message":"用一个例子解释 Java 的引用类型"}。
预期成功时返回包含 text 的 JSON,文本内容由模型生成,不是固定输出。凭证、模型权限、网络或配额异常会进入失败路径。示例没有加入登录、费用限制或正式错误协议,公开接入业务系统时需要补上这些能力。
2. 沿调用链观察请求和结果
最终字符串之外,元信息决定错误与费用如何解释。
从入口到模型的实际调用链
HTTP 请求与参数校验
→ ChatClient 构建消息和选项
→ Advisor 处理上下文、记忆或检索
→ ChatModel 转换并发送提供方请求
→ 获取 ChatResponse
→ Advisor 处理返回值
→ content / entity 等形式返回业务层
默认 system 消息来自服务端,user 消息承载用户内容。不要把用户输入直接拼成具有更高信任等级的系统指令。模型仍可能受到恶意内容影响,业务授权必须由程序执行。
call() 表达同步调用方式,流式则使用 stream()。结果提取是调用链的一部分,不能假定构建了 prompt 就已经发出网络请求。
为什么不能只保留 content?
业务只是展示文本时,content 很方便。需要分析 Token、结束原因、工具调用或提供方请求标识时,应保留 ChatResponse 等更完整信息。
文本为空不一定等于成功生成了空答案。可能涉及工具调用、内容过滤、输出限制或提供方协议差异,需要结合元数据判断。输出达到长度上限,也不能当作完整业务答案保存。
不要为了观察请求直接打印全部 Prompt。会话、检索结果和工具数据可能包含敏感信息,日志应有明确的脱敏与采样策略。
默认设置与每次请求如何管理?
应用可以设置固定角色和模型选项,每次请求再提供问题与经过授权的上下文。模型、输出限制和工具列表不应完全由外部用户任意指定,否则可能改变成本和权限范围。
多模型切换需要确定 Bean 的选择、提供方能力差异和错误协议。相同 Java 接口不代表所有模型都支持相同结构化输出、工具或图像能力。
构建完成的 ChatClient 可作为服务依赖,每次创建新的请求规格。不要在共享对象中保存某个用户的 question 或 conversationId,造成请求串线。
3. Advisor 顺序为什么影响结果
记忆、查询和检索都可能改变下一层看见的上下文。
Advisor 与普通 AOP 有什么区别?
Spring AOP 通常拦截代理方法;ChatClient Advisor 直接参与 AI 请求与响应的处理链。它可以读取和修改 Prompt、共享调用上下文、继续下一个 Advisor,或者明确提前返回。
| 能力 | 例子 |
|---|---|
| 请求增强 | 加入已授权记忆、检索片段 |
| 结果处理 | 保存记忆、提取元数据 |
| 调用控制 | 预算耗尽时提前拒绝 |
| 观测 | 记录耗时与错误类型 |
拦截 AI 请求不代表自动拦截所有工具或业务方法。不同版本的工具循环位置也有区别,必须核对实际调用链。
顺序像一层层包裹
当 A 的 order 小于 B,请求一般先经过 A,再经过 B;返回沿相反方向处理。可以理解为:
A 请求处理
B 请求处理
模型调用
B 响应处理
A 响应处理
相同 order 不应作为稳定业务顺序的保证。日志想同时记录增强前问题和增强后上下文,可以拆成不同观测点,而不是猜测同一个位置能看到一切。Spring AI Advisors
记忆和检索怎样配合?
用户问“它的过期策略呢”,当前问题不完整。可以基于已授权历史改写成独立查询,再执行检索。不能把其他用户的会话混入改写,也不能让改写凭空增加一个不存在的产品或版本。
RAG 检索结果加入 Prompt 后,应保留来源 ID 与权限范围。最终模型只应读取已经允许提供的文档,而不是先加入所有资料,再提示“不要泄露”。
保存记忆时,还要明确是否保留大段检索内容。把每轮检索片段都长期加入会话,会增加费用,也可能让过期知识继续影响回答。
流式 Advisor 为什么不能照搬同步写法?
流在订阅后执行,并通过完成、错误和取消信号结束。同步方法返回 Flux 的时间,不等于生成结束时间。
流式观测应在响应式生命周期内记录,用 doFinally 等方式区分终止原因,避免手动 subscribe。聚合全文后记录会增加内存使用,完整 Prompt 与答案还可能包含敏感数据。
同一个 Advisor 只实现 CallAdvisor,不意味着它也处理 stream 请求。需要对应 StreamAdvisor 能力,并按目标版本实现。
共享上下文与并发有什么要求?
Advisor 通常作为共享组件使用,不能把当前用户或问题保存到实例字段。每次调用的状态应放在请求上下文或局部变量中,并在异步边界上明确传递。
上下文中的标识也要区分信任来源。服务端认证得到的租户、客户端输入的查询词和模型提出的工具参数,不能不加区分地放在一张 Map 里随意覆盖。
4. 设计判断与适用边界
先建立最小同步链路,再添加记忆、检索与工具。每加入一个能力,记录它修改了什么输入、依赖什么上下文以及失败如何传播。多个组件同时变化时,很难判断一次退化来自模型、检索还是记忆顺序。
本文沿用 Java 17、Boot 3.5.3 与 Spring AI 1.1.8,依赖和示例按这一组合阅读。模型供应商的功能仍需要独立核对,不因框架拥有某个接口就假设每个模型都支持相同结构化或工具能力。
5. 问答
问:ChatClient 和 ChatModel 是两个模型吗?
答:不是。ChatModel 执行模型请求,ChatClient 在其上提供请求构建、Advisor 和结果处理。
问:system 消息能代替后端权限吗?
答:不能。提示词影响模型行为,实际数据访问和工具权限必须由服务端校验。
问:order 越大越早执行吗?
答:一般是较小值先处理请求,返回沿相反方向处理;相同值不要依赖固定顺序。
问:可以把用户信息保存在 Advisor 字段中吗?
答:共享对象会并发使用,应采用每次请求的上下文和局部状态。