pipeline-llm/README.md

210 lines
17 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 — 产线平台模型治理模块
产线平台的模型层:供应商/账号/模型治理 + 组织容错策略 + 限额限流 + 双维度记账。
所有表 `llm_` 前缀建在宿主应用库pipeline
## 模块定位
解决五类问题:
1. **供给侧治理**:供应商协议+全球端点目录、账号(钱包)、模型注册、适配模板
2. **容量治理**:供应商限流 → 多账号轮转;多供应商 → 主备模型容错
3. **需求侧治理**:组织级模型容错策略、组织限额限流、个人限额限流
4. **计费**:双维度记账(成本侧记账号、消费侧记组织/个人),模型挂定价 `pricing_program.ppid`
5. **端点选择**端点目录配在供应商下全球配置一次客户按区域偏好prefer/must过滤。
**2026-09-12 端点可互换重构**:端点 = 纯连接点(主机根+region+timeout不再带
protocol 标签、不参与协议过滤;多端点只为冗余/区域/多 key每个端点支持全部模型。
请求形态(协议/版本段/接口路径)完全由模型适配模板 profile.path 承载——path 是从
主机根起的完整相对路径(含版本段,如 /compatible-mode/v1/chat/completions、
/api/v1/services/aigc/...),运行时 url = 端点主机根 + path。账号 endpoint_ids
空 = 选用全部端点。inference 删除「path 为全 URL 则无视端点」的覆盖分支(模型
禁把端点写进 path。这根治了 t2t 间歇 404无 profile 模型被轮询到 api/v1
原生端点拼 chat/completions。auto-config 提取提示词与归一化_norm_relative_path
/ normalize_endpoints 主机根收敛)已同步新约定。
## 数据表9 张)
| 表 | 说明 |
|---|---|
| `llm_vendor` | 供应商(端点目录 JSON2026-09-05 起不存协议字段,请求形态归模型挂的适配模板) |
| `llm_account` | 供应商账号(钱包:余额/充值/选端点api_key AES 加密;模型可绑定账号 account_id 收窄候选池) |
| `llm_model` | 模型注册(能力分类/挂定价 ppid/sync_mode/query_profile_idsname 唯一2026-09 起不存单价,定价走 ppid |
| `llm_api_profile` | 适配模板(提交模板按协议×能力×形态指纹去重复用,见 _tpl_fingerprintdata/response 模板全 Jinja2含媒体转换函数method 独立列2026-09-06 起不塞 request_template |
| `llm_org_policy` | 组织容错策略(主备模型 + 辅助模型 utility_model_id + 端点偏好) |
| `llm_org_quota` | 组织限额限流(池余额 + QPM |
| `llm_user_quota` | 个人限额限流(额度上限 + QPM |
| `llm_usage` | 用量流水双维度记账事实表status 统一枚举 SUCCEEDED/FAILED/PENDING/RUNNING/rechargeaccounting_status 仅 SUCCEEDED 行有意义否则 NULL**usages 是计费唯一事实源**——上游 usage 原文 JSON 全量不截断token 计数与按量因子都在其中req_tokens/resp_tokens 冗余列已删m0021统计查询走 JSON_EXTRACTproject_id 关联 sd_projects 按项目统计费用(非项目调用哨兵'0',查询不传=不区分项目m0025call_id 关联 llm_call_trace 上行/下行原文) |
| `llm_call_trace` | 调用原文追踪2026-09-10 用户定夺):每次上游 HTTP 往返一行chat/gen/submit/query请求/响应原文脱敏落盘 filesroot/llm_trace/<>/<call_id>_<seq>.req/.resp.jsonAuthorization 等敏感 header 值替换 [REDACTED]URL 去 querycall_id 与 llm_usage.call_id 同值——按流水反查全部原文列表页「查看IO」弹窗llm_call_trace_io.dspy → env.llm_call_trace_io展示上下文摘要+请求/响应 CodeEditor 三 Tabrealpath 前缀校验防路径穿越机构隔离org_id='0' 平台可看全部);保留 90 天TRACE_RETENTION_DAYS过期清理由记账 worker 循环每日调 cleanup_expiredaccounting._maybe_cleanup_traces文件不进 dbackup 库备份,需另行同步备份 |
## 调用门禁链
```
① 个人限流 → ② 组织限流 → ③ 个人额度 → ④ 组织池(预授权)
→ ⑤ 组织策略选模型(主→备) → ⑥ 账号×端点候选池(偏好过滤+钱包门禁+冷却避让+会话粘性+轮询)
→ ⑦ 调用 → ⑧ 结算(实际用量,双维度记账)
```
任何一级不过:快速失败 + 明确中文原因(不挂起、不静默)。
### 账号钱包门禁与会话粘性2026-09-11 用户定夺)
- **钱包粒度**1 个供应商 = 多个 llm_account每个 APIKEY 账号一个钱包,
balance 字段;充值入口 llm_account_recharge。accounting 的供应商记账
粒度不变(记账金额 = supplier_cost
- **门禁**:选账号时 balance < 阈值params `llm_wallet_balance_threshold`
缺省 5 )→ 从轮询集剔除全部剔除 报可行动错误禁静默回退)。
- **粘性**同一会话对同一模型固定同一账号Redis `llm_sticky:{session_id}:
{model_id}`TTL 1h 滑动续期)——上游 KV/前缀缓存按账号(api_key)隔离,
固定账号命中缓存省钱;粘性账号冷却/剔除时自动重选。session_id 经
llm_bridge payload `_session_id` 透传(不进 token 缓存键)。
- **轮询**:跨会话 Redis INCR `llm_rr:{model_id}` 取模轮流用不同账号;
Redis 不可用退化为首个可用账号fail-open
- **扣减**产品记账product_accounting_generic**成功之后**,出账 worker
按流水 account_id 扣「调用模型的那个钱包」balance -= supplier_cost
cost 回填同一事务原子cost≠0 = 已扣过,幂等跳过(防 accounted 行
重置重跑重复扣。允许透支为负60s 出账窗口并发超扣是真实成本,宁可
透支不可丢钱流/打死流水)。
- **待办**:余额不足是状态 → todos.py provider 状态派生待办(充值到账自动
消失,天然单发)给属主机构 operator 角色params `llm_wallet_todo_role`
可配,按角色名 `.` 后缀动态匹配不锁 orgtypeid详情弹窗内直接充值
llm_wallet_todo_popup.dspy
- **充值登记**2026-09-15供应商账号列表llm_account_by_vendor工具栏
「💰 充值登记」按钮selected_row须先选中账号行→ 弹窗
api/llm_account_recharge_popup.dspy展示当前余额/累计充值)→ 输金额
POST api/llm_account_recharge.dspy → gateway.recharge_account
balance += 金额、total_recharge 累加、写 recharge 流水、清治理缓存;
成功后前端自动关弹窗并刷新列表。机构隔离同待办弹窗(业主'0'全量,
否则本属主机构)。真实充值动作在供应商侧,本平台只做登记;登记成功
msg 带回前后值(余额/累计充值 旧→新),眼见累计充值随之改变。
## 集成方式(复用不改建)
模块只提供治理引擎与配置管理;实际 LLM 转发仍由宿主已有的
`pipeline_service.llm_bridge` / `llm_proxy` 承担,在两处挂钩子:
- `llm_bridge._get_model_config`:治理启用时前置解析(主备容错+端点选择)
- `llm_proxy.proxy_chat_completion`:调用前后接门禁与结算(运行环境 token 路径)
机构未配置治理(无策略、无新模型)→ 返回 `__LEGACY__`,调用方回退旧 `llm` 表,向后兼容。
## 宿主接入清单
1. `app/pipeline_app.py` init() 加 `load_pipeline_llm()`try/except ImportError 兜底;规范名 load_<模块名>,旧名 `load_llm` 保留为别名)
2. `build.sh`clone 枚举 + pip install 枚举 + CRUD 生成枚举 + wwwroot 符号链接枚举
3. `scripts/create_tables.py`IDE 幂等建表枚举加 `pipeline-llm`(或执行本模块 `mysql.ddl.sql`
4. `scripts/import_init.py`INIT_MODULES 加本模块 `init/data.json`appcodes 字典)
5. `conf/rp.json``/pipeline-llm/**` 注册到 `logined`;运行 `scripts/load_path.py`
6. `wwwroot/index.ui` 主菜单加「模型治理」项URL `/pipeline-llm`
7. `restart-pipeline.sh` 重启(.py 变更必须重启;`api_key` 有缓存,改 key 也须重启)
## 关键约定
- **api_key 用 AES 单层加密**`appPublic.aes.aes_encode_b64`password_key 做密钥)。
⚠️ 不用 RC4`password()/unpassword()` 盐不对称,加密后永久不可解。
- **限流用 Redis 分钟窗口原子计数**db4与会话 db3 隔离Redis 故障时限流放行fail-open
余额仍由 DB 条件 UPDATE 守住。
- **429 → (账号,端点)对冷却 60s**;账号级冷却用 `*` 通配键。
- **status 统一枚举 + 记账三态2026-09-05 定夺2026-09-06 补 NULL 语义m0012**
`llm_usage.status` 各家模型归一为 SUCCEEDED/FAILED/PENDING/RUNNING其他=recharge 充值对账行)。
`accounting_status` 只有三个值——`created`(待记账)→ `accounted`(记账成功)/ `failed`(记账失败);
**只有 status='SUCCEEDED' 才有记账意义**:仅成功行写 created非 SUCCEEDED 行保持 **NULL**
(不冒充待记账)。出账循环扫 `accounting_status='created' AND status='SUCCEEDED'`。
自用 vs 跨机构分流由产品层 `product_accounting_generic` 的 is_self_use 决定(自用只记 PAY* 采购成本,
跨机构另记 PAY 客户应付)。流水 `usages` 存上游 usage 原文 JSON**全量不截断**
2026-09-10 用户定夺usages 是计费唯一事实源req_tokens/resp_tokens 冗余列删除
m0021此前 [:2000] 截断会产生非法 JSON → 出账 json.loads 静默失败 → derived
兜底 0 → 永不计费),异步出账循环(独立进程,需 load_appbase 提供
get_business_date读入定价引擎usages 非法 JSON 或为空 → 出账置 failed 写明
原因(禁静默 0 元账)。未挂 ppid 的模型出账置 failed 并写明原因(不静默)。
- **method 独立列2026-09-06 用户定夺m0012**`llm_api_profile.method`GET/POST
默认 POST不塞 request_template JSON运行时读该列决定 HTTP 动词,存量行由迁移
从 request_template 的 $.method 回填,查询步骤兼容回退解析。
- **媒体转换铁律2026-09-06 三数组契约,对齐 sage/llmage**:生成类模型适配模板
data 用 `b64media2url(request, _f)` 逐元素转公网 URL、response 用
`downloadfile2url(request, url)`(生成物落地本地,上游 URL 仅 24 小时);
上行媒体统一三数组参数 `image_files`/`audio_files`/`video_files`(字符串或数组
均可Jinja `is string` 动态判断),变长输入用 `{% for %}` 按文档示例组装。
- **异步模型执行器**sync_mode=async 时走提交→按 query_profile_ids 轮询→
取结果;等待预算默认 600 秒,`_timeout` 可覆盖(上限 900
- **记账 derived 计费双保险2026-09-07 根治 qwen3.8-max 记账 failed**
① `inference.py` 同步路径把模型返回的 usage 原文落 `llm_usage.usages`(对齐异步路径与
sage/llmage 范式)——定价 YAML 的 derived 变量引用(`prompt_tokens_details.cached_tokens`
依赖原始结构,此前同步路径丢弃 usages 导致 uncache_tokens 兜底成 0、输入 token 永不计费;
② `accounting.py` `_settle_one` 出账时对缺失的 `prompt_tokens_details` 补 `{'cached_tokens': 0}`
并按 `created_at` 注入 `hour` 维度(忙闲时分档计价用,如 deepseek-v4-pro-0813
定价 YAML 铁律定价项顶层只放运行时必带的维度Batch/显式缓存/限时折扣等备选模式绝不入
YAML缺失键会炸整个方案——详见 pricing-data-format 技能 §14-16。
- **会话 agent 模型选择收窄为对话能力2026-09-06 用户定夺)**
`selection.CHAT_CAPS = ('t2t','i2t','m2t')` 是会话形态能力白名单的**唯一事实源**——
所有会话 agent 模型下拉agent_model_options / cockpit_model_options /
get_model_options 角色配置)传 `capabilities='chat'` 过滤;持久化入口
set_agent_model 个人默认 / cockpit_agent 角色模型 / pipeline_service
gateway._resolve_and_persist_model 项目模型)同样只接受对话能力,防绕过下拉。
`chat_inference` 同步门禁从「只认 t2t」放宽到 CHAT_CAPS实测根因测试库
机构策略主模型 qwen3.8-max 能力是 i2t旧门禁直接 FAILED
「capability mismatch: i2t != t2t」会话 agent 选它必挂)。
存量模型 `capability` 空串按 t2t 语义归一COALESCE+NULLIF
embedding/rerank/图视频生成等非对话模型各有专属入口list_models 按能力分栏)。
- **m2t多媒体生文入能力字典四处同步m0013**:种子 `init/data.json` +
端点注释 `api/v1/models.dspy` + `docs/design-spec.md §6` + 提取提示词
`pipeline_platform.platform_ability._EXTRACT_PROMPT`,判定表补
「多媒体输入→文本 ⇒ m2t」「仅图像→文本 ⇒ i2t」「仅文本→文本 ⇒ t2t」
且 r2v 判定加「输出为视频」限定防误吸文本输出模型);
`_MEDIA_INPUT_CAPS` 加 m2t模板生成按输入媒体处理
## 统一超时预算2026-09-16 用户裁定:单点收敛,禁散落)
**语义**`payload._timeout` = 一次调用的【总预算】秒数(含内部重试),由调用侧
bridge`pipeline_service.llm_bridge._resolve_budget`)恒传——忘传走平台缺省
chat 300 / 生成类 900不再落到供应商端点小配置。
**端点侧单点**`inference._post_upstream` deadline
- `ctx['budget']` = 总预算attempt 超时 = 剩余预算,总时长≈预算(旧实现每
attempt 独享全额超时,最坏 3×预算+退避 > 客户端等待 → 客户端先断连报模糊
TimeoutError → 分类器误判永久错误2026-09-16 pbls QC 事故链一环)。
- 剩余 < `_MIN_ATTEMPT_SECONDS`(15s) 不再发起必然超时的 attempt抛结构化错误。
- **端点配置 timeoutllm_vendor.endpoints百炼 120 等)不再掐断 attempt**——
供应商参数不知道调用方需求,正是 09-05/09-14/09-16 三次事故共同根因;仅在
直连端点未传 `_timeout` 时与 `_TOTAL_TIMEOUT`(300) 取大兜底。
- 异步模型轮询总预算同语义(`_async_inference`req_timeout 覆盖,缺省 600
**三层对齐铁律**:调用方总预算 T → `_timeout`=T端点 deadline→ bridge 客户端
aiohttp=T+60恒覆盖上游→ 调用方外层 wait_for若有≥ T+60。
验证特征串:`[inference] 组包 ... 预算=Ns` 日志行llm_call_trace status_code=0
断连计数(治理前 27 次/日 → 治理后 0
## 治理页面弹窗化2026-09-07 用户定夺)
`wwwroot/index.ui` 顶部统计条llm_dashboard_widget+ 7 张功能卡片。
**每张卡片点击后弹窗处理PopupWindow 85%×85%),不再在页面下方 llm_content 容器展示**
- `api/card_popup.dspy`target 白名单llm_vendor/llm_model/llm_api_profile/llm_org_policy/
llm_org_quota/llm_user_quota/llm_usage→ 弹窗内嵌对应 CRUD 页 urlwidget
非白名单 target 返回「无效的功能入口」提示(不拼 URL
- 模型注册弹窗target=llm_model顶部额外两个按钮
- **🤖 自动配置** → `api/auto_config_popup.dspy`:输入 API 文档与定价信息的链接或正文,
「开始自动配置」按钮 script 组装指令后驱动弹窗内嵌 AgentIO
pipeline_id=platform_general弹窗级独立 session_idspec 锚定会话隔离天然成立),
内部助手全链完成(抓取/提取/落库/定价/测试/记账检查)后汇报模型状态。
- **✅ 上线检查** → `api/online_check_popup.dspy`:模型下拉
`api/get_llm_online_check_models.dspy`value=模型名test_model_call 按名调用)+
「上线检查」按钮 → 内部助手按模型配置后的测试过程处理test_model_call 真实调用 →
check_model_accounting 记账检查 → platform_llm_status 状态复核),最后明确告知通过/不通过。
- 驱动 AgentIO 的机制:`io.inputw.textw.setValue(指令)` + `io.inputw.input_finished()`
AgentInput 原生方法 dispatch('inputed') → AgentIO POST 流),零自定义 JS 文件。
- 权限:新 dspy 都在 `/pipeline-llm/**` logined 通配内,无需新增 RBAC
内部助手工具本身有 owner 角色代码层硬门禁pipeline-platform
## API 文档端点2026-09-05
`/pipeline-llm/api/v1/docs/`RBAC 授权 any公开文档照 sage /bricks/docs 先例):
- `index.ui` 按 `_lang` 参数分发 ApiDoc 组件zh/en/ja/ko 四语言,带白名单回落 zh
- `api_{zh,en,ja,ko}.md` 四语言文档chat/completions + models 端点、异步模型参数、
媒体转换约定、token 认证、计费说明)
- 依赖宿主 conf/config.json 的 processors 注册 `[".md","md"]`MarkdownProcessor
## 文档
- `docs/design-spec.md` — 完整设计规范(架构/表/机制/功能点/分期)
- `docs/test-cases.md` — 测试用例30 例,部署后全部执行)
## 开发日志
见 `docs/work-log-2026-09-01.md`。