1. 正文还是从真实场景说起:Spring AI 这个系列是怎么走到“高阶”这一步的
先交代一下背景。我写 Spring AI 落地这个系列,今天是第九篇。前面几篇分别聊了模型接入、提示词工程、流式输出、Agent 基础、RAG 入门这些内容,能坚持看到这一篇的,基本都是在实际项目里被 AI 集成“折磨”过的朋友。这期标题里带“高阶用法”,我不想把它搞成 API 文档搬运,而是想解决一个更现实的问题:当你的 Spring Boot 项目里接了开源模型之后,怎么让它从“能聊天”进化成“能干活”,尤其是在餐饮 SaaS、企业知识库这类真实业务里,AI 要能稳定输出、能查数据库、能处理多模态内容、能被监控、能控制成本,这才叫落地。
Spring AI 从 0.8.x 一路走到 1.0,再到现在的 2.0 方向,API 虽然还在演进,但核心抽象已经稳定下来了。很多人问“现在到底用 Spring AI 还是 LangGraph4j”,我的观点很直接:如果你的项目本身是 Spring Boot 技术栈,团队以 Java 为主,Spring AI 一定是成本最低的选择;LangGraph4j 更适合复杂编排、状态机驱动型 Agent,但那是另一个维度的问题。本期内容围绕“高阶用法”展开,适合已经跑通基础链路、想在真实业务里把 AI 能力做深做稳的开发者,无论是做内部工具还是对外产品,这篇的实操内容都能直接抄作业。
先给个本期路线图:第一块聊 Spring AI 的高阶功能边界和核心抽象;第二块把 RAG 从“能检索”做到“能用于生产”;第三块讲结构化输出与函数调用,也就是让模型真正操作业务数据;第四块说多模态、Prompt 模板和对话记忆这些被低估的技巧;第五块是企业级工程化,包括重试、缓存、负载均衡、可观测性;第六块对比 Spring AI 和 LangGraph4j 的选型逻辑;最后是排坑实录,把我在真实项目里踩过的坑一次性列出来。
2. Spring AI 高阶功能边界:版本脉络与核心抽象的底层逻辑
2.1 为什么 Spring AI 适合做开源模型落地的“统一门面”
先回答一个高频问题:开源模型那么多,Llama、Qwen、DeepSeek、GLM 各有各的 API,为什么非要通过 Spring AI 再接一层?我最初也觉得“直接调 HTTP 不就行了”,但真实项目跑两个月你就会发现问题:多模型切换时要改代码、流式输出的格式各不相同、Prompt 模板散落各处、对话记忆要自己维护、函数调用协议不统一。Spring AI 的价值在于把这些问题收敛成一套统一抽象,你的业务代码依赖的是 ChatClient 接口,而不是某一家模型的 SDK。
举个例子,我们在一个餐饮 SaaS 项目里同时接了两套开源模型做不同场景:轻量模型处理简单的菜品归类,大参数模型负责复杂的自然语言转 SQL 查询。如果没有 Spring AI,这两套接入逻辑会写出两套完全不同的 HTTP 调用层;有了 Spring AI,只需要在配置里切换 model 的 endpoint 和 API key,业务代码零改动。这才是“开源模型应用落地”真正需要的东西——不是某个模型多聪明,而是整个集成层足够稳。
2.2 核心抽象一览:ChatClient、ChatModel、Advisor、VectorStore
Spring AI 1.0 之后,核心抽象已经非常清晰。我习惯用一张表给团队做培训,建议你也这样梳理一遍,比读十遍文档有用:
| 核心抽象 | 职责定位 | 典型场景 |
|---|---|---|
| ChatClient | 面向业务的 Facade,支持同步/流式调用 | 日常业务代码里唯一需要依赖的入口 |
| ChatModel | 模型调用的底层封装,屏蔽各家协议差异 | 框架内部用,业务侧几乎不直接碰 |
| Advisor | 围绕 ChatClient 的过滤器链,拦截请求/响应 | 上下文增强、RAG 前置检索、敏感词过滤 |
| ChatMemory | 对话历史存储与窗口管理 | 多轮对话场景,控制 token 成本 |
| EmbeddingModel | 文本向量化接口 | RAG 链路中的文本转向量 |
| VectorStore | 向量数据库统一抽象 | 文档检索、相似度查询 |
| FunctionCallback | 模型函数调用回调,把自然语言映射到 Java 方法 | 查订单、查库存、写工单 |
这套抽象设计的最大好处是可测试性。因为业务侧只依赖 ChatClient,单元测试里可以轻松 mock 整个 AI 链路,不需要真的去调模型。我们有十几个 Agent 业务类,全部基于这个思路做单元测试覆盖,回归效率提升非常明显。
2.3 版本选择建议:别盲目追新,也别死守旧版
Spring AI 目前处于比较微妙的阶段:1.0 版本已经可以生产使用,2.0 的模块划分还在调整。我的建议是:新项目直接上 1.0 GA 后的稳定版,老项目如果已经在 0.8.x 停留很久,迁移成本要评估清楚再动。重点看三个变化:一是 Advisor 体系的 API 调整,二是 ChatClient 的构建方式变化,三是模块体积变化。
很多人忽略的一点是 Spring AI 对 Java 版本的要求。如果你还在 Java 8,很多新特性其实用不上,不光是语法层面的问题,而是 JDK 的虚拟线程、Record 支持这些特性直接影响了 API 设计。我碰到过有团队用 Java 8 硬跑 Spring AI,结果函数调用里的类型映射各种别扭。所以我在项目里统一 Java 17 起步,有条件直接上 21,这不是炫技,是减少底层摩擦。
3. RAG 从“能检索”到“能用于生产”:链路细节比模型参数更重要
3.1 完整 RAG 链路的设计拆解:哪一环最容易成为瓶颈
RAG 这个词现在很热,但很多初学者以为“连个向量数据库,塞点文档进去,就能做知识库问答了”。等你真上了生产环境就会发现,瓶颈往往不在生成环节,而在“检索质量”和“上下文组织”。完整的 RAG 链路包含六个环节:
文档加载、文本切分、向量化、存储索引、检索召回、上下文增强生成。第六代的 RAG 还会加一层 rerank 重排,但重排模型的选择本身又是一门学问。我的实践经验是:在大多数业务场景里,先把前三环做好,检索效果能提升一半,没必要一上来就上 rerank。
具体到 Spring AI 的实现,链路对应的组件分别是 DocumentReader、TextSplitter、EmbeddingModel、VectorStore、QuestionAnswerAdvisor、ChatClient。很多教程只展示最后一步 QuestionAnswerAdvisor 的调用,但前期的文档处理如果做不好,召回结果差,后面再怎么调 Prompt 都白搭。
3.2 文档切分的参数细节:从 chunkSize 到 overlap 的实操建议
文本切分是 RAG 里最容易被轻视的环节。我见过太多人直接用默认参数,结果检索出来的内容要么太碎、要么超出模型上下文。在 Spring AI 里最常用的是 TokenTextSplitter,它按 token 数切分,而不是按字符切分,这对中文场景非常重要。
我常用的配置长这样:
TokenTextSplitter splitter = TokenTextSplitter.builder() .withChunkSize(800) .withChunkOverlap(200) .withDefaultTokenCounter() .build();注意几个细节:chunkSize 不是越大越好,800 到 1000 是我在中文知识库场景下的经验值,太大检索粒度会变粗;chunkOverlap 设置 200 左右,避免切分点切断语义,尤其是那些有固定结构的业务文档。另外一定要做 Metadata 打标,把文档标题、章节号、来源 URL 存进去,这样回答时才能溯源,后面排查问题时也有据可查。
我还建议切完文本后打印几条样本看看,别急着入库。经常出现的问题是:一个段落被切成了两半、代码块被切断、表格内容丢失。这些在文本层面就有问题,后续向量化也不会好到哪去。
3.3 检索策略与上下文组织:如何让模型“看到”真正需要的知识
检索环节我推荐用多路召回而不是单路 Top-K。比如在一个运维知识库里,用户问“服务启动失败怎么处理”,单靠相似度检索可能会召回一堆泛泛的运维文档,但如果同时按 Metadata 里的标签过滤一遍,召回效果会好很多。Spring AI 里可以通过 VectorStore 的相似性检索方法加 filter 表达式,这个能力很容易被忽略。
上下文组织上,有两点实战经验。第一,不要把十几条检索结果全部塞进去,模型会“注意力稀释”,反而回答质量下降。我一般控制在 4 到 6 条,前提是切分质量有保障。第二,给检索到的内容加上清晰的标识前缀,比如“【用户问题】...【参考文档1】...”,这比直接拼接更容易让模型区分哪部分是资料、哪部分是要求。
最后强调一个容易忽略的点:RAG 场景下要给模型讲清楚“不知道就说不知道”。在 Prompt 里加上一句“如果参考文档中没有明确的答案,请直接说明未找到相关内容,不要根据已有知识猜测”。这条规则在落地时非常重要,知识库类产品如果让模型自由发挥,很可能一本正经地编造答案,用户信任度很快就崩了。
4. 让模型“说准话”:结构化输出与函数调用的高阶玩法
4.1 结构化输出的典型场景与实现手段
纯文本聊天在业务里几乎没有用,真正有价值的是结构化输出,模型直接返回 JSON,然后被你自己的代码消费。举个实际例子:一个客服工单系统里,AI 要把用户的描述自动转换成工单结构化数据,包括标题、分类、紧急程度、处理人建议。如果模型输出的是自由文本,后面还得做一层解析,很麻烦;如果直接让模型输出 JSON,就可以一步到位。
Spring AI 里最常用的做法是 BeanOutputConverter,把目标类型定义成 Java record,然后用它在构建 Prompt 时作为格式说明,在拿到模型输出后再反序列化。代码大致长这样:
record OrderInfo(String orderNo, String status, BigDecimal amount, String customerPhone) {} BeanOutputConverter<OrderInfo> converter = new BeanOutputConverter<>(OrderInfo.class); String promptTemplate = """ 请从下面的用户消息中提取订单信息: {userMessage} 结果请严格按照以下 JSON Schema 输出: {format} """; ChatClient client = ChatClient.builder(chatModel).build(); String content = client.prompt() .user(u -> u.text(promptTemplate) .param("userMessage", userMessage) .param("format", converter.getFormat())) .call() .content(); OrderInfo orderInfo = converter.convert(content);这里 BeanOutputConverter 的 getFormat() 方法很关键,它会把 Java 类型的信息转成 JSON Schema 描述,模型按照 Schema 输出,转换成功率能到 95% 以上。唯一要提醒的是:如果模型输出的 JSON 不合法,convert 会抛出异常,建议包一层 try-catch,或者用宽松一点的方式先拿字符串再手动转。
4.2 函数调用:让模型操作业务系统的正确姿势
我见过很多团队的花式调用法:让模型自己构造 SQL 去查库、让模型去调用内部的 HTTP 接口。这些做法看着很炫,但极不安全,模型输出不可控,生成的 SQL 很可能带出全表数据或产生脏读。正确的姿势是走 Function Calling,也就是把业务操作封装成函数,让模型只是“决定调用哪个函数、传什么参数”,真正的执行权始终掌握在 Java 代码里。
还是用餐饮 SaaS 的场景举例。用户问“帮我查一下尾号 8888 的订单走到哪一步了”,我们先定义一个查询函数:
@Bean public FunctionCallback orderStatusFunction() { return FunctionCallback.builder() .function("queryOrderStatus", (String orderNo) -> orderService.queryStatus(orderNo)) .description("根据订单号查询订单当前状态,输入参数为订单号") .inputType(String.class) .build(); }然后在 ChatClient 里启用这个函数:
ChatClient client = ChatClient.builder(chatModel) .defaultFunctions("queryOrderStatus") .build();这样模型收到用户问题后,会先判断“需要查订单状态”,然后把订单号提取出来,调用 queryOrderStatus 函数拿到真实数据,再基于这份数据组织语言回答。整个过程对用户是透明的,但数据边界非常清晰:模型永远接触不到数据库连接,只能拿到函数返回的有限字段。
函数调用有几个坑必须注意。第一个是函数描述要写清楚,因为模型是靠描述来决定何时调用函数的,描述写得模糊,模型就不知道该不该调。第二个是参数校验要做在函数内部,模型提取的参数经常有格式问题,宁可多写几个校验分支,也别信任模型给的参数。第三个是小心函数调用的死循环,模型可能反复调用同一个函数,最好设置最大迭代次数。
4.3 结构化输出与函数调用的结合:一个更完整的实战组合
单看结构化输出和函数调用都简单,但真实业务里它们经常要组合使用。比如一个餐饮 SaaS 的运营场景,用户说“统计一下今天各门店的订单量和营业额,顺便分析有没有异常波动”。这个需求如果只走函数调用,模型查到的是原始报表数据;如果想让它输出一份 JSON 格式的分析报告,就得在函数调用返回后,再走一次结构化输出。
我的做法是把两步串成一条链路:第一步用 ChatClient 的工具调用,让模型拿到各门店的经营数据;第二步把数据重新作为上下文,要求模型按固定 Schema 输出分析报告,包括 trend、abnormalStores、suggestions 几个字段。这样既保证了数据来源真实,又保证了输出格式标准,后面前端直接拿 JSON 渲染。
这条链路在 Spring AI 里实现很直接,核心是把用户消息变成多轮会话,第一轮让模型调函数,第二轮让模型组织输出。不复杂,但比单纯一个 chat 请求要多一步状态管理,建议用一个 Service 类包装链路逻辑,而不是把代码散落在 Controller 里。
5. 被低估的高阶技巧:多模态、Prompt 模板与对话记忆
5.1 多模态输入:让模型“看”图片而不只是“读”文字
很多人在 Spring AI 里只用了纯文本能力,忽略了它已经支持多模态输入。开源模型里 Qwen-VL、GLM-4V 这些能力已经相当能打,在 Spring AI 里通过 Message 体系可以很自然地传入图片、音频内容。
我记得有个项目是餐饮门店的 AI 巡检,需要让模型看门店上传的照片,判断有没有“员工未戴口罩”“后厨卫生不合格”等问题。如果只靠人工描述,审核效率极低;直接传图片给模型,让它基于图像内容做检查,再结合结构化输出,整个链路马上就能跑通。
代码实现上其实不复杂,构造一个包含文本和图片的 UserMessage 就可以了:
var userMessage = new UserMessage.Builder() .text("请检查这张餐饮门店后厨照片,从卫生规范角度列出问题清单,并以JSON格式返回") .media(Media.builder() .dataType(MediaType.IMAGE_PNG) .data(imageBytes) .build()) .build(); String result = chatClient.prompt() .messages(userMessage) .call() .content();需要注意两点:一是预览版本的模型可能不支持某些媒体类型,要提前确认;二是图片二进制过大会导致 token 成本和请求耗时暴涨,我一般先把图片压缩到 2MB 以内再传给模型。
5.2 Prompt 模板的工程化:不要在生产代码里硬拼字符串
我经常看到项目里把 Prompt 字符串直接写死在 Java 代码里,用加号拼接变量。这套做法在小 Demo 里没问题,一旦 Prompt 需要迭代、需要多语言、需要按不同场景切换,硬拼字符串就是灾难。Spring AI 提供了 PromptTemplate,可以把 Prompt 模板外置到 resource 目录下的文件里,用变量占位符来做渲染。
提倡的做法是建立一个 prompt 目录,每个业务场景一个模板文件。比如order-analysis.st文件里边写:
你是一个餐饮数据分析助手。以下是某门店今天的经营数据: {storeData} 请从以下维度进行分析: 1. 订单量变化趋势 2. 营业额是否异常 3. 存在的问题与改进建议 注意:如果 {storeData} 中没有任何内容,请直接说明“未获取到相关数据”。然后在代码里加载渲染:
var template = new PromptTemplate(new ClassPathResource("prompts/order-analysis.st")); template.add("storeData", jsonData); Message message = template.createMessage();这样做的好处是 Prompt 的维护者可以是非技术人员,运营同学也能直接改提示词文件,不需要动代码。我自己的项目里把 Prompt 模板都纳入了 Git 管理,每次变更都有 diff,回溯问题特别方便。
5.3 对话记忆的窗口设计:既要多轮体验,也要控制成本
多轮对话如果不好好做记忆管理,会出现两个极端:要么每轮请求都把所有历史消息发给模型,token 成本飙升;要么记忆窗口太小,用户问第三轮问题的时候模型把第一轮的关键信息忘记了。Spring AI 提供了 MessageWindowChatMemory,它在内存里维护一个固定窗口的会话记录,自动做滑动截断。
比如设置 20 条消息的窗口:
var chatMemory = MessageWindowChatMemory.builder() .windowSize(20) .build(); ChatClient client = ChatClient.builder(chatModel) .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) .build();窗口大小不是拍脑袋定的。我算过一笔账:假设每轮用户消息 100 token,AI 回复 200 token,20 条消息窗口约等于 10 轮对话,模型上下文输入大概是 3000 token 左右,这个量级对成本和响应时间都还算友好。如果业务上确实需要更长记忆,可以考虑用向量库做长期记忆,这其实是 RAG 的另一种应用形态,把历史对话向量化,按需检索回填。
6. 企业级落地:从“能跑”到“稳定跑”的工程化清单
6.1 超时、重试与限流:模型服务不稳定是常态,不是意外
很多团队接开源模型后遇到的第一个生产事故,就是模型服务超时或限流。开源模型要么自托管,要么走第三方 API,稳定性远远达不到业务系统对中间件的那种预期。所以工程上必须做防御性设计。
超时配置方面,我习惯分两层控制:底层 HTTP 连接超时设短一点,比如 3 到 5 秒;整个 ChatClient 调用的 ReadTimeout 根据具体场景设 30 到 60 秒。要区分用户交互场景和后台批处理场景,交互场景等不了那么久,宁可失败让用户重试;后台批处理可以放宽。
重试策略别用固定间隔,模型限流往往集中在瞬间,固定间隔重试会加剧拥堵。我一般用 Spring 的 RetryTemplate,配置指数退避加上随机抖动,最大重试 3 次。这里有个细节:只有网络异常和服务端 429/5xx 才值得重试,模型返回的“内容质量差”重试没有意义,那是提示词和参数的问题。
内部限流也很重要。防止某个用户把你的模型额度一次性耗尽,我会用 Resilience4j 或者简单地用令牌桶做 RateLimiter。按 API Key 维度做配额,比如单用户每分钟最多 30 次请求,某条业务线每天最多消耗多少 token 也要有统计,否则月底账单出来的时候你会发现成本失控。
6.2 缓存策略:业务级缓存比模型调优更见效
在真实项目里,很多高重复度的查询根本不必调模型。比如“什么是满 200 减 30 的活动规则”“这笔订单怎么退款”这类高频问题,用户的问法可能有好几种,但答案完全可以缓存。我的做法是基于对话主题对缓存内容做归一化。
Spring AI 的 ChatClient 本身不提供开箱即用的缓存切片,但你可以自己封装一层。从实现角度,最简单的方案是加一个基于 Spring Cache 的方法级缓存,命中不到再走模型。缓存 key 的设计要小心,不能直接用用户原始文本,因为问法千变万化,我通常先用一个小的分类模型或关键词规则把问题归一化成意图+槽位,再用归一化后的 key 去查缓存。
这样做的收益非常明显:我们的一个餐饮客服助手,模型调用量直接降了 40%,响应时间平均从 8 秒降到 1 秒以内。成本与体验同时优化,比任何参数调优都见效快。
6.3 多模型负载均衡与智能路由:不同场景用不同模型
既然是在聊开源模型落地,项目的模型选择从来不是单一的。我们生产环境里就同时跑着三种规格的模型:轻量模型做意图识别、中等模型做客服回答、大模型做复杂报表分析。Spring AI 支持多 model 配置,我们可以创建多个 ChatModel bean,然后根据业务场景注入不同的那个。
但其实更优雅的做法是动态路由。我们可以写一个自定义的 RouterAdvisor,根据用户问题的长度、复杂度、当前系统负载,动态切换底层模型。比如短问题走轻量模型,长文档分析走大模型。这样一个 ChatClient 入口,背后自动分流,业务代码完全无感。
负载均衡层面,如果自托管了多个模型副本,可以用 Spring Cloud LoadBalancer 对多实例 endpoint 做轮询或最少连接数调度。很多人在 Spring AI 里忽略了这层,觉得模型推理不是常规服务,但自托管场景下,模型副本的扩缩容和负载均衡与普通微服务没有任何区别。
6.4 可观测性:traceId 贯穿、指标采集与内容审计
AI 应用的可观测性与普通后端不太一样,除了看接口延迟和错误率,还得看模型返回的内容质量、token 消耗、是否有敏感信息泄漏。Spring AI 本身已经接入了 Micrometer 和观测链路,你可以通过配置把 AI 调用链路暴露出去:
spring: ai: observability: enabled: true include-content: true这里有个隐私考虑,include-content 会把 Prompt 和响应内容也采集进来,如果系统涉及客户隐私数据,就要小心处理。建议默认不包含正文内容,只采集 token 数量、耗时、模型名称这些元数据。内容审计可以单独做:在 Advisor 层记录原始输入和输出摘要,存到审计日志表里,供事后追溯。
traceId 我建议从外部请求一路透传到 AI 调用链,排查问题的时候才能把“用户说了什么”和“模型回了什么”对应起来。Spring AI 的 observation 机制天然支持 traceId 透传,只要在 HTTP 调用入口注入好,全链路就能串起来。这块在排查“为什么某次回答特别差”时极其重要,没有日志关联基本无法定位。
7. 生态对比与实战:Spring AI vs LangGraph4j,NL2SQL 怎么选
7.1 对比分析:不是谁替代谁,而是场景决定选型
LangGraph4j 是 LangGraph 的 Java 移植版,我看最近社区热度涨得很快,很多人在问“到底用 Spring AI 还是 LangGraph4j”。我的理解是它们根本不是同一层的东西。Spring AI 更像是一个“模型接入标准件”,强调的是统一的模型接入、调用、工具调用、RAG 支持;LangGraph4j 强调的是图状态机编排,有明确的节点、边、状态、条件分支,适合做复杂 Agent。
如果你的业务是让 AI 做“问答、知识库检索、结构化信息提取”这类的线性任务,Spring AI 的开发效率要高得多,半天就能跑通。但如果你要做多个 Agent 协作,比如一个负责拆解任务、一个负责检索、一个负责生成、一个负责质检,并且流程中涉及循环、条件跳转和持久化状态,LangGraph4j 的表达能力就更合适。
我自己现在的建议是:70% 的常见业务用 Spring AI 就够了,别过度设计;只有当你清楚感觉到线性调用已经难以表达流程控制时,再引入 LangGraph4j 也不迟。团队的技术栈和学习成本也要纳入决策,Java 团队学 LangGraph4j 的图模型还是有一定门槛的。
7.2 NL2SQL 实战:以餐饮 SaaS 的场景为例
接着前面餐饮 SaaS 的场景说,NL2SQL 是开源模型落地里一个非常典型的“看着炫、做起来坑多”的功能。用户想用自然语言查数据库,理想状态下模型生成 SQL,后台执行后返回结果。但直接生成 SQL 太容易出错,我在生产落地时采用的是混合策略:意图识别优先走模板匹配,模板不行再走生成;同时把 SQL 执行限制在只读事务里,所有查询都经过一个白名单校验的 SQL 解析器。
Spring AI Alibaba 社区提供了 nl2sql 相关的组件,它把问题理解、SQL 生成、结果解释串成了流水线。我的实际落地路径是这样的:
第一层,先定义好数据库的 Schema 上下文,把表结构、字段注释、常用枚举值整理成模型看得懂的格式。这一步是所有知识库类应用的共同基础,没有清晰的数据字典,再好的模型也生成不了正确的 SQL。在 Spring AI 里,可以通过自定义 Advisor 把 Schema 描述注入到每次请求的上下文里。
第二层,把常见问题分类成固定模板。比如“查询今日订单量”“查询某门店营业额”“显示销量前五的菜品”,这些需求是有限的、高频的,直接写死 SQL 模板,不依赖模型生成。模型只负责解析参数填充模板。这条路径稳定、成本低、效果好。
第三层,对于模板覆盖不到的复杂查询,才让模型生成 SQL,但必须过一层校验:先解析 SQL 语法,再检查是否只包含只读操作,最后验证表名和字段名是否在数据字典白名单里。校验不通过就直接拒绝,不允许无条件执行模型生成的 SQL。
这条链路跑下来,覆盖率大概能到 80% 以上,剩下的 20% 复杂查询走人工或者提示用户换个问法,反而比强行让模型生成更符合实际。记住一个原则:在企业系统里,安全边界永远比智能体验更重要。
7.3 什么时候引入 Spring AI Spring Boot 餐饮 SaaS 集成的完整架构
如果整个餐饮 SaaS 都要做 AI 化改造,我建议的架构分层是:接入层用 Spring AI 搞定所有模型调用;意图层做意图识别与多轮管理;业务层走函数调用连接订单、菜品、用户等服务;数据层做 RAG 知识库和 NL2SQL;监控层做成本、耗时、内容审计。这是一个可以支撑到规模化用户的完整架构,而不是只做了一个聊天机器人 Demo。
我特别想强调模块边界意识:不要让 AI 的代码侵入到现有的业务 Service 里。我们团队的做法是建一个独立的 ai-assistant 模块,所有 AI 相关逻辑全部收敛在里面,对外暴露标准接口,业务模块只调用这个接口。这样即使模型换了、Prompt 改了、向量库换了,也不影响主业务流程,同时测试和回滚都简单很多。
8. 排坑实录:这些高频问题,我都是踩过坑才总结出来的
8.1 模型输出 JSON 不合法怎么办
这是结构化输出场景里出现频率最高的问题,模型偶尔会输出 Markdown 代码块包裹的 JSON、带逗号遗漏的非法 JSON、甚至干脆多输出一段解释文字。我后来养成了一个习惯:不直接依赖转换器解析,而是先做一层清洗,把 ```json 包裹都去掉,必要时截取第一个左大括号到最后一个右大括号之间的内容,再交给解析器。
再往深一层,模型输出不合法往往是因为 Prompt 里格式要求不够严格,或者模型本身的指令遵循能力不够强。开源小模型的 JSON 稳定性确实不如大模型,所以如果同一类结构的转换失败率超过 5%,建议换更大参数量的模型,或者用 Few-Shot 给两个样例。我在生产里就是这么做的:优先保证格式稳定,再考虑省成本。
8.2 向量化后检索效果差,怎么排查
检索效果差是个大锅,问题可能出在任何环节。我一般按顺序排查:先看切分质量,打印几个 chunk 看看有没有语义破碎;再看 Embedding 模型选型,中文场景下直接使用通用英文向量模型效果通常很糟糕,选用支持中文的 Embedding 模型会更合适;最后看检索参数,Top-K 太小召不回,太大噪声多。
还有一个容易被忽视的坑:向量数据库里的文档更新机制。很多团队只做增量插入,老文档修改后没有重新向量化,导致检索出来的内容是过期的。我在项目里引入了一个最小更新机制:文档变更时解绑旧的向量,再重新切分入库,而不是图省事往库里堆。
8.3 上下文无限膨胀导致超时或费用暴涨
随着对话轮数增加,如果 Prompt 里把所有历史消息全部带上,最终必然超出模型上下文窗口。我见过有同事把 30 轮对话的完整历史都塞进去,单次请求输入 token 超过几万。解决思路在前面已经提到,MessageWindowChatMemory 的窗口管理是基础,另外还可以加一层“重要信息提取”,每轮对话结束后用一个小模型提取关键信息(比如用户偏好、待办事项、业务实体),把这些槽位信息一直保留,而丢弃原文。
这种方式有点类似“会话摘要法”,效果比单纯截断历史好得多,用户问后面的问题时模型依然记得关键内容。成本上每轮只额外消耗一次摘要提取的少量 token,通常可控,换来的体验提升却非常明显。
8.4 函数调用的幻觉与参数错误
模型有时候会调用错误的函数,或者给函数传了错误的参数。比如用户问“帮我查一下今天的天气”,模型可能错误地调用了一个业务函数。为了防这个,我把每个函数描述写得非常具体,并且在实现里对参数做严格校验,不允许出错的场景下甚至要求用户二次确认。
二次确认是一个很实用的设计。具体做法是:当模型判断要调用某个函数时,不直接执行,而是先向用户展示“我将为您查询订单尾号 8888 的状态,确认吗?”,用户说确认后再真正执行。这个交互虽然多了一步,但对涉及资金操作、数据修改的场景来说是必须的安全保障。函数的权限边界也要控制,比如只读函数和写函数的调用权限分开,写操作必须走人工审批,这属于业务安全层面的基本要求。
8.5 高频排坑清单速查
把上面这些常见问题整理成一张速查表,方便以后排查直接查参考。
| 问题表现 | 可能原因 | 解决办法 |
|---|---|---|
| JSON 解析失败 | 模型返回了 Markdown 包裹或格式不完整 | 清洗后截取再解析;换指令遵循更强的模型 |
| RAG 答非所问 | 切分粒度不当或 Embedding 模型不适配 | 调 chunkSize/overlap;换中文向量模型 |
| 上下文超限/费用暴涨 | 历史消息无窗口管理 | 用 MessageWindowChatMemory 并做摘要提取 |
| 函数调用乱触发 | 函数描述不清晰、参数校验缺失 | 细化 description 并加二次确认机制 |
| 模型不稳定偶发限流 | 服务端限流或自托管资源不足 | 指数退避加重试;负载均衡多实例 |
| 响应内容不可控 | 缺约束性 Prompt | 加“不知道就拒绝回答”等安全约束 |
9. 最后的个人经验:做开源模型落地,最重要的不是模型选型,而是把边界划清楚
这个系列写到第九篇,我反复强调的核心其实就一句话:开源模型落地的重点不是模型本身有多强,而是你能否在业务代码和模型能力之间划出一条清晰的边界。模型负责理解语言、组织内容、提取信息;你的代码负责执行真实业务、保护数据、保证安全、控制成本。Spring AI 提供的整套抽象,恰恰是帮你划定这条边界的最好工具。
我个人在实际项目里最大的体会是,AI 功能上线只是第一步,后续持续调优的过程才是真正拉开差距的地方。RAG 的召回质量、函数调用的触发准确率、缓存命中率、成本指标、内容安全性,每一块都需要单独的监控和迭代节奏。不要指望一劳永逸,而是要把它当作一个长期维护的“AI 业务系统”来运营。
最后再分享一个具体的小技巧:所有 ChatClient 调用,我都建议包一层自定义的 Service 结构,统一记录输入输出摘要、计算 token 消耗、设置业务级超时。这层“薄封装”看起来是多写了几个类,但上生产之后你会发现,排查问题的速度、后续替换模型的能力、对账审计的便利,全部来自这层薄封装。Spring AI 的灵活性足以支撑这种设计,关键取决于你自己的工程习惯。