Appearance
现有 AI 调用缺了什么:找到 CostFlow 的第一个计费入口
上一篇我们已经沿着一次真实聊天,把调用链从前端一直追到了 Spring AI:
text
AI 聊天页面
↓
AiChatMessageController
↓
AiChatMessageServiceImpl
↓
buildPrompt()
↓
ChatModel / StreamingChatModel
↓
Spring AI
↓
模型 Provider
↓
ChatResponse而且我们还发现了一件很关键的事情。
当前项目使用的 Spring AI 1.1.8 中,ChatResponse 不只有模型生成的文本,响应元数据还提供了统一承载 Usage 的位置:
java
ChatResponse
↓
getMetadata()
↓
getUsage()
↓
getPromptTokens()
getCompletionTokens()
getTotalTokens()Spring AI 1.1.8 的 Usage 接口确实提供 getPromptTokens()、getCompletionTokens()、getTotalTokens() 和 getNativeUsage();但某次响应是否携带完整 Usage,还要看具体 Provider 和调用方式。
但是当前 RuoYi Vue Pro 的 Chat 业务代码拿到 ChatResponse 后,主要只读取:
java
AiUtils.getChatResponseContent(chunk);
AiUtils.getChatResponseReasoningContent(chunk);然后把回答内容写回 ai_chat_message。
也就是说,现在真正缺少的并不是:
“怎么调用大模型?”
而是:
Spring AI 的响应链路中已经有 Usage 的承载位置,但当前业务代码还没有把它稳定地转化为业务数据。
这就是 CostFlow 第一阶段需要先解决的问题。
先把现在缺的东西说清楚
如果只看当前聊天页面,一次调用已经相当完整:
text
用户是谁
↓
userId
在哪个对话里
↓
conversationId
用了哪个模型
↓
modelId / model
问了什么
↓
content
模型回答什么
↓
content / reasoningContent
是否使用知识库、搜索、附件
↓
segmentIds / webSearchPages / attachmentUrls但如果从“计量和计费”的角度看,这条记录还少了最关键的一段:
text
这次模型调用到底消耗了多少资源?例如最基本的 Chat 模型调用,我们至少希望知道:
text
Prompt Tokens
Completion Tokens
Total Tokens而当前:
text
ai_chat_message并没有保存这些数据。
上一篇已经看过,AiChatMessageDO 主要保存的是消息、模型、知识库、搜索和附件等业务上下文,并不存在 promptTokens、completionTokens、totalTokens 这样的实际模型用量字段。
Usage 和计费还不是一回事
既然已经拿到了:
text
promptTokens
completionTokens那是不是直接:
text
Token × 单价 = 费用然后扣费就结束了?
不是。
我们需要先把几个概念分开。
text
模型实际返回
↓
Usage
↓
Metering
↓
Pricing
↓
Billing其中第一层:
text
Usage只是记录:
这次到底用了多少。
例如:
text
promptTokens = 1250
completionTokens = 380
totalTokens = 1630它还没有回答:
text
这 1250 个输入 Token 应该按什么价格计算?
这 380 个输出 Token 单价是多少?
这是哪个价格版本?
是否赠送?
是否需要收费?
从哪个账户扣?这些都属于后面的事情。
现有代码到底在哪里丢掉了 Usage
还是回到:
text
backend/yudao-module-ai/src/main/java/
cn/iocoder/yudao/module/ai/service/chat/
AiChatMessageServiceImpl.java当前流式聊天中真正调用模型的是:
java
Prompt prompt = buildPrompt(
conversation,
historyMessages,
knowledgeSegments,
webSearchResponse,
model,
sendReqVO
);
Flux<ChatResponse> streamResponse =
chatModel.stream(prompt);随后:
java
return streamResponse.map(chunk -> {
String newContent =
AiUtils.getChatResponseContent(chunk);
String newReasoningContent =
AiUtils.getChatResponseReasoningContent(chunk);
// 拼接文本、返回前端
// ...
});这里的 chunk 本身就是:
java
ChatResponse而 Spring AI 的:
java
ChatResponse#getMetadata()会返回 ChatResponseMetadata。
在当前依赖中,继续进入:
java
ChatResponseMetadata#getUsage()就可以尝试取得:
java
UsageSpring AI 源码中已经明确:
java
public Usage getUsage() {
return this.usage;
}因此理论上,在这一层已经可以:
java
Usage usage =
chunk.getMetadata().getUsage();
Integer promptTokens =
usage.getPromptTokens();
Integer completionTokens =
usage.getCompletionTokens();
Integer totalTokens =
usage.getTotalTokens();但当前业务代码没有继续做这件事。
它只走了:
text
ChatResponse
├── Result → Content ✓ 已使用
├── Result → ReasoningContent ✓ 已使用
│
└── Metadata → Usage ✗ 没有进入业务数据这就是现在最明确的一处缺口。
一条 Usage 记录至少应该描述什么
现在先不设计数据库字段。
只站在业务事实的角度思考:
一次模型调用完成以后,我们到底希望知道什么?
最少应该能够回答下面这些问题。
| 问题 | 示例 |
|---|---|
| 谁使用的 | userId = 1001 |
| 属于哪个租户 | tenantId = 1 |
| 从哪个业务产生 | CHAT |
| 对应哪个业务记录 | messageId = 1024 |
| 使用哪个平台 | DeepSeek |
| 使用哪个模型 | deepseek-chat |
| 消耗多少输入 Token | 1250 |
| 消耗多少输出 Token | 380 |
| 总共多少 Token | 1630 |
| 结果如何 | success / failed / cancelled |
| 什么时候发生 | 某个时间点 |
注意这里还没有:
text
单价
费用
余额
账本因为我们现在只是定义:
text
Usage Fact而不是:
text
Billing Result这是后面非常重要的一条边界。
为什么不直接给 ai_chat_message 加几个 Token 字段
一种最直接的办法是给:
text
ai_chat_message加上:
text
prompt_tokens
completion_tokens
total_tokens对于一个纯聊天系统,这么做可能也可以。
但 CostFlow 的目标明显不是:
“给 RuoYi Chat 加三个字段。”
因为前面已经看到:
text
AI Chat
AI Write
MindMap
Workflow
Image
未来 Agent都可能产生资源消耗。
假设我们把 Usage 全塞进业务表:
text
ai_chat_message
→ prompt_tokens
ai_write
→ prompt_tokens
ai_mind_map
→ prompt_tokens
ai_xxx
→ prompt_tokens很快就会出现:
text
每个业务表都复制一套字段
每个业务 Service 都复制一套采集代码
查询总用量需要扫很多业务表
定价时还要重新判断业务类型更麻烦的是:
一条业务记录并不一定只对应一次模型调用。
以后 Agent 很可能出现:
text
一次 Agent Run
↓
LLM 调用 1
↓
Tool
↓
LLM 调用 2
↓
Tool
↓
LLM 调用 3如果 Usage 只挂在业务结果表上,很快就不够用了。
因此,这一篇先不确定最终表结构,只确认 Usage 不应该天然绑定在某一个具体业务表上。至于后面采用独立 Usage 表、事件记录、类似账本的结构还是其他方案,要进入方案设计后再决定。
交给 Coding Agent
这一篇不让 Coding Agent 实现计费。
它的任务是把我们刚刚得到的判断,在当前真实仓库中重新验证一遍,并盘点所有模型调用入口,为下一阶段方案设计准备材料。
总结
到这里,我们第一章已经差不多完成了。
我们没有一上来凭空设计一个“计费系统”,而是先把真实项目跑起来,再沿着真实模型调用找到问题:
text
Spring AI
已经能拿到 Usage
↓
原项目没有把它变成业务事实
↓
更没有 Metering
↓
没有 Pricing
↓
没有 Billing现在问题已经明确,下一阶段我们会先调研成熟的 AI 计费方案,再据此确定 CostFlow V1 的产品边界和实现方向。
后续我们会先调研成熟的 AI 计费方案,再据此确定 CostFlow V1 的产品边界和实现方向。