序章 · 为何要一座译馆
下一章序章 · 为何要⼀座译馆
"我不替你说话,我只让你被听懂。" ——若让 Model<TReq, TRes> 这个接⼝开⼝⾃⽩,它⼤概会这么说。
故事得从⼀个尴尬的午后讲起。
某位 Java ⼯程师——就叫他客官吧——接到⼀个看似简单的活:给系统接⼀个⼤模型,做点对话和摘要。他打开 OpenAI 的 SDK,写好请求体,调通了。三周后产品说"OpenAI 太贵,换成国产的 DeepSeek 吧"。他翻出 DeepSeek 的⽂档,发现请求字段名不⼀样、流式返回的分块格式不⼀样、错误码语义也不⼀样。⼜三周,合规要求"本地化部署,上 Ollama"。客官盯着满屏的 if (provider == ...) 分⽀,第⼀次认真考虑了转⾏。
这就是 AI 集成的"巴别塔诅咒":每⼀家模型诸侯都说⾃⼰的⽅⾔。番邦众多,OpenAI、Anthropic、DeepSeek、Ollama、Mistral、Google……各说各话,各发各的国书格式。客官⼿⾥没有⼀纸通⾏的契约,只能⼀家⼀家地学⽅⾔、⼀家⼀家地改代码。
于是,有⼈盖了⼀座译馆。
⼀、译馆是什么:给 Java ⽣态的 AI 应⽤框架先把隐喻放⼀边,说⼈话。
Spring AI 是 Spring 官⽅出品、⾯向 Java/Spring ⽣态的 AI 应⽤框架。 它的官⽅⾃述写得很克制( README.md:5 ):
Its goal is to apply Spring ecosystem design principles, such as portability and modular design, to the AI domain and promote using strongly-typeddata structures and APIs as the building blocks of an application.翻译过来:把 Spring ⽣态那套⽼规矩——可移植(portability)、模块化(modular design)、强类型(strongly-typed)——原样搬进 AI 领域。它要解决的根本难题,README 第 9 ⾏说得直⽩:connecting your enterprise Data and APIs with the AI Models——把你的企业数据、你的接⼝,接到那些 AI 模型上去。
所以它不是⼀个"调 LLM 的⼯具库",⽽是⼀座译馆:你的应⽤是远道⽽来的客官,要和天下各路模型诸侯做⽣意却语⾔不通;译馆提供⼀整套通译服务,让你⼀纸⽂牒⾛天下,且开箱即起馆。
这两句不是我编的⼝号,它们正是这本书要反复回扣的两根设计⽀柱。我们⼀根⼀根看。
⼆、第⼀根⽀柱:⼀纸⽂牒⾛天下(portable abstraction)
诅咒的本质,是客官每换⼀家诸侯就要重学⼀套⽅⾔。译馆的破解之法,是发明⼀张通⽤⽂牒——⼀份所有诸侯都认、所有诸侯都按它来回话的统⼀契约。客官只填这⼀张⽂牒,⾄于把它翻成 OpenAI ⽅⾔还是 DeepSeek ⽅⾔,是译馆⾥驻馆翻译官的活,与客官⽆关。
这张⽂牒的真身,是⼀个朴素到近乎吝啬的泛型接⼝( spring-ai-model/.../model/Model.java:31 ):
public interface Model<TReq extends ModelRequest<?>, TRes extends ModelResponse<?>> {
TRes call(TReq request);}就这么⼀个⽅法。⼀进⼀出,请求换响应。两个泛型参数 TReq / TRes 是留给各模态去特化的卡槽。对话场景把它特化成 ChatModel ( spring-ai-
model/.../chat/model/ChatModel.java:30 ):
public interface ChatModel extends Model<Prompt, ChatResponse>, StreamingChatModel {@Override
ChatResponse call(Prompt prompt);// ...
}Prompt 进, ChatResponse 出。换成⽂本嵌⼊就是 EmbeddingModel ,换成⽂⽣图就是 ImageModel ,全都是同⼀张⽂牒的不同填法。客官学会⼀种,等于学会全部——这就是"⼀纸⽂牒⾛天下"的字⾯意思。
🦴 ⻣灰级细节:default ⽅法把⼈体⼯学缝进了契约⾥。 看 ChatModel 的两个 default 重载( ChatModel.java:32-43 ):
default @Nullable String call(String message) {
Prompt prompt = new Prompt(new UserMessage(message));
Generation generation = call(prompt).getResult();
return (generation != null) ? generation.getOutput().getText() : "";
}接⼝的抽象⽅法只有 call(Prompt) 这⼀个真正要驱动翻译官去实现,⽽ call(String) 、 call(Message...) 全是 default——它们在接⼝内部就把字符串裹成Prompt 、再把 ChatResponse 拆成纯⽂本。意思是:翻译官只需⽼⽼实实实现那⼀个最严格的⽅法,客官却⽩⽩多得了⼏个"懒⼈⼊⼝"。契约的硬核与调⽤的甜头,被⼀⼑切在了 default 这条线上。
⚠ 暗礁: stream 是张"可以不接的牌"。 别以为实现了 ChatModel 就⾃动⽀持流式。看接⼝尾巴( ChatModel.java:64-66 ):
default Flux<ChatResponse> stream(Prompt prompt) {
throw new UnsupportedOperationException("streaming is not supported");
}stream 是个默认直接抛异常的 default ⽅法。也就是说,⽂牒在纸⾯上"承诺"了流式能⼒,但某个翻译官完全可以⼀句不接、原样继承这个抛异常的实现。客官若不查证就对⼀个冷⻔ provider 调 stream() ,运⾏期才会被 UnsupportedOperationException 糊⼀脸。"可移植"是接⼝形状的可移植,不等于能⼒的处处对⻬——隐喻在这⾥有个边界:⽂牒⻓得⼀样,不保证每家诸侯都把每⼀栏都填上。本书后⾯会反复撞⻅这种"形状统⼀、能⼒参差"的缝。
顺带点⼀处真实的"新旧之变",很能说明这框架还活着: getOptions() 是 2.0.0 才转正的⽅法( ChatModel.java:52 , @since 2.0.0 ),⽼的getDefaultOptions() 已经挂上 @Deprecated(forRemoval = true) ( ChatModel.java:59-62 )。⽂牒不是刻在⽯头上的,它在迭代——这意味着你读到的某些"标准答案",过两个版本可能就改了措辞。这是好事,也是读源码要时刻校准版本的理由。
三、第⼆根⽀柱:开箱即起馆(Spring Boot ⼀等公⺠)
光有通⽤⽂牒还不够。客官最怕的第⼆件事,是"配置地狱"——为了让译馆运转,要⼿写⼀⼤堆 Bean、连⼀堆线、调⼀沓参数。
Spring 的祖传绝活正好是这个:约定优于配置。Spring AI 把它发挥到极致——你在 start.spring.io 勾⼀个模型,引⼀个 starter 依赖,填⼀个 API key,馆就起来了。 ChatClient 直接能注⼊,开⼝就能对话。这就是开箱即起馆:⼀个 starter,⾃动起⼀座馆。
但这⾥藏着⼀个容易被忽略、却是整本书最该先讲清楚的设计抉择——译馆的"馆⼼"故意不依赖 Spring Boot。
这不是我猜的,是写在设计⽂档⾥的硬约束( design/02-boot-modularity.adoc:9-14 ):
The Spring AI core classes should be usable without Spring Boot and as such should not depend on boot. Core classes include common modules,various model implementations ..., VectorStore abstraction and implementations, but also the ChatClient abstraction and Advisors , MCPsupport. As a matter of fact, all functionality of Spring AI is available to a plain Spring Framework app.读懂这段,你就握住了整座译馆的承重墙。它说:所有核⼼能⼒——模型抽象、向量库、ChatClient、Advisor,连 MCP 都点名在内——都能在⼀个纯 SpringFramework 应⽤⾥裸跑,不需要 Spring Boot。 Boot 只是锦上添花的"快速接线⼯",不是地基。
为什么这很重要?因为它把"框架的能⼒"和"框架的便利"彻底解耦了。你想要便利,引 starter;你不想被 Boot 绑死(⽐如⽼的纯 Spring MVC ⼯程、或某些受限运⾏时),照样能⽤全部核⼼。"⼀等公⺠"不等于"唯⼀公⺠"——Boot 是 VIP,但译馆的⼤⻔对所有 Spring 应⽤敞开。
四、三层架构⻦瞰:馆⼼ / 装配房 / 套装铺把上⾯那段设计⽂档落到⼯程⽬录,就是贯穿全书的三层⻣架( design/02-boot-modularity.adoc:9-44 ):
┌─────────────────────────────────────────────────────────┐│ 套装铺 starters/ (不含代码,纯依赖聚合) │ ← 开箱│ ↓ 拉起对应 autoconfig + 传递依赖 │├─────────────────────────────────────────────────────────┤│ 装配房 auto-configurations/ (依赖 Boot,@AutoConfiguration)│ ← 接线│ ↓ 把馆⼼的类⽤ Boot 约定⾃动装成 Bean │├─────────────────────────────────────────────────────────┤│ 馆⼼ spring-ai-model / -client-chat / vector-stores / │ ← 能⼒│ mcp / advisors … (不依赖 Boot,可裸跑) │└─────────────────────────────────────────────────────────┘
馆⼼(core): spring-ai-model 、 spring-ai-client-chat 、 vector-stores/ 、 mcp/ 、 advisors/ 这些——所有真本事都在这层,且不碰
Boot( design/02-boot-modularity.adoc:9-14 )。
装配房(auto-configurations): auto-configurations/ 下⼀堆 @AutoConfiguration ,负责"底层库在 classpath 上就⾃动把馆⼼的类接成 Bean"。规矩很死:底层库不在 classpath 时必须零影响,所以它对外的依赖(连触发 @AutoConfiguration 的 spring-boot-autoconfigure 都算)统统标
optional ( design/02-boot-modularity.adoc:16-26 )。套装铺(starters): starters/ 下⼀排不含⼀⾏代码的 pom,职责就是"把某个特性 X 的装配房 + 它需要的传递依赖⼀并拉来",并通过 spring-boot-
starter 引⼊ spring-boot-autoconfigure 让装配房的魔法⽣效( design/02-boot-modularity.adoc:28-35 )。物理布局上⼀眼可证:根⽬录⾥ mcp/ (馆⼼)、 auto-configurations/ (装配房)、 starters/ (套装铺)三个⽂件夹各司其职、泾渭分明。
✨ 爽点时刻:这条边界不是靠⼝头君⼦协定,是⽤构建⼯具焊死的。 从 Spring AI 2.0 起,⼯程在 maven-enforcer-plugin ⾥配了 bannedDependencies 规则,在
编译期机器校验两条铁律( design/02-boot-modularity.adoc:37-44 ):the core codebase does NOT depend on boot,
all dependencies of auto-configurations have been marked optional=true except those to other auto-configurations意思是:哪个新来的贡献者⼀时⼿滑,在馆⼼⾥ import 了⼀个 Boot 的类,Maven 直接编不过去,PR 红着脸打回。 架构纪律不靠⼈盯,靠机器守。这是我个⼈最欣赏Spring AI 的⼀处⼯程素养——很多框架嘴上说"core 解耦",实则 import 早就缠成⼀团乱麻,因为没⼈拦得住。Spring AI 把"解耦"从⼀句愿景变成了⼀条 CI 红线。
当然,也得说句公道的质疑:这套"machine-enforced 边界"的代价,是 optional 依赖满天⻜——⽤户若绕开 starter、直接依赖某个 auto-configuration 模块,就得⾃⼰⼿动把那些被标成 optional 的传递依赖补回来。设计⽂档⾃⼰也承认了这个⽤户影响( design/02-boot-modularity.adoc:45-48 ):绕过 starter 的⽤户"may require adding back a dependency that was incorrectly provided in the past"。换⾔之,纪律的整洁,是⽤"⾮主流⽤法的繁琐"换来的。对 99% ⾛ starter的⼈⽆感,对那 1% 想精细控制依赖的⼈略硌脚。没有银弹,只有取舍——这也是本书会⼀直保持的态度:夸要挣来,坑也照说。
五、阿芽登场到这⾥,该让这本书的向导出场了。
它叫阿芽——⼀株戴着书⽣帽、围着围⼱的绿⾊⼩芽,呼应 Spring 那⽚标志性的绿叶。阿芽是译馆⾥跑得最勤的学徒:你递⽂牒,它替你跑腿;你问哪条调⽤链通向哪⼉,它领你穿过驿丞的关卡、推开藏经阁的⻔、踩进差役调度房的回路。往后每⼀章,阿芽都会在配图⾥冒头,有时⼀脸"这设计真妙"的得意,有时⼀脸"这⾥有坑"的警觉。
阿芽只是调味。真正的主菜,永远是那些标着 file:line 的真实代码。 隐喻负责让你记得住,代码坐标负责让你信得过——这是本书的铁律,也是阿芽反复叮嘱你的:
别只听故事,要回源核实。
六、⼀张全书地图(七卷,只报站名不剧透)
阿芽⼿⾥有⼀张译馆的总图。七卷,从最⾥的馆⼼⼀路⾛到最外的⼝岸:
1. 卷⼀ · 通⽤⽂牒—— Model 泛型三件套与可移植抽象。⽂牒到底⻓什么样、 Prompt / ChatResponse /四种消息⻆⾊如何成型。(就是你正要去的下⼀章)
2. 卷⼆ · 前台掌柜—— ChatClient 的 fluent API,与那⼀排层层盘查的驿丞(Advisor)责任链。
3. 卷三 · 差役调度房——Tool Calling:模型如何"申请"派差,以及回路在 2.0 ⾥从 ChatModel 上移到了 ToolCallingAdvisor 的那次⼤搬家。
4. 卷四 · 藏经阁与调阅之道——VectorStore、⽂档 ETL ⼊藏流⽔线,和简版/模块化两种 RAG。
5. 卷五 · 起居注——ChatMemory:对话记忆的窗⼝裁剪,与可插拔的档案库房。
6. 卷六 · 驻馆翻译官——以 OpenAI 为样本,看⼀个 provider 实现如何把通⽤⽂牒翻成番邦⽅⾔(这⼀版已改封装官⽅ SDK,不再⼿写 OpenAiApi )。
7. 卷七 · 通商⼝岸——MCP 邦交协议,与 Spring Boot ⾃动配置/starter 的装配范式,外加账房(观测)、备⽤驿⻢(重试)、公⽂模板(StringTemplate)。
地图就摊到这。具体每⼀站的机关、暗格、坑洞,留到对应章节再逐⼀拆解——序章不剧透,只负责让你知道:这座译馆值得⼀章章⾛下去。
🎯 三句带⾛1. Spring AI 是 Spring 官⽅的 Java AI 应⽤框架,核⼼命题是⽤统⼀的强类型抽象,把企业数据/接⼝接到各家 AI 模型上。
2. 两根设计⽀柱:可移植抽象( Model<TReq,TRes> ⼀接⼝⾛天下, Model.java:31 )+ Spring Boot ⼀等公⺠(starter 开箱即⽤,但 core 不依赖 Boot、可裸跑)。
3. 三层架构 core / auto-configurations / starters,边界由 maven-enforcer-plugin 的 bannedDependencies 机器焊死( design/02-boot-
modularity.adoc:37-44 )。阿芽已经把通⽤⽂牒从案头取了下来,正摊在你⾯前。它薄得只有⼀个 call ⽅法,却号称能让你跟天下诸侯做⽣意——这张纸凭什么"⾛天下"? Prompt 进、ChatResponse 出,中间那四种消息⻆⾊⼜各⾃封着什么印?推开第⼀道⻔,我们就去验明这张⽂牒的正身。
下⼀章:第⼀章 · ⼀纸⽂牒⾛天下:Model 泛型三件套与可移植抽象。