1. 为什么要在 Spring Boot 里做 AI 应用平台
1.1 从“能跑通”到“能上线”之间隔着什么
我见过太多团队做 AI 应用的路径是这样的:先用 Python 写个脚本调一下模型接口,跑通了,效果不错,然后老板说“给我做成一个产品”。这时候问题就来了——脚本能跑,不代表能上线。上线意味着什么?意味着要处理并发请求、要管理会话状态、要做权限控制、要记录调用日志、要控制成本、要能灰度发布、要能监控异常。这些东西,Python 脚本里一个都没有。
Spring Boot 在这个场景下的价值就体现出来了。它本身就是一个经过十年以上生产环境验证的 Web 框架,IoC 容器、自动配置、Actuator 监控、事务管理、安全框架这些东西都是现成的。你不需要从零搭建一套工程化体系,直接把 AI 能力作为一个 Service 层嵌进去就行。说白了,Spring Boot 负责“工程”,AI 框架负责“智能”,两者各司其职。
我选择 Spring Boot 作为底座,还有一个很实际的原因:团队里大部分后端工程师对 Java 生态更熟悉。你让一个写了五年 Java 的人去学 Python 的异步编程、虚拟环境管理、依赖冲突解决,这个学习成本是实打实的。而 Spring Boot 对他们来说是舒适区,上手 AI 应用开发的心理门槛会低很多。
1.2 Spring AI 和 LangChain4j 到底选哪个
这是被问得最多的问题之一。我的结论是:如果你的主战场是 Java 生态,优先选 Spring AI;如果你需要更灵活的 Agent 编排能力,LangChain4j 值得考虑。
Spring AI 的优势在于它和 Spring Boot 的集成是“原生”的。自动配置、条件装配、配置文件绑定这些东西都是 Spring 团队自己做的,用起来非常顺手。你只需要在application.yml里配好模型参数,注入一个ChatClient就能用。而且 Spring AI 的抽象层次设计得比较合理,ChatModel、EmbeddingModel、VectorStore这些接口定义清晰,切换底层实现(比如从 OpenAI 换到通义千问)只需要改配置。
LangChain4j 的优势在于它的 Agent 编排能力更强。它提供了AiServices、Tools、Chains这些更高级的抽象,做多步骤推理、工具调用、RAG 流程编排的时候会更灵活。但代价是它的 Spring Boot 集成不如 Spring AI 那么“丝滑”,有些地方需要手动配置。
我实际的做法是:核心对话链路用 Spring AI,复杂的 Agent 编排用 LangChain4j 补充。两者可以在同一个项目里共存,因为它们底层都是 HTTP 调用模型接口,不存在冲突。
1.3 生产级平台的核心能力清单
一个能上生产的 AI 应用平台,至少要具备以下能力:
- 多模型接入与动态切换:不能绑死一家模型供应商,要能根据成本、延迟、效果动态路由。
- 会话管理与上下文维护:多轮对话的上下文不能丢,但也不能无限增长,需要滑动窗口或摘要压缩。
- 流式输出:用户等不了模型一次性返回全部内容,必须支持 SSE 或 WebSocket 流式推送。
- RAG 检索增强:光靠模型自身知识不够,需要接入企业私有知识库。
- Agent 工具调用:模型要能调用外部 API、查数据库、执行计算。
- 可观测性:每次调用的 token 消耗、延迟、成功率都要有记录。
- 限流与降级:模型接口挂了或者超时了,要有兜底策略。
- 安全与合规:输入输出都要过审,敏感信息要脱敏。
下面这张表是我整理的核心能力与对应技术方案的对照:
| 核心能力 | 技术方案 | 关键依赖 |
|---|---|---|
| 多模型接入 | Spring AI ChatModel 抽象 | spring-ai-openai, spring-ai-alibaba |
| 会话管理 | Redis + 滑动窗口 | spring-data-redis |
| 流式输出 | SSE / WebSocket | spring-boot-starter-webflux |
| RAG | VectorStore + Embedding | spring-ai-pgvector |
| Agent | Function Calling | spring-ai-core |
| 可观测性 | Micrometer + Actuator | spring-boot-starter-actuator |
| 限流降级 | Resilience4j | resilience4j-spring-boot3 |
2. 项目骨架搭建与核心依赖选型
2.1 版本选择:别踩 Spring Boot 2.x 和 3.x 的坑
Spring AI 对 Spring Boot 的版本有硬性要求。Spring AI 1.0.x 要求 Spring Boot 3.2+,Spring AI 0.8.x 可以跑在 Spring Boot 2.7 上。如果你还在用 Spring Boot 2.3.x 或 2.6.x,要么升级,要么用老版本的 Spring AI。
我的建议是直接上 Spring Boot 3.2+ 和 Spring AI 1.0.x。原因很简单:Spring Boot 3.x 对 GraalVM 原生镜像的支持更好,启动速度更快;而且 Spring AI 1.0.x 的 API 已经稳定了,不会像 0.8.x 那样经常变。JDK 版本至少 17,推荐 21,虚拟线程对 IO 密集型的 AI 调用场景提升很明显。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> </parent> <properties> <java.version>21</java.version> <spring-ai.version>1.0.0</spring-ai.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-alibaba-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies>注意:Spring AI 的依赖仓库需要额外配置 milestone 仓库,因为 1.0.0 版本可能还没进 Maven Central。
2.2 配置文件设计:把模型参数外置
生产环境和开发环境的模型配置肯定不一样。开发环境用便宜的模型,生产环境用效果好的;开发环境超时设短一点,生产环境设长一点。所以所有模型相关的参数都必须外置到配置文件,不能硬编码在代码里。
spring: ai: openai: api-key: ${OPENAI_API_KEY} base-url: ${OPENAI_BASE_URL:https://api.openai.com} chat: options: model: gpt-4o-mini temperature: 0.7 max-tokens: 2048 embedding: options: model: text-embedding-3-small alibaba: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus temperature: 0.7这里有个细节:temperature参数控制输出的随机性。做客服机器人这种需要稳定回答的场景,设 0.3 左右;做创意写作这种需要多样性的场景,设 0.8 到 1.0。max-tokens控制单次返回的最大 token 数,设太大浪费钱,设太小回答会被截断。我一般设 2048,够用且不会太贵。
2.3 多模型动态路由的实现
光配一个模型不够,生产环境需要根据场景动态切换。比如简单问答走便宜的小模型,复杂推理走贵的大模型。我的做法是定义一个ModelRouter组件,根据请求的元数据决定用哪个ChatModel。
@Component public class ModelRouter { private final Map<String, ChatModel> modelMap; public ModelRouter(List<ChatModel> models) { this.modelMap = new HashMap<>(); // 根据 Bean 名称注册 for (ChatModel model : models) { modelMap.put(model.getClass().getSimpleName(), model); } } public ChatModel route(String scene) { return switch (scene) { case "simple_qa" -> modelMap.get("OpenAiChatModel"); case "complex_reasoning" -> modelMap.get("DashScopeChatModel"); default -> modelMap.get("OpenAiChatModel"); }; } }这个路由逻辑可以做得更复杂,比如根据当前模型的错误率、平均延迟动态调整。但核心思路就是:把模型选择从代码里抽出来,变成一个可配置、可动态调整的策略。
3. 会话管理与上下文窗口的工程实现
3.1 为什么不能把全部历史塞给模型
多轮对话的核心问题是:上下文长度是有限的。GPT-4o 的上下文窗口是 128K token,听起来很多,但如果你把用户和模型的每一轮对话都塞进去,几轮下来就爆了。而且 token 是要花钱的,历史越长,每次调用的成本越高。
我见过最粗暴的做法是把最近 10 轮对话拼成一个字符串直接发给模型。这样做有两个问题:第一,10 轮之前的上下文完全丢失,用户会觉得“你怎么不记得我刚才说的”;第二,10 轮对话可能很短也可能很长,token 消耗不可控。
3.2 滑动窗口 + 摘要压缩的组合策略
我的方案是滑动窗口 + 摘要压缩。具体来说:
- 保留最近 N 轮完整对话(N 可配置,默认 6 轮)。
- 超出 N 轮的历史,用模型生成一段摘要,作为 system message 的一部分。
- 摘要不是每轮都重新生成,而是每积累 M 轮(比如 4 轮)才更新一次,减少调用次数。
@Service public class ConversationManager { private final ChatModel chatModel; private final RedisTemplate<String, Object> redisTemplate; private static final int WINDOW_SIZE = 6; private static final int SUMMARY_THRESHOLD = 10; public List<Message> buildContext(String sessionId, String userInput) { List<Message> history = getHistory(sessionId); List<Message> context = new ArrayList<>(); // 如果历史超过阈值,生成摘要 if (history.size() > SUMMARY_THRESHOLD) { String summary = generateSummary(history.subList(0, history.size() - WINDOW_SIZE)); context.add(new SystemMessage("以下是之前对话的摘要:" + summary)); context.addAll(history.subList(history.size() - WINDOW_SIZE, history.size())); } else { context.addAll(history); } context.add(new UserMessage(userInput)); return context; } private String generateSummary(List<Message> messages) { String prompt = "请用简洁的语言总结以下对话的关键信息:\n" + messages.stream().map(Message::getContent).collect(Collectors.joining("\n")); return chatModel.call(prompt); } }实操心得:摘要生成用便宜的小模型就行,不需要用 GPT-4 这种贵的。摘要的质量对最终回答的影响没有想象中那么大,但成本差距可能是 10 倍以上。
3.3 Redis 存储会话的 Key 设计
会话数据我存在 Redis 里,Key 的设计很关键。我的方案是:
chat:session:{sessionId}:messages→ List 结构,存消息列表chat:session:{sessionId}:summary→ String 结构,存摘要chat:session:{sessionId}:meta→ Hash 结构,存创建时间、最后活跃时间、用户 ID
过期时间设 24 小时,太短用户会觉得“怎么又忘了”,太长浪费内存。如果业务需要长期记忆,可以把重要会话持久化到数据库。
public void saveMessage(String sessionId, Message message) { String key = "chat:session:" + sessionId + ":messages"; redisTemplate.opsForList().rightPush(key, message); redisTemplate.expire(key, Duration.ofHours(24)); // 更新元数据 String metaKey = "chat:session:" + sessionId + ":meta"; redisTemplate.opsForHash().put(metaKey, "lastActive", Instant.now().toString()); redisTemplate.expire(metaKey, Duration.ofHours(24)); }4. 流式输出与 WebSocket 的工程细节
4.1 SSE 还是 WebSocket
流式输出有两种主流方案:SSE(Server-Sent Events)和 WebSocket。我的选择是优先用 SSE,需要双向通信时才用 WebSocket。
SSE 的优势是简单,它是基于 HTTP 的,不需要额外的协议升级,浏览器原生支持EventSource。对于“用户发一条消息,模型流式返回”这种单向场景,SSE 完全够用。WebSocket 的优势是双向通信,适合需要实时推送、多人协作的场景,但实现复杂度更高。
Spring Boot 里用 SSE 很简单,返回Flux<String>就行:
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> streamChat(@RequestParam String sessionId, @RequestParam String message) { return chatService.streamResponse(sessionId, message); }4.2 WebSocket 配置的坑
如果你确实需要 WebSocket,Spring Boot 的配置有几个容易踩的坑。首先,@EnableWebSocket和@EnableWebSocketMessageBroker是两套不同的东西,前者是原生 WebSocket,后者是 STOMP 协议。如果你只是做简单的消息推送,用原生 WebSocket 就够了。
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new ChatWebSocketHandler(), "/ws/chat") .setAllowedOrigins("*"); } }注意:
setAllowedOrigins("*")在生产环境要改成具体的域名,否则会有安全风险。另外,WebSocket 连接在 Nginx 反向代理后面需要额外配置proxy_set_header Upgrade和proxy_set_header Connection,否则连接会断。
4.3 流式输出的背压处理
流式输出有一个容易被忽略的问题:背压。如果模型返回速度很快,但客户端消费速度很慢,数据会在服务端堆积,最终导致内存溢出。Reactor 的Flux天然支持背压,但你需要确保下游消费者正确处理。
我的做法是在 WebSocket 场景下加一个缓冲区,当缓冲区满了就暂停从模型读取:
public Flux<String> streamWithBackpressure(String prompt) { return chatModel.stream(prompt) .onBackpressureBuffer(100, dropped -> log.warn("背压丢弃: {}", dropped), BufferOverflowStrategy.DROP_OLDEST); }5. RAG 检索增强的落地细节
5.1 文档切分策略决定检索质量
RAG 的效果好不好,七分靠切分,三分靠模型。文档切分太粗,检索出来的内容包含太多无关信息;切分太细,又可能丢失上下文。我的经验是:
- 技术文档按段落切分,每段 500 到 800 字。
- 法律合同按条款切分,每条独立。
- 会议纪要按议题切分,每个议题一段。
切分的时候要保留一定的重叠(overlap),一般设 10% 到 20%。比如每段 500 字,重叠 100 字。这样做的目的是防止关键信息刚好被切在边界上。
public List<Document> splitDocument(String content) { TokenTextSplitter splitter = new TokenTextSplitter(500, 100, 5, 10000, true); return splitter.apply(List.of(new Document(content))); }5.2 Embedding 模型的选择
Embedding 模型负责把文本转成向量。选择 Embedding 模型要考虑三个因素:维度、效果、成本。
维度越高,表达能力越强,但存储和计算成本也越高。OpenAI 的text-embedding-3-small是 1536 维,text-embedding-3-large是 3072 维。国内的通义千问text-embedding-v2是 1536 维。我的建议是:除非效果明显不够,否则用 1536 维就够了。
成本方面,OpenAI 的 Embedding 价格是每百万 token 0.02 美元,非常便宜。但如果你有大量文档要处理,还是建议先算一下成本。一个 10 万字的文档大概 15 万 token,处理一次不到 1 分钱,可以接受。
5.3 向量数据库的选型
向量数据库的选择很多:PgVector、Milvus、Qdrant、Weaviate、Chroma。我的建议是:
- 小规模(< 100 万向量):直接用 PgVector,不用额外部署组件。
- 中大规模(100 万到 1 亿):用 Milvus 或 Qdrant,性能更好。
- 超大规模(> 1 亿):考虑分布式方案,或者用云服务。
PgVector 的优势是它就是一个 PostgreSQL 插件,你不需要额外维护一个数据库。对于大多数中小型项目来说,PgVector 完全够用。
CREATE EXTENSION vector; CREATE TABLE documents ( id BIGSERIAL PRIMARY KEY, content TEXT, metadata JSONB, embedding vector(1536) ); CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);实操心得:
ivfflat索引的lists参数建议设为sqrt(行数)。比如 100 万行数据,lists设 1000。设太小检索慢,设太大召回率低。
6. Agent 工具调用的实现与避坑
6.1 Function Calling 的工作原理
Agent 的核心能力是工具调用。模型本身不能查数据库、不能调 API,但它可以“告诉”你它想调用哪个工具、传什么参数。你的代码执行完工具后,把结果返回给模型,模型再决定下一步。
Spring AI 里定义工具很简单,用@Tool注解就行:
@Component public class WeatherTools { @Tool(description = "查询指定城市的天气") public String getWeather(@ToolParam(description = "城市名称") String city) { // 实际调用天气 API return weatherApi.query(city); } @Tool(description = "查询指定城市的空气质量指数") public int getAqi(@ToolParam(description = "城市名称") String city) { return aqiApi.query(city); } }然后在调用模型时注册这些工具:
ChatResponse response = chatClient.prompt() .user("北京今天天气怎么样?") .tools(new WeatherTools()) .call() .chatResponse();6.2 工具调用的死循环问题
Agent 最容易出的问题是死循环。模型调用工具,工具返回结果,模型又调用同一个工具,如此反复。我遇到过模型连续调用 20 次同一个工具的情况,token 消耗直接爆炸。
解决方案是加一个最大调用次数限制:
public String executeWithTools(String prompt, int maxIterations) { List<Message> messages = new ArrayList<>(); messages.add(new UserMessage(prompt)); for (int i = 0; i < maxIterations; i++) { ChatResponse response = chatModel.call(new Prompt(messages)); AssistantMessage assistantMessage = response.getResult().getOutput(); messages.add(assistantMessage); if (assistantMessage.getToolCalls().isEmpty()) { return assistantMessage.getContent(); } for (ToolCall toolCall : assistantMessage.getToolCalls()) { String result = executeTool(toolCall); messages.add(new ToolResponseMessage(result, toolCall.id())); } } return "抱歉,处理超时,请换个方式提问。"; }maxIterations我一般设 5 到 8。超过这个次数还没得到最终答案,说明要么工具定义有问题,要么用户的问题太模糊,直接返回兜底话术比继续烧 token 划算。
6.3 工具参数的校验
模型生成的工具参数不一定合法。比如用户说“帮我查一下明天的天气”,模型可能生成getWeather(city="明天"),这显然不对。所以工具方法内部必须做参数校验:
@Tool(description = "查询指定城市的天气") public String getWeather(@ToolParam(description = "城市名称,必须是有效的中国城市名") String city) { if (!isValidCity(city)) { return "无法识别城市:" + city + ",请提供有效的城市名称。"; } return weatherApi.query(city); }返回错误信息而不是抛异常,这样模型可以根据错误信息调整参数重新调用。
7. 可观测性与成本控制
7.1 每次调用都要记录的关键指标
生产环境必须知道每次模型调用的:输入 token 数、输出 token 数、总 token 数、延迟、模型名称、是否成功。这些数据用来做成本核算、性能优化和故障排查。
Spring AI 提供了ChatResponse的元数据,可以拿到 token 使用情况:
@Around("execution(* com.example.ai.service.*.*(..))") public Object logUsage(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); long latency = System.currentTimeMillis() - start; if (result instanceof ChatResponse response) { Usage usage = response.getMetadata().getUsage(); log.info("模型调用: model={}, inputTokens={}, outputTokens={}, latency={}ms", response.getMetadata().getModel(), usage.getPromptTokens(), usage.getGenerationTokens(), latency); // 上报到 Micrometer meterRegistry.counter("ai.tokens.input").increment(usage.getPromptTokens()); meterRegistry.counter("ai.tokens.output").increment(usage.getGenerationTokens()); meterRegistry.timer("ai.latency").record(latency, TimeUnit.MILLISECONDS); } return result; }7.2 成本控制的三个手段
第一,设置每日预算上限。在 Redis 里维护一个当日 token 消耗计数器,超过阈值就降级到便宜模型或者直接拒绝请求。
第二,缓存重复请求。很多用户问的问题是重复的,比如“你们公司的退货政策是什么”。把问题和回答缓存起来,下次同样的问题直接返回缓存,不调模型。
第三,用便宜模型做预处理。比如用户输入先过一个小模型判断意图,简单意图直接走规则引擎,复杂意图才走大模型。
public String chatWithCache(String sessionId, String userInput) { String cacheKey = "chat:cache:" + DigestUtils.md5Hex(userInput); String cached = redisTemplate.opsForValue().get(cacheKey); if (cached != null) { return cached; } String response = chatModel.call(userInput); redisTemplate.opsForValue().set(cacheKey, response, Duration.ofHours(1)); return response; }注意:缓存只适用于事实型问答,不适用于需要结合上下文的对话。缓存 Key 要把 sessionId 排除掉,因为同一个问题在不同会话里可能需要不同回答。
8. 常见问题与排查技巧实录
8.1 模型返回超时怎么办
模型接口超时是最常见的问题。我的处理策略是分级超时 + 自动重试 + 降级兜底:
- 连接超时设 5 秒,读取超时设 30 秒。
- 超时后自动重试 1 次,重试时换一个模型节点。
- 重试还失败,返回兜底话术,同时记录日志告警。
@Bean public ChatClient chatClient(ChatModel chatModel) { return ChatClient.builder(chatModel) .defaultOptions(ChatOptions.builder() .timeout(Duration.ofSeconds(30)) .build()) .build(); }8.2 流式输出中断的排查
流式输出中断通常有三个原因:Nginx 超时、客户端断开、模型接口异常。排查顺序是:
- 看 Nginx 的
proxy_read_timeout是不是设太短了,建议设 300 秒。 - 看客户端是不是主动断开了,可以在服务端加
doOnCancel回调记录。 - 看模型接口的返回是不是正常的 SSE 格式,有时候模型接口会返回 JSON 错误而不是流。
return chatModel.stream(prompt) .doOnCancel(() -> log.warn("客户端取消流式请求")) .doOnError(e -> log.error("流式输出异常", e)) .onErrorResume(e -> Flux.just("抱歉,服务暂时不可用,请稍后重试。"));8.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 模型返回 401 | API Key 无效或过期 | 检查配置文件和环境变量 | 更新 API Key |
| 模型返回 429 | 触发限流 | 查看响应头中的 Retry-After | 加退避重试或换模型 |
| 流式输出卡住 | Nginx 缓冲未关闭 | 检查proxy_buffering | 设为 off |
| 上下文丢失 | Redis 过期或 Key 错误 | 检查 Redis 中的 Key | 调整过期时间 |
| 工具调用死循环 | 模型无法理解工具返回 | 查看工具返回内容 | 加最大迭代限制 |
| token 消耗异常 | 上下文过长 | 查看每次请求的 token 数 | 加滑动窗口 |
| 中文乱码 | 编码未指定 | 检查 Content-Type | 设 UTF-8 |
8.4 几个我踩过的坑
第一个坑:temperature设太高导致回答不稳定。做客服机器人的时候,我把temperature设成了 1.0,结果同一个问题每次回答都不一样,用户投诉“你们机器人怎么一会儿一个说法”。后来改成 0.3,稳定多了。
第二个坑:没有限制max-tokens导致费用失控。有一次上线忘了设max-tokens,模型有时候会返回几千 token 的长篇大论,一天下来费用是预期的 5 倍。后来强制设了 2048 的上限。
第三个坑:Redis 会话没有设过期时间。上线跑了一周,Redis 内存爆了。后来所有会话 Key 都加了 24 小时过期。
第四个坑:WebSocket 没有处理重连。用户网络切换的时候 WebSocket 会断开,前端没有自动重连,用户以为“机器人挂了”。后来在前端加了指数退避重连逻辑。
9. 部署与性能优化的实战建议
9.1 虚拟线程的开启方式
JDK 21 的虚拟线程对 AI 应用提升很明显,因为模型调用是 IO 密集型操作,虚拟线程可以在等待模型返回的时候处理其他请求。开启方式很简单:
spring: threads: virtual: enabled: true但要注意,虚拟线程不适合 CPU 密集型的操作。如果你的应用里有大量的向量计算、文档切分等 CPU 操作,这些还是要放在平台线程池里。
9.2 连接池的配置
模型接口的 HTTP 连接池要单独配置,不能用默认的。默认的连接池太小,高并发下会排队。
spring: ai: openai: chat: options: # 连接池配置如果 Spring AI 没有暴露连接池配置,可以自定义RestClient或WebClient:
@Bean public WebClient webClient() { return WebClient.builder() .clientConnector(new ReactorClientHttpConnector( HttpClient.create() .responseTimeout(Duration.ofSeconds(30)) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) )) .build(); }9.3 灰度发布的策略
模型切换是有风险的,新模型可能效果更差。所以模型切换必须支持灰度发布。我的做法是在路由层加一个百分比控制:
public ChatModel routeWithCanary(String userId) { int hash = Math.abs(userId.hashCode()) % 100; if (hash < canaryPercent) { return newModel; } return oldModel; }canaryPercent从 5 开始,观察一周没问题再逐步加到 100。如果发现异常,立刻改回 0。
9.4 监控告警的配置
Actuator 暴露的指标要接入监控系统。关键告警规则:
- 模型调用错误率超过 5% 告警。
- 平均延迟超过 10 秒告警。
- 单日 token 消耗超过预算 80% 告警。
- Redis 内存使用超过 80% 告警。
management: endpoints: web: exposure: include: health,metrics,prometheus metrics: tags: application: ai-platform10. 一些关于架构演进的个人体会
这个平台我从零搭到现在,经历了三个阶段。第一个阶段是“能跑就行”,所有逻辑写在一个 Service 里,模型调用、会话管理、工具调用全混在一起。第二个阶段是“拆开”,把模型调用、会话管理、RAG、Agent 拆成独立的模块,通过接口交互。第三个阶段是“可配置”,把模型选择、切分策略、窗口大小这些参数全部外置,不改代码就能调整行为。
如果让我重新来一遍,我会一开始就按第三个阶段的思路设计。因为 AI 应用的变化太快了,今天用这个模型,明天可能就换了;今天用这种切分策略,明天发现效果不好又要调。如果这些逻辑硬编码在代码里,每次调整都要重新部署,效率太低。
另外一个体会是:不要过度设计。我见过一些团队,一上来就搞多 Agent 协作、搞复杂的编排引擎,结果连基本的对话稳定性都没做好。先把单轮对话做稳定,再把多轮上下文做可靠,然后再考虑 RAG 和 Agent。这个顺序不能乱。
最后分享一个我常用的调试技巧:把每次模型调用的完整 Prompt 和 Response 都打到日志里,但只保留最近 100 条。出问题的时候,翻日志比猜原因快得多。日志级别设 DEBUG,生产环境通过动态调整日志级别来开启,不用重启服务。