后端: 1. LLM 独立服务与统一计费出口落地:新增 `cmd/llm`、`client/llm` 与 `services/llm/rpc`,补齐 BillingContext、CreditBalanceGuard、价格规则解析、stream usage 归集与 `credit.charge.requested` outbox 发布,active-scheduler / agent / course / memory / gateway fallback 全部改走 llm zrpc,不再各自本地初始化模型。 2. TokenStore 收口为 Credit 权威账本:新增 credit account / ledger / product / order / price-rule / reward-rule 能力与 Redis 快照缓存,扩展 tokenstore rpc/client 支撑余额快照、消耗看板、商品、订单、流水、价格规则和奖励规则,并接入 LLM charge 事件消费完成 Credit 扣费落账。 3. 计费旧链路下线与网关切口切换:`/token-store` 语义整体切到 `/credit-store`,agent chat 移除旧 TokenQuotaGuard,userauth 的 CheckTokenQuota / AdjustTokenUsage 改为废弃,聊天历史落库不再同步旧 token 额度账本,course 图片解析请求补 user_id 进入新计费口径。 前端: 4. 计划广场从 mock 数据切到真实接口:新增 forum api/types,首页支持真实列表、标签、搜索、防抖、点赞、导入和发布计划,详情页补齐帖子详情、评论树、回复和删除评论链路,同时补上“至少一个标签”的前后端约束与默认标签兜底。 5. 商店页切到 Credit 体系并重做展示:顶部改为余额 + Credit/Token 消耗看板,支持 24h/7d/30d/all 周期切换;套餐区展示原价与当前价;历史区改为当前用户 Credit 流水并支持查看更多,整体视觉和交互同步收口。 仓库: 6. 配置与本地启动体系补齐 llm / outbox 编排:`config.example.yaml` 增加 llm rpc 和统一 outbox service 配置,`dev-common.ps1` 把 llm 纳入多服务依赖并自动建 Kafka topic,`docker-compose.yml` 同步初始化 agent/task/memory/active-scheduler/notification/taskclass-forum/llm/token-store 全量 outbox topic。
16 KiB
统一出口 Credit 计费最终计划
1. 文档目的
本计划用于把当前系统从“各业务服务进程内直接持有 LLM 组件、调用结束后再走旧 token 累加”的模式,重构为:
LLM成为真正的独立服务,统一掌管模型调用入口、出口、usage 采集与计费事件发布。TokenStore服务切换为 Credit 语义权威服务,掌管余额、商品、订单、流水与最终扣费落账。- 前端商店直接消费 Credit 语义接口,并能展示“当前登录用户自己的 Credit 流水”。
这份计划改成“三步走”版本,便于直接按阶段批准与执行。
2. 总体结论
2.1 核心结论
backend/services/llm不能再以“进程内共享组件”存在,必须升级为独立服务。- 所有用户态模型调用统一经过
LLM RPC,由LLM服务统一采集 usage。 LLM服务自己做同步准入CreditBalanceGuard,并使用 Redis 余额快照提速。LLM服务不做同步扣费,也不允许裸 goroutine 异步扣费。LLM服务在拿到最终 usage 后,把credit.charge.requested写入 自己的 outbox 表。TokenStore服务异步消费扣费事件,更新 Credit 余额与流水。- 旧 token 累加链路、旧 token 商店链路和旧
/token-store接口本轮直接删除,不保留兼容层。
2.2 关键原则
- 统一出口:所有 LLM 计费只在
LLM服务出口发生,不允许业务层各自重复扣费。 - 强制入参:不能只靠
Metadata约定传user_id,必须改 RPC/函数签名,让漏传在编译期报错。 - 准入同步、结算异步:调用模型前同步做
CreditBalanceGuard,拿到最终 usage 后通过 outbox 异步结算。 - 旧商店直删:旧 token 商店前后端都未真正接线,本轮不做 token/credit 双轨并存。
- 调用面兼容优先:
LLM RPC可以新增内部协议,但backend/client/llm对业务侧暴露的调用方式应尽量贴近当前services/llm,把迁移成本压到最低。
3. 为什么要这样改
3.1 当前最大问题
services/llm名字像服务,实际上不是服务,而是多个进程直接 new 的本地组件。- 计费边界分散,谁发起调用谁就得自己记账,很容易遗漏。
- 当前只有旧
token_usage累加,没有人民币成本与 Credit 扣费真相。 userauth当前承担的是token quota门禁,不是 Credit 余额门禁。- 旧 token 商店前后端都没有真正接上,因此没有保留兼容层的价值。
3.2 这次改完后的好处
- LLM 调用入口统一。
- usage 采集统一。
- 余额准入统一。
- 扣费事件发布统一。
- TokenStore 只做 Credit 权威账本,不再和模型调用散乱耦合。
4. 三步走总览
第一步:把 LLM 变成真正独立服务
目标:
- 所有模型调用统一改走
LLM RPC LLM服务拥有自己的CreditBalanceGuardLLM服务拥有自己的 outbox 表
第二步:把 TokenStore 改造成 Credit 权威服务
目标:
- 所有余额、流水、商品、订单都切成 Credit 语义
- 异步消费
credit.charge.requested - 提供“查看自己流水”的正式接口
第三步:切 Gateway 与前端,并删旧链路
目标:
- 上线
/credit-store/* - 商店页接 Credit 真接口与流水列表
- 删掉旧 token 累加和旧 token 商店链路
5. 第一步:把 LLM 变成真正独立服务
5.1 这一步的目标
把当前“各进程内直接持有 LLM 组件”的模式,改成“统一调 LLM 服务”。
5.2 新增/改造内容
5.2.1 新增独立 LLM 进程与 RPC
新增:
backend/cmd/llm/main.gobackend/client/llm/client.gobackend/services/llm/rpc/pb/llm.protobackend/services/llm/rpc/handler.gobackend/services/llm/rpc/server.gobackend/services/llm/dao/connect.go
说明:
- 这样
services/llm放在services目录下才真正名副其实。 - 业务服务以后全部只依赖
client/llm,不再直接 importservices/llm的本地组件。
5.2.2 统一 RPC 能力,但业务侧调用面尽量保持原状
服务间 LLM RPC 至少提供三类底层能力:
GenerateTextStreamTextGenerateResponsesText
但是对业务代码,不建议直接散落调用底层 RPC 方法名,而是由 backend/client/llm 继续暴露与当前本地 llm client 基本一致的门面:
(*Client).GenerateText(ctx, messages, options)GenerateJSON[T](ctx, client, messages, options)(*Client).Stream(ctx, messages, options)(*ArkResponsesClient).GenerateText(ctx, messages, options)GenerateArkResponsesJSON[T](ctx, client, messages, options)
迁移目标:
- 业务层尽量只替换 client 来源,不重写 prompt 组织方式。
GenerateOptions、ArkResponsesOptions现有字段语义尽量不变。generateJson / stream / responses json这些现有能力名和使用习惯尽量延续,避免把改造面扩大成“全链路重写调用协议”。
使用分工:
GenerateText:memory、active-scheduler、agent 非流式调用StreamText:agent chat / plan / execute / deliverGenerateResponsesText:course 图片解析
5.2.3 统一 BillingContext,但不要污染现有 GenerateOptions
所有请求都必须携带:
type BillingContext struct {
UserID int
EventID string
Scene string
RequestID string
ConversationID string
ModelAlias string
SkipCharge bool
}
要求:
UserID必须明确,普通用户态调用禁止省略。EventID必须稳定,作为幂等扣费键。Scene必须明确,方便价格表与审计。SkipCharge只允许系统内部场景显式声明。BillingContext不再塞进GenerateOptions.Metadata里,避免继续靠弱约定传关键字段。GenerateOptions、ArkResponsesOptions继续只承载模型行为参数,例如Temperature / MaxTokens / Thinking。
落地建议:
client/llm对调用方继续保留现有GenerateText / GenerateJSON / Stream / Responses风格方法。- 计费必填信息通过独立的调用包装层传入,例如“绑定过 BillingContext 的 client”或“显式 request 参数对象”,但要由
client/llm吸收复杂度。 - 调用方改造目标应控制在“补一处 BillingContext 组装 + 替换 client 初始化来源”,而不是每个业务点重写整段调用代码。
5.2.4 LLM 服务自己的 CreditBalanceGuard
这一步必须同时完成,否则独立 LLM 服务没有准入价值。
Guard 设计要求:
- 模型真正发起前同步执行。
- 不直接查 MySQL 真相表,必须走 Redis 快照优先。
- 缓存 miss 或过期时,再回源
TokenStore RPC。
建议缓存 key:
smartflow:credit_balance_snapshot:{user_id}smartflow:credit_balance_blocked:{user_id}
执行顺序:
- 先查
blocked键 - 再查
snapshot键 - miss 时调用 TokenStore 获取余额快照
- 若余额不足,写短 TTL 的
blocked键
5.2.5 LLM 服务自己的 outbox
这一步也必须同时完成。
新增:
llm_outbox_messages
并在 outbox 目录注册:
ServiceLLMServiceNameLLMsmartflow.llm.outboxsmartflow-llm-outbox-consumer
涉及文件:
backend/shared/infra/outbox/service_catalog.gobackend/shared/infra/outbox/service_route.gobackend/services/llm/dao/connect.go
5.2.6 LLM 服务统一 usage 采集
在 LLM 服务内部统一输出:
type BillingUsage struct {
InputTokens int64
OutputTokens int64
CachedTokens int64
ReasoningTokens int64
TotalTokens int64
ModelName string
ProviderName string
}
5.2.7 LLM 服务只发 charge 事件,不同步扣费
新增事件:
credit.charge.requested
事件载荷至少包含:
event_iduser_idscenerequest_idconversation_idprovider_namemodel_nameinput_tokensoutput_tokenscached_tokensreasoning_tokensrmb_cost_microscredit_costtriggered_at
时机:
- 拿到最终 usage
- 算出本次
credit_cost - 写入
llm_outbox_messages - 结果即可返回调用方
5.3 这一步涉及哪些调用方改造
以下服务都要从“本地 import services/llm”改成“调用 client/llm”:
agentmemoryactive-schedulercourse
6. 第二步:把 TokenStore 改造成 Credit 权威服务
6.1 这一步的目标
让 TokenStore 不再只是“充值/奖励入账中心”,而是:
- Credit 余额真相
- Credit 商品与订单中心
- Credit 流水中心
- LLM 扣费事件的最终结算方
6.2 新的 Credit 数据表
本轮不复用旧 token_* 表,直接建立纯 Credit 语义的新表:
credit_productscredit_orderscredit_accountscredit_ledgercredit_price_rulescredit_reward_rules
6.2.1 credit_products
用途:商店展示的 Credit 商品
核心字段:
skunamedescriptioncredit_amountprice_centcurrencybadgestatussort_order
6.2.2 credit_orders
用途:购买订单与 mock paid 状态机
核心字段:
order_nouser_idproduct_idproduct_snapshot_jsonquantitycredit_amountamount_centstatuspayment_modeidempotency_keypaid_atcredited_at
6.2.3 credit_accounts
用途:余额真相与门禁回源表
核心字段:
user_idbalancetotal_rechargedtotal_rewardedtotal_consumedversion
6.2.4 credit_ledger
用途:统一正负流水,也是“查看自己流水”的唯一真相源
核心字段:
event_iduser_iddirectionsourcescenedescriptioncredit_deltabalance_afterinput_tokensoutput_tokenscached_tokensreasoning_tokensrmb_cost_microscreated_at
6.2.5 credit_price_rules
用途:模型价格表
核心字段:
provider_namemodel_namesceneinput_price_micros_per_1koutput_price_micros_per_1kcached_price_micros_per_1kreasoning_price_micros_per_1kcredits_per_yuanstatus
6.2.6 credit_reward_rules
用途:社区奖励规则
核心字段:
sourcenamecredit_amountstatusconfig_json
6.3 TokenStore 如何消费 charge 事件
新增消费逻辑:
- 订阅
credit.charge.requested - 幂等校验
event_id - 在事务中写
credit_ledger - 扣减
credit_accounts.balance - 刷新或删除 Redis 余额快照
6.4 用户查看自己流水接口
这是本轮必须落的能力。
对外新增:
ListCreditTransactions
要求:
- 明确用于“当前登录用户查看自己的 Credit 流水”
- 必须覆盖购买入账、社区奖励入账、LLM 消费扣费三类记录
HTTP 接口:
GET /api/v1/credit-store/transactions
必须满足:
- 只返回当前登录用户自己的 Credit 流水
- 支持分页:
page、page_size - 支持可选筛选:
direction、source、scene - 返回字段至少包含:
transaction_idevent_iddirectionsourcescenedescriptioncredit_deltabalance_aftercreated_at
6.5 这一步要删除什么旧东西
直接删除:
token_productstoken_orderstoken_grantstoken_reward_rules
7. 第三步:切 Gateway/前端并删旧链路
7.1 这一步的目标
- 上线
/credit-store/* - 商店页接 Credit 真接口
- 删掉旧 token 累加和旧 token 商店链路
7.2 Gateway 切换
只保留 Credit 语义路由:
GET /api/v1/credit-store/summaryGET /api/v1/credit-store/productsPOST /api/v1/credit-store/ordersPOST /api/v1/credit-store/orders/:order_id/mock-paidGET /api/v1/credit-store/transactions
直接删除:
/token-store/*backend/gateway/api/tokenstoreapi/*
7.3 前端商店页切换
商店页必须改成:
- 接
/credit-store/* - 新增“我的 Credit 流水”展示区
- 流水数据源只允许来自
GET /api/v1/credit-store/transactions - 至少展示:
- 流水类型
- 流水说明
- Credit 增减值
- 流水发生时间
- 可选余额快照
7.4 旧 token 累加机制下线
直接删除:
backend/services/runtime/eventsvc/chat_token_usage_adjust.gobackend/services/runtime/model/agent.go中ChatTokenUsageAdjustPayloadbackend/services/agent/sv/agent_graph.go中旧 token 调整调用backend/services/agent/sv/agent_meta.go中旧 token 调整调用backend/services/runtime/dao/agent.go中tokens_total累加写路径backend/gateway/middleware/token_quota_guard.go在聊天主链的使用userauth中CheckTokenQuota / AdjustTokenUsage的计费用途agent_chats.tokens_total字段
8. 这三步分别改哪些地方
第一步改动面
backend/services/llm/*backend/client/llm/*backend/cmd/llm/*backend/shared/infra/outbox/service_catalog.gobackend/shared/infra/outbox/service_route.goagent / memory / active-scheduler / course的所有本地 LLM 调用点
第二步改动面
backend/services/tokenstore/*backend/client/tokenstore/*- TokenStore 的 outbox consumer
- Redis 余额快照与失效逻辑
第三步改动面
backend/gateway/*frontend/src/views/StoreView.vue- 可能新增商店流水列表组件
runtime/userauth旧 token 累加链删除
9. 验证清单
9.1 第一步验证
- 业务服务代码里不再直接 import
services/llm - 所有模型调用都改经
LLM RPC - 所有请求都必须带
BillingContext CreditBalanceGuard命中缓存、miss 回源、余额不足封禁都通过测试llm_outbox_messages能正常写入credit.charge.requested
9.2 第二步验证
TokenStore能幂等消费credit.charge.requestedcredit_accounts余额更新正确credit_ledger流水写入正确ListCreditTransactions返回当前用户自己的流水
9.3 第三步验证
- 商店页能展示 Credit 概览
- 商店页能展示商品
- 商店页能展示“我的 Credit 流水”
- 全文搜索确认以下旧链路不再出现在主路径:
PublishChatTokenUsageAdjustRequestedAdjustTokenUsage(CheckTokenQuota(TokenQuotaGuardtokens_total +tokenstoreapi/token-storetoken_productstoken_orderstoken_grantstoken_reward_rules
10. 风险与取舍
10.1 这次改造面确实更大
因为这次不是只改计费,而是顺带把 LLM 从“共享组件”升级成真正独立服务,所以会涉及:
- 新 RPC
- 新 client
- 新 outbox
- 业务服务调用方式切换
10.2 但长期结构更对
收益是:
- LLM 调用入口统一
- usage 采集统一
- 准入 guard 统一
- 计费事件发布统一
- TokenStore 只做账本,不再和模型调用散乱耦合
11. 服务影响总表
必改服务
backend/services/llmbackend/services/tokenstorebackend/gatewaybackend/services/agentbackend/services/coursebackend/services/memorybackend/services/active_scheduler
新增
backend/cmd/llmbackend/client/llmbackend/services/llm/rpc/*llm_outbox_messages
后续前端接入
frontend/src/views/StoreView.vue- 若商店页拆组件,则补充独立的 Credit 流水列表组件