Files
taiji-AI-PAD/plans/计费系统问题分析和修复方案.md
T
2026-03-10 06:40:38 +00:00

1552 lines
46 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# mcp-server 计费系统问题分析和修复方案
> **分析日期**: 2026-03-09
> **优先级**: 🔴 高优先级(涉及用户资金安全)
> **影响范围**: 计费模块、配额管理、余额扣款
---
## 📊 执行摘要
mcp-server 实现了**两种独立的计费模式**:
1. **Agent 运行时长计费** - 基于 Pod 运行时间和资源使用
2. **模型 Token 计费** - 基于 LiteLLM 模型调用的 Token 消耗
经过深入分析,发现了 **8 个严重问题**,其中包括:
- 🔥 潜在的重复扣款风险
- 💥 配额释放与 Pod 删除顺序错误
- 🔒 并发操作缺少保护
- 💾 数据双重存储不一致
---
## 🏗️ 计费架构概览
### 系统架构图
```
┌─────────────────────────────────────────────────────────────┐
│ 用户操作 │
├─────────────────┬────────────────────────┬──────────────────┤
│ 创建 Agent │ Agent 调用模型 │ 删除 Agent │
└────────┬────────┴────────────┬───────────┴─────────┬────────┘
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ Agent Manager │ │ LiteLLM │ │ Agent Manager │
│ 部署 Pod │ │ 调用模型 │ │ 删除 Pod │
└────────┬───────┘ └────────┬───────┘ └────────┬────────┘
│ │ │
▼ ▼ ▼
┌────────────────────────────────────────────────────────────┐
│ mcp-server 计费模块 │
├────────────────┬────────────────────────┬──────────────────┤
│ 创建计费记录 │ 接收 LiteLLM 回调 │ 结束计费记录 │
│ AgentBilling │ ModelBillingRecord │ 更新 end_time │
│ Record │ 实时扣款 Token │ 扣款+释放配额 │
└────────┬───────┴────────────┬───────────┴─────────┬────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ 周期性计费任务(每小时) │
│ periodic_billing.py - 扫描运行中的 Agent │
│ 计算增量成本并扣款 (cost_increment) │
└────────┬────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Balance 表(用户余额) │
│ eu_balance - 统一扣款来源 │
└─────────────────────────────────────────────────────────────┘
```
### 数据模型
#### 1. AgentBillingRecord(Agent 计费记录)
| 字段 | 类型 | 说明 |
|------|------|------|
| `user_id` | GUID | 用户ID |
| `agent_name` | String | Agent 名称 |
| `is_platform_agent` | Boolean | 是否为平台 Agent |
| `start_time` | DateTime | 启动时间 |
| `end_time` | DateTime | 停止时间(NULL = 运行中) |
| `duration_seconds` | Integer | 运行时长(秒) |
| `cpu_used` | String | CPU 配置(如 "100m") |
| `memory_used` | String | 内存配置(如 "256Mi") |
| `cost` | Numeric | 成本金额 |
| `eu_consumed` | Integer | EU 消耗量 |
#### 2. ModelBillingRecord(模型计费记录)
| 字段 | 类型 | 说明 |
|------|------|------|
| `tenant_id` | GUID | 租户ID |
| `litellm_call_id` | String | LiteLLM 调用ID(唯一) |
| `model_name` | String | 模型名称 |
| `input_tokens` | Integer | 输入 Token 数 |
| `output_tokens` | Integer | 输出 Token 数 |
| `total_tokens` | Integer | 总 Token 数 |
| `total_cost` | Numeric | 总成本 |
| `eu_consumed` | Numeric | EU 消耗量 |
#### 3. Balance(用户余额)
| 字段 | 类型 | 说明 |
|------|------|------|
| `user_id` | GUID | 用户ID(唯一) |
| `eu_balance` | Numeric(15,2) | EU 余额 |
---
## 🔴 严重问题清单
### 问题 1:双重计费风险 ⚠️⚠️⚠️
**严重程度**: 🔴 高
**影响**: 用户可能为同一服务支付两次费用
#### 问题描述
当用户启动 Agent 并通过 Agent 调用模型时,系统会从**同一个余额账户**扣款两次:
```
用户启动 Agent(平台/自定义)
├─> Agent Pod 运行 → AgentBillingRecord 按时长计费 ($1.00/小时)
│ └─> 每小时扣款:periodic_billing.py
│
└─> Agent 调用 LiteLLM 模型 → ModelBillingRecord 按 Token 计费 ($0.02/1K tokens)
└─> 实时扣款:billing_webhook.py (LiteLLM 回调)
结果:用户余额被扣款两次 ❌
```
#### 代码位置
1. **Agent 运行时长计费**
- 文件:`services/mcp-server/app/routes/user.py`
- 行号:L3219-L3232
```python
billing_record = AgentBillingRecord(
user_id=user_id,
agent_name=req.name,
start_time=datetime.utcnow(), # 开始计时
cpu_used=req.cpuRequest,
memory_used=req.memoryRequest,
)
```
2. **周期性扣款**
- 文件:`services/mcp-server/app/periodic_billing.py`
- 行号:L180-L185
```python
cost_increment = cost - previous_cost
success, message = await deduct_balance(
user_id, cost_increment, db,
f"Agent周期计费: {record.agent_name}"
)
```
3. **模型调用扣款**
- 文件:`services/mcp-server/app/routes/billing_webhook.py`
- 行号:L281-L284
```python
old_balance = Decimal(str(balance.eu_balance))
new_balance = old_balance - Decimal(str(eu_consumed))
balance.eu_balance = float(new_balance) # 再次扣款 ❌
```
#### 实际影响评估
**如果是设计预期(类似 AWS 双重计费)**:
- ✅ 合理的计费模式(基础设施费用 + 使用费用)
- ❌ 缺少文档说明
- ❌ 前端未分开展示两种费用
**如果不是设计预期**:
- ❌ 用户被过度收费
- ❌ 可能导致法律纠纷
#### 修复建议
**方案 A:明确双重计费机制**(推荐)
```markdown
1. 在用户文档中明确说明:
- Agent 基础设施费用:按 CPU/内存/时长计费
- 模型调用费用:按 Token 消耗计费
- 总费用 = 基础设施费用 + 模型调用费用
2. 前端展示优化:
- 计费明细页面分开显示两种费用
- 实时显示:当前 Agent 运行成本 + 模型调用成本
```
**方案 B:合并计费**
```python
# 只对实际使用的服务计费
# 如果 Agent 调用了模型 → 只按 Token 计费
# 如果 Agent 未调用模型 → 只按时长计费
```
---
### 问题 2:周期计费和回调扣款重复 🔥🔥
**严重程度**: 🔴 高
**影响**: 并发场景下可能重复扣款
#### 问题描述
`periodic_billing.py`(周期任务)和 `billing_webhook.py`(Agent Manager 回调)可能**同时**更新同一条 `AgentBillingRecord`,导致重复扣款。
#### 竞争条件示例
```
时间线:正常流程
00:00 - Agent 启动,创建 billing_record (cost=0)
00:30 - 周期任务运行,计算 cost=30, 扣款 30
00:35 - Agent Manager 回调,计算 cost=35, 扣款 35-30=5 ✅
01:00 - 周期任务再次运行,计算 cost=60, 扣款 60-35=25 ✅
总扣款:30 + 5 + 25 = 60 ✅
时间线:竞争条件
00:30:00 - 周期任务开始执行
00:30:01 - 周期任务读取 record.cost = 0
00:30:02 - 周期任务计算 cost_increment = 30 - 0 = 30
00:30:03 - Agent Manager 回调开始执行
00:30:04 - 回调读取 record.cost = 0 (周期任务还未 commit)
00:30:05 - 回调计算 cost_increment = 30 - 0 = 30
00:30:06 - 周期任务执行 deduct_balance(30)
00:30:07 - 回调执行 deduct_balance(30)
00:30:08 - 周期任务 commit (record.cost = 30)
00:30:09 - 回调 commit (record.cost = 30) ❌ 覆盖
总扣款:30 + 30 = 60 ❌(实际应该扣 30)
```
#### 根本原因
1. **没有行锁保护**
- 周期任务和回调都直接查询 `AgentBillingRecord`
- 没有使用 `with_for_update()` 行锁
2. **更新操作不是原子的**
- 读取 `record.cost` → 计算增量 → 扣款 → 更新 `record.cost`
- 中间有多步操作,可能被打断
3. **没有分布式锁**
- 周期任务和回调可能在不同进程/线程中执行
- PostgreSQL 行锁只能保护同一数据库连接内的事务
#### 代码位置
1. **周期计费**
- 文件:`services/mcp-server/app/periodic_billing.py`
- 行号:L122-L200
```python
result = await db.execute(
select(AgentBillingRecord).where(
AgentBillingRecord.end_time == None
)
# ❌ 缺少 .with_for_update()
)
```
2. **Agent Manager 回调**
- 文件:`services/mcp-server/app/routes/billing_webhook.py`
- 行号:L454-L490
```python
existing_result = await db.execute(
select(AgentBillingRecord).where(
and_(
AgentBillingRecord.agent_name == callback_data.agentName,
AgentBillingRecord.user_id == callback_data.userId,
AgentBillingRecord.end_time == None
)
)
# ❌ 缺少 .with_for_update()
)
```
#### 修复建议
**方案 A:只用周期计费,回调只更新记录**(推荐)
```python
# periodic_billing.py - 保持不变,继续扣款
# billing_webhook.py - 只更新记录,不扣款
if existing_record:
existing_record.end_time = end_time
existing_record.duration_seconds = duration_seconds
existing_record.cost = new_cost # 只更新成本,不扣款
existing_record.tools_used = callback_data.toolsUsed
existing_record.request_id = callback_data.requestId
# ❌ 删除这里的 deduct_balance 调用
```
**方案 B:只在回调时扣款,周期任务只更新记录**
```python
# periodic_billing.py - 只更新记录,不扣款
record.duration_seconds = duration_seconds
record.eu_consumed = eu_consumed
record.cost = float(cost)
# ❌ 删除 deduct_balance 调用
# billing_webhook.py - 保持不变,继续扣款
```
**方案 C:使用分布式锁(Redis)**
```python
import redis
async def update_agent_billing_with_lock(agent_name, user_id):
lock_key = f"billing_lock:{user_id}:{agent_name}"
lock = redis.lock(lock_key, timeout=30)
if lock.acquire(blocking=True):
try:
# 获取锁后执行扣款操作
...
finally:
lock.release()
```
---
### 问题 3:配额释放与 Pod 删除顺序错误 💥
**严重程度**: 🔴 高
**影响**: Pod 删除失败时,用户可以免费使用资源
#### 问题描述
删除 Agent 时,系统先在数据库中标记 Agent 已停止并释放配额,**然后**才调用 Agent Manager 删除 Pod。如果删除 Pod 失败:
- 数据库显示 Agent 已停止
- 配额已释放
- Pod 仍在 K8s 中运行
- 周期计费找不到对应的计费记录(`end_time != None`)
**结果:用户免费使用资源** ❌
#### 错误流程
```python
# 当前实现(错误的顺序)
# 文件:services/mcp-server/app/routes/user.py
# 行号:L3330-L3390
# 1. 标记结束
billing_record.end_time = datetime.utcnow()
# 2. 计算成本并扣款
cost = calculate_agent_cost_by_resources(cpu_cores, memory_gb, duration)
billing_record.cost = float(cost)
success, message = await deduct_balance(user_id, cost, db)
# 3. 释放配额
quota.cpu_used = max(0, float(quota.cpu_used or 0) - cpu_released)
quota.memory_used = max(0, float(quota.memory_used or 0) - memory_released)
quota.agent_count = max(0, (quota.agent_count or 0) - 1)
# 4. 提交数据库
await db.commit()
# 5. 删除 Pod(可能失败!)❌
await client.delete_agent(agent_full_name)
```
#### 失败场景分析
```
场景:Agent Manager 服务宕机或网络问题
步骤 1-4:✅ 成功执行
- billing_record.end_time = "2026-03-09 10:00:00"
- quota.cpu_used 从 5 降到 3
- 数据库已提交
步骤 5:❌ 失败
- client.delete_agent() 抛出异常
- Pod 仍在 K8s 中运行
后续影响:
- 周期计费任务扫描运行中的 Agent
- 查询条件:end_time == None
- 找不到该 Agent 的计费记录(因为 end_time 已设置)
- Agent 继续运行但不再计费 ❌
- 用户可以免费使用 CPU 和内存资源
```
#### 代码位置
文件:`services/mcp-server/app/routes/user.py`
行号:L3313-L3402
```python
@router.delete("/custom-agents/{name}", response_model=SuccessResponse)
async def delete_custom_agent(
name: str,
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""删除自定义 Agent"""
user_id = principal.get("user_id")
# 查找计费记录
billing_result = await db.execute(
select(AgentBillingRecord).where(...)
)
billing_record = billing_result.scalar_one_or_none()
try:
# ❌ 错误:先更新数据库
billing_record.end_time = datetime.utcnow()
await deduct_balance(user_id, cost, db, ...)
quota.cpu_used -= cpu_released
await db.commit() # ❌ 提交后无法回滚
# ❌ 然后才删除 Pod(可能失败)
await client.delete_agent(agent_full_name)
except AgentManagerError as e:
# ❌ 此时数据库已提交,无法回滚
raise HTTPException(status_code=e.status_code, ...)
```
#### 修复建议
**正确顺序**:
```python
@router.delete("/custom-agents/{name}", response_model=SuccessResponse)
async def delete_custom_agent(
name: str,
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""删除自定义 Agent"""
user_id = principal.get("user_id")
# 1. 先查找计费记录
billing_result = await db.execute(
select(AgentBillingRecord).where(...)
)
billing_record = billing_result.scalar_one_or_none()
if not billing_record:
raise HTTPException(404, "Agent 不存在")
# 2. ✅ 先删除 Pod(如果失败,整个操作终止)
try:
await client.delete_agent(agent_full_name)
except AgentManagerError as e:
logger.error(f"删除 Pod 失败: {e.message}")
raise HTTPException(
status_code=e.status_code,
detail=f"删除 Agent 失败: {e.message}"
)
# 3. ✅ Pod 删除成功后,再更新数据库
billing_record.end_time = datetime.utcnow()
duration = (billing_record.end_time - billing_record.start_time).total_seconds()
billing_record.duration_seconds = int(duration)
# 4. 计算成本并扣款
cost = calculate_agent_cost_by_resources(cpu_cores, memory_gb, int(duration))
billing_record.cost = float(cost)
success, message = await deduct_balance(
user_id, cost, db,
f"自定义Agent使用: {name} (运行{int(duration)}秒)"
)
if not success:
logger.warning(f"扣款失败: {message}")
# 5. 释放配额
quota.cpu_used = max(0, float(quota.cpu_used or 0) - cpu_released)
quota.memory_used = max(0, float(quota.memory_used or 0) - memory_released)
quota.agent_count = max(0, (quota.agent_count or 0) - 1)
# 6. 提交数据库
await db.commit()
return SuccessResponse(
message=f"自定义 Agent {name} 已删除",
data={"quotaReleased": {"cpu": cpu_released, "memory": memory_released}}
)
```
---
### 问题 4:配额更新缺少并发保护 🔒
**严重程度**: 🟡 中
**影响**: 并发操作时配额计算可能出错
#### 问题描述
创建 Agent 时使用了行锁(`with_for_update()`),但删除 Agent 时没有使用行锁,导致并发删除时可能出现配额计算错误。
#### 代码对比
**✅ 创建 Agent 时(有行锁)**
文件:`services/mcp-server/app/routes/user.py`
行号:L3148-L3155
```python
# ✅ 有行锁保护
quota_result = await db.execute(
select(TenantCustomAgentQuota)
.where(TenantCustomAgentQuota.tenant_id == user_id)
.with_for_update() # ✅ 行锁
)
quota = quota_result.scalar_one_or_none()
# 安全的更新操作
quota.cpu_used = cpu_used + cpu_request
quota.memory_used = memory_used + memory_request
quota.agent_count = (quota.agent_count or 0) + 1
```
**❌ 删除 Agent 时(无行锁)**
文件:`services/mcp-server/app/routes/user.py`
行号:L3358-L3368
```python
# ❌ 没有行锁
quota_result = await db.execute(
select(TenantCustomAgentQuota).where(
TenantCustomAgentQuota.tenant_id == user_id
)
# ❌ 缺少 .with_for_update()
)
quota = quota_result.scalar_one_or_none()
# 不安全的更新操作
quota.cpu_used = max(0, float(quota.cpu_used or 0) - cpu_released)
quota.memory_used = max(0, float(quota.memory_used or 0) - memory_released)
quota.agent_count = max(0, (quota.agent_count or 0) - 1)
```
#### 并发问题示例
```
初始状态:quota.cpu_used = 5
线程 1:删除 Agent A(释放 2 CPU)
1. 读取 cpu_used = 5
2. 计算 new_value = 5 - 2 = 3
线程 2:删除 Agent B(释放 2 CPU)
1. 读取 cpu_used = 5(线程1还未 commit)
2. 计算 new_value = 5 - 2 = 3
线程 1:写入 cpu_used = 3
线程 2:写入 cpu_used = 3 ❌
最终结果:cpu_used = 3
正确结果:cpu_used = 1 (5 - 2 - 2)
丢失更新:2 CPU
```
#### 修复建议
```python
# 删除 Agent 时也使用行锁
quota_result = await db.execute(
select(TenantCustomAgentQuota)
.where(TenantCustomAgentQuota.tenant_id == user_id)
.with_for_update() # ✅ 添加行锁
)
quota = quota_result.scalar_one_or_none()
if quota:
quota.cpu_used = max(0, float(quota.cpu_used or 0) - cpu_released)
quota.memory_used = max(0, float(quota.memory_used or 0) - memory_released)
quota.agent_count = max(0, (quota.agent_count or 0) - 1)
```
---
### 问题 5:余额数据双重存储 💾
**严重程度**: 🟡 中
**影响**: 数据不一致,可能显示错误余额
#### 问题描述
系统中同时存在两个余额字段:
- `User.eu_balance` - 标记为废弃但未删除
- `Balance.eu_balance` - 当前使用的字段
代码只更新 `Balance` 表,导致 `User.eu_balance` 永远是旧数据。
#### 代码位置
文件:`services/mcp-server/models.py`
行号:L85-L88, L518-L527
```python
class User(BaseModel, Base):
__tablename__ = "users"
credit_limit = Column(sa.Numeric(12, 2), default=0) # 授信额度
# ❌ 废弃但未删除
eu_balance = Column(sa.Numeric(15, 2), default=0)
# 注释:[DEPRECATED - 使用 Balance 表]
balance = Column(sa.Numeric(12, 2), default=0) # [DEPRECATED]
class Balance(BaseModel, Base):
"""用户余额模型(当前使用)"""
__tablename__ = "balances"
user_id = Column(GUID(), ForeignKey("users.id"), nullable=False, unique=True)
eu_balance = Column(sa.Numeric(15, 2), nullable=False, default=0)
```
#### 潜在问题
1. **数据不一致**
```python
# 所有代码都更新 Balance 表
balance.eu_balance = new_balance
# 但 User.eu_balance 不会更新
# 如果有代码查询 User.eu_balance → 显示错误余额
```
2. **存储浪费**
- `User.eu_balance` 占用空间但不使用
- 每个用户浪费 15 bytes
3. **维护困难**
- 新开发者可能不知道哪个字段是最新的
- 可能误用废弃字段
#### 修复建议
**步骤 1:确认没有代码使用 `User.eu_balance`**
```bash
# 搜索是否有代码读取该字段
cd services/mcp-server
grep -r "user\.eu_balance" .
grep -r "User\.eu_balance" .
```
**步骤 2:创建数据库迁移脚本**
```python
# migrations/remove_deprecated_balance_fields.py
from alembic import op
import sqlalchemy as sa
def upgrade():
# 删除废弃字段
op.drop_column('users', 'eu_balance')
op.drop_column('users', 'balance')
def downgrade():
# 回滚(如果需要)
op.add_column('users',
sa.Column('eu_balance', sa.Numeric(15, 2), default=0))
op.add_column('users',
sa.Column('balance', sa.Numeric(12, 2), default=0))
```
**步骤 3:更新模型**
```python
class User(BaseModel, Base):
__tablename__ = "users"
credit_limit = Column(sa.Numeric(12, 2), default=0) # 授信额度
# ✅ 删除废弃字段
```
---
### 问题 6:计费记录 start_time 可能为空 🕐
**严重程度**: 🟡 中
**影响**: 部分 Agent 可能不计费
#### 问题描述
周期计费任务会跳过 `start_time` 为 `None` 的记录,导致这些 Agent 免费运行。
#### 代码位置
文件:`services/mcp-server/app/periodic_billing.py`
行号:L132-L138
```python
for record in running_agents:
try:
# 跳过没有开始时间的记录
if record.start_time is None:
logger.warning(f"Agent {record.agent_name} 缺少 start_time,跳过计费")
stats["failed"] += 1
stats["errors"].append(f"Agent {record.agent_name}: start_time 为空")
continue # ❌ 跳过计费 → Agent 免费运行
```
#### 根本原因
创建 `AgentBillingRecord` 时可能没有设置 `start_time`。
#### 已检查的代码位置
✅ 以下位置都正确设置了 `start_time`:
- `services/mcp-server/app/routes/user.py:1428` - 平台 Agent
- `services/mcp-server/app/routes/user.py:2483` - 平台 Agent(use接口)
- `services/mcp-server/app/routes/user.py:3232` - 自定义 Agent
⚠️ 潜在问题:
- `billing_webhook.py` 创建兜底记录时可能没有 `start_time`
#### 修复建议
**方案 A:确保所有创建路径都设置 start_time**
```python
# billing_webhook.py - 创建兜底记录时
billing_record = AgentBillingRecord(
user_id=callback_data.userId,
agent_name=callback_data.agentName,
start_time=start_time or datetime.utcnow(), # ✅ 确保有值
end_time=end_time or datetime.utcnow(),
...
)
```
**方案 B:数据库约束**
```python
# models.py
class AgentBillingRecord(BaseModel, Base):
start_time = Column(DateTime, nullable=False) # ✅ 不允许为空
```
**方案 C:周期任务中补救**
```python
# periodic_billing.py
if record.start_time is None:
# 使用创建时间作为开始时间
record.start_time = record.created_at
logger.warning(f"修复 Agent {record.agent_name} 的 start_time")
```
---
### 问题 7:余额不足时的处理不一致 💰
**严重程度**: 🟡 中
**影响**: 用户体验不一致,可能透支
#### 问题描述
不同场景下对余额不足的处理方式不一致:
- 创建 Agent:只记录日志,Agent 继续运行
- 周期计费:只记录警告,继续计费
- 模型调用:只记录警告,不阻止调用
#### 代码位置
**1. 创建计费记录时**
文件:`services/mcp-server/app/billing.py`
行号:L352-L356
```python
success, message = await deduct_balance(tenant_id, cost, db)
if not success:
# ❌ 只是跳过,Agent 继续运行
pass
```
**2. 周期计费时**
文件:`services/mcp-server/app/periodic_billing.py`
行号:L185-L191
```python
success, message = await deduct_balance(
str(record.user_id), cost_increment, db,
f"Agent周期计费: {record.agent_name}"
)
if not success:
logger.warning(
f"周期计费扣款失败: {message}, "
f"用户: {record.user_id}, Agent: {record.agent_name}"
)
# ❌ 只记录警告,不停止 Agent
```
**3. 模型调用时**
文件:`services/mcp-server/app/routes/billing_webhook.py`
行号:L299-L305
```python
if new_balance < 0:
logger.warning(
f"⚠️ 用户余额不足: user_id={tenant_id}, "
f"balance={new_balance:.4f}, "
f"建议充值"
)
# ❌ 只警告,继续记录,允许透支
```
#### 行业最佳实践对比
| 场景 | AWS | 阿里云 | 当前系统 |
|------|-----|--------|----------|
| 创建资源 | ✅ 预检查余额,不足拒绝 | ✅ 预检查余额,不足拒绝 | ❌ 不检查,直接创建 |
| 运行中透支 | ✅ 暂停/停止资源 | ✅ 暂停资源 | ❌ 继续运行 |
| API 调用透支 | ✅ 拒绝请求 | ✅ 拒绝请求 | ❌ 允许透支 |
#### 修复建议
**创建 Agent 前检查余额**
```python
@router.post("/custom-agents/create-with-tools")
async def create_custom_agent_with_tools(...):
# ✅ 预检查余额
estimated_cost = estimate_agent_cost(
cpu=req.cpuRequest,
memory=req.memoryRequest,
duration=3600 # 预估至少运行1小时
)
balance, credit_limit, available = await get_available_balance(user_id, db)
if available < estimated_cost:
raise HTTPException(
status_code=403,
detail=f"余额不足,无法创建 Agent。"
f"预估成本: ${estimated_cost:.2f}, "
f"可用余额: ${available:.2f},"
f"请先充值"
)
# 继续创建 Agent
...
```
**周期计费余额不足时停止 Agent**
```python
# periodic_billing.py
success, message = await deduct_balance(user_id, cost_increment, db)
if not success:
logger.warning(f"周期计费扣款失败: {message}")
# ✅ 检查是否透支
balance, credit_limit, available = await get_available_balance(user_id, db)
if available < 0:
# ✅ 余额不足,停止该用户所有 Agent
logger.warning(f"用户 {user_id} 余额不足,将停止所有 Agent")
stopped = await stop_user_agents(str(record.user_id), db)
stats["stopped_agents"].extend(stopped)
```
**模型调用余额不足时拒绝**
```python
# billing_webhook.py
# 扣款前检查余额
old_balance = Decimal(str(balance.eu_balance))
if old_balance < eu_consumed:
# ✅ 余额不足,不允许透支
logger.warning(f"用户 {tenant_id} 余额不足,拒绝模型调用")
raise HTTPException(
status_code=403,
detail=f"余额不足,无法调用模型。"
f"所需: {eu_consumed} EU, "
f"可用: {old_balance} EU"
)
# 余额充足,继续扣款
new_balance = old_balance - eu_consumed
balance.eu_balance = float(new_balance)
```
---
### 问题 8:配额数据"假用量" 📊 ✅
**严重程度**: 🟡 中
**影响**: 配额显示不准确,用户无法创建 Agent
**修复状态**: ✅ 已完成(2026-03-09)
#### 问题描述
历史原因导致配额表中记录了"假用量":
- 配额显示 `cpu_used=5`
- 但 K8s 中没有对应的 Pod 运行
- 用户无法创建新 Agent(配额不足)
- 实际资源完全空闲
#### 根本原因
来自代码注释(`fix_fake_quota.py`):
```python
"""
修复脚本:清理历史假用量数据
此脚本用于清理因旧接口(/api/user/tools/generate)产生的假用量数据。
旧接口会在数据库中创建Agent记录并扣除配额,但不实际部署到K8s,
导致quota表中记录了用量但实际没有Pod运行。
"""
```
#### 证据
项目中存在 3 个修复脚本:
1. **fix_agent_quotas.py**
- 功能:修复 Agent 资源配置和配额记录
- 说明:配额使用量与实际 Agent 不匹配
2. **fix_fake_quota.py**
- 功能:清理假用量数据
- 说明:统计 `type='custom'` 且 `status='active'` 的真实 Agent
3. **fix_channel_quota_records.py**
- 功能:修复渠道配额记录
- 说明:渠道使用旧格式配额但缺少新表记录
#### 修复建议
**✅ 已实现方案:综合健康检查工具**
创建了 `check_billing_health.py` 提供完整的健康检查功能:
```bash
# 完整健康检查(推荐)
python check_billing_health.py
# 只检查假用量
python check_billing_health.py --check-quota-only
# 只检查余额状态
python check_billing_health.py --check-balance-only
# 测试自动停止功能
python check_billing_health.py --test-auto-stop <user_id>
```
**功能特性**:
- ✅ 自动检测配额不一致(假用量)
- ✅ 识别透支用户、余额不足用户、风险用户
- ✅ 生成健康评分报告(0-100分)
- ✅ 提供自动修复建议
- ✅ 可集成到监控系统(Prometheus/Grafana)
**输出示例**:
```
============================================================
🔍 开始检查配额一致性(假用量检测)
============================================================
✅ 所有租户配额数据一致,无假用量问题
💰 开始检查用户余额状态
📊 余额状态汇总:
总用户数: 150
透支用户: 0
余额不足: 5
风险用户: 2
============================================================
📊 健康检查完成
============================================================
健康评分: 100/100 (HEALTHY)
报告保存: /app/logs/billing_health_report.json
💡 建议:
无需操作,系统健康
```
**修复假用量流程**:
```bash
# 1. 检测问题
python check_billing_health.py --check-quota-only
# 2. 执行修复(会自动备份)
python fix_fake_quota.py
# 3. 验证修复
python check_billing_health.py --check-quota-only
```
**运维文档**:
详细使用说明请参阅:[计费系统运维工具使用指南](../Docs/计费系统运维工具使用指南.md)
---
**原建议方案(归档)**:
**临时方案:定期运行修复脚本** ✅ 已包含在健康检查工具中
```bash
# 每天运行一次修复脚本
0 2 * * * cd /app && python fix_fake_quota.py
```
**长期方案:修复根本原因**
```python
# 废弃旧接口 /api/user/tools/generate
@router.post("/user/tools/generate")
@deprecated(reason="使用新接口 /custom-agents/create-with-tools")
async def generate_tool_agent(...):
raise HTTPException(410, "此接口已废弃,请使用新接口")
```
**预防方案:配额一致性检查**
```python
# 创建后台任务,定期检查配额一致性
async def check_quota_consistency():
"""检查配额与实际 Agent 是否一致"""
async with AsyncSessionLocal() as db:
# 查询所有租户配额
quotas = await db.execute(select(TenantCustomAgentQuota))
for quota in quotas.scalars():
# 统计实际运行的 Agent
real_agents = await db.execute(
select(Agent)
.where(Agent.owner_id == quota.tenant_id)
.where(Agent.type == "custom")
.where(Agent.status == "active")
)
real_cpu = sum(float(a.cpu or 0) for a in real_agents)
real_memory = sum(float(a.memory or 0) for a in real_agents)
# 如果不一致,发送告警
if abs(real_cpu - float(quota.cpu_used)) > 0.1:
logger.warning(
f"配额不一致: 租户={quota.tenant_id}, "
f"配额显示={quota.cpu_used}核, "
f"实际使用={real_cpu}核"
)
```
---
## 📋 修复优先级矩阵
| 问题 | 严重程度 | 紧急程度 | 修复难度 | 优先级 |
|------|---------|---------|---------|--------|
| 问题1: 双重计费风险 | 🔴 高 | 🔴 高 | 🟡 中 | **P0** |
| 问题2: 重复扣款 | 🔴 高 | 🔴 高 | 🔴 高 | **P0** |
| 问题3: 删除顺序错误 | 🔴 高 | 🔴 高 | 🟢 低 | **P0** |
| 问题4: 并发保护缺失 | 🟡 中 | 🟡 中 | 🟢 低 | **P1** |
| 问题5: 数据双重存储 | 🟡 中 | 🟢 低 | 🟡 中 | **P2** |
| 问题6: start_time 为空 | 🟡 中 | 🟡 中 | 🟢 低 | **P1** |
| 问题7: 余额处理不一致 | 🟡 中 | 🟡 中 | 🟡 中 | **P1** |
| 问题8: 假用量数据 | 🟡 中 | 🟢 低 | 🟢 低 | **P2** |
---
## 🎉 修复完成总结
所有 8 个问题已完成修复!系统现在具备:
✅ **核心功能修复**
- 统一计费逻辑,消除重复扣款
- 正确的删除顺序,保证数据一致性
- 完善的并发保护机制
✅ **用户保护机制**
- 创建前余额检查
- 余额不足自动停止 Agent
- 透支保护(授信额度)
✅ **运维工具完善**
- 自动化健康检查工具 `check_billing_health.py`
- 假用量检测(配额一致性检查)
- 余额状态监控(透支/余额不足/风险用户识别)
- 自动停止功能测试
- 健康评分报告生成
- 假用量自动修复工具 `fix_fake_quota.py`
- 配额详情查询工具 `check_quota_data.py`
- 详细的运维文档([计费系统运维工具使用指南](../Docs/计费系统运维工具使用指南.md))
✅ **文档完善**
- 计费机制说明文档
- 运维工具使用指南(包含使用方法、故障排查、最佳实践)
- 故障排查手册
- 健康检查报告模板
---
## 🚀 快速修复清单(归档)
### 第一阶段:核心问题修复(P0)
#### ✅ 修复 1:统一计费扣款逻辑
**目标**:消除重复扣款风险
**方案**:周期计费保留,回调只更新记录
**文件**:`services/mcp-server/app/routes/billing_webhook.py`
**修改**:
```python
# 行号:L454-L490
# 将 billing_webhook.py 中的扣款逻辑改为只更新记录
if existing_record:
# 更新现有记录
previous_cost = Decimal(str(existing_record.cost or 0))
existing_record.end_time = end_time or datetime.utcnow()
existing_record.duration_seconds = duration_seconds
existing_record.eu_consumed = eu_consumed
existing_record.cost = float(new_cost)
existing_record.period_end = end_time or datetime.utcnow()
existing_record.tools_used = callback_data.toolsUsed
existing_record.request_id = callback_data.requestId
billing_record = existing_record
# ❌ 删除这部分:不再在回调时扣款
# cost_increment = new_cost - previous_cost
# if cost_increment > 0:
# success, message = await deduct_balance(...)
logger.info(
f"📝 更新Agent计费记录(不扣款): agent={callback_data.agentName}, "
f"最终成本={new_cost}"
)
```
#### ✅ 修复 2:调整删除 Agent 的顺序
**目标**:确保 Pod 删除成功后再更新数据库
**文件**:`services/mcp-server/app/routes/user.py`
**修改**:
```python
# 行号:L3313-L3402
# 调整 delete_custom_agent 函数的执行顺序
@router.delete("/custom-agents/{name}", response_model=SuccessResponse)
async def delete_custom_agent(...):
# 1. 查找计费记录
billing_record = ...
# 2. ✅ 先删除 Pod
try:
await client.delete_agent(agent_full_name)
except AgentManagerError as e:
raise HTTPException(status_code=e.status_code, detail=str(e))
# 3. ✅ 删除成功后再更新数据库
billing_record.end_time = datetime.utcnow()
await deduct_balance(user_id, cost, db)
quota.cpu_used -= cpu_released
await db.commit()
```
#### ✅ 修复 3:添加并发保护
**目标**:所有配额更新操作都加行锁
**文件**:`services/mcp-server/app/routes/user.py`
**修改**:
```python
# 行号:L3358
# 在删除 Agent 时添加行锁
quota_result = await db.execute(
select(TenantCustomAgentQuota)
.where(TenantCustomAgentQuota.tenant_id == user_id)
.with_for_update() # ✅ 添加行锁
)
```
### 第二阶段:改进和优化(P1)
#### ✅ 修复 4:确保 start_time 不为空
**文件**:`services/mcp-server/app/routes/billing_webhook.py`
**修改**:
```python
# 创建兜底记录时确保 start_time 有值
billing_record = AgentBillingRecord(
...
start_time=start_time or datetime.utcnow(), # ✅ 确保有值
...
)
```
**文件**:`services/mcp-server/models.py`
**修改**:
```python
# 数据库约束:start_time 不允许为空
class AgentBillingRecord(BaseModel, Base):
start_time = Column(DateTime, nullable=False) # ✅ 不允许为空
```
#### ✅ 修复 5:完善余额检查
**文件**:`services/mcp-server/app/routes/user.py`
**修改**:
```python
# 创建 Agent 前检查余额
@router.post("/custom-agents/create-with-tools")
async def create_custom_agent_with_tools(...):
# ✅ 预估成本
estimated_cost = estimate_agent_cost(
cpu=req.cpuRequest,
memory=req.memoryRequest,
duration=3600 # 预估1小时
)
# ✅ 检查余额
balance, credit_limit, available = await get_available_balance(user_id, db)
if available < estimated_cost:
raise HTTPException(403, "余额不足,请先充值")
# 继续创建
...
```
### 第三阶段:清理和文档(P2)
#### ✅ 修复 6:删除废弃字段
**文件**:`services/mcp-server/models.py`
**修改**:
```python
class User(BaseModel, Base):
credit_limit = Column(sa.Numeric(12, 2), default=0)
# ❌ 删除以下废弃字段
# eu_balance = Column(sa.Numeric(15, 2), default=0)
# balance = Column(sa.Numeric(12, 2), default=0)
```
#### ✅ 修复 7:添加文档说明
**新建文件**:`Docs/计费机制说明.md`
**内容**:
```markdown
# 计费机制说明
## 费用组成
Taiji AI PAD 采用双重计费模式:
### 1. Agent 基础设施费用
- 计费对象:Agent Pod 的运行时间和资源配置
- 计费公式:成本 = CPU 用量 × CPU 单价 + 内存用量 × 内存单价 × 运行时长
- 扣款时机:每小时自动扣款(周期性计费任务)
- 示例:1 核 + 1GB 内存运行 1 小时 = $1.00
### 2. 模型调用费用
- 计费对象:通过 LiteLLM 调用的模型 Token 消耗
- 计费公式:成本 = Token 数量 × 模型单价
- 扣款时机:每次调用后实时扣款
- 示例:gpt-4 调用 1000 tokens = $0.02
### 总费用
**总费用 = Agent 基础设施费用 + 模型调用费用**
注意:如果您的 Agent 调用了模型,您需要同时支付两种费用。
```
---
## 🧪 测试计划
### 单元测试
#### 测试 1:重复扣款保护
```python
async def test_no_duplicate_deduction():
"""测试周期计费和回调不会重复扣款"""
# 1. 创建 Agent 计费记录
record = await create_agent_billing_record(...)
# 2. 模拟周期任务扣款
await periodic_billing.update_running_agent_billing(db)
balance_after_periodic = await get_balance(user_id)
# 3. 模拟 Agent Manager 回调
await billing_webhook.agent_callback(callback_data)
balance_after_callback = await get_balance(user_id)
# 4. 验证:两次扣款总和等于实际成本
total_deducted = initial_balance - balance_after_callback
assert total_deducted == expected_cost
assert total_deducted != expected_cost * 2 # 不是双倍
```
#### 测试 2:删除顺序保护
```python
async def test_delete_agent_rollback_on_pod_failure():
"""测试 Pod 删除失败时不会释放配额"""
# 1. 创建 Agent
await create_agent(...)
initial_quota = await get_quota(user_id)
# 2. Mock Agent Manager 删除失败
with mock.patch("client.delete_agent", side_effect=AgentManagerError):
with pytest.raises(HTTPException):
await delete_custom_agent(agent_name)
# 3. 验证:配额没有被释放
current_quota = await get_quota(user_id)
assert current_quota.cpu_used == initial_quota.cpu_used
# 4. 验证:计费记录仍在运行中
record = await get_billing_record(agent_name)
assert record.end_time is None
```
#### 测试 3:并发配额更新
```python
async def test_concurrent_quota_updates():
"""测试并发删除 Agent 时配额计算正确"""
# 1. 创建 2 个 Agent,每个 2 CPU
await create_agent(name="agent1", cpu=2)
await create_agent(name="agent2", cpu=2)
initial_quota = await get_quota(user_id)
assert initial_quota.cpu_used == 4
# 2. 并发删除两个 Agent
await asyncio.gather(
delete_custom_agent("agent1"),
delete_custom_agent("agent2")
)
# 3. 验证:配额正确减少
final_quota = await get_quota(user_id)
assert final_quota.cpu_used == 0 # 不是 2
```
### 集成测试
#### 测试 4:端到端计费流程
```python
async def test_end_to_end_billing():
"""测试完整的计费流程"""
# 1. 充值
await recharge(user_id, amount=100)
initial_balance = await get_balance(user_id)
# 2. 创建 Agent
await create_agent(cpu=1, memory=1)
# 3. 运行 1 小时
await asyncio.sleep(3600)
# 4. Agent 调用模型
await call_litellm_model(tokens=1000)
# 5. 删除 Agent
await delete_agent()
# 6. 验证:总扣款 = Pod 费用 + Token 费用
final_balance = await get_balance(user_id)
total_cost = initial_balance - final_balance
expected_pod_cost = 1.0 # 1核1G运行1小时
expected_token_cost = 0.02 # 1000 tokens
expected_total = expected_pod_cost + expected_token_cost
assert abs(total_cost - expected_total) < 0.01
```
### 压力测试
#### 测试 5:高并发计费
```python
async def test_high_concurrency_billing():
"""测试高并发场景下计费的正确性"""
# 1. 模拟 100 个用户同时创建 Agent
tasks = []
for i in range(100):
tasks.append(create_agent(user_id=f"user_{i}", ...))
await asyncio.gather(*tasks)
# 2. 模拟 1000 次并发模型调用
tasks = []
for i in range(1000):
tasks.append(call_model(user_id=random.choice(users), ...))
await asyncio.gather(*tasks)
# 3. 验证:所有计费记录都已创建
agent_records = await count_agent_billing_records()
model_records = await count_model_billing_records()
assert agent_records == 100
assert model_records == 1000
# 4. 验证:余额计算正确(无重复扣款)
total_balance = await sum_all_balances()
total_cost = await sum_all_costs()
assert abs(total_balance - (initial_total - total_cost)) < 1.0
```
---
## 📈 监控和告警
### 关键指标
#### 1. 扣款失败率
```python
# Prometheus metrics
billing_deduction_failures = Counter(
'billing_deduction_failures_total',
'Total number of billing deduction failures',
['reason']
)
# 告警规则
rate(billing_deduction_failures_total[5m]) > 0.01
```
#### 2. 配额不一致率
```python
quota_inconsistency = Gauge(
'quota_inconsistency_count',
'Number of tenants with quota inconsistency',
['tenant_id']
)
# 告警规则
quota_inconsistency_count > 0
```
#### 3. 计费记录异常
```python
billing_record_anomalies = Counter(
'billing_record_anomalies_total',
'Billing records with anomalies',
['type'] # missing_start_time, negative_cost, etc.
)
# 告警规则
billing_record_anomalies_total{type="missing_start_time"} > 0
```
### 日志监控
#### 关键日志模式
```python
# 1. 重复扣款检测
logger.warning("可能的重复扣款: user_id=%s, agent=%s, cost=%s", ...)
# 2. 配额不一致检测
logger.error("配额不一致: quota.cpu_used=%s, actual=%s", ...)
# 3. 余额不足但继续运行
logger.critical("余额不足但Agent继续运行: user_id=%s, balance=%s", ...)
```
### 告警配置(Prometheus + Alertmanager)
```yaml
groups:
- name: billing_alerts
rules:
# 告警 1:扣款失败率过高
- alert: HighBillingFailureRate
expr: rate(billing_deduction_failures_total[5m]) > 0.01
for: 5m
labels:
severity: critical
annotations:
summary: "扣款失败率过高"
description: "过去5分钟扣款失败率 {{ $value | humanizePercentage }}"
# 告警 2:配额不一致
- alert: QuotaInconsistency
expr: quota_inconsistency_count > 0
for: 1m
labels:
severity: warning
annotations:
summary: "检测到配额不一致"
description: "{{ $value }} 个租户存在配额不一致"
# 告警 3:计费记录异常
- alert: BillingRecordAnomaly
expr: increase(billing_record_anomalies_total[5m]) > 5
for: 1m
labels:
severity: warning
annotations:
summary: "计费记录异常增加"
description: "异常类型: {{ $labels.type }}, 数量: {{ $value }}"
```
---
## 📝 修复进度跟踪
| 修复项 | 优先级 | 状态 | 完成时间 |
|--------|--------|------|----------|
| 1. 统一计费扣款逻辑 | P0 | ✅ 已完成 | 2026-03-09 |
| 2. 调整删除顺序 | P0 | ✅ 已完成 | 2026-03-09 |
| 3. 添加并发保护 | P0 | ✅ 已完成 | 2026-03-09 |
| 4. start_time 非空约束 | P1 | ✅ 已完成 | 2026-03-09 |
| 5. 完善余额检查 | P1 | ✅ 已完成 | 2026-03-09 |
| 6. 删除废弃字段 | P2 | ✅ 已完成 | 2026-03-09 |
| 7. 添加计费文档 | P2 | ✅ 已完成 | 2026-03-09 |
| 8. 配额一致性检查工具 | P2 | ✅ 已完成 | 2026-03-09 |
**附加完成项**:
- ✅ 创建综合健康检查工具 `check_billing_health.py`
- ✅ 编写运维工具使用指南 `/Docs/计费系统运维工具使用指南.md`
- ✅ 提供完整的故障排查流程
- ✅ 建立最佳实践指南
---
## 🔗 相关文档
- [计费与资源平面对接文档](../Docs/项目文档/计费与资源平面对接文档.md)
- [LiteLLM和AgentManager回调接口文档](../Docs/项目文档/LiteLLM和AgentManager回调接口文档.md)
- [数据库迁移指南](../Docs/数据库迁移指南-postgres到taiji_prod.md)
- **[计费系统运维工具使用指南](../Docs/计费系统运维工具使用指南.md)** ⭐️ 新增
---
## 📞 联系方式
如有疑问或需要讨论,请联系:
- 技术负责人:[待填写]
- 邮件:[待填写]
- Slack:#taiji-ai-pad-dev
---
**文档版本**: v1.0.0
**最后更新**: 2026-03-09
**审核状态**: 待审核