31-AI服务预算观测与开发质量

O泡李华 6

AI 接口怎样成为稳定服务?预算、观测与开发质量

HTTP 成功率很高,用户仍认为答案差;重试提高完成率,费用却突然增加。AI 服务要把质量、等待、工具与用量放在一起观察,不能只沿用普通接口的一项成功率。

Spring AI 接入片段使用 Java 17、Spring Boot 3.5.3 与 Spring AI 1.1.8;协议与第三方平台示例另行标明适用范围。

1. 预算先于重试

并发、时间、尝试次数和费用约束同时生效。

预算至少有四种

预算 约束对象 示例
请求次数 单位时间的请求量 用户每分钟可发起多少次
并发 在途生成数量 同时占用多少模型连接
Token / 费用 输入、输出及多轮调用 单任务允许消耗多少额度
时间 / 步数 整体执行链 最多几轮工具、多久结束

一个短问题和一份长文档可能只有一次请求,却有完全不同的成本。工具循环也可能让一个用户请求对应多次模型请求。

费用预估需要按当前提供方、模型和实际计价项计算,不能用一个固定“每 Token 价格”覆盖全部输入、输出、缓存和推理相关计费。本文不提供可能过期的价格表。

限并发的最小接入组件

下面的 Java 17 类接收一个同步任务,限制当前进程的在途调用数。可以在Java 服务变慢怎样定位?从 CPU、GC 到内存保留链业务入口包装同步 ChatClient 请求。

import java.util.concurrent.Semaphore;
import java.util.function.Supplier;

public final class ModelBulkhead {
    private final Semaphore permits;

    public ModelBulkhead(int concurrency) {
        if (concurrency < 1) throw new IllegalArgumentException("positive concurrency required");
        permits = new Semaphore(concurrency);
    }

    public <T> T call(Supplier<T> task) {
        if (!permits.tryAcquire()) {
            throw new IllegalStateException("model capacity exhausted");
        }
        try {
            return task.get();
        } finally {
            permits.release();
        }
    }
}

预期容量用完时立即拒绝,正常返回或异常时释放许可。它只限制单进程,不是集群全局配额,也不控制费用。

不能将这个同步包装器直接套在“返回 Flux”的方法外面:方法返回时流可能尚未完成,许可却已经释放。流式许可应在订阅时获取,并在终止信号中释放。

费用额度为什么需要预留?

多个并发请求都读取到“还剩一元”,然后各自消费一元,最终超过预算。额度检查与预留需要原子化,不能只读余额。

一种流程是:创建生成任务,原子预留估算额度,执行请求,按返回 usage 结算,再释放剩余额度。异常、取消和无 usage 的请求需要进入待对账状态,不能一律按零费用处理。

预留额度不是提供方费用的精确上限。模型可能有不同计价项,外部执行与超时的实际情况也可能未知。业务内部预算与提供方账单应分别记录并定期对账。

哪些错误值得重试?

类型 常见处理
凭证或参数错误 修复配置,通常不立即重复
业务额度耗尽 拒绝或等待额度恢复
临时过载、限流 按协议等待,结合退避和总预算
响应超时 结果可能未知,记录已有执行情况
格式错误 少量受控修正,不重复副作用

还要核对框架底层已有重试。Spring AI 的模型适配和 HTTP 客户端可能各有策略,业务再套循环时,应计算实际总尝试次数。同步、流式和不同提供方的重试行为也不一定相同。

退避、抖动和 Retry-After 可以帮助控制恢复流量,但不能突破剩余总时限。还有十秒就超时,却安排三十秒后重试,协议本身就不一致。

生成重试与工具重试要分开

模型生成失败,不代表之前工具没有执行。保存工具操作号和真实结果,重跑时查询已有结果或者进行幂等执行,避免重复扣款、发券或提交审批。

对纯文本生成,可以接受重新生成,但应记录尝试号与费用。用户看到了第一轮部分内容后,第二轮内容可能不同,界面需要说明是重新生成而不是无缝续写。

供应商支持的请求幂等功能也需要按具体接口核对。自定义 requestId 用于追踪,不会自动让外部 API 去重计费。

超时以后怎样停止工作?

区分连接、读取空闲和整体任务时限。线程等待超时、Future 被取消或者浏览器断开,都不保证外部请求立即停止。

任务结束后应阻止新的工具步骤,释放应用资源,并保存完成、取消或失败状态。已经发生的外部副作用继续通过对应业务记录确认。

2. 观测链路把问题归到对应阶段

检索、模型、工具和渲染分别记时,结果保存必要元信息。

一次请求分成哪些阶段?

入口授权、会话读取、查询改写、文档检索、重排、模型生成、工具执行、结果保存,各有耗时与失败原因。

可以建立父任务 trace,再关联模型请求、检索和工具 span。一个用户请求包含三次模型调用,应同时看到整体任务和每次尝试,避免把三个成功请求误算成三次用户操作。

指标 要回答的问题
首段内容延迟 用户多久开始看到答案?
总耗时 整个生成多久结束?
完成、失败、取消率 最终状态是什么?
输入与输出 usage 消耗规模如何变化?
工具次数与错误 是否出现循环或依赖故障?
无证据与拒答率 知识库能覆盖多少问题?

不能把第一个 SSE 心跳当作首段模型内容,否则会让指标看起来很好,用户仍在等待。

从 ChatResponse 获取 usage

下面的接入方法使用 Spring AI 1.1.8,适合放入Java 服务变慢怎样定位?从 CPU、GC 到内存保留链应用中的业务服务。它保留了调用元数据入口,但不代表完整观测实现。

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;

public final class UsageExample {
    private final ChatClient client;

    public UsageExample(ChatClient client) {
        this.client = client;
    }

    public String answer(String question) {
        ChatResponse response = client.prompt().user(question).call().chatResponse();
        if (response == null || response.getResult() == null) {
            throw new IllegalStateException("response missing");
        }
        var usage = response.getMetadata().getUsage();
        System.out.println("reportedUsage=" + usage);
        return response.getResult().getOutput().getText();
    }
}

实际字段和内容取决于提供方。usage 缺失、流式断开或中途失败时,不能把未知消耗记成真实零;可以标记 UNKNOWN 并待对账。流式 usage 也可能出现在特定结束片段,需要核对提供方设置。

Spring AI 的自动观测提供什么?

Spring AI 可通过 Micrometer Observation 关联 ChatClient、模型、Advisor、Embedding 等调用信息。实际可见指标取决于依赖、注册器、导出配置和提供方实现,添加 Actuator 并不自动生成所有业务指标。Spring AI 1.1 Observability

业务仍应记录生成任务状态、权限拒绝和资料不足等结果。框架调用成功之后,结构化结果可能被业务校验拒绝,这种失败不能从 HTTP 200 的提供方指标直接看出。

标签应控制基数。模型类型、操作类别和结果码适合聚合,用户 ID、会话 ID 和完整问题不适合作为无限增长的指标标签。具体请求关联可以放在受控日志或 trace 中。

成本不能只乘一个单价

分别记录模型版本、计价版本、输入、输出及提供方特定项,再结合账单对账。缓存命中、批处理或其他计价项可能改变计算,不能把它们都混成普通 Token。

为一次用户任务汇总全部尝试与工具费用,既看到单次模型调用成本,也看到业务平均成本。重试次数增加时,最终成功率可能稳定,费用却显著上升。

本文只给核算方法,不使用虚构价格或优化百分比。成本是否下降,需要在相同任务与质量要求下比较真实数据。

日志怎样保留信息又不泄露?

可以保留请求 ID、模板版本、模型配置版本、资料 ID、结果码、耗时和参数摘要。Prompt、会话、工具结果和文档可能包含个人信息、令牌或商业数据,不应默认全文采集。

脱敏策略不能只做“手机号替换”:文档标题、下载链接和订单编号也可能暴露业务信息。需要明确采集目的、访问权限、期限和删除方式。

调试全量日志应有受控范围和过期机制。被用户删除的会话,还可能存在于日志与追踪平台,数据生命周期必须一起考虑。

怎样定位“成功但答案差”?

保存可重现的配置版本与经过授权的评估样本,分别检查:检索是否找到资料,重排是否保留正确片段,Prompt 是否包含所需证据,模型是否引用并正确解释。

模型自评可以辅助筛选,不能当作唯一事实标准。对重要问题需要人工标注和失败样本,不能只看用户点赞比例或模型置信度。

3. 辅助开发仍要形成可审查证据

需求约束、代码差异和真实验证决定交付质量。

需求输入要包含约束

先说明已有项目版本、模块结构、业务数据和接口契约,再说明需要改变的行为。缺少上下文时,生成器可能自行引入新的认证方式、表结构或框架版本。

例如创建售后工单,需求应明确:只能针对本人的订单提交,同一操作标识重复请求返回同一结果,金额使用精确类型,数据库失败时按设计回滚,异常不暴露内部信息。

下面是一份任务输入模板:

目标:在现有订单模块增加只读订单状态接口。
环境:Java 17、Spring Boot 3.5.3,沿用当前项目依赖。
身份:使用现有登录上下文,不接受前端 userId/tenantId。
查询:订单编号 + 当前租户 + 当前用户。
输出:状态、更新时间和脱敏物流摘要,不返回实体全部字段。
失败:不存在与不可访问使用统一对外结果。
要求:先阅读现有同类接口,说明修改文件和关键判断。
验证:列出授权、边界和异常用例,不虚构运行结果。

明确“沿用现有机制”可以减少重复建设,但仍要检查现有机制是否满足新需求。

人工审查先看业务风险

优先检查身份来源、数据范围、事务边界、并发约束和异常路径。格式与命名随后处理。一个代码风格漂亮但能够越权的接口仍然不可接受。

下面是常见问题与修正方向:

生成问题 为什么有问题 修正方向
请求接受 userId 用户可指定其他身份 服务器读取登录身份
先查询再插入去重 并发可能同时通过 唯一约束与事务处理
金额使用 double 二进制浮点误差 BigDecimal 与明确精度
捕获异常继续提交 可能留下不一致状态 明确回滚与错误映射
返回数据库实体 暴露额外字段 专用响应 DTO

再核对 API 是否真实存在、版本是否匹配。模型可能把其他版本的方法或包名拼进当前项目,解释看起来合理却不能编译。

修正指令应该指向证据

不要只说“优化一下”。指出具体问题,例如“查询条件只有 orderNo,没有租户和用户条件;请沿用当前权限上下文补充查询,并保持不可访问响应一致”。

修正时要求解释行为变化和涉及文件,但不把解释当作完成证明。再次检查 diff,确认补充权限没有破坏管理员合法查询或已有接口。

如果生成器建议删除测试、关闭校验或扩大权限来让代码通过,需要回到业务要求判断,而不是接受“这样更简单”的理由。

循环修正也要有停止条件:关键约束已经满足、验证通过、没有新增问题,就进入正常交付;不是不断要求“再优化”直到代码结构越来越复杂。

验证与证据怎样留下?

真实开发中按变化运行适当编译和测试,保存失败与修复记录。本文只是方法文章,没有运行任务中的任何代码。

验证重点来自业务不变量:其他用户不能查询,同一提交只产生一次结果,异常后没有半完成数据,实际响应符合契约。测试不应仅复制实现条件后断言自身正确。

还要检查依赖与构建产物。辅助工具修改配置后,IDE 能运行不代表 CI 和发布镜像同样正确。

记录最终人工判断:哪些建议采纳、哪些拒绝、理由是什么。面试时用真实差异与错误案例解释自己如何控制质量,比说“AI 写代码很快”更有说服力。

一个最小 Java 工作流

将以下内容保存为项目的 .github/workflows/ci.yml。项目需要有效的 pom.xml,示例使用已有的固定 major 标签;正式团队可以将 action 锁定到审核过的完整提交 SHA:

name: java-ci
on:
  push:
  pull_request:
permissions:
  contents: read
jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '17'
          cache: maven
      - name: Verify
        run: mvn --batch-mode --no-transfer-progress verify
      - name: Upload reports
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-reports
          path: |
            **/target/surefire-reports/
            **/target/failsafe-reports/
          if-no-files-found: warn

报告不存在时,这个配置只警告,所以还需要在项目中确认测试插件与发现规则正确。缓存用于加快依赖下载,不替代版本控制与产物验证。Java 与 Maven CI 指南。

从提交到镜像怎样保持一致?

构建一次通过验证的 JAR,再用这个产物生成镜像。给镜像记录提交 SHA、构建编号和版本标签,发布时保存 digest。

不要测试一个 JAR,却在发布服务器重新构建另一个 JAR。新的依赖下载、不同 profile 或未提交文件,都可能改变最终结果。

多模块项目要明确实际服务模块,不能用 target/*.jar 任意选择一个文件。镜像入口、运行 Java 版本和外部配置同样纳入发布记录。

提交 SHA → CI 构建编号 → 验证报告 → JAR 校验值
                                    ↓
                              镜像 digest → 发布记录

这条关联链让回滚选择一个已知产物,而不是临时寻找“上一份包”。

4. 设计判断与适用边界

重试只在可恢复且副作用安全的条件下进行,并消耗整体预算。输入、最大输出和正在执行的请求一起参与费用预留。生成重试和业务工具重试分开,避免一次网络故障触发重复创建。

观测保留请求与任务关系,却不默认全量记录用户问题。指标控制标签基数,费用按模型和价格版本计算。AI 辅助开发时,生成代码仍经过权限、事务和异常审查;CI 记录与产物对应,解释不能代替验证。

AWS 的文章讨论超时、退避和重试放大。AI 请求还需要在这类预算之外加入模型费用和工具副作用约束。 AWS:Timeouts, retries, and backoff with jitter。

5. 问答

问:限制 QPS 就限制了模型费用吗?

答:没有。输入长度、输出量、工具轮数和模型计价都会影响成本,需要另外预算。

问:用户请求 ID 能保证提供方不重复收费吗?

答:不能默认保证,它通常只用于关联。必须核对提供方接口的幂等契约。

问:模型 HTTP 成功就是任务成功吗?

答:还需看生成是否完整、工具结果、业务校验与答案依据。

问:usage 没有返回就记零吗?

答:应标记未知并按规则估算或对账,不能当作实际零费用。

问:AI 生成代码后只要能编译就够吗?

答:编译不能验证权限、并发和业务状态,需要针对约束检查。

问:怎样写有效修正指令?

答:指出具体代码、违反的约束和预期行为,而不只给“优化”这种宽泛要求。

参考资料