news 2026/11/2 9:09:36

Spring Boot 构建生产级 AI 应用平台:核心技术与实战避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot 构建生产级 AI 应用平台:核心技术与实战避坑

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 / WebSocketspring-boot-starter-webflux
RAGVectorStore + Embeddingspring-ai-pgvector
AgentFunction Callingspring-ai-core
可观测性Micrometer + Actuatorspring-boot-starter-actuator
限流降级Resilience4jresilience4j-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 超时、客户端断开、模型接口异常。排查顺序是:

  1. 看 Nginx 的proxy_read_timeout是不是设太短了,建议设 300 秒。
  2. 看客户端是不是主动断开了,可以在服务端加doOnCancel回调记录。
  3. 看模型接口的返回是不是正常的 SSE 格式,有时候模型接口会返回 JSON 错误而不是流。
return chatModel.stream(prompt) .doOnCancel(() -> log.warn("客户端取消流式请求")) .doOnError(e -> log.error("流式输出异常", e)) .onErrorResume(e -> Flux.just("抱歉,服务暂时不可用,请稍后重试。"));

8.3 常见问题速查表

问题现象可能原因排查方法解决方案
模型返回 401API 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-platform

10. 一些关于架构演进的个人体会

这个平台我从零搭到现在,经历了三个阶段。第一个阶段是“能跑就行”,所有逻辑写在一个 Service 里,模型调用、会话管理、工具调用全混在一起。第二个阶段是“拆开”,把模型调用、会话管理、RAG、Agent 拆成独立的模块,通过接口交互。第三个阶段是“可配置”,把模型选择、切分策略、窗口大小这些参数全部外置,不改代码就能调整行为。

如果让我重新来一遍,我会一开始就按第三个阶段的思路设计。因为 AI 应用的变化太快了,今天用这个模型,明天可能就换了;今天用这种切分策略,明天发现效果不好又要调。如果这些逻辑硬编码在代码里,每次调整都要重新部署,效率太低。

另外一个体会是:不要过度设计。我见过一些团队,一上来就搞多 Agent 协作、搞复杂的编排引擎,结果连基本的对话稳定性都没做好。先把单轮对话做稳定,再把多轮上下文做可靠,然后再考虑 RAG 和 Agent。这个顺序不能乱。

最后分享一个我常用的调试技巧:把每次模型调用的完整 Prompt 和 Response 都打到日志里,但只保留最近 100 条。出问题的时候,翻日志比猜原因快得多。日志级别设 DEBUG,生产环境通过动态调整日志级别来开启,不用重启服务。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 9:09:32

OpenCloudOS与openEuler混合环境Ansible安装配置实战

1. 先搞清楚&#xff1a;这两套系统上的 Ansible 到底难在哪一批新机器上架&#xff0c;系统盘里装的不是同一个发行版——一部分是 OpenCloudOS&#xff0c;一部分是欧拉&#xff08;openEuler&#xff09;&#xff0c;版本还横跨 8 和 9、20.03 和 22.03。机器到位之后&#…

作者头像 李华
网站建设 2026/11/2 9:08:44

AI写3D游戏实测:三大代码模型生成Three.js寻宝迷宫对比

最近我把 Step 5 Preview、DeepSeek V4 Pro 和 GLM5.3 拉到同一条起跑线上&#xff0c;让它们分别写同一个 3D 游戏。任务很简单粗暴&#xff1a;一个基于 Three.js 的浏览器 3D 寻宝迷宫&#xff0c;第一人称视角&#xff0c;有碰撞、有胜利判定、还要求能塞进手机浏览器里跑得…

作者头像 李华
网站建设 2026/11/2 9:08:20

基于斑马优化算法的风电功率预测模型优化

1. 项目背景与核心价值风电功率预测在新能源并网调度中扮演着关键角色。传统BP神经网络因其强大的非线性拟合能力被广泛采用&#xff0c;但存在收敛速度慢、易陷入局部最优等问题。斑马优化算法(ZOA)作为新型仿生优化算法&#xff0c;通过模拟斑马群觅食行为中的社会等级机制和…

作者头像 李华
网站建设 2026/11/2 9:05:32

OpenCV任意角度图像旋转原理与工程实践指南

简介&#xff1a;本资源是一份基于OpenCV实现图像旋转的完整C工程实践包&#xff0c;面向计算机视觉初学者、图像处理学习者及C开发人员&#xff0c;解决图像方向调整、预处理与增强中的核心旋转操作问题。压缩包共14个文件&#xff0c;包含核心源码myRoteteImage.cpp、可执行程…

作者头像 李华
网站建设 2026/11/2 9:05:17

PDFLIB PDI 实战指南:PDF 解析、图像提取与文本坐标定位

简介&#xff1a;本资源是一份基于Visual Studio 2010平台、使用PDFLIB TET库实现PDF文件读取的完整C开发实践项目&#xff0c;面向具备基础C/C编程能力的开发者&#xff0c;解决PDF文本与图像提取、元数据获取及页面布局解析等实际工程需求。压缩包共49个文件&#xff0c;涵盖…

作者头像 李华
网站建设 2026/11/2 9:04:47

PyFlink向量化UDF性能优化与实战指南

1. 向量化UDF的本质&#xff1a;Arrow与Pandas的化学反应第一次在PyFlink生产环境看到向量化UDF性能报表时&#xff0c;我盯着那个比传统UDF快8倍的数字愣了半天。这种性能飞跃的背后&#xff0c;是Apache Arrow列式内存格式与Pandas向量化计算的完美配合。1.1 Arrow批传输的底…

作者头像 李华