Skip to content

Spring AI ChatClient同步、流式与Advisor调用原理 ​

ChatClient 是 Spring AI 面向应用层的模型调用门面。它用 Fluent API 组合默认配置、System/User Message、Prompt Template、ChatOptions、Advisor、Tool 和输出转换,再把请求交给底层 ChatModel。

只会写 chatClient.prompt().user(question).call().content(),只能跑通最小 Demo。生产中还必须理解默认值怎样合并、Advisor 顺序怎样改变 Prompt、完整 ChatResponse 包含什么、流式取消怎样传播、结构化解析失败怎样处理,以及为什么 ChatClient 不应直接散落在 Controller 和领域代码中。

学习目标 ​

完成本页后,你应该能够:

  1. 区分 ChatClient.Builder、ChatClient、Request Spec 和 Response Spec。
  2. 解释 ChatClient、ChatModel、Prompt、Message、ChatOptions、ChatResponse 的关系。
  3. 说明默认 system/options/advisors/tools 与请求级配置怎样组合。
  4. 写出同步文本、完整响应、结构化对象和流式 SSE Demo。
  5. 解释 .call() 和 .stream() 的执行、延迟和错误边界。
  6. 解释 Advisor 前置/后置链和顺序为什么重要。
  7. 理解 Memory、RAG、Tool Calling 在 ChatClient 链中的位置。
  8. 设计业务封装、版本记录、输入校验、超时、成本和审计。
  9. 排查 Bean 注入失败、回答跑题、空内容、流式中断、上下文超长和 Token 暴涨。

一、ChatClient不是ChatModel ​

mermaid
flowchart TD
    A["业务Service"] --> B["ChatClient:应用Fluent API"]
    B --> C["Prompt、Message、Options和Advisor"]
    C --> D["ChatModel:底层模型抽象"]
    D --> E["Provider Adapter"]
    E --> F["远程模型API/本地推理服务"]
对象主要职责
ChatClient.Builder构建带默认配置的 ChatClient
ChatClient每次调用创建请求规格、执行 Advisor 和模型调用
ChatModel接受 Prompt,返回 ChatResponse 的底层抽象
PromptMessage 列表和 ChatOptions 的完整输入
MessageSystem/User/Assistant/Tool 等角色内容
ChatResponseGeneration、模型输出、Usage、Finish Reason等

ChatClient 降低了 Provider 协议耦合,但不保证不同模型在 Tool、JSON、多模态和参数上完全等价。

二、为什么需要ChatClient ​

自己拼HTTPChatClient提供的抽象仍需业务负责
手写厂商请求体Prompt/Message/Options场景和规则
手写同步和SSE解析call/stream外层SSE治理和取消
每处拼System PromptdefaultSystem和模板Prompt版本/评估
手工插RAG/MemoryAdvisor链ACL、Token预算
手工工具循环Tool Calling集成权限、幂等、确认
手工解析JSONentity/ConverterSchema和业务校验

三、Builder什么时候出现 ​

Provider Starter 自动配置满足条件后,Spring Boot 通常创建 ChatModel 以及可注入的 ChatClient.Builder。业务配置使用 Builder 创建一个或多个按场景划分的 ChatClient:

java
@Configuration
public class ChatClientsConfiguration {

    @Bean
    ChatClient knowledgeChatClient(ChatClient.Builder builder) {
        return builder
                .defaultSystem("""
                        你是企业知识库助手。
                        只能根据已授权资料回答。
                        资料不足时明确拒答。
                        """)
                .build();
    }
}

建议创建场景 Client,例如 knowledge、extract、customerService,而不是一个万能 ChatClient 承载所有默认 Prompt/Advisor/Tool。

四、Builder、Client和请求对象生命周期 ​

mermaid
flowchart TD
    A["应用启动注入Builder"] --> B["配置默认System/Options/Advisor"]
    B --> C["build得到可复用ChatClient"]
    C --> D["请求1调用prompt产生Request Spec"]
    C --> E["请求2调用prompt产生独立Request Spec"]
    D --> F["call或stream执行"]
    E --> G["call或stream执行"]

ChatClient 设计用于作为 Bean 复用;每次 prompt() 构建请求级 Spec。不要把某个请求的 mutable Map、用户身份或 conversationId 保存到单例字段中,否则并发用户会串数据。

具体线程安全保证以当前 Spring AI 版本实现和自定义 Advisor 为准。自定义 Advisor 也必须避免共享可变状态。

五、默认配置和请求级配置 ​

常见默认项:

  • defaultSystem。
  • defaultUser。
  • defaultOptions。
  • defaultAdvisors。
  • defaultTools/Tool callbacks。

请求级调用可添加/覆盖部分配置:

java
String answer = chatClient.prompt()
        .system("本次只返回三条结论。")
        .user("解释线程池拒绝策略")
        .options(OpenAiChatOptions.builder()
                .temperature(0.1)
                .maxTokens(600)
                .build())
        .call()
        .content();

不同字段是覆盖、追加还是合并,可能随 API 和对象类型不同。不要凭感觉认为请求 system 一定完全替换 defaultSystem;通过单元测试或调试最终 Prompt 验证当前版本行为。

六、Prompt由什么组成 ​

mermaid
flowchart TD
    A["Prompt"] --> B["System Message"]
    A --> C["User Message"]
    A --> D["Assistant历史"]
    A --> E["Tool Result"]
    A --> F["ChatOptions"]

System Message ​

定义任务、边界、输出和拒答策略。它不是后端权限边界,也不能防住所有 Prompt Injection。

User Message ​

用户当前输入。必须限制长度、内容类型和敏感信息,不能直接拼进系统规则文本。

Assistant Message ​

多轮历史中的模型回答。模型 API 通常无状态,历史需要应用重新发送。

Tool Message/Result ​

工具执行结果回传模型。必须最小化和脱敏。

七、Prompt Template参数绑定 ​

java
String answer = chatClient.prompt()
        .user(user -> user.text("""
                请解释以下主题:{topic}

                读者水平:{level}
                输出要求:先结论,再原理,再给Demo。
                """)
                .param("topic", topic)
                .param("level", level))
        .call()
        .content();

模板参数仍是不可信数据。绑定可以避免手工字符串拼接混乱,但不等于防 Prompt Injection。用户输入与系统规则应有明确标记,Tool和权限由后端控制。

八、同步调用全过程 ​

mermaid
flowchart TD
    A["Service调用chatClient.prompt"] --> B["合并默认值与请求配置"]
    B --> C["渲染Message模板和参数"]
    C --> D["构造Prompt"]
    D --> E["执行Advisor请求链"]
    E --> F["ChatModel转Provider请求"]
    F --> G["HTTP发送并等待完整响应"]
    G --> H["解析ChatResponse和Metadata"]
    H --> I["执行Advisor响应链"]
    I --> J["Response Spec提取content/entity/response"]

.call() 表达阻塞式业务调用:当前请求要等完整响应或错误。模型越慢、输出越长,占用连接和资源越久。

九、同步文本Demo ​

java
@Service
public class SummaryService {

    private final ChatClient chatClient;

    public SummaryService(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    public String summarize(String article, String requestId) {
        if (article == null || article.isBlank()) {
            throw new IllegalArgumentException("文章不能为空");
        }

        if (article.length() > 20_000) {
            throw new IllegalArgumentException("文章过长,请使用异步摘要");
        }

        String content = chatClient.prompt()
                .system("""
                        你是企业内容摘要助手。
                        只根据输入文章总结,不补充外部事实。
                        """)
                .user(user -> user.text("""
                        ARTICLE:
                        {article}

                        输出三部分:核心结论、关键依据、风险提醒。
                        """)
                        .param("article", article))
                .call()
                .content();

        if (content == null || content.isBlank()) {
            throw new IllegalStateException("模型返回空摘要");
        }

        return content;
    }
}

JDK 8 没有文本块和 String.isBlank(),需使用普通字符串和 trim().isEmpty()。

十、为什么要读取完整ChatResponse ​

java
ChatResponse response = chatClient.prompt()
        .user(question)
        .call()
        .chatResponse();

完整响应可能提供:

  • 实际模型。
  • 一个或多个 Generation。
  • Assistant Message和Tool Call。
  • Finish Reason。
  • Prompt/Completion/Total Token。
  • Provider Request ID和扩展Metadata。

如果 .content() 是空,可能是 Tool Call、Safety拦截、长度截断或兼容层映射问题,不能直接统一报“模型无回答”。

API 中 getText()、getContent()、Metadata 类名随版本可能变化,使用项目版本 Javadoc。

十一、ChatOptions怎样设计 ​

参数作用常见误区
model选择模型允许前端随意选高价/越区模型
temperature控制随机性设0就以为不会幻觉
maxTokens限制最大输出忽略上下文总窗口
topP/topK采样策略同时乱调无评估
stop终止序列截断合法结构化JSON

Options 由场景策略产生,不应直接把前端 JSON 映射到 Provider Options。

Provider-specific Options 会让业务与供应商耦合。封装在模型路由/配置层,并用统一业务配置映射。

十二、结构化输出 ​

java
public record TicketClassification(
        String category,
        String priority,
        String reason) {
}
java
TicketClassification result = chatClient.prompt()
        .system("你是工单分类器,只根据工单内容分类。")
        .user(ticketText)
        .call()
        .entity(TicketClassification.class);

成功反序列化不代表业务合法:

  • category 必须在白名单。
  • priority 必须符合场景规则。
  • reason 长度和敏感内容要校验。
  • 高风险分类可能需要人工复核。

详见:Prompt与结构化输出。

十三、流式调用全过程 ​

mermaid
flowchart TD
    A["Controller返回Flux/SSE"] --> B["ChatClient.stream构造流式请求"]
    B --> C["Advisor流式链处理"]
    C --> D["Streaming ChatModel连接Provider"]
    D --> E["Provider逐步生成Chunk"]
    E --> F["适配器转成Flux事件"]
    F --> G["后端转发给浏览器"]
    G --> H{"完成、错误或客户端取消"}
    H --> I["释放连接并记录Usage/partial状态"]

流式主要降低 TTFT,不一定减少总耗时和 Token 成本。

十四、SSE完整Demo ​

java
@RestController
@RequestMapping("/api/ai")
public class AiStreamController {

    private final ChatClient chatClient;

    public AiStreamController(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    @GetMapping(
            value = "/chat/stream",
            produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<String>> stream(
            @RequestParam
            @Size(min = 1, max = 2000)
            String message) {

        Flux<ServerSentEvent<String>> deltas = chatClient.prompt()
                .user(message)
                .stream()
                .content()
                .map(text -> ServerSentEvent.builder(text)
                        .event("delta")
                        .build());

        return deltas
                .timeout(Duration.ofSeconds(60))
                .concatWithValues(
                        ServerSentEvent.builder("completed")
                                .event("done")
                                .build())
                .doOnCancel(() ->
                        log.info("AI stream canceled by client"));
    }
}

生产不能把内部异常堆栈发给浏览器。中途错误转换为受控 event:error,并确保取消向上游传播。

十五、流式content和ChatResponse流有什么区别 ​

.stream().content() 只返回增量文本,使用简单;完整 Response 流可能包含 Usage、Finish Reason、Tool Call 等元数据,具体 API 依版本。

如果要准确记录 Token,不能只在每个文本 Chunk 上累加字符数。Provider Usage 常在最后事件出现;客户端中断时可能拿不到最终 Usage,需要估算和账单对账。

十六、背压和慢客户端 ​

浏览器消费速度低于模型输出速度时,缓冲会增长。需要:

  • 限制每连接缓冲。
  • 设置超时和慢消费者策略。
  • 避免在 Reactor 事件线程执行阻塞 Tool/数据库操作。
  • 客户端断开取消上游。
  • Nginx/Ingress关闭不合适的SSE缓冲并设置空闲超时。

流式不等于可以无上限输出。

十七、Advisor调用链 ​

mermaid
flowchart TD
    A["Request Spec"] --> B["权限/租户Advisor"]
    B --> C["Memory Advisor"]
    C --> D["RAG Advisor"]
    D --> E["审计/观测Advisor"]
    E --> F["ChatModel"]
    F --> G["响应沿Advisor链返回"]

Advisor 可以:

  • 修改或补充请求。
  • 读取请求上下文参数。
  • 调用下一环节。
  • 检查/修改响应。
  • 记录观测数据。

版本中可能区分 Call Advisor 和 Stream Advisor。自定义 Advisor 必须同时考虑同步与流式能力,不能只在 .call() 生效却让 .stream() 绕过安全规则。

十八、Advisor顺序为什么重要 ​

错误:先全库 RAG 召回,再做权限 Advisor。越权资料已经进入上下文。

正确思路:

text
认证上下文
→ 权限Filter
→ Memory裁剪
→ RAG检索
→ Prompt Token预算
→ 模型
→ 输出安全/审计

Memory与问题改写顺序、RAG与Token统计顺序也会改变结果。为 Advisor 链建立版本和顺序测试。

十九、请求级Advisor参数 ​

动态 tenantId、conversationId 等不应写入单例 Advisor 字段。通过请求级上下文传递,具体 API 按版本:

java
String answer = chatClient.prompt()
        .user(question)
        .advisors(advisorSpec -> advisorSpec
                .param("tenantId", authenticatedTenantId)
                .param("conversationId", authorizedConversationId))
        .call()
        .content();

Advisor 内仍需验证这些值来自受信后端上下文,不能让前端随意查询其他 conversationId。

二十、Memory怎样进入调用 ​

mermaid
flowchart TD
    A["本轮问题"] --> B["按conversationId读取历史"]
    B --> C["裁剪/摘要/敏感过滤"]
    C --> D["历史Message加入Prompt"]
    D --> E["调用模型"]
    E --> F["保存本轮消息"]

模型不持久记忆用户。历史越多,Prompt Token越多;必须限制轮数/Token、做摘要并防 conversationId 越权。

二十一、RAG怎样进入调用 ​

RAG Advisor 在调用前查询 VectorStore,把授权 Chunk 添加到 Prompt。生产还要保存检索结果、分数、索引版本和引用,详见:Spring AI RAG。

二十二、Tool Calling怎样进入调用 ​

ChatClient 把 Tool Schema 发给模型;模型返回调用意图,后端执行 Java Tool,再将最小结果回传模型。模型参数不可信,详见:Spring AI Tool Calling。

二十三、ChatClient应该放在哪一层 ​

text
controller
  └── 接收HTTP、Bean Validation、响应转换
application/service
  └── 场景编排、权限、配额、RAG/Tool决策
ai/client
  └── ChatClient封装、模型调用、错误转换
prompt
  └── 模板、版本和Hash
advisor
  └── Memory/RAG/观测等可测试增强
tool
  └── 最小化业务工具适配器
evaluation
  └── 回归集和评分

不要让领域 Service 到处依赖 Provider-specific Options。建立场景接口:

java
public interface TicketClassifier {
    TicketClassification classify(String ticketText, AiRequestContext context);
}

实现内部使用 ChatClient,业务测试可替换 Fake。

二十四、为什么不直接在Controller调用 ​

  • 无法复用权限、配额和审计。
  • Prompt和模型参数散落。
  • Controller难以单元测试。
  • 切换模型/降级污染HTTP层。
  • Tool事务和业务规则容易放错层。

二十五、测试ChatClient业务封装 ​

不要只做真实模型集成测试。测试分层:

层内容
单元测试Prompt参数、权限、Options、输出校验,使用Fake接口
契约测试Provider适配、结构化输出、Tool Schema
集成测试测试Key/本地模型,少量固定样例
评估回归质量、RAG、拒答、安全和成本

模型输出不确定,单元测试不要断言完整自然语言逐字相等;断言必须字段、禁止字段和业务结果。

二十六、超时和重试边界 ​

ChatClient API 不替代底层 HTTP Client 超时和外层服务 Deadline。必须配置连接、读取、总请求和网关超时。

重试:

  • 401/403/400不重试。
  • 429尊重Retry-After。
  • 5xx/连接失败有限重试。
  • 读取超时可能已生成并计费。
  • Tool写操作结果未知先按幂等键确认。

详见:生产SLO、成本与事故排查。

二十七、故障一:ChatClient.Builder不存在 ​

检查:

  1. 是否引入正确 Provider Starter,而非只有 core。
  2. 1.x/2.x Artifact 是否混用。
  3. BOM 和 Boot/JDK 是否兼容。
  4. 自动配置是否被排除。
  5. Condition Evaluation Report 哪个条件未满足。
  6. 自定义 ChatModel/Builder Bean 是否冲突。

二十八、故障二:回答跑题或忽略System ​

检查最终 Prompt:

  • defaultSystem与请求system怎样合并。
  • Advisor是否加入冲突指令。
  • RAG文档是否含恶意指令。
  • 历史消息是否污染当前场景。
  • 用户输入是否过长导致系统规则被稀释/截断。
  • 模型是否适合该指令任务。

不能只不断加长 System Prompt。先消除冲突并用评估集比较。

二十九、故障三:多轮对话串用户 ​

根因:

  • conversationId由前端任意指定。
  • Memory Key未包含tenant/user。
  • 单例Advisor保存可变用户字段。
  • 缓存Key只用问题文本。
  • 测试/生产共享Memory存储。

立即停止相关场景,保存访问和Memory证据,修复授权、清理泄露历史并做安全评估。

三十、故障四:流式几秒后中断 ​

分层:浏览器取消 → CDN/Ingress/Nginx超时/缓冲 → 应用Flux异常/阻塞 → Provider连接/队列。检查是否发送SSE心跳、取消是否上游传播、Tool阶段是否阻塞事件线程。

三十一、故障五:Token突然变多 ​

  • Memory未裁剪。
  • Advisor重复加入System/RAG。
  • RAG TopK/Chunk变大。
  • Tool结果完整回传。
  • Prompt模板重复渲染。
  • Retry产生多次调用。

记录 Advisor 前后 Prompt Token 和版本,不能只看最终总Token。

三十二、故障六:content为空 ​

检查完整 ChatResponse:Tool Call、Safety、Finish Reason、Generation列表、Provider兼容字段、流式是否取消。空文本不是单一错误类型。

三十三、可观测字段 ​

text
requestId, traceId, scene, tenantScopeHash,
chatClientName, model, routeVersion,
promptVersion, advisorChainVersion,
messageCount, estimatedInputTokens,
ragIndexVersion, retrievedChunkIds,
toolNames, inputTokens, outputTokens,
ttftMs, totalMs, finishReason, errorCode

不记录 API Key、完整 Prompt、敏感 RAG 文本和完整 Tool 结果。

三十四、常见误区 ​

误区准确结论后果
ChatClient就是模型它是应用门面,底层是ChatModel/Provider排障层级错误
一个Client适合所有场景默认Prompt/Tool/Advisor不同规则冲突
request system一定覆盖default合并语义需按版本验证Prompt重复/冲突
content为空就是模型挂了可能Tool/Safety/截断错误降级
stream让总耗时更短主要改善TTFT成本/总延迟误判
System Prompt能做权限权限必须后端强制越权
Advisor顺序无关会改变权限、上下文和Token泄露/质量下降
Memory模型自动保存应用每次重新加入历史上下文/越权误解
流式可直接做完整JSON中途片段不是完整结构解析失败
所有异常重试即可结果未知和写Tool会重复重复计费/副作用

三十五、面试标准回答 ​

ChatClient和ChatModel区别 ​

ChatModel是底层模型调用抽象,接受Prompt并返回ChatResponse;ChatClient是在其上面向应用的Fluent门面,组合默认/请求级Message、Template、Options、Advisor、Tool和输出转换。它减少Provider协议耦合,但模型能力差异、权限、评估和故障治理仍需业务处理。

ChatClient一次call怎样执行 ​

prompt创建请求Spec,合并默认值与请求配置,渲染Message模板并构造Prompt;请求经过Advisor前置链,ChatModel适配为Provider请求并等待完整响应;适配器解析ChatResponse和Usage,再经Advisor后置链,由Response Spec提取content、完整response或结构化entity。任一后续解析/Advisor异常都可能让业务调用失败。

call和stream有什么区别 ​

call等待完整响应,适合短任务和结构化输出;stream返回响应式流,让首Token更早到达,适合聊天长文本,但总计算时间不一定降低。流式还要处理客户端取消、背压、SSE代理、心跳、中途错误、Tool阶段和最终Usage缺失。

Advisor顺序为什么重要 ​

Advisor可在模型调用前后加入权限上下文、Memory、RAG、安全和观测。权限必须在检索前生效,Memory裁剪会影响问题改写,RAG加入后影响Token预算。顺序错误可能召回越权文档、重复上下文或让stream绕过安全,因此Advisor链要版本化并分别测试call/stream。

ChatClient为什么不应直接写Controller ​

Controller只负责HTTP参数和响应;场景路由、权限、配额、Prompt版本、RAG/Tool编排、输出校验和降级应放应用Service与AI Client层。否则规则散落、无法复用/测试/回滚,也容易把Provider Options和Tool事务污染Web层。

三十六、学习实验与验收 ​

  1. 用同一ChatModel分别调用ChatModel和ChatClient,比较对象和响应。
  2. 配置defaultSystem,再添加请求system,观察当前版本最终Prompt合并。
  3. 读取完整ChatResponse,记录Usage、Finish Reason和模型名。
  4. 用entity映射对象,构造合法JSON但业务非法字段并拦截。
  5. 编写SSE接口,测TTFT、总耗时和客户端取消。
  6. 创建两个Advisor,交换顺序观察RAG/Token变化。
  7. 使用请求级conversationId,验证跨用户授权。
  8. 模拟Tool Call导致content为空,正确识别响应类型。
  9. 模拟429、读取超时和流式中断,验证错误分类。
  10. 用Fake场景接口测试Prompt参数和业务校验,不依赖真实模型。

验收时必须能回答:

  • Builder、ChatClient、Request Spec和Response Spec是什么关系?
  • Prompt、Message和Options怎样组合?
  • 默认值和请求级配置怎样验证最终行为?
  • 为什么完整ChatResponse比content更适合排查?
  • 流式为什么只改善TTFT而不保证总耗时?
  • Advisor同步和流式链为什么都要实现?
  • Memory、RAG和Tool分别在哪个阶段进入?
  • 为什么共享可变Advisor会串用户?
  • 如何测试自然语言输出而不逐字断言?
  • Token暴涨时怎样定位是Memory、RAG、Tool还是重试?

关联知识点 ​