pipeline-llm/docs/design-spec.md
yumoqing 69fe362dde feat(trace): 调用原文追踪「查看IO」前端闭环+按项目费用页(2026-09-10定夺落地)
- json/llm_call_trace_list.json: noedit只读列表;exclouded隐藏id/req_path/resp_path/org_id;
  url/note加宽;kind/method内联枚举(含gen同步生成,此前草稿引用的get_llm_kind_options.dspy不存在);
  删color_mapping(bricks全库0消费的死配置);toolbar查看IO+binds urlwidget→PopupWindow(85%)
  url不带?id=${id}$——生成期ArgsConvert会eval成<built-in function id>垃圾串,
  行数据id经_add_event_data自动并入params(dapi/llmage生产同款)
- api/llm_call_trace_io.dspy: 薄代理(get_user门禁+get_userorgid机构隔离)→env.llm_call_trace_io;
  修草稿4处实锤bug:get_user未await/user.get当dict/sor块外使用/返回PopupWindow套娃
- init.py: llm_call_trace_io读表+同批次+llm_usage上下文(块内读全拷dict)→读文件
  (trace.trace_root realpath前缀校验防穿越)→TabPanel三tab(摘要Html+pre escape防注入/
  请求/响应CodeEditor readonly mode=null);单侧150K截断提示盘上路径;env注册
- api/llm_project_cost_query.dspy: 改{total,rows}契约接Tabular data_url(PageDataLoader);
  wwwroot/llm_project_cost/index.ui: InlineForm(date_from/date_to/status)+Tabular;
  script内getWidgetById起点bricks.app(弹窗DOM挂body下,app.root搜不到)
- card_popup.dspy: titles加llm_call_trace;新增page_targets白名单挂按项目费用页;
  index.ui挂两张卡片(调用原文追踪/按项目费用)
- accounting.py: _maybe_cleanup_traces每日一次接cleanup_expired(trace.py铁律3
  文档承诺worker每日调但循环没接,补齐);放抢锁前(清理幂等多进程同日无害)
- scripts/load_path.py: 注释说明/**通配已覆盖新页面(rbac check_roles_path前缀匹配)
- models/mysql.ddl/README/design-spec: 9张表同步(kind补gen枚举;3.9节;F19/F20)
- scripts/test_llm_call_trace_io.py: stub harness 22断言全过(隔离/穿越/转义/截断/每日门控)
- .gitignore: wwwroot/llm_call_trace/生成目录不入库
2026-09-11 14:58:21 +08:00

289 lines
15 KiB
Markdown
Raw Permalink 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.

# pipeline-llm 模块设计规范
> 版本1.0 | 日期2026-09-01 | 状态:待实施
> 宿主产线平台pipeline-app共享 `pipeline` 库) | 表前缀:`llm_`
## 1. 背景与目标
产线平台现有模型层是 `llm` 单表name/provider/model_id/api_base/api_key/capabilities
内嵌接入信息、单一 OpenAI-chat 调用形态、能力是标签不是契约。随着模型增多(非 t2t 能力、
多供应商、多账号),需要独立的模型治理模块。本模块解决五类问题:
1. **供给侧治理**:供应商协议/端点目录、账号(钱包)、模型注册、适配模板
2. **容量治理**:供应商限流 → 多账号轮转;多供应商 → 主备模型容错
3. **需求侧治理**:组织级模型容错策略、组织限额限流、个人限额限流
4. **计费**:双维度记账(成本侧记账号、消费侧记组织/个人),挂定价平台 `pricing_program.ppid`
5. **端点选择**供应商全球端点配置一次账号选用端点客户按区域偏好prefer/must
## 2. 架构总览
```
供给侧(容量) 需求侧(治理)
───────────── ─────────────
llm_vendor (协议+端点目录) llm_org_policy (组织容错策略)
└─ llm_account (钱包+选端点) llm_org_quota (组织限额限流)
└─ llm_model (模型注册) llm_user_quota (个人限额限流)
└─ llm_api_profile (适配)
llm_usage (用量流水,双维度)
```
**调用门禁链**(所有 LLM 调用收敛点 `llm_call`
```
① 个人限流窗口 → ② 组织限流窗口
→ ③ 个人额度余额 → ④ 组织池余额
→ ⑤ 组织容错策略选模型(主→备)
→ ⑥ 账号×端点候选池:区域偏好过滤 → 剩余额度加权轮转 + 冷却避让
→ ⑦ 预授权(冻结预估用量)→ 调用 → ⑧ 结算(实际用量,双维度记账)
任何一级不过:快速失败 + 明确原因(不挂起、不静默)
```
## 3. 数据表9 张,全部 `llm_` 前缀,建在 `pipeline` 库)
### 3.1 llm_vendor — 供应商
| 字段 | 类型 | 说明 |
|---|---|---|
| id | str(32) PK | |
| name | str(100) | 供应商名(如 阿里云百炼) |
| protocol | str(20) | openai_compat / dashscope_async / custom |
| endpoints | text(JSON) | 端点目录 `[{region, base_url, proxy, timeout, note}]` |
| description | text | |
| status | str(20) | active / disabled默认 active |
| org_id | str(32) | 0=系统级 |
| created_at/updated_at | str(30) | |
端点目录挂在 vendor 下(全球端点配置一次),账号选用。`endpoints` JSON 元素:
`region`domestic/international/europe/...)、`base_url``proxy`(可选,国外端点走 SOCKS
`timeout`(秒,默认 60`note`
### 3.2 llm_account — 供应商账号(钱包)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | str(32) PK | |
| vendor_id | str(32) | → llm_vendor.id |
| name | str(100) | 账号名 |
| api_key | str(500) | **AES 单层加密**appPublic.aes.aes_encode_b64password_key 做密钥,对称可解密)。⚠️ 不能用 RC4rc4.password()/unpassword() 盐不对称加密后永久不可解2026-08 实测坑)。改 key 须重启进程(有缓存) |
| endpoint_ids | text(JSON) | 选用的端点索引列表(对应 vendor.endpoints 下标),有序;空=不启用 |
| balance | decimal(14,4) | 余额(充值-消耗),单位与定价币种一致 |
| total_recharge | decimal(14,4) | 累计充值 |
| status | str(20) | active / suspended / exhausted默认 active |
| org_id | str(32) | |
| created_at/updated_at | str(30) | |
独立充值、独立记账。轮转限流时余额不足的账号直接跳过(不消耗)。
### 3.3 llm_model — 模型注册(旧 `llm` 表的替代)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | str(32) PK | |
| vendor_id | str(32) | → llm_vendor.id |
| account_id | str(32) | 默认账号(可空,运行时轮转选) |
| name | str(100) UNIQUE | 模型注册名(平台内唯一,调用方用此名) |
| vendor_model_id | str(100) | 供应商侧模型名(各家命名不一) |
| capability | str(20) | 能力类型,默认 t2t见 §6 能力标准) |
| sync_mode | str(10) | sync / async_task默认 sync |
| profile_id | str(32) | → llm_api_profile.id适配模板 |
| ppid | str(32) | → 定价平台 `pricing_program.id`(挂定价) |
| default_params | text(JSON) | 默认参数temperature/max_tokens 等) |
| status | str(20) | active / disabled |
| description | text | |
| org_id | str(32) | |
| created_at/updated_at | str(30) | |
### 3.4 llm_api_profile — 适配模板(协议×能力形态去重)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | str(32) PK | |
| name | str(100) | 如 openai-chat、dashscope-image-async |
| protocol | str(20) | 同 vendor.protocol |
| capability | str(20) | 能力类型 |
| request_template | text | Jinja2参数 → 供应商请求体 |
| response_template | text | Jinja2供应商响应 → 统一结果(含 usage |
| param_schema | text(JSON) | bricks input_fields 格式(参数表单定义) |
| status | str(20) | active / disabled |
| created_at/updated_at | str(30) | |
模板按「协议×能力形态」开,不按模型开——同一端点 N 个模型共用一条,杜绝重复记录。
一期只内置 `openai-chat`sync非 chat 形态模型进来时再加对应 profile。
### 3.5 llm_org_policy — 组织模型容错策略
| 字段 | 类型 | 说明 |
|---|---|---|
| id | str(32) PK | |
| org_id | str(32) UNIQUE | 一组织一策略 |
| primary_model_id | str(32) | → llm_model.id 主模型 |
| backup_model_ids | text(JSON) | 备模型 id 列表(有序) |
| endpoint_pref | str(20) | prefer_domestic / prefer_intl / must_domestic / must_intl / any默认 any |
| status | str(20) | active / disabled |
| created_at/updated_at | str(30) | |
生效优先级:会话/项目选择 > 组织策略 > 平台默认(与现有
`sd_projects.default_model > pipeline_agent_settings.default_llm_id` 链兼容,组织策略插中间)。
must 模式:候选端点耗尽宁可报错冒泡,绝不跨端点。
### 3.6 llm_org_quota — 组织限额限流
| 字段 | 类型 | 说明 |
|---|---|---|
| id | str(32) PK | |
| org_id | str(32) UNIQUE | |
| balance | decimal(14,4) | 组织池余额(充值-消耗) |
| total_recharge | decimal(14,4) | 累计充值 |
| rate_limit | int | 组织 QPM 上限0=不限) |
| status | str(20) | active / disabled |
| created_at/updated_at | str(30) | |
### 3.7 llm_user_quota — 个人限额限流
| 字段 | 类型 | 说明 |
|---|---|---|
| id | str(32) PK | |
| user_id | str(32) | |
| org_id | str(32) | 所属组织(额度挂组织池) |
| quota_limit | decimal(14,4) | 个人额度上限0=不限) |
| quota_used | decimal(14,4) | 个人已用 |
| rate_limit | int | 个人 QPM 上限0=不限) |
| status | str(20) | active / disabled |
| created_at/updated_at | str(30) | |
关系默认「共享池 + 个人上限」:组织充值一个池,个人上限防单人打爆池子。
个人额度耗尽不影响他人;组织池耗尽全员不可用(冒泡提醒充值)。
### 3.8 llm_usage — 用量流水(双维度记账事实表)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | str(32) PK | |
| org_id / user_id | str(32) | 消费侧维度 |
| model_id / account_id | str(32) | 供给侧维度model_id 空+source=recharge 时为充值记录) |
| endpoint_region | str(20) | 实际走的端点区域 |
| usages | text | 用量原文JSON计费唯一事实源token计数+按量因子,全量不截断) |
| cost | decimal(12,6) | 成本侧金额(账号扣减,按定价) |
| charge | decimal(12,6) | 消费侧金额(组织池扣减,按产品定价) |
| ppid | str(32) | 定价项目(冗余自模型,便于对账) |
| project_id | str(32) NOT NULL DEFAULT '0' | 项目IDsd_projects.id'0'=非项目调用哨兵值——按项目统计费用,查询不传=不区分项目2026-09-10token 绑定 project_id 全链透传) |
| task_ref | str(100) | 调用来源(任务/会话标识) |
| call_id | str(32) | 调用批次ID关联 llm_call_trace 原文追踪,一次调用=N个往返2026-09-10 |
| status | str(20) | 统一枚举 SUCCEEDED/FAILED/PENDING/RUNNING其他如 recharge 充值记录) |
| note | str(200) | 失败原因/充值备注 |
| created_at | str(30) | |
一次调用两条线:**成本侧** cost 记账号(供应商对账),**消费侧** charge 记组织池+个人
(客户计费)。充值是 balance 变动事件,记 `source=recharge`model_id/account_id 空)。
### 3.9 llm_call_trace — 模型调用原文追踪(上行/下行)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | str(32) PK | |
| call_id | str(32) | 调用批次ID与 llm_usage.call_id 同值;一次 chat_inference=N 个往返) |
| seq | int | 往返序号(同步重试 0..2;异步提交 0轮询 1..N |
| kind | str(20) | chat 对话 / gen 同步生成 / submit 异步提交 / query 任务轮询 |
| url | str(500) | 上游 URL去 query 防签名泄露) |
| method | str(10) | HTTP 方法 |
| status_code / elapsed_ms | int | 响应状态码 / 耗时 |
| req_path / resp_path | str(300) | 原文文件路径filesroot 相对headers 已脱敏) |
| org_id | str(32) | 机构ID查看端点机构隔离用 |
| note | str(200) | 错误摘要 |
| created_at | timestamp | |
原文落盘(唯一收敛点 inference._post_upstream / _http_request 层各记一次):
`filesroot/llm_trace/<YYYYMMDD>/<call_id>_<seq>.req.json / .resp.json`。铁律:
脱敏后落盘Authorization 等敏感 header 值 → [REDACTED]);先文件后库行;
record_roundtrip 永不抛(审计不反噬计费/调用链);保留 90 天,过期清理挂记账
worker 循环accounting._maybe_cleanup_traces 每日一次);文件需纳入部署备份
dbackup 只备库。查看入口列表页「查看IO」→ api/llm_call_trace_io.dspy →
env.llm_call_trace_ioTabPanel 三页:上下文摘要/请求原文/响应原文CodeEditor
readonlyrealpath 前缀校验不逃逸 llm_trace 目录;机构隔离 org_id='0' 平台可看全部)。
## 4. 核心机制
### 4.1 选择链与轮转gateway.py `llm_call`
```python
async def llm_call(prompt, model=None, org_id=None, user_id=None, task_ref='', **kw)
```
1. 解析模型:调用方指定 model → 直接用;否则查组织策略主备链
2. 候选对构建模型的账号池account.status=active× 账号选用端点
3. 区域偏好过滤must_* 只留对应区域端点对prefer_* 排序(匹配区域优先)
4. 加权轮转:按账号余额 + 端点窗口剩余配额加权;限流冷却中的对跳过
5. 调用 → 429 标记该(账号,端点)对冷却 → 换下一对;账号池耗尽 → 降备模型;
备模型也耗尽 → must 模式冒泡报错 / prefer 模式跨端点重试
6. 401/403 标记账号失效不轮转5xx 短暂重试(最多 2 次)
### 4.2 预授权-结算(防并发击穿余额)
- 调用前:按 max_tokens 预估(默认 2000 token × 单价Redis 原子冻结组织池+个人额度
- 调用后:按实际 token 多退少补,写双维度流水
- GeneratorExit/超时:结算兜底(按预估释放,写 failed 流水)
### 4.3 限流Redis 滑动窗口)
- key`llm_rl:{user|org}:{id}:{minute}`,原子 INCR + EXPIRE
- 超窗拒绝:返回明确错误(个人限流/组织限流),下窗口自动恢复,不涉及钱
- 供应商侧 429(账号,端点)对冷却 60s窗口内不再选它
### 4.4 定价对接ppid
- 模型挂 `pricing_program.id`(定价平台的定价项目)
- 消费侧单价从定价平台读取(跨库:定价模块库),一期支持手动设置 `price_per_1k`
兜底(组织策略页配置),二期接定价平台实时价
- 成本侧单价:账号维度配置(供应商结算价),一期也走手动配置
## 5. 功能点清单(共 20 项)
| # | 功能点 | 表/文件 | 优先级 |
|---|---|---|---|
| F01 | 供应商管理(含端点目录配置) | llm_vendor + CRUD | P0 |
| F02 | 账号管理(充值、余额、选端点) | llm_account + CRUD | P0 |
| F03 | 模型注册(挂能力、挂定价、挂适配) | llm_model + CRUD | P0 |
| F04 | 适配模板管理 | llm_api_profile + CRUD | P1一期内置 openai-chat |
| F05 | 组织容错策略(主备模型+端点偏好) | llm_org_policy + CRUD | P0 |
| F06 | 组织限额限流充值、QPM | llm_org_quota + CRUD | P0 |
| F07 | 个人限额限流 | llm_user_quota + CRUD | P0 |
| F08 | 调用门禁(①-④ 限流限额检查) | gateway.py | P0 |
| F09 | 主备容错(策略选模型→备链) | gateway.py | P0 |
| F10 | 账号×端点轮转(加权+冷却) | gateway.py | P0 |
| F11 | 区域偏好过滤prefer/must | gateway.py | P0 |
| F12 | 预授权-结算 | gateway.py | P0 |
| F13 | 双维度记账流水 | llm_usage | P0 |
| F14 | 充值操作(账号/组织池) | recharge dspy | P0 |
| F15 | 用量查询(按组织/模型/账号/时间) | usage 列表页 | P1 |
| F16 | 模型管理入口(主菜单) | index.ui 菜单项 | P0 |
| F17 | 与现有调用链兼容llm_bridge 切换) | llm_bridge.py 适配 | P0 |
| F18 | 机构未配置冒泡提示 | gateway 错误路径 | P0 |
| F19 | 调用原文追踪(上行/下行脱敏落盘+查看IO弹窗+90天保留 | llm_call_trace + trace.py | P1 |
| F20 | 按项目费用统计project_id 分组汇总) | llm_usage.project_id + llm_project_cost_query | P1 |
## 6. 能力类型标准capabilities 取值,先行收敛)
| 值 | 含义 | 同步性 |
|---|---|---|
| t2t | 文本对话/生成 | sync |
| t2i | 文生图 | async_task |
| i2t | 图生文(多模态理解) | sync |
| m2t | 多媒体生文(视频/音频等多媒体输入→文本) | sync |
| t2v | 文生视频 | async_task |
| embedding | 文本向量化 | sync |
| mm-embedding | 多模态向量化 | sync |
| rerank | 重排序 | sync |
| mm-rerank | 多模态重排序 | sync |
| tts | 语音合成 | sync |
| asr | 语音识别 | sync |
新增能力类型走评审(改本文档 + init/data.json appcodes禁止野生标签。
## 7. 集成点
1. **宿主加载**`pipeline_app.py` init() 加 `load_llm()`try/except ImportError 兜底)
2. **菜单**`wwwroot/index.ui` 主菜单加「模型管理」项 → `/llm`TabPanel
3. **RBAC**`/llm/**` 注册到 superuser静态资源 anyload_path.py 注册各路径
4. **i18n**zh/en 双语文案(模块内文案走 appcodes + i18n msg.txt
5. **建表**mysql.ddl.sqljson2ddl 生成 + 手写补齐),存量库 ALTER 幂等
6. **兼容**:旧 `llm` 表保留,`llm_bridge.py` 优先查新表(按 name无则回退旧表——
现有调用零改动,迁移平滑
## 8. 分期
- **一期(本次交付)**:全部 10 张表 + F01-F03/F05-F14/F16-F18 + F04 内置 openai-chat + F15
- **二期(触发:第一个非 t2t 模型)**:异步任务形态适配模板、任务轮询端点
- **三期(触发:定价平台实时价接入)**:成本/消费侧单价自动读取