news 2026/9/4 23:34:36

大模型工程化实践:用Spring Boot给AI调用加预算与安全减速带

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大模型工程化实践:用Spring Boot给AI调用加预算与安全减速带

在 AI Agent 和大模型应用中提到 p(doom) 时,很多人首先想到的是“未来通用 AI 会不会失控”这类宏大概率。但对于正在把大模型接入业务系统的工程师来说,p(doom) 更需要被翻译成一个工程问题:模型进入真实链路后,产生不可控、不可用、不可追责输出的概率有多大。这个概率才是可以通过代码、配置和流程去降低的。如果把标题里的 Decelerate AI 理解为“在模型和业务结果之间增加减速带”,把 “using Capitalism Itself” 理解为“用调用预算、成本核算、责任归因和止损机制去约束模型行为”,整个话题就变成了一条完整的 AI 工程实践主线。本文会用 Spring Boot 实现一个带预算限制、输入检查、输出校验、审计日志和人工审批位的受控模型调用入口,用真实可运行的代码说明如何让一次模型调用从“裸奔”变为“有流程可追责”。

1. 先理清:p(doom) 在应用层指什么,减速带应该加在哪里

1.1 从末日概率到线上事故概率

p(doom) 本质是一种对未来结果的概率估计。对通用 AI 的末日预测很难验证,但应用层的风险却可以被定义成非常具体的指标,例如:

  • 用户通过提示词注入让模型绕过系统规则。
  • 模型把内部信息、密钥或不在授权范围内的内容输出给请求方。
  • 模型生成了超出业务边界的建议,例如误导性的代码、医疗结论或财务操作。
  • 高并发调用导致模型费用失控。
  • 调用过程没有日志,出事之后无法归因。

这些风险发生一次,对团队来说就像一次真实的 “small doom”。它们的共同点是:当模型被直接暴露给用户时,风险几乎无法被提前拦截。模型是一个概率系统,不能只靠“告诉它不要做什么”来保证安全。要降低事故概率,就必须在模型入口和出口上插入确定性的规则,让每一次调用都先经过检查,再执行;先经过预算核算,再把输出返回给用户。

1.2 用预算、成本和责任链给模型调用装上刹车

为什么企业内的 AI 应用需要按“成本机制”来控制模型行为,而不是只依靠提示词约束?原因是成本机制能够产生真实的可执行反馈。模型每次调用都产生成本,公司为这个成本设置了预算,团队就必须关心请求是否合理;模型输出引发事故,责任链就会出现明确归属,团队就必须补充规则;一旦调用量触发熔断,上线流程就必须先止血,而不是继续无限放量。

把这个机制落到代码里,就是三个动作:

  1. 记录每次调用的 Token 消耗,并把它换算成费用。
  2. 设置单次请求费用上限和每日总费用预算,超过阈值立即熔断。
  3. 每一次调用都记录用户、请求内容、模型输出、拦截结果、费用和告警原因。

这三个动作构成了一个可核算的闭环。模型输出不再只是“模型说了什么”,而是一笔带责任的业务操作。这也是标题里 “by using Capitalism Itself” 在工程中最朴素的落地方式:有成本,就有取舍;有责任,就有保护。

1.3 一条受控调用链路常见的四类减速带

在系统链路中,“减速带”不是某一个过滤器,而是一组位于模型前后的规则和决策点。下面这张表可以作为设计模型入口时的检查清单。

减速带典型机制解决的典型问题可观察指标
输入护栏长度限制、敏感信息检查、系统提示注入检查、URL 白名单异常数据进入模型输入拦截次数
预算护栏Token 估算、费用统计、单次限额、每日熔断模型费用失控今日成本、熔断次数
输出护栏格式校验、敏感词检测、长度限制、结构化输出约束违规内容离开模型输出拦截次数
人工决策高风险动作进入待审批状态,由业务人员确认后执行模型无法为高风险操作负责待审批数量、审批通过率
审计日志记录请求、响应、拦截原因、调用方、费用出现事故无法回溯完整调用链条

这里要强调一个原则:模型本身的判断是概率性的,不能作为唯一防线。真正可靠的安全兜底应该由确定性代码完成,例如长度检查、格式解析、预算判断、权限校验。模型能力适合做内容生成、意图分类这类开放问题,不适合做“是否允许通过”的最终决策。

2. 准备工程环境:搭建一个可复现的最小模型调用服务

2.1 环境准备

为了让示例可以离线运行,这里采用本地可部署的 OpenAI 兼容模型服务。如果你已经在使用云厂商的大模型接口,只需要替换 base-url 和模型名称即可。

依赖项要求说明
JDK17 或更高Spring Boot 3 的基础要求
Maven3.8+用于依赖管理
Spring Boot3.3.x示例使用 3.3.5
本地模型服务Ollama 或者其他 OpenAI 兼容服务示例使用 Ollama 默认地址http://localhost:11434
模型名称qwen2.5:7b可按需替换为其他兼容模型

前置知识需要具备 Spring Boot 基础、Maven 项目结构以及 REST API 的基本概念。真实项目接入商业大模型时,要按供应商文档核对接口路径、鉴权方式和 token 计费规则。

2.2 初始化 Maven 项目

创建一个新的 Maven 项目,包名使用com.example.aigateway。下面是 pom.xml 的核心配置。

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.5</version> <relativePath/> </parent> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>

代码中不引入具体大模型的 SDK,而是通过 Spring 的RestClient直接调用 OpenAI 兼容接口。这样做的原因是依赖面更小,后续无论是切换到本地 Ollama、内部 vLLM 服务,还是云厂商兼容网关,都只需要改配置。

2.3 配置文件与模型参数

src/main/resources/application.yml中设置模型服务地址、模型名称和预算相关参数。

server: port: 8080 ai: gateway: base-url: http://localhost:11434 model: qwen2.5:7b api-key: ${LLM_API_KEY:} max-cost-per-request: 0.02 daily-budget: 0.5 pricing: input-per-million: 1.2 output-per-million: 3.0

注意以下几点:

  • api-key通过环境变量注入,仓库中不提交真实密钥。
  • 本地 Ollama 通常不需要 api-key;接入商业模型时保留该字段。
  • daily-budget示例中设置 0.5 美元,是为了方便演示熔断,真实项目需要按业务预算调整。
  • 价格参数只是演示用,实际接入时必须改成模型供应商的计费单价。

为了让配置能绑定到 Java 对象,编写一个AiGatewayProperties类。

package com.example.aigateway; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "ai.gateway") public record AiGatewayProperties( String baseUrl, String model, String apiKey, double maxCostPerRequest, double dailyBudget, Pricing pricing ) { public record Pricing(double inputPerMillion, double outputPerMillion) { } }

在启动类上开启配置绑定。

package com.example.aigateway; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; @SpringBootApplication @EnableConfigurationProperties(AiGatewayProperties.class) public class AiGatewayApplication { public static void main(String[] args) { SpringApplication.run(AiGatewayApplication.class, args); } }

2.4 先实现一个能工作的模型客户端

为了先验证模型链路是否通畅,写一个最小的ModelClient,它接收用户消息和一个可选的系统提示词,然后调用 OpenAI 兼容的 chat completions 接口。

package com.example.aigateway; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; @Service public class ModelClient { private final AiGatewayProperties properties; private final RestClient restClient; public ModelClient(AiGatewayProperties properties) { this.properties = properties; RestClient.Builder builder = RestClient.builder() .baseUrl(properties.baseUrl()); if (properties.apiKey() != null && !properties.apiKey().isBlank()) { builder.defaultHeader("Authorization", "Bearer " + properties.apiKey()); } this.restClient = builder.build(); } public ChatResult complete(String userMessage, String systemMessage) { List<Map<String, String>> messages = new ArrayList<>(); if (systemMessage != null && !systemMessage.isBlank()) { messages.add(Map.of("role", "system", "content", systemMessage)); } messages.add(Map.of("role", "user", "content", userMessage)); Map<String, Object> body = new HashMap<>(); body.put("model", properties.model()); body.put("messages", messages); body.put("stream", false); body.put("temperature", 0.2); ChatCompletionResponse response = restClient.post() .uri("/v1/chat/completions") .contentType(MediaType.APPLICATION_JSON) .body(body) .retrieve() .body(ChatCompletionResponse.class); if (response == null || response.choices() == null || response.choices().isEmpty()) { throw new IllegalStateException("模型没有返回任何内容"); } return new ChatResult( response.choices().get(0).message().content(), response.usage() == null ? 0 : response.usage().promptTokens(), response.usage() == null ? 0 : response.usage().completionTokens(), response.usage() == null ? 0 : response.usage().totalTokens() ); } public ChatResult complete(String userMessage) { return complete(userMessage, null); } public record ChatResult( String content, int promptTokens, int completionTokens, int totalTokens ) { } public record ChatCompletionResponse( List<Choice> choices, Usage usage ) { public record Choice(Message message) { } public record Message(String role, String content) { } public record Usage( @JsonProperty("prompt_tokens") int promptTokens, @JsonProperty("completion_tokens") int completionTokens, @JsonProperty("total_tokens") int totalTokens ) { } } }

这段代码有两点需要说明:

  • messages使用List<Map<String, String>>构建,便于适配多种模型服务。
  • 响应体的usage中包含 token 消耗,预算系统要靠它计算成本。

2.5 不接护栏时的一个裸调用

新增一个直接调用接口,用于演示没有护栏时的样子。

package com.example.aigateway; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @Validated @RestController public class DirectChatController { private final ModelClient modelClient; public DirectChatController(ModelClient modelClient) { this.modelClient = modelClient; } @PostMapping("/direct/chat") public ModelClient.ChatResult direct(@Valid @RequestBody PromptRequest request) { return modelClient.complete(request.prompt()); } public record PromptRequest( @NotBlank String prompt ) { } }

启动本地模型服务后,运行项目。

ollama pull qwen2.5:7b mvn spring-boot:run

在另一个终端发送请求。

curl -s http://localhost:8080/direct/chat \ -H "Content-Type: application/json" \ -d '{"prompt":"用一句话介绍 Spring Boot"}'

会得到一个类似下面的响应。

{ "content": "Spring Boot 是一个用于简化 Spring 应用初始搭建和开发过程的框架。", "promptTokens": 18, "completionTokens": 26, "totalTokens": 44 }

这个接口能工作,但存在几个明显问题:

  • 没有区分调用方身份,无法做权限控制。
  • 没有记录日志,调用失败了也无法排查。
  • 没有预算限制,模型可以无限调用。
  • 请求内容没有校验,用户可以直接尝试提示词注入。
  • 输出没有检查,模型返回敏感内容也无法拦截。

后面所有章节都会围绕这些问题逐步补上减速带。

3. 实现带预算和审计的受控调用入口

3.1 用 Token 用量把成本变成可监控的数字

大模型服务的费用通常由输入 token 和输出 token 共同决定。ModelClient返回的ChatResult已经包含 token 消耗,接下来把它换算成费用。

package com.example.aigateway; import org.springframework.stereotype.Component; @Component public class CostCalculator { private final AiGatewayProperties properties; public CostCalculator(AiGatewayProperties properties) { this.properties = properties; } public double calculate(ModelClient.ChatResult result) { AiGatewayProperties.Pricing pricing = properties.pricing(); double inputCost = result.promptTokens() / 1_000_000.0 * pricing.inputPerMillion(); double outputCost = result.completionTokens() / 1_000_000.0 * pricing.outputPerMillion(); return inputCost + outputCost; } }

这里的费用计算使用每百万 token 单价。真实项目中,如果模型供应商按缓存命中、batch 等不同维度计费,还需要扩展计价规则。

3.2 用预算管理器控制今日总费用

预算管理器需要支持两个能力:

  • 单次请求费用是否超过maxCostPerRequest
  • 当日累计费用是否超过dailyBudget

为了简单,这里使用内存中的synchronized方法做累计。生产环境应当把预算数据放到 Redis 或数据库中,避免多实例下计数不准确。

package com.example.aigateway; import org.springframework.stereotype.Component; @Component public class BudgetManager { private final AiGatewayProperties properties; private final Object lock = new Object(); private double spentToday = 0.0; public BudgetManager(AiGatewayProperties properties) { this.properties = properties; } public void checkBeforeRequest() { synchronized (lock) { if (spentToday >= properties.dailyBudget()) { throw new BudgetExceededException("DAILY_BUDGET_EXCEEDED", "今日模型调用预算已用完"); } } } public void recordCost(double cost) { synchronized (lock) { if (cost > properties.maxCostPerRequest()) { throw new BudgetExceededException( "REQUEST_COST_EXCEEDED", "单次请求费用超过上限: " + cost ); } if (spentToday + cost > properties.dailyBudget()) { throw new BudgetExceededException( "DAILY_BUDGET_EXCEEDED", "剩余预算不足以完成本次调用" ); } spentToday += cost; } } public double spentToday() { synchronized (lock) { return spentToday; } } }

单独定义预算异常,避免业务方把底层异常直接抛出。

package com.example.aigateway; public class BudgetExceededException extends RuntimeException { private final String code; public BudgetExceededException(String code, String message) { super(message); this.code = code; } public String getCode() { return code; } }

这里有一个取舍需要注意:真正调用模型之前无法精确知道会用多少 token,所以recordCost放在模型返回之后执行。如果某一次请求消耗过大,输出虽然已经生成,但会被后续逻辑拦截,不会直接回到用户手里。这能阻止费用继续扩大,但无法避免这一次超支。要更早拦截,可以引入 prompt tokenizer 做预估算,但会增加复杂度,小型项目可以先不追求精确预估。

3.3 不让 Controller 直接碰模型客户端

裸调用的问题在于 Controller 直接依赖ModelClient,任何校验都容易被跳过。受控入口应该由ControlledChatService统一处理,Controller 只负责接收 HTTP 请求和返回响应。

package com.example.aigateway; import org.springframework.stereotype.Service; @Service public class ControlledChatService { private static final String DEFAULT_SYSTEM_PROMPT = "你是一个受内部规则约束的助手。不要生成违反法律法规的内容," + "不要输出明显越权的操作建议,回答尽量简洁。"; private final ModelClient modelClient; private final BudgetManager budgetManager; private final CostCalculator costCalculator; private final AuditLogger auditLogger; private final InputGuard inputGuard; private final OutputGuard outputGuard; public ControlledChatService( ModelClient modelClient, BudgetManager budgetManager, CostCalculator costCalculator, AuditLogger auditLogger, InputGuard inputGuard, OutputGuard outputGuard ) { this.modelClient = modelClient; this.budgetManager = budgetManager; this.costCalculator = costCalculator; this.auditLogger = auditLogger; this.inputGuard = inputGuard; this.outputGuard = outputGuard; } public ModelClient.ChatResult chat(String userId, String prompt) { budgetManager.checkBeforeRequest(); inputGuard.validate(prompt); auditLogger.record(new AuditLogger.AuditEvent( userId, "CHAT_START", prompt, null, 0.0 )); ModelClient.ChatResult result = modelClient.complete( prompt, DEFAULT_SYSTEM_PROMPT ); double cost = costCalculator.calculate(result); budgetManager.recordCost(cost); outputGuard.validate(result.content()); auditLogger.record(new AuditLogger.AuditEvent( userId, "CHAT_SUCCESS", prompt, result.content(), cost )); return result; } }

这个 Service 定义了当前受控调用链路的顺序:

  1. 先检查预算余量。
  2. 进入输入护栏。
  3. 记录开始日志。
  4. 调用模型。
  5. 计算成本并记录到预算。
  6. 进入输出护栏。
  7. 记录成功日志。

如果第 2、5、6 步任意一个失败,日志仍然会记录一次失败动作,而不是让请求静默消失。

3.4 审计日志先落本地,生产环境再替换数据库

短时间内用一个简单的文件追加方式记录审计日志。生产环境应该使用数据库或消息队列,但文件采集日志的方式足够用来验证链路。

package com.example.aigateway; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.BufferedWriter; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.time.Instant; import org.springframework.stereotype.Component; @Component public class AuditLogger { private final ObjectMapper objectMapper = new ObjectMapper(); private final Path path = Path.of("logs", "ai-audit.jsonl"); public void record(AuditEvent event) { try { Files.createDirectories(path.getParent()); String line = objectMapper.writeValueAsString(event) + System.lineSeparator(); try (BufferedWriter writer = Files.newBufferedWriter( path, StandardOpenOption.CREATE, StandardOpenOption.APPEND )) { writer.write(line); } } catch (Exception ex) { throw new IllegalStateException("写入审计日志失败", ex); } } public record AuditEvent( String userId, String action, String prompt, String response, double cost ) { public AuditEvent { if (prompt != null &&
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 23:32:36

Ice:macOS 菜单栏图标管理与整理工具

Ice&#xff1a;macOS 菜单栏图标管理与整理工具 【免费下载链接】Ice Powerful menu bar manager for macOS 项目地址: https://gitcode.com/GitHub_Trending/ice/Ice 周五下午&#xff0c;你又一次在 Mac 顶部那排密密麻麻的图标里找 WiFi 开关。Ice 这款菜单栏管理工…

作者头像 李华
网站建设 2026/9/4 23:26:11

WezTerm 完整教程:GPU 加速终端与多路复用怎么上手

WezTerm 完整教程&#xff1a;GPU 加速终端与多路复用怎么上手 【免费下载链接】wezterm A GPU-accelerated cross-platform terminal emulator and multiplexer written by wez and implemented in Rust 项目地址: https://gitcode.com/GitHub_Trending/we/wezterm 还在…

作者头像 李华
网站建设 2026/9/4 23:25:56

iOS AI 客户端媒体输入链路:库支持与媒体筛选的工程化实现

在 Grok 这类移动端 AI 助手逐步进入更多客户端形态的背景下&#xff0c;“iOS 库支持”和“媒体筛选”一直是开发团队绕不开的两个关键词。表面上看&#xff0c;它们只是一个“用户选择图片→上传给模型”的动作&#xff0c;但真正落到 iOS 工程里&#xff0c;至少要处理系统相…

作者头像 李华
网站建设 2026/9/4 23:22:20

3 分钟跑通 Pixelle-Video:多语言短视频批量生成完整教程

3 分钟跑通 Pixelle-Video&#xff1a;多语言短视频批量生成完整教程 【免费下载链接】Pixelle-Video &#x1f680; AI 全自动短视频引擎 | AI Fully Automated Short Video Engine 项目地址: https://gitcode.com/GitHub_Trending/pi/Pixelle-Video 同一支视频要做多个…

作者头像 李华
网站建设 2026/9/4 23:19:40

长沙AI视频剪辑培训哪家好,梦想蓝途8城直营无隐形消费

正文摘要本文从运营模式、教学管控、收费规范三个维度&#xff0c;拆解长沙 AI 视频剪辑培训的品质与消费差异&#xff0c;结合梦想蓝途湖南 8 城直营实训点的布局特点&#xff0c;分析直营机构在教学标准统一、消费透明等方面的特征&#xff0c;为学习者筛选机构提供客观参考依…

作者头像 李华