终章 · 译馆的野心与边界
else if (this.validationMode == ValidationMode.THROW) {
throw new IllegalStateException(VALIDATION_MESSAGE.formatted(missingVariables));
}
}
return missingVariables;
}—— StTemplateRenderer.java:134-149它算出⼀个差集:模板⾥要求的变量 − 你实际提供的变量 = 缺失的变量,然后把缺的变量名直接写进异常消息("Missing variable names are: ...")。这个默认值的选择,是公⽂房的态度宣⾔:prompt 漏填变量,是必须当场暴露的 bug,不是可以静默吞掉的⼩事。 想想看,如果默认是 NONE ,你模板⾥ {context} 没填上,渲染出来就是个缺了上下⽂的残缺 prompt,模型照样给你⼀本正经地胡说⼋道——⽽你毫⽆察觉,排查半天都想不到是模板漏了变量。Spring AI 选择"宁可在渲染期就炸,也不让残缺 prompt 溜到模型⾯前",这是经验换来的偏执。
🦴 ⻣灰级细节:它怎么知道模板"要求"哪些变量?——直接啃 ST 的 token 流。 getInputVariables 没有⽤正则去暴⼒扫 {xxx} ,⽽是拿到 StringTemplate
编译后的 token 流( st.impl.tokens )逐个 token 分析( StTemplateRenderer.java:151-196 )。它能精确区分⼏种容易误判的情况:列表变量带选项{items; separator=", "} —— items 算输⼊变量;属性访问 {user.name} —— name 跟在点号后⾯( STLexer.DOT ),不算输⼊变量(你只需要给 user );
函数调⽤ {trim(x)} —— trim 后⾯跟着左括号,是函数不是变量,不算。它甚⾄还认得 StringTemplate 的内置函数名( Compiler.funcs ):默认情况下first 、 rest 、 length 这些内置函数名不会被当成必须提供的变量,除⾮你显式开了 validateStFunctions ( :164 、 :189 )。这种"懂语法、不靠猜"的解析,正是为什么它的缺失变量报告能精准到不误伤——⽤正则糊⼀个,迟早会把 user.name ⾥的 name 当成漏填的变量报给你,徒增噪⾳。这是把"看似⼀个简单的字符串替换"做到⻣灰级的功夫。
六、收束:两条腿⾛路,⼀把焊枪守⻔把这⼀章合上之前,我想把整座译馆的设计哲学拧成⼀句话。
Spring AI 同时押注两件看似⽭盾的事,⽽且两条腿都站稳了:
⼀条腿是"可移植抽象":馆⼼(core)不挂靠任何朝廷,⼀纸通⽤⽂牒⾛天下。你可以裸⽤ ChatModel、VectorStore、MCP,哪怕你的⼯程跟 Spring Boot ⼋竿⼦打不着。账房记账只认 Micrometer、可以 NOOP;驿⻢重试只认 Spring Framework 原⽣ RetryTemplate ;公⽂房只认 StringTemplate——馆⼼的每⼀处基础设施,都刻意不去依赖 Boot。
另⼀条腿是"Spring Boot ⼀等公⺠":装配房⽤ @AutoConfiguration + ObjectProvider 软依赖,把馆⼼的零件⾃动接好;套装铺⽤⼀个零代码的 starter⼀键起馆,约定优先于配置。
⽽⽀撑这两条腿不会随时间⻓歪的,是那把 maven-enforcer 的焊枪——把"core 不依赖 boot""autoconfig 依赖必须 optional"这两条祖训,从⽂档⾥的⼀句话,变成了每次构建都要过的⼀道关卡。这是 Spring AI 2.0 最该被记住的⼯程姿态:能机器化的纪律,绝不留给⼈的⾃觉。
爽是真爽。但这⼀章我也按住了你的兴奋,点了两处暗礁:分层的优雅,代价是模块数量爆炸和新⼈的认知负荷;默认重试把 429 ⼀⼑切成"不可重试",在瞬时限流场景下偏保守。⼀座设计精良的译馆,既值得你赞叹它的地基,也值得你警惕它的盲区——这才是⻣灰级读者该有的眼光。
🎯 三句带⾛1. 三层是硬约定,不是软建议:core 不依赖 Boot(可裸跑)、auto-configuration 的⾮测试依赖全标 optional (库不在 classpath 则零影响)、starter 不含代码只
聚合;Spring AI 2.0 ⽤ maven-enforcer 的 bannedDependencies 在构建期机器化校验这两条边界。2. 三套基础设施贯穿全框架,且都"core 只依赖中⽴库":可观测⽤ Micrometer Observation 四件套(模型调⽤与 VectorStore 的 add/delete/search 全被
.observe(...) 包裹,registry 可 NOOP);重试⽤ Spring Framework 原⽣ RetryTemplate ,4xx(含 429)→ NonTransientAiException 不重试、其余
→ TransientAiException 重试,默认指数退避 maxRetries=10/初值 2s/倍数 5/封顶 3min。3. PromptTemplate 默认从严: StTemplateRenderer 基于 StringTemplate v4、每次新建 ST 实例保证线程安全、 ValidationMode 默认 THROW ,靠解析ST token 流精确算出缺失变量并把变量名写进异常——漏填即炸,不让残缺 prompt 溜到模型⾯前。
承上启下我们从⼀纸通⽤⽂牒出发,⾛过了前台掌柜、⼀排驿丞、藏经阁、起居注、差役调度房、通商⼝岸,最后退到⾼坡上看清了整座译馆的三进地基和三套基础设施。架构看懂了,哲学也拎清了——可移植与开箱即⽤两条腿,加上⼀把焊死边界的焊枪。
可是,⼀座野⼼如此之⼤、想"⼀纸⽂牒通天下"的译馆,它的边界究竟在哪⾥?它擅⻓什么、⼜故意不碰什么?那些被它抽象掉的东⻄,代价⼜由谁来付?当所有零件都拆完、所有祖训都读透,我们终于可以问出最后⼀个、也是最尖锐的问题:这座译馆的野⼼,撑得起它的承诺吗?它的边界,⼜划在了哪⾥?
下⼀章,也是全书最后⼀章——「终章 · 译馆的野⼼与边界」,我们⼀起替这趟旅程画上句点。
终章 · 译馆的野⼼与边界
"我让你被听懂——但我从没承诺,让你听懂的那⼀刻不再付代价。" ——若让整座春芽译馆开⼝收尾,它⼤概会这么说。
序章⾥,客官盯着满屏的 if (provider == ...) 想转⾏。如今我们已经把这座译馆从最⾥的馆⼼⾛到了最外的⼝岸:验过那张薄得只有⼀个 call ⽅法的通⽤⽂
牒,数过驿丞责任链上每⼀道关卡的 order,蹲守过差役调度房⾥那条"派差→办差→回执回填→再请模型"的回路,推开过藏经阁的⻔,翻过起居注的窗⼝裁剪,看过驻馆翻译官如何把通⽤⽂牒译成 OpenAI ⽅⾔,最后站在通商⼝岸看 MCP 与 starter 如何让两座译馆互认差役、开箱起馆。
七卷⾛完,该有⼈给这座译馆做⼀份结账单了。不是软⽂式的"样样精妙",也不是泄愤式的"处处是坑"——⽽是把它的野⼼和它的边界,⼀笔⼀笔摊在案头。阿芽搬来⼀把椅⼦,坐下了。这⼀章,它不领路,它陪你算账。
⼀、先把功劳记清楚:这座译馆挣到的四块匾夸要挣来。Spring AI 有⼏处设计,是我⾛完全书后愿意真⼼实意拍板的。
第⼀块匾:可移植抽象,是真扎实,不是 PPT 话术。 整座译馆的承重墙,是那个吝啬到只有⼀个⽅法的接⼝( spring-ai-model/.../model/Model.java:31 , @since
0.8.0 ):
public interface Model<TReq extends ModelRequest<?>, TRes extends ModelResponse<?>> {
TRes call(TReq request);
}对话特化成 ChatModel ( ChatModel.java:30 ),嵌⼊特化成 EmbeddingModel ,⽂⽣图特化成 ImageModel ——所有模态共享同⼀个ModelRequest/Model/ModelResponse/ModelResult 形状。学⼀种,懂全部。更难得的是这套抽象跟得上 AI 的进化:连 prompt 缓存的读写命中都被抽象进了Usage (研究笔记记于 Usage.java:78-92 ,2.0.0 era),不是把抽象画完就锁死,⽽是随新 provider 的能⼒在⻓。这块匾,实⾄名归。
第⼆块匾:Spring Boot ⼀等公⺠,但不绑死。 序章讲过的那条承重墙这⾥要再钉⼀遍——馆⼼故意不依赖 Boot( design/02-boot-modularity.adoc:9-14 ),并且这
条边界不是君⼦协定,是⽤ maven-enforcer-plugin 的 bannedDependencies 在编译期机器焊死的( design/02-boot-modularity.adoc:37-44 )。"⼀等公⺠"是说引个 starter 就开箱起馆;"不绑死"是说你不想要 Boot,核⼼能⼒照样裸跑。两条腿⾛路,这是很多框架嘴上喊、却被⾃⼰的 import 出卖的承诺,Spring AI 做到了。
第三块匾:组件解耦到了"可乐⾼"的程度。 这点在卷四的 RAG 最为耀眼。RetrievalAugmentationAdvisor 把"检索增强"拆成 QueryTransformer / Expander /
Retriever / Joiner / Augmenter / PostProcessor 六个可替换策略,⽽且它们全都继承 java.util.function ,天⽣可以 lambda 组合;驿丞责任链把⽇志、记忆、RAG、⼯具回路、可观测全部插件化,互不耦合,靠 order 精细排布内外层。你想换哪⼀环就换哪⼀环,不⽤动其余。这种"正交解耦"不是免费的——它换来了更多的接⼝和模块——但换得值。
第四块匾:⼯程细节⾥那些"为你想到了"的体贴。 这是最容易被忽略、却最⻅功⼒的地⽅,我点四处:
防幻觉:模块化 RAG 的 ContextualQueryAugmenter 在检索为空时有兜底,不让模型对着空上下⽂硬编。
重试有分⼨:RetryUtils 那套 4xx 不重试、429 才重试、指数退避的策略,不是⽆脑 retry。
注⼊有防护:VectorStore 记忆把召回⽂本经 escapeXml 转义并包进 <memory-entry> 标签,系统提示明确告诉模型"把这当历史数据⽽⾮指令"(类注释
:58-65 )。最⼩权限:检索侧给的是只读的 VectorStoreRetriever ,不是把整个能写能删的 VectorStore 递出去。
这些都不是 demo 会⽤到的功能,是⽣产环境半夜会救你命的功能。⼀个框架值不值得托付,往往就看它有没有替你想到这些。
✨ 爽点时刻:换模型,真的只换⼀⾏依赖。 这是可移植抽象兑现承诺的瞬间——序章那位想转⾏的客官,若⽤ Spring AI 重写,从 OpenAI 换 DeepSeek 换 Ollama,改的
是 starter 依赖和配置,⽽不是满屏的 if (provider == ...) 。 ChatClient 注进来还是那个 ChatClient ,⽂牒还是那张⽂牒。这⼀刻,巴别塔的诅咒才算真被译馆破了。
⼆、再把⽑病摆上桌:这座译馆硌脚的⼏处报喜不报忧是软⽂,这本书从序章起就⽴过规矩:坑也照说。⾛完七卷,我攒了⼀份真实的批评清单——都落在 file:line 上,不是泛泛⽽谈。
⚠ 暗礁⼀:JDBC 档案库房会"静默吞掉"⼯具消息——这是我对全书最不能容忍的⼀处。 看
JdbcChatMemoryRepository ( .../repository/jdbc/JdbcChatMemoryRepository.java:113-118 ):
.filter(m -> !(m instanceof ToolResponseMessage)
&& !(...带 toolCalls 的 AssistantMessage...))// ...
logger.warn("JdbcChatMemoryRepository does not support tool call messages. "
+ "Some messages were filtered out for conversation: " + ...);它把 ToolResponseMessage 和带 toolCalls 的 AssistantMessage 直接过滤掉,只打⼀条 warn ⽇志就算交代。后果很现实:你做⼀个有⼯具的 Agent,⼜选了 JDBC 持久化记忆,那么"模型申请了哪些差役、差役回了什么"这段最关键的上下⽂,在落库时被悄悄抹掉了。下⼀轮对话从库⾥读回来的起居注,是残缺的。⼀条warn 拦不住⼀个粗⼼的⼯程师踩进这个坑——这不是"暂不⽀持"那么轻巧,这是把⼀个语义缺失藏在了⽇志⾥。要做⼯具型 Agent,记忆这块得绕开 JDBC,或者⾃⼰实现 Repository 把⼯具消息存全。
⚠ 暗礁⼆:合并范式不统⼀,⼼智负担散落各处。 同样是"options 怎么合并",规则却散落多处、各说各话: ChatOptions.combineWith ⾥ stopSequences 是「追
加」⽽其余字段是「覆盖」( DefaultChatOptionsBuilder.java:133-142 ); ToolCallbacks ⼜是「runtime ⾮空则整体替换」
( ToolCallingChatOptions.java:69-75 )。更别扭的是 provider 层:OpenAI 的 Chat 把 options merge 上提给了 ChatClient,⽽同⼀个 OpenAI 模块的
Embedding/Image 却还在 model 内部⾃⼰ from().merge() (笔记记于 OpenAiEmbeddingModel.java:220-223 / OpenAiImageModel.java:167-170 )。同⼀个译馆⾥,Chat 和 Embedding ⽤着两套不⼀样的合并规矩,读源码时得反复换挡。缺⼀个单⼀权威的合并⼊⼝,是这套抽象⽬前最明显的"⼯艺裂缝"。
暗礁三:⼏处过胖与过时,暴露了"活框架"的代价。 OpenAiChatOptions ⼀个类 1187 ⾏(我亲⾃ wc -l 数过),把连接参数
(baseUrl/apiKey/proxy/timeout/maxRetries)和模型参数混在⼀起, combineWith ⾥⼤量⼿写 if ,新增字段极易漏改。 OpenAiChatModel 的 createRequest是个 400 ⾏的巨型⽅法(笔记记于 :490-899 ),消息映射和选项映射全堆⼀处,可读性和可测性都偏弱。还有过时注释残留:Anthropic 的 internalCall 注释还写着"recursively",但⼯具回路早已上移、实际不再递归(笔记记于 AnthropicChatModel.java:522-523 )——这种注释⽐没注释更坏,它会骗读源码的⼈以为 model 层还在跑⼯具循环。
暗礁四:⼏处"过渡态"和"仅测试⽤"的东⻄,别当⽣产件⽤。 SimpleVectorStore 全表扫描算余弦相似度,O(n),笔记和类设计都明说仅供测试,⽣产必须换专业库。
doDelete(Filter.Expression) 在抽象基类 AbstractObservationVectorStore ⾥还是 UnsupportedOperationException 的过渡态(笔记记于 :162-167 ),各库⽀持度不⼀。向量库当前只⽀持⽂本⽂档——往⾥塞带媒体的 Document, add() 会直接抛( AbstractObservationVectorStore.java 那段 "Onlytext documents are supported for now" 我核过原⽂)。 TokenTextSplitter 默认⽆重叠(overlap=0),跨块语义可能被切断,这个默认值对很多 RAG 场景并不友好。
这些⽑病不致命,但它们共同说明⼀件事:Spring AI 还很年轻,版本演进很快。⼯具回路从 ChatModel 上移到了 ToolCallingAdvisor 、internalToolExecutionEnabled 被重构、 PromptChatMemoryAdvisor 被移除、OpenAI 模块从⼿写 OpenAiApi 改成封装官⽅ SDK——这⼀连串"新旧之变"是好事(框架在⻓),但代价是⽂档和注释经常滞后于代码。你读到的任何"标准答案",都要先问⼀句:这是哪个版本的答案?
三、同源不同路:译馆与它的"远房同⻔"
⾛到这⾥,有⼀段渊源不挑明就太可惜了。
卷六解剖 OpenAI provider 时,在 OpenAiSetup 的类注释⾥,埋着⼀句很坦诚的话( models/spring-ai-
openai/src/main/java/org/springframework/ai/openai/setup/OpenAiSetup.java:46-48 ):/**
* Helps configure the OpenAI Java SDK, depending on the platform used. This code is
* inspired by LangChain4j's
* `dev.langchain4j.model.openaiofficial.InternalOpenAiOfficialHelper` class, which is
* coded by the same author (Julien Dubois, from Microsoft).*/
Spring AI 的 OpenAI 配置逻辑,明说借鉴⾃ LangChain4j——⽽且是同⼀位作者(Julien Dubois,来⾃微软)亲⼿写的。 这不是抄袭,是 Java AI ⽣态⾥⼀段很健康的"同源"佳话:⼀个⼈在两个框架⾥都解过同⼀道题(怎么优雅地配置那个官⽅ OpenAI SDK、怎么⼀套代码同时服务 OpenAI / Azure / GitHub Models),于是好的解法在两边都开了花。
但同源,不同路。LangChain4j 是社区驱动、起步更早、⻛格更"框架⾃由派";Spring AI 是 Spring 官⽅出品,从⻣⼦⾥就带着 Spring 的执念——三层架构、约定优先、 @AutoConfiguration 装配、与 Spring Boot / Micrometer Observation / Spring Retry 深度共⽣。同⼀段 SDK 配置代码可以互相借鉴,但整座译馆的形状
是被 Spring ⽣态的设计哲学定型的。
所以选型时,这条岔路就是关键判据:你的应⽤如果已经⻓在 Spring ⽣态⾥(Spring Boot、Spring Security、Spring Data ⼀整套),Spring AI 是顺理成章的⼀等公⺠,它和你已有的 Bean、配置、可观测体系⽆缝咬合;你如果不在 Spring ⽣态、或想要更轻更⾃由的取舍,LangChain4j 这条同⻔别路也许更趁⼿。同⼀道题,两种⻛格的好答案,挑合脚的那只穿。
四、给客官的落地⼿册:怎么⽤、什么时候⾃⼰动⼿讲了这么多源码,最后得落到"我到底该怎么⽤"。阿芽把它整理成⼀张决策梯,从最省事到最折腾,按你的需求往下⾛。
第⼀档·最⼩集成,馆⼼裸跑。 你不⼀定⾮要 Spring Boot。序章那条设计⽂档说得明⽩:模型抽象、VectorStore、ChatClient、Advisor,连 MCP,都能在纯 SpringFramework 应⽤⾥裸跑( design/02-boot-modularity.adoc:9-14 )。所以⼆开 / 嵌⼊⽼⼯程 / 受限运⾏时的客官,可以只引馆⼼模块,⾃⼰ new 出 provider 的ChatModel,⼿动接线。代价是那些被标 optional 的传递依赖要⾃⼰补回来(序章那处"machine-enforced 边界的代价"),但你换来了完全的掌控。
第⼆档·⾛ starter,开箱起馆(99% 的⼈该⾛这条)。 引⼀个 spring-ai-starter-model-xxx ,填⼀个 API key, ChatClient 直接注⼊。绝⼤多数应⽤到此为⽌就够了。需要 RAG?加⼀个 QuestionAnswerAdvisor (简版"⼀问⼀查")。需要记忆?加 MessageChatMemoryAdvisor 。需要⼯具?⽤ @Tool 招募差役。直接⽤ChatClient + 内置 Advisor,不要⾃⼰造轮⼦——这是默认姿势。
第三档·何时该⾃⼰写 Advisor / Provider / Repository? 这是分⽔岭,给三条明确的判据:
⾃定义 Advisor:当你要插⼊⼀段横切逻辑(审计、限流、敏感词、⾃定义 RAG 流程、按租户改写 prompt),⽽内置 Advisor 覆盖不到时。记住卷⼆那条反直觉的坑:order 数值越⼩越外层,记忆 Advisor 和⼯具 Advisor 的 order 关系搞错会让历史/⼯具回路⾏为异常。还有流式那条: BaseAdvisor 的流式 after 只在 finishReason 帧触发,别指望逐帧。
⾃定义 Provider:当 Spring AI 还没收录你那家诸侯时。照着卷六的范式,优先封装官⽅ SDK(像 OpenAI 模块那样),⽽不是⼿搓 HTTP——把retry/timeout/observation 下沉到 HTTP 层,model 层只做映射,会⼲净很多。
⾃定义 Repository:上⾯那个 JDBC 吞⼯具消息的暗礁就是触发点——做⼯具型 Agent ⼜要持久化记忆,⾃⼰实现 ChatMemoryRepository 把⼯具消息存全,只需实现 4 个⽅法。
🦴 ⻣灰级细节: ChatModel 的 default ⽅法,是你写"原型"和"裸跑"时最被低估的便利。 序章点过 call(String) 是 default,这⾥要把它的落地价值挑明:接⼝
的抽象⽅法只有 call(Prompt) ⼀个真要 provider 去实现,⽽ call(String) / call(Message...) 全是 default,在接⼝内部就把字符串裹成 Prompt 、把ChatResponse 拆成纯⽂本( ChatModel.java:32 起的那⼏个重载)。这意味着你做最⼩集成 / 快速验证时,⼀⾏ chatModel.call("你好") 就能跑通,完全不必先学会 Prompt / Message / ChatResponse 那⼀整套。等你要精细控制⻆⾊、options、流式了,再升级到 call(Prompt) 。契约的硬核留给 provider,调⽤的甜头⽩送给你——这条 default 线,是 Spring AI 把"上⼿⻔槛"和"⼯程严谨"同时照顾到的⼀个缩影。
什么时候它不适合你? 也得说清楚,免得误⽤:
你只想糊⼀个⼀次性脚本 / Notebook 调⼀下 LLM——Spring AI 那套三层架构、Bean 装配、Advisor 链是杀鸡⽤⽜⼑,直接调 provider SDK 更省事。
你的团队完全不在 JVM ⽣态——别为了⼀个框架把整个技术栈搬到 Java。
你要的是 LangChain 那种"重 Python ⽣态、海量社区集成、研究态实验"——Spring AI 的强类型和⼯程纪律在那种场景反⽽是束缚。
你需要某些极冷⻔ provider 的极冷⻔能⼒——可移植抽象保证的是"接⼝形状⼀致",不保证"能⼒处处对⻬"(序章那条 stream() 默认抛异常的暗礁就是明
证)。这种时候,要么⽤ provider 的 native 逃⽣舱( getNativeClient() / getNativeUsage() / extraBody ),要么直接贴着官⽅ SDK 写。⼀句话:Spring AI 是给"认真做产品、且⻓在 Spring ⽣态⾥"的团队准备的。它⽤前期的抽象学习成本,换你后期的可移植性、可观测性和⼯程纪律。这笔账,对⻓期项⽬划算,对⼀次性玩具不划算。
五、阿芽收束:野⼼与边界,本就是⼀体两⾯
七卷⾛完,阿芽合上了那张总图。
回头看,这座春芽译馆的野⼼写在它的名字⾥:让⼀个 Java ⼯程师,⼀纸⽂牒⾛天下——不管对⾯是 OpenAI 还是 Ollama,是对话还是嵌⼊,是简单问答还是模块化RAG,是本地⼯具还是跨⼝岸的 MCP 差役,客官只学⼀套契约、只引⼀个 starter,就能跟天下诸侯做⽣意。这份野⼼,它兑现了⼤半。
⽽它的边界,同样诚实地写在源码⾥: stream() 默认抛异常,提醒你"形状统⼀不等于能⼒对⻬";JDBC 吞掉⼯具消息,提醒你"便利的存储未必存得全";1187 ⾏的Options 类和 400 ⾏的巨型⽅法,提醒你"年轻的框架还在⻓身体";合并范式的不统⼀,提醒你"优雅的抽象也会有⼯艺裂缝"。
但你发现没有——这些边界,恰恰是因为它有野⼼才暴露出来的。 ⼀个只想做"调 LLM 的⼩⼯具库"的框架,根本不会去碰可移植抽象、不会去焊三层架构的边界、不会去做防幻觉和注⼊防护,⾃然也就没有这些"理想很⼤、现实有缝"的张⼒。正因为它想做⼀座译馆⽽不是⼀个翻译插件,它才会有译馆该有的⽓派,也才会有译馆难免的漏⻛。
序章⾥,客官盯着满屏的 if (provider == ...) 想转⾏。现在,他⼿⾥攥着⼀张通⽤⽂牒,身后是⼀排驿丞、⼀间藏经阁、⼀条差役回路、⼀座通商⼝岸。他不会再
为"换个模型"⽽想转⾏了——他会为别的更⾼阶的难题发愁(⽐如这章列的那些暗礁),但那是站在更⾼处才看得⻅的愁。这就是⼀座好框架能为你做的:它不消灭所有难题,它把你从低级的重复劳动⾥赎出来,让你有资格去操⼼更值得操⼼的事。
阿芽把帽⼦摘下来,朝你拱了拱⼿。
它说:别只听故事,记得回源核实——这座译馆还在⻓,等你下次再来,⽂牒上的某⼀栏,说不定⼜改了措辞。
到那时,再推开⻔便是。
🎯 三句带⾛1. 优点站得住:可移植抽象扎实( Model.java:31 ⼀接⼝统管所有模态)、core 不依赖 Boot 可裸跑且边界由 maven-enforcer 机器焊死、组件解耦彻底、防幻觉/重试/注⼊防护/最⼩权限等⽣产细节到位。
2. 缺点别忽视:JDBC 记忆静默丢⼯具消息( JdbcChatMemoryRepository.java:113-118 )、合并范式不统⼀、 OpenAiChatOptions 1187 ⾏过胖与createRequest 400 ⾏巨型⽅法、 SimpleVectorStore 仅测试⽤、版本演进快导致⽂档/注释易滞后。
3. 落地决策:在 Spring ⽣态 + 认真做产品 → ⾛ starter ⽤ ChatClient + 内置 Advisor;要横切逻辑才⾃定义 Advisor、要新诸侯才封装 SDK 写 Provider、做⼯具型 Agent 要持久化记忆务必⾃写 Repository 绕开 JDBC 丢消息坑;⼀次性脚本 / ⾮ JVM 团队 / 极冷⻔能⼒ → 直接⽤ provider 原⽣ SDK。
全书到此为⽌。从序章那张薄薄的通⽤⽂牒,到终章这份算清功过的结账单,我们⼀章章⾛完了整座春芽译馆。它不是完美的——没有框架是——但它是⼀座有野⼼、有边界、且对⾃⼰的边界⾜够诚实的译馆。这份诚实,值得你把它读到源码这⼀层。