Spring AI学习笔记(二)ChatClient从怎么调到怎么封装

发表于 2026-05-15 22:31 1549 字 8 min read

Spring AI学习笔记(四)工具调用和MCPSpring AI学习笔记(三)RAG从文档入库到回答Spring AI学习笔记(二)ChatClient从怎么调到怎么封装Spring AI学习笔记(一)它到底解决什么问题java新版本-java25学习笔记(四)用JFR和GC日志做一次体检java新版本-java25学习笔记(三)虚拟线程要和资源边界一起看java新版本-java25学习笔记(二)运行时基线先统一java新版本-java25学习笔记(一)LTS版本对比和学习路线主流AI Agent能力对比与工程选型我用Kiro做了个自己的工具站盘一盘虚拟线程我用Trae做了个AstrBot的AI角色扮演插件Python初学笔记(六)常用标准库先学这几个Python初学笔记(五)读写文件和处理异常Python初学笔记(四)函数让代码开始有结构Python初学笔记(三)条件、循环和推导式Python初学笔记(二)变量和基础类型比想象中重要Python初学笔记(一)先把环境和运行方式弄明白主流AI大模型能力对比Java 21和Spring Boot 3升级笔记(五)日志指标与可观测性Java 21和Spring Boot 3升级笔记(四)数据访问层适配Java 21和Spring Boot 3升级笔记(三)虚拟线程使用边界Java 21和Spring Boot 3升级笔记(二)Jakarta迁移要点Java 21和Spring Boot 3升级笔记(一)工程基线整理魔法値をどうやって我慢する?JPAのSpecification大改造!处理生僻字乱码:JPA框架对于Oracle的NVarchar2,NChar,NClob类型支持Redis Stream能不能当轻量消息队列用RocketMQ 5学习笔记:普通消息之外要看什么事件流不是换个消息队列这么简单Kubernetes学习笔记04:发布、排障和观测Kubernetes学习笔记03:配置、密钥和存储Kubernetes学习笔记02:Deployment、Service和IngressKubernetes学习笔记01:Pod和控制器mysql索引原理02--存储引擎索引的实现mysql索引原理01--索引的数据结构
この投稿は「日本語」では表示できません。元の投稿を表示しています。
  理解 Spring AI 以后,第二步就该看 ChatClient。   在 Java 后端里,ChatClient 是最容易入门的入口。它把模型对话封装成比较符合 Spring 风格的调用方式,让我们不用一开始就直接处理各个模型厂商的 HTTP 细节。   但我觉得学习 ChatClient...

前言

  理解 Spring AI 以后,第二步就该看 ChatClient。

  在 Java 后端里,ChatClient 是最容易入门的入口。它把模型对话封装成比较符合 Spring 风格的调用方式,让我们不用一开始就直接处理各个模型厂商的 HTTP 细节。

  但我觉得学习 ChatClient 时不能只停在“怎么发一句话”。真正要落地,应该关心怎么封装、怎么管理 prompt、怎么处理结构化输出、怎么做超时和观测。

最小调用长什么样

  最简单的调用大概是这样:

String answer = chatClient.prompt()
        .user("解释一下 Java 虚拟线程适合什么场景")
        .call()
        .content();

这段代码很直观。用户输入进入模型,模型返回文本。

如果只是内部工具或学习 demo,这样已经够了。但业务系统不能让 Controller 到处直接写这段代码。因为一旦分散,后面就很难统一处理模型、日志、异常、成本和权限。

先封装场景服务

  我会按业务场景封装,而不是按模型封装。

比如摘要场景:

public interface DocumentSummaryService {

    SummaryResult summarize(String title, String content);
}

分类场景:

public interface TicketClassifyService {

    TicketClassifyResult classify(String description);
}

这样业务层只知道自己要摘要或分类,不知道底层用哪个模型,也不关心 ChatClient 的细节。

模型切换、提示词调整、输出校验都放在实现里。

系统提示和用户输入要分开

  很多初学写法会把所有内容拼成一个大字符串。

String prompt = "你是客服助手,帮我分类:" + text;

这样能跑,但不利于管理。

更好的方式是把 system 和 user 分开。system 描述角色、规则和输出要求,user 放用户输入。

String result = chatClient.prompt()
        .system("你是工单分类助手,只能返回指定 JSON 结构。")
        .user(description)
        .call()
        .content();

这样后续调整系统规则时,不会和用户输入混在一起。安全上也更清楚,因为用户输入不能轻易覆盖系统规则。

模板变量要显式

  复杂 prompt 通常会有变量。

比如摘要模板需要标题、正文、摘要长度、输出语言。如果直接字符串拼接,很容易漏转义,也不利于测试。

我更喜欢把输入定义成明确对象:

public record SummaryCommand(
        String title,
        String content,
        int maxWords) {
}

然后在服务层组装 prompt。这样参数来源清楚,也方便做校验。

比如 content 最大长度不能无限大,maxWords 要有范围。不要等输入进入模型以后才发现 token 爆了。

结构化输出要校验

  现在做 AI 工程,结构化输出已经是基本功。

很多业务不需要一段自由文本,而是需要 JSON、枚举、字段值。

比如:

public record TicketClassifyResult(
        String category,
        String priority,
        String reason) {
}

即使 Spring AI 能帮助把输出映射成对象,后端仍然要校验。

category 必须在系统支持的分类里,priority 只能是高、中、低,reason 不能太长。解析失败时要有降级逻辑,不能直接抛给用户一段模型原始输出。

流式输出适合交互,不适合所有场景

  ChatClient 支持流式返回时,用户体验会好很多。聊天、长文生成、解释类功能都适合边生成边展示。

但不是所有场景都适合流式。

如果模型输出要进入业务判断,比如分类、审核、字段抽取,就更适合一次性拿到完整结果再解析校验。流式输出中间状态不完整,业务处理会更复杂。

所以我会按场景选:

1.用户阅读体验优先,用流式。 2.结构化业务处理,非流式。 3.后台任务,非流式更简单。 4.长文本生成,可以流式但要有取消机制。

超时和取消要设计

  模型调用比普通接口更慢,也更不稳定。

用户实时请求要有明确超时。后台任务可以放宽,但也不能无限等待。

如果前端用户关闭页面,后端最好能取消不必要的生成。否则用户已经走了,模型还在继续消耗 token。

工程上至少要考虑:

1.连接超时。 2.读取超时。 3.总耗时限制。 4.用户取消。 5.失败重试。 6.降级提示。

AI 调用不是普通本地方法,它是外部依赖。外部依赖就必须有边界。

日志记录要克制

  ChatClient 调用最容易想记录完整 prompt 和 response。

这对开发调试很方便,但生产环境要谨慎。用户输入可能包含隐私、合同、账号、内部资料。模型输出也可能包含敏感推断。

我建议默认记录元信息:

1.场景名。 2.模型名。 3.请求 ID。 4.输入 token。 5.输出 token。 6.耗时。 7.是否成功。 8.错误类型。

完整内容可以采样、脱敏,或者进入受控审计存储。不要直接全量打应用日志。

成本从第一版开始算

  AI 功能上线后,成本问题来得很快。

如果没有按场景统计 token,用一段时间后很难知道钱花在哪里。

ChatClient 封装层应该记录每次调用的用量。按场景、模型、用户或租户统计都可以。这样才能回答:哪个功能最贵,哪个输入过长,哪里可以换小模型,哪里可以加缓存。

很多 AI 功能不是技术上不能做,而是成本上不能无限做。

小结

  ChatClient 是 Spring AI 里最容易上手的入口,但真正落地要从“怎么调”走到“怎么封装”。

  业务层不要直接散落模型调用。按场景封装服务,分开 system 和 user,显式管理模板变量,校验结构化输出,处理超时、取消、日志和成本。

  这样 ChatClient 才不是一个漂亮的 demo API,而是能放进 Java 后端项目里的稳定基础能力。

気に入ったならばコメントを残してくださいね~

© 2019 - 2026 VincentHo @VincentHo
Powered by theme astro-koharu · Inspired by Shoka