Files
smartmate/docs/backend/统一出口Credit计费最终计划.md
Losita 61db646805 Version: 0.9.80.dev.260506
后端:
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。
2026-05-06 20:16:53 +08:00

16 KiB
Raw Blame History

统一出口 Credit 计费最终计划

1. 文档目的

本计划用于把当前系统从“各业务服务进程内直接持有 LLM 组件、调用结束后再走旧 token 累加”的模式,重构为:

  1. LLM 成为真正的独立服务统一掌管模型调用入口、出口、usage 采集与计费事件发布。
  2. TokenStore 服务切换为 Credit 语义权威服务,掌管余额、商品、订单、流水与最终扣费落账。
  3. 前端商店直接消费 Credit 语义接口,并能展示“当前登录用户自己的 Credit 流水”。

这份计划改成“三步走”版本,便于直接按阶段批准与执行。


2. 总体结论

2.1 核心结论

  1. backend/services/llm 不能再以“进程内共享组件”存在,必须升级为独立服务。
  2. 所有用户态模型调用统一经过 LLM RPC,由 LLM 服务统一采集 usage。
  3. LLM 服务自己做同步准入 CreditBalanceGuard,并使用 Redis 余额快照提速。
  4. LLM 服务不做同步扣费,也不允许裸 goroutine 异步扣费。
  5. LLM 服务在拿到最终 usage 后,把 credit.charge.requested 写入 自己的 outbox 表
  6. TokenStore 服务异步消费扣费事件,更新 Credit 余额与流水。
  7. 旧 token 累加链路、旧 token 商店链路和旧 /token-store 接口本轮直接删除,不保留兼容层。

2.2 关键原则

  1. 统一出口:所有 LLM 计费只在 LLM 服务出口发生,不允许业务层各自重复扣费。
  2. 强制入参:不能只靠 Metadata 约定传 user_id,必须改 RPC/函数签名,让漏传在编译期报错。
  3. 准入同步、结算异步:调用模型前同步做 CreditBalanceGuard,拿到最终 usage 后通过 outbox 异步结算。
  4. 旧商店直删:旧 token 商店前后端都未真正接线,本轮不做 token/credit 双轨并存。
  5. 调用面兼容优先LLM RPC 可以新增内部协议,但 backend/client/llm 对业务侧暴露的调用方式应尽量贴近当前 services/llm,把迁移成本压到最低。

3. 为什么要这样改

3.1 当前最大问题

  1. services/llm 名字像服务,实际上不是服务,而是多个进程直接 new 的本地组件。
  2. 计费边界分散,谁发起调用谁就得自己记账,很容易遗漏。
  3. 当前只有旧 token_usage 累加,没有人民币成本与 Credit 扣费真相。
  4. userauth 当前承担的是 token quota 门禁,不是 Credit 余额门禁。
  5. 旧 token 商店前后端都没有真正接上,因此没有保留兼容层的价值。

3.2 这次改完后的好处

  1. LLM 调用入口统一。
  2. usage 采集统一。
  3. 余额准入统一。
  4. 扣费事件发布统一。
  5. TokenStore 只做 Credit 权威账本,不再和模型调用散乱耦合。

4. 三步走总览

第一步:把 LLM 变成真正独立服务

目标:

  1. 所有模型调用统一改走 LLM RPC
  2. LLM 服务拥有自己的 CreditBalanceGuard
  3. LLM 服务拥有自己的 outbox 表

第二步:把 TokenStore 改造成 Credit 权威服务

目标:

  1. 所有余额、流水、商品、订单都切成 Credit 语义
  2. 异步消费 credit.charge.requested
  3. 提供“查看自己流水”的正式接口

第三步:切 Gateway 与前端,并删旧链路

目标:

  1. 上线 /credit-store/*
  2. 商店页接 Credit 真接口与流水列表
  3. 删掉旧 token 累加和旧 token 商店链路

5. 第一步:把 LLM 变成真正独立服务

5.1 这一步的目标

把当前“各进程内直接持有 LLM 组件”的模式,改成“统一调 LLM 服务”。

5.2 新增/改造内容

5.2.1 新增独立 LLM 进程与 RPC

新增:

  1. backend/cmd/llm/main.go
  2. backend/client/llm/client.go
  3. backend/services/llm/rpc/pb/llm.proto
  4. backend/services/llm/rpc/handler.go
  5. backend/services/llm/rpc/server.go
  6. backend/services/llm/dao/connect.go

说明:

  1. 这样 services/llm 放在 services 目录下才真正名副其实。
  2. 业务服务以后全部只依赖 client/llm,不再直接 import services/llm 的本地组件。

5.2.2 统一 RPC 能力,但业务侧调用面尽量保持原状

服务间 LLM RPC 至少提供三类底层能力:

  1. GenerateText
  2. StreamText
  3. GenerateResponsesText

但是对业务代码,不建议直接散落调用底层 RPC 方法名,而是由 backend/client/llm 继续暴露与当前本地 llm client 基本一致的门面:

  1. (*Client).GenerateText(ctx, messages, options)
  2. GenerateJSON[T](ctx, client, messages, options)
  3. (*Client).Stream(ctx, messages, options)
  4. (*ArkResponsesClient).GenerateText(ctx, messages, options)
  5. GenerateArkResponsesJSON[T](ctx, client, messages, options)

迁移目标:

  1. 业务层尽量只替换 client 来源,不重写 prompt 组织方式。
  2. GenerateOptionsArkResponsesOptions 现有字段语义尽量不变。
  3. generateJson / stream / responses json 这些现有能力名和使用习惯尽量延续,避免把改造面扩大成“全链路重写调用协议”。

使用分工:

  1. GenerateTextmemory、active-scheduler、agent 非流式调用
  2. StreamTextagent chat / plan / execute / deliver
  3. GenerateResponsesTextcourse 图片解析

5.2.3 统一 BillingContext但不要污染现有 GenerateOptions

所有请求都必须携带:

type BillingContext struct {
    UserID         int
    EventID        string
    Scene          string
    RequestID      string
    ConversationID string
    ModelAlias     string
    SkipCharge     bool
}

要求:

  1. UserID 必须明确,普通用户态调用禁止省略。
  2. EventID 必须稳定,作为幂等扣费键。
  3. Scene 必须明确,方便价格表与审计。
  4. SkipCharge 只允许系统内部场景显式声明。
  5. BillingContext 不再塞进 GenerateOptions.Metadata 里,避免继续靠弱约定传关键字段。
  6. GenerateOptionsArkResponsesOptions 继续只承载模型行为参数,例如 Temperature / MaxTokens / Thinking

落地建议:

  1. client/llm 对调用方继续保留现有 GenerateText / GenerateJSON / Stream / Responses 风格方法。
  2. 计费必填信息通过独立的调用包装层传入,例如“绑定过 BillingContext 的 client”或“显式 request 参数对象”,但要由 client/llm 吸收复杂度。
  3. 调用方改造目标应控制在“补一处 BillingContext 组装 + 替换 client 初始化来源”,而不是每个业务点重写整段调用代码。

5.2.4 LLM 服务自己的 CreditBalanceGuard

这一步必须同时完成,否则独立 LLM 服务没有准入价值。

Guard 设计要求:

  1. 模型真正发起前同步执行。
  2. 不直接查 MySQL 真相表,必须走 Redis 快照优先。
  3. 缓存 miss 或过期时,再回源 TokenStore RPC

建议缓存 key

  1. smartflow:credit_balance_snapshot:{user_id}
  2. smartflow:credit_balance_blocked:{user_id}

执行顺序:

  1. 先查 blocked
  2. 再查 snapshot
  3. miss 时调用 TokenStore 获取余额快照
  4. 若余额不足,写短 TTL 的 blocked

5.2.5 LLM 服务自己的 outbox

这一步也必须同时完成。

新增:

  1. llm_outbox_messages

并在 outbox 目录注册:

  1. ServiceLLM
  2. ServiceNameLLM
  3. smartflow.llm.outbox
  4. smartflow-llm-outbox-consumer

涉及文件:

  1. backend/shared/infra/outbox/service_catalog.go
  2. backend/shared/infra/outbox/service_route.go
  3. backend/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 事件,不同步扣费

新增事件:

  1. credit.charge.requested

事件载荷至少包含:

  1. event_id
  2. user_id
  3. scene
  4. request_id
  5. conversation_id
  6. provider_name
  7. model_name
  8. input_tokens
  9. output_tokens
  10. cached_tokens
  11. reasoning_tokens
  12. rmb_cost_micros
  13. credit_cost
  14. triggered_at

时机:

  1. 拿到最终 usage
  2. 算出本次 credit_cost
  3. 写入 llm_outbox_messages
  4. 结果即可返回调用方

5.3 这一步涉及哪些调用方改造

以下服务都要从“本地 import services/llm”改成“调用 client/llm”:

  1. agent
  2. memory
  3. active-scheduler
  4. course

6. 第二步:把 TokenStore 改造成 Credit 权威服务

6.1 这一步的目标

TokenStore 不再只是“充值/奖励入账中心”,而是:

  1. Credit 余额真相
  2. Credit 商品与订单中心
  3. Credit 流水中心
  4. LLM 扣费事件的最终结算方

6.2 新的 Credit 数据表

本轮不复用旧 token_* 表,直接建立纯 Credit 语义的新表:

  1. credit_products
  2. credit_orders
  3. credit_accounts
  4. credit_ledger
  5. credit_price_rules
  6. credit_reward_rules

6.2.1 credit_products

用途:商店展示的 Credit 商品

核心字段:

  1. sku
  2. name
  3. description
  4. credit_amount
  5. price_cent
  6. currency
  7. badge
  8. status
  9. sort_order

6.2.2 credit_orders

用途:购买订单与 mock paid 状态机

核心字段:

  1. order_no
  2. user_id
  3. product_id
  4. product_snapshot_json
  5. quantity
  6. credit_amount
  7. amount_cent
  8. status
  9. payment_mode
  10. idempotency_key
  11. paid_at
  12. credited_at

6.2.3 credit_accounts

用途:余额真相与门禁回源表

核心字段:

  1. user_id
  2. balance
  3. total_recharged
  4. total_rewarded
  5. total_consumed
  6. version

6.2.4 credit_ledger

用途:统一正负流水,也是“查看自己流水”的唯一真相源

核心字段:

  1. event_id
  2. user_id
  3. direction
  4. source
  5. scene
  6. description
  7. credit_delta
  8. balance_after
  9. input_tokens
  10. output_tokens
  11. cached_tokens
  12. reasoning_tokens
  13. rmb_cost_micros
  14. created_at

6.2.5 credit_price_rules

用途:模型价格表

核心字段:

  1. provider_name
  2. model_name
  3. scene
  4. input_price_micros_per_1k
  5. output_price_micros_per_1k
  6. cached_price_micros_per_1k
  7. reasoning_price_micros_per_1k
  8. credits_per_yuan
  9. status

6.2.6 credit_reward_rules

用途:社区奖励规则

核心字段:

  1. source
  2. name
  3. credit_amount
  4. status
  5. config_json

6.3 TokenStore 如何消费 charge 事件

新增消费逻辑:

  1. 订阅 credit.charge.requested
  2. 幂等校验 event_id
  3. 在事务中写 credit_ledger
  4. 扣减 credit_accounts.balance
  5. 刷新或删除 Redis 余额快照

6.4 用户查看自己流水接口

这是本轮必须落的能力。

对外新增:

  1. ListCreditTransactions

要求:

  1. 明确用于“当前登录用户查看自己的 Credit 流水”
  2. 必须覆盖购买入账、社区奖励入账、LLM 消费扣费三类记录

HTTP 接口:

  1. GET /api/v1/credit-store/transactions

必须满足:

  1. 只返回当前登录用户自己的 Credit 流水
  2. 支持分页:pagepage_size
  3. 支持可选筛选:directionsourcescene
  4. 返回字段至少包含:
    • transaction_id
    • event_id
    • direction
    • source
    • scene
    • description
    • credit_delta
    • balance_after
    • created_at

6.5 这一步要删除什么旧东西

直接删除:

  1. token_products
  2. token_orders
  3. token_grants
  4. token_reward_rules

7. 第三步:切 Gateway/前端并删旧链路

7.1 这一步的目标

  1. 上线 /credit-store/*
  2. 商店页接 Credit 真接口
  3. 删掉旧 token 累加和旧 token 商店链路

7.2 Gateway 切换

只保留 Credit 语义路由:

  1. GET /api/v1/credit-store/summary
  2. GET /api/v1/credit-store/products
  3. POST /api/v1/credit-store/orders
  4. POST /api/v1/credit-store/orders/:order_id/mock-paid
  5. GET /api/v1/credit-store/transactions

直接删除:

  1. /token-store/*
  2. backend/gateway/api/tokenstoreapi/*

7.3 前端商店页切换

商店页必须改成:

  1. /credit-store/*
  2. 新增“我的 Credit 流水”展示区
  3. 流水数据源只允许来自 GET /api/v1/credit-store/transactions
  4. 至少展示:
    • 流水类型
    • 流水说明
    • Credit 增减值
    • 流水发生时间
    • 可选余额快照

7.4 旧 token 累加机制下线

直接删除:

  1. backend/services/runtime/eventsvc/chat_token_usage_adjust.go
  2. backend/services/runtime/model/agent.goChatTokenUsageAdjustPayload
  3. backend/services/agent/sv/agent_graph.go 中旧 token 调整调用
  4. backend/services/agent/sv/agent_meta.go 中旧 token 调整调用
  5. backend/services/runtime/dao/agent.gotokens_total 累加写路径
  6. backend/gateway/middleware/token_quota_guard.go 在聊天主链的使用
  7. userauthCheckTokenQuota / AdjustTokenUsage 的计费用途
  8. agent_chats.tokens_total 字段

8. 这三步分别改哪些地方

第一步改动面

  1. backend/services/llm/*
  2. backend/client/llm/*
  3. backend/cmd/llm/*
  4. backend/shared/infra/outbox/service_catalog.go
  5. backend/shared/infra/outbox/service_route.go
  6. agent / memory / active-scheduler / course 的所有本地 LLM 调用点

第二步改动面

  1. backend/services/tokenstore/*
  2. backend/client/tokenstore/*
  3. TokenStore 的 outbox consumer
  4. Redis 余额快照与失效逻辑

第三步改动面

  1. backend/gateway/*
  2. frontend/src/views/StoreView.vue
  3. 可能新增商店流水列表组件
  4. runtime/userauth 旧 token 累加链删除

9. 验证清单

9.1 第一步验证

  1. 业务服务代码里不再直接 import services/llm
  2. 所有模型调用都改经 LLM RPC
  3. 所有请求都必须带 BillingContext
  4. CreditBalanceGuard 命中缓存、miss 回源、余额不足封禁都通过测试
  5. llm_outbox_messages 能正常写入 credit.charge.requested

9.2 第二步验证

  1. TokenStore 能幂等消费 credit.charge.requested
  2. credit_accounts 余额更新正确
  3. credit_ledger 流水写入正确
  4. ListCreditTransactions 返回当前用户自己的流水

9.3 第三步验证

  1. 商店页能展示 Credit 概览
  2. 商店页能展示商品
  3. 商店页能展示“我的 Credit 流水”
  4. 全文搜索确认以下旧链路不再出现在主路径:
    • PublishChatTokenUsageAdjustRequested
    • AdjustTokenUsage(
    • CheckTokenQuota(
    • TokenQuotaGuard
    • tokens_total +
    • tokenstoreapi
    • /token-store
    • token_products
    • token_orders
    • token_grants
    • token_reward_rules

10. 风险与取舍

10.1 这次改造面确实更大

因为这次不是只改计费,而是顺带把 LLM 从“共享组件”升级成真正独立服务,所以会涉及:

  1. 新 RPC
  2. 新 client
  3. 新 outbox
  4. 业务服务调用方式切换

10.2 但长期结构更对

收益是:

  1. LLM 调用入口统一
  2. usage 采集统一
  3. 准入 guard 统一
  4. 计费事件发布统一
  5. TokenStore 只做账本,不再和模型调用散乱耦合

11. 服务影响总表

必改服务

  1. backend/services/llm
  2. backend/services/tokenstore
  3. backend/gateway
  4. backend/services/agent
  5. backend/services/course
  6. backend/services/memory
  7. backend/services/active_scheduler

新增

  1. backend/cmd/llm
  2. backend/client/llm
  3. backend/services/llm/rpc/*
  4. llm_outbox_messages

后续前端接入

  1. frontend/src/views/StoreView.vue
  2. 若商店页拆组件,则补充独立的 Credit 流水列表组件