sdpy-kit 0.6.8__tar.gz → 0.6.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/PKG-INFO +140 -4
  2. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/README.md +131 -1
  3. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/__init__.py +1 -1
  4. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/ai/base_service.py +26 -9
  5. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/ai/chat_service.py +5 -12
  6. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/ai/embedding_service.py +8 -1
  7. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/config/kit_config.py +35 -0
  8. sdpy_kit-0.6.9/kit/decima_kit.py +853 -0
  9. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/pyproject.toml +12 -2
  10. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/.gitignore +0 -0
  11. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/ai/__init__.py +0 -0
  12. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/ai/exceptions.py +0 -0
  13. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/ai/failover.py +0 -0
  14. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/ai/image_service.py +0 -0
  15. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/ai/model_builder.py +0 -0
  16. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/ai/model_client.py +0 -0
  17. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/ai/pool.py +0 -0
  18. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/ai/profiles.py +0 -0
  19. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/ai/rerank_service.py +0 -0
  20. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/ai/runner.py +0 -0
  21. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/ai/types.py +0 -0
  22. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/common_kit.py +0 -0
  23. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/config/__init__.py +0 -0
  24. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/config/log_config.py +0 -0
  25. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/crawl/__init__.py +0 -0
  26. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/crawl/article_io.py +0 -0
  27. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/crawl/article_models.py +0 -0
  28. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/crawl/deepseek_kit.py +0 -0
  29. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/crawl/web_article_kit.py +0 -0
  30. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/crawl/wx_article_kit.py +0 -0
  31. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/db/__init__.py +0 -0
  32. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/db/audit.py +0 -0
  33. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/db/base_db_kit.py +0 -0
  34. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/db/crud.py +0 -0
  35. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/db/engine.py +0 -0
  36. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/db/exceptions.py +0 -0
  37. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/db/models/__init__.py +0 -0
  38. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/db/models/base.py +0 -0
  39. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/db/mysql_kit.py +0 -0
  40. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/db/pg_kit.py +0 -0
  41. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/db/query.py +0 -0
  42. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/db/session.py +0 -0
  43. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/db/sqlite_kit.py +0 -0
  44. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/db/transaction.py +0 -0
  45. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/docs/__init__.py +0 -0
  46. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/docs/anydoc_kit.py +0 -0
  47. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/docs/docx_kit.py +0 -0
  48. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/docs/marker_kit.py +0 -0
  49. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/docs/md2doc_kit.py +0 -0
  50. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/docs/mineru_kit.py +0 -0
  51. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/docs/rc_docparser_kit.py +0 -0
  52. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/file_kit.py +0 -0
  53. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/laya_kit.py +0 -0
  54. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/license_kit.py +0 -0
  55. {sdpy_kit-0.6.8 → sdpy_kit-0.6.9}/kit/redis_kit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sdpy-kit
3
- Version: 0.6.8
3
+ Version: 0.6.9
4
4
  Summary: SDPY Kit——统一数据库访问层(PG/MySQL/SQLite)与 AI/Redis/文件/文档/向量工具集
5
5
  Author-email: Clark <changhongyuan@126.com>
6
6
  License-Expression: MIT
@@ -26,14 +26,16 @@ Provides-Extra: ai
26
26
  Requires-Dist: httpx>=0.28.1; extra == 'ai'
27
27
  Requires-Dist: python-dotenv>=1.2.4; extra == 'ai'
28
28
  Provides-Extra: all
29
+ Requires-Dist: decima-ai>=2.5.1; extra == 'all'
29
30
  Requires-Dist: docxtpl>=0.20.2; extra == 'all'
30
31
  Requires-Dist: firecrawl-anydoc>=0.2.4; extra == 'all'
31
32
  Requires-Dist: httpx>=0.28.1; extra == 'all'
32
33
  Requires-Dist: huggingface-hub<2,>=1.5; extra == 'all'
33
- Requires-Dist: laya>=0.4.12; extra == 'all'
34
+ Requires-Dist: laya>=0.4.21; extra == 'all'
34
35
  Requires-Dist: markdown-it-py>=4.2.0; extra == 'all'
35
36
  Requires-Dist: marker-pdf>=2.0.0; extra == 'all'
36
37
  Requires-Dist: numpy>=2.5.4; extra == 'all'
38
+ Requires-Dist: onnxruntime>=1.18; extra == 'all'
37
39
  Requires-Dist: pikepdf>=10.17.0; extra == 'all'
38
40
  Requires-Dist: pimd[pdfa]>=2.2.5; extra == 'all'
39
41
  Requires-Dist: psycopg2-binary>=2.9.13; extra == 'all'
@@ -47,13 +49,17 @@ Requires-Dist: trafilatura>=2.3.1; extra == 'all'
47
49
  Provides-Extra: anydoc
48
50
  Requires-Dist: firecrawl-anydoc>=0.2.4; extra == 'anydoc'
49
51
  Requires-Dist: pikepdf>=10.17.0; extra == 'anydoc'
52
+ Provides-Extra: decima
53
+ Requires-Dist: decima-ai>=2.5.1; extra == 'decima'
54
+ Requires-Dist: huggingface-hub<2,>=1.5; extra == 'decima'
55
+ Requires-Dist: onnxruntime>=1.18; extra == 'decima'
50
56
  Provides-Extra: docx
51
57
  Requires-Dist: docxtpl>=0.20.2; extra == 'docx'
52
58
  Provides-Extra: docx-parse
53
59
  Requires-Dist: rc-docparser>=0.2.2; extra == 'docx-parse'
54
60
  Provides-Extra: laya
55
61
  Requires-Dist: huggingface-hub<2,>=1.5; extra == 'laya'
56
- Requires-Dist: laya>=0.4.12; extra == 'laya'
62
+ Requires-Dist: laya>=0.4.21; extra == 'laya'
57
63
  Provides-Extra: marker
58
64
  Requires-Dist: marker-pdf>=2.0.0; extra == 'marker'
59
65
  Provides-Extra: md2doc
@@ -80,7 +86,7 @@ SDPY 统一工具集(Kit 体系),供 Python 项目开箱即用:
80
86
 
81
87
  - **统一数据库访问层**:PostgreSQL / MySQL / SQLite,三个 Kit API 完全一致,切换数据库只改一行 import;内置审计字段自动填充(`created_*` / `updated_*` / `deleted`)与软删除查询过滤;
82
88
  - **AI 多模型服务池**:chat / embedding / rerank / image 四类能力,多厂商候选链 + 故障自动转移;
83
- - **工具集**:Redis / 文件 / 雪花 ID / 离线授权签发与校验 / 文档互转 / Word 模板填充 / MinerU 解析 / Marker 本地解析(PDF + 图片 → md + images/) / 离线 docx 解析(md + images/) / Laya 类型化决策 / 微信公众号文章抓取存储 / 通用网页文章抓取存储 / DeepSeek 搜索与抓取。
89
+ - **工具集**:Redis / 文件 / 雪花 ID / 离线授权签发与校验 / 文档互转 / Word 模板填充 / MinerU 解析 / Marker 本地解析(PDF + 图片 → md + images/) / 离线 docx 解析(md + images/) / Laya 类型化决策 / Decima 决策模型(选项乱序不变、置信度可门控) / 微信公众号文章抓取存储 / 通用网页文章抓取存储 / DeepSeek 搜索与抓取。
84
90
 
85
91
  > 要求 Python 3.13+
86
92
 
@@ -102,6 +108,7 @@ SDPY 统一工具集(Kit 体系),供 Python 项目开箱即用:
102
108
  | `kit.docs.mineru_kit` | `MinerUKit` | MinerU 文档解析(精准 + Agent 轻量) | `mineru` |
103
109
  | `kit.docs.marker_kit` | `MarkerKit` | 本地解析(PDF / 图片 → Markdown + Images) | `marker` |
104
110
  | `kit.laya_kit` | `LayaKit` | 类型化决策:choice / multi_choice 多选 / score / noul,另含 decide 多字段 / decide_batch 批量 / predict_long 长文本 | `laya` |
111
+ | `kit.decima_kit` | `DecimaKit` | 决策模型:choice / score / noul / multi_choice(原生 rank);选项乱序不变、置信度可门控;另含 predict / predict_long / brief / choice_batch(方法名与 `LayaKit` 对齐) | `decima` |
105
112
  | `kit.crawl.article_models` | `ArticleInfo` / `ArticleResult` / `SavedOutput` 等 | 文章抓取通用模型(三个 `*ArticleKit` 共用,含 `reason` 分类与非致命 `warnings`) | 无 |
106
113
  | `kit.crawl.article_io` | `resolve_stem` / `save_article` 等 | 落盘公共层(目录命名 / 图片本地化 / YAML 头 / `meta.json`),三个 `*ArticleKit` 共用 | 无 |
107
114
  | `kit.crawl.wx_article_kit` | `WxArticleKit` | 微信公众号文章 → Markdown + 图片转存 | 无 |
@@ -132,6 +139,7 @@ uv add sdpy-kit
132
139
  | `mineru` | MinerUKit 解析 API(精准解析 + Agent 轻量解析) | `pip install "sdpy-kit[mineru]"` |
133
140
  | `marker` | MarkerKit 本地解析(marker-pdf → Markdown + Images;PDF + 图片,依赖体积大) | `pip install "sdpy-kit[marker]"` |
134
141
  | `laya` | LayaKit 类型化决策(laya + torch,本地小模型 choice / multi_choice / score / noul;支持多字段 / 批量 / 长文本) | `pip install "sdpy-kit[laya]"` |
142
+ | `decima` | DecimaKit 决策模型(decima-ai + onnxruntime,**无 torch**;选项乱序不变、置信度可门控;磁盘约 137 MB) | `pip install "sdpy-kit[decima]"` |
135
143
  | `web-article` | WebArticleKit 通用网页文章抓取转存(trafilatura,纯 HTTP 无浏览器) | `pip install "sdpy-kit[web-article]"` |
136
144
  | `all` | 全部能力 | `pip install "sdpy-kit[all]"` |
137
145
 
@@ -186,6 +194,14 @@ SQLITE_DB_PATH=./data/sqlite.db
186
194
  # LAYA_HF_DISABLE_XET=true
187
195
  # LAYA_PRELOAD= # true 时只预热当前 LAYA_MODEL
188
196
  # LAYA_MAX_LOADED= # 同时驻留检查点上限,留空用 laya 默认 2
197
+
198
+ # Decima(决策模型,可选依赖 sdpy-kit[decima])
199
+ # DECIMA_MODEL=amyrmahdy/decima-small
200
+ # DECIMA_PRECISION=int8 # int8(127 MB)/ fp32(489 MB)
201
+ # DECIMA_THREADS=1 # batch-1 单线程最快,不建议调大
202
+ # DECIMA_LANG=zh # 仅 fa/ar 有实质归一化
203
+ # DECIMA_HF_ENDPOINT=https://hf-mirror.com
204
+ # DECIMA_LOCAL_ONLY=false # true = 只用本地缓存,离线部署
189
205
  ```
190
206
 
191
207
  ### 环境变量 ↔ 能力对照表
@@ -284,6 +300,21 @@ SQLITE_DB_PATH=./data/sqlite.db
284
300
  > `LAYA_HF_*` 同样由 kit 在**导入 laya 之前**经 `os.environ.setdefault` 应用,不覆盖显式配置;
285
301
  > `LAYA_MODEL` / `LAYA_DEVICE` / `LAYA_PRELOAD` / `LAYA_MAX_LOADED` 由 `Settings` 读取(进程内缓存,改后需重启进程)。
286
302
 
303
+ **Decima** → `kit.decima_kit`
304
+
305
+ | 环境变量 | 默认值 | 作用 |
306
+ |----------|--------|------|
307
+ | `DECIMA_MODEL` | `amyrmahdy/decima-small` | HF repo id 或本地 ONNX 导出目录 |
308
+ | `DECIMA_PRECISION` | `int8` | `int8`(127 MB,与 fp32 一致率 99.4%)/ `fp32`(489 MB) |
309
+ | `DECIMA_THREADS` | `1` | ONNX Runtime intra-op 线程数;batch-1 单线程最快 |
310
+ | `DECIMA_LANG` | `zh` | 传给 `Question.lang`;仅 fa/ar 有实质归一化 |
311
+ | `DECIMA_HF_ENDPOINT` | `https://hf-mirror.com` | 权重下载入口(国内镜像);置空则用官方源 |
312
+ | `DECIMA_LOCAL_ONLY` | `false` | `true` 时只用本地缓存、不联网 |
313
+
314
+ > `DECIMA_HF_ENDPOINT` 由 kit 在**导入 decima 之前**经 `os.environ.setdefault` 应用,不覆盖显式配置;
315
+ > `HF_HUB_DISABLE_XET=1` 由 kit 固定设置(国内镜像不支持 xet,会挂起)。
316
+ > 不占用上游环境变量 `DECIMA_MODELS`(短名→路径覆盖)与 `DECIMA_DECODE`(自动解码)。
317
+
287
318
  **DeepSeek Search / Fetch** → `kit.crawl.deepseek_kit`
288
319
 
289
320
  | 环境变量 | 默认值 | 作用 |
@@ -331,6 +362,7 @@ SQLITE_DB_PATH=./data/sqlite.db
331
362
  | `LOG_*`、`DB_*`、`MYSQL_*`、`REDIS_*`、`FILE_STORAGE_ROOT`、`MINERU_*`、`MARKER_MODE` | `get_settings()` 带 `lru_cache`,进程内首次读取后固定 → **重启进程**(或调用 `AI.reload_config()`,它会顺带清空该缓存) |
332
363
  | `MARKER_HF_ENDPOINT` / `MARKER_HF_DISABLE_XET` / `MARKER_LLAMA_CPP_BINARY` | 仅在 `import kit.docs.marker_kit` 时经 `os.environ.setdefault` 应用 → 改动需**重启进程**重新导入模块 |
333
364
  | `LAYA_HF_ENDPOINT` / `LAYA_HF_DISABLE_XET` | 仅在 `import kit.laya_kit` 时经 `os.environ.setdefault` 应用 → 改动需**重启进程**重新导入模块 |
365
+ | `DECIMA_HF_ENDPOINT` / `HF_HUB_DISABLE_XET` | 仅在 `import kit.decima_kit` 时经 `os.environ.setdefault` 应用 → 改动需**重启进程**重新导入模块 |
334
366
  | AI 密钥写在 `.env` 里 | `.env` 只在加载档案时注入一次 → 重启,或 `AI.reload_config(reload_file=True)` |
335
367
  | AI 密钥由自己写入 `os.environ` | `AI.reload_config()`(默认 `reload_file=False`,只重读 `os.environ`) |
336
368
  | `ai_models.json`(增删档案、换 model) | `AI.reload_config(reload_file=True)` |
@@ -1308,6 +1340,110 @@ if result.success:
1308
1340
  > 前提:中文标题依赖 `w:outlineLvl` 或「标题 N」样式(都没有时退化为正文);合并单元格表格会重建为
1309
1341
  > 等宽 GFM(覆盖位留空,不输出 HTML `<table>`);页眉页脚/脚注不提取。纯本地,无需模型或 GPU。
1310
1342
 
1343
+ ## kit.decima_kit —— 选项乱序不变、置信度可门控的决策模型
1344
+
1345
+ 122M 参数的 late-interaction 决策模型(int8 ONNX,单 CPU 线程,**不依赖 torch**)。
1346
+ 每个选项独立编码、独立与 state 打分,因此**调换选项顺序不会改变答案**。
1347
+
1348
+ ```bash
1349
+ pip install "sdpy-kit[decima]"
1350
+ ```
1351
+
1352
+ ```python
1353
+ from kit.decima_kit import DecimaKit
1354
+
1355
+ state = "我们三月份被重复扣费了两次,请把重复的那笔退款。"
1356
+
1357
+ # 单选题:选项顺序不影响结果
1358
+ r = DecimaKit.choice(
1359
+ state,
1360
+ "这封来信应该由哪个部门处理?",
1361
+ {"账务": "账单、付款、退款", "技术": "故障、报错"},
1362
+ )
1363
+ r["decision"], r["answer_confidence"], r["probabilities"]
1364
+
1365
+ # 是否题 / 评分题:第一个参数是待判断文本,第二个是命题
1366
+ DecimaKit.noul(state, "客户是否明确要求退款?")
1367
+ DecimaKit.score(state, "有多紧急?", ["不紧急", "较快处理", "紧急"])
1368
+
1369
+ # 多选:原生 rank(每项独立 sigmoid),返回 0-N 个
1370
+ DecimaKit.multi_choice(state, "涉及哪些事项?", {"账务": "账单", "技术": "故障"})
1371
+
1372
+ # 批量 / 多问题 / 长文本(方法名与 LayaKit 对齐)
1373
+ DecimaKit.choice_batch(
1374
+ [state, "系统登录后一直报错。"], "哪个部门?", {"账务": "账单", "技术": "故障"}
1375
+ )
1376
+ raw = DecimaKit.predict(
1377
+ state,
1378
+ {"dept": {"type": "choice", "instructions": "哪个部门?",
1379
+ "criteria": {"账务": "账单", "技术": "故障"}}},
1380
+ )
1381
+ DecimaKit.brief(raw)
1382
+ DecimaKit.predict_long(state * 50, {"r": {"type": "noul", "instructions": "是否要求退款?"}})
1383
+ ```
1384
+
1385
+ **返回字段**(`answer_confidence` 与 `LayaKit` 同名同口径):
1386
+
1387
+ | 字段 | 含义 |
1388
+ |------|------|
1389
+ | `decision` | 选中标签 / 序数期望值 / P(true) |
1390
+ | `answer_confidence` | top-1 概率 `max(p)`;**与 laya 同名同口径,但在本 Kit 上已标定、可做门控** |
1391
+ | `confidence` | 上游归一化置信度 `(k·max(p)−1)/(k−1)`,0~1(本 Kit 特有) |
1392
+ | `top_prob` | 原始 top-1 概率 `max(p)`,与 `answer_confidence` 相同 |
1393
+ | `probabilities` | 完整分布(`score` 的键为档位文本) |
1394
+ | `abstained` | 仅当传了 `min_confidence` 才有布尔值,否则为 `None` |
1395
+
1396
+ **从 LayaKit 迁移**:`choice` / `score` / `noul` / `multi_choice` / `predict` / `predict_long` /
1397
+ `brief` / `router` / `unload` 同名同形,**只改 import 即可**;
1398
+ `decide`(schema 多字段)、`decide_batch`、`route` 是 laya 独有能力,本 Kit 不提供
1399
+ (`decide_batch` 显式抛 `NotImplementedError`,不会静默按单选跑)。
1400
+
1401
+ ### 与 `LayaKit` 的取舍(同环境实测)
1402
+
1403
+ | 维度 | `LayaKit`(0.4.21) | `DecimaKit`(small 1.1) |
1404
+ |---|---|---|
1405
+ | 选项顺序导致的决策变化 | 中文工单 15.0%、文献 17% | **0%**(构造性) |
1406
+ | 门控覆盖率(放行准确率 ≥90%) | 25.0% | **52.5%** |
1407
+ | 中文工单分诊(40 条) | 0.700 | **0.750** |
1408
+ | 其中三个实质类(剔除「其他」,**推算**) | **0.867** | 0.667 |
1409
+ | JevBench 231 题 | 0.485 | **0.563** |
1410
+ | JevBench easy 48 题 | **0.896** | 0.812 |
1411
+ | JevBench hard 111 题 | 0.333 | **0.441** |
1412
+ | 磁盘(单检查点) | 617 MB | **137.5 MB** |
1413
+ | 依赖 | laya + torch | decima-ai + onnxruntime |
1414
+
1415
+ **怎么选**:要「动态选项 + 顺序稳定 + 低置信转人工」用 `DecimaKit`;要「固定标签 + 简单明确分诊 +
1416
+ 多字段 schema / GPU / 语言自动路由」用 `LayaKit`。两者**并列存在**,不互相替代。
1417
+ (`unload()` 两边都不该当内存回收 API:laya 实测只回落 0.4%,decima 回落约 50% 但也不回基线。)
1418
+
1419
+ ### 限制
1420
+
1421
+ - **不提供默认门控阈值**:阈值随选项数与业务分布变化(4 选项约 0.80、6 选项约 0.85、二选一约 0.54),
1422
+ 必须用自有数据标定后传入 `min_confidence`。
1423
+ - **不适用于复杂推理**:JevBench 231 题 0.563,同基准 Jev 1.13.0 为 0.986;
1424
+ `multi_hop` 0.28、`trap` 0.12。多跳策略判断请用大模型。
1425
+ - **`score` 最弱**(上游自认 benchmark 0.447):只返回分布与期望值,不承诺准确性。
1426
+ - **`multi_choice` 的 0.5 阈值偏严**:`rank` 是每选项独立 sigmoid,概率普遍低于 laya 的 `noul`
1427
+ (实测「账务 0.49 / 技术 0.12」会返回空列表);先看 `probabilities` 再定 `threshold`。
1428
+ - **无 schema 多字段能力**:`decide` / `decide_batch` 留在 laya 侧。
1429
+ - **仅 CPU**;**无真实释放接口**:上游没有 `unload`,`onnxruntime` 的 `InferenceSession`
1430
+ 也没有 `close()`,`unload()` 只能丢引用 + GC。实测 WorkingSet 882.7 → 440.2 MB(约 50%),
1431
+ 不回基线,反复加载/释放以 14~29 MB/轮 累积——长驻进程建议只加载一次。
1432
+ - **窗口 512 token 且不可调**:超长文本截断,或传 `long="chunk"` / `predict_long` 分窗。
1433
+ 分窗固定「512 token 窗口 + 64 token 重叠」,`predict_long` 的 `window` / `stride` /
1434
+ `batch_size` **只是为兼容 LayaKit 签名而接受,内部丢弃**(首次传参会告警一次);
1435
+ 需要更细粒度扫描请自行切分文本后逐段调用。长文档优先只喂标题 + 摘要。
1436
+ - **倾向选兜底类**:业务有「其他 / 无法判定」类时表现正常(实测该类 10/10 全对),
1437
+ 但代价是三个实质类低于 laya(账务 0.50 vs 0.80、销售 0.60 vs 0.80)——
1438
+ **业务没有兜底类时准确率会下降**。
1439
+
1440
+ > 实测数据来自 `research/decima/`(中文工单 40 条由 AI 标注、未人工复核,只作方向参考;
1441
+ > JevBench 为本轮独立复现,231 题实测 0.563 与模型卡自报 0.571 同量级)。
1442
+
1443
+ > **许可提示**:权重为 Apache-2.0,但 `decima-small` 训练数据含 CC BY-NC 数据集
1444
+ > (ANLI / XNLI / multilingual-NLI-26lang)。商用前请自行复核许可条款;纯商用数据版本为
1445
+ > `decima-base-clean`(体积与延迟更大,改 `DECIMA_MODEL` 后需重新验证效果)。
1446
+
1311
1447
  ## kit.crawl.wx_article_kit —— 微信公众号文章抓取存储
1312
1448
 
1313
1449
  零第三方依赖(标准库 `urllib` + 内置 HTML→Markdown 转换器),**无需 extra**,按需 import:
@@ -4,7 +4,7 @@ SDPY 统一工具集(Kit 体系),供 Python 项目开箱即用:
4
4
 
5
5
  - **统一数据库访问层**:PostgreSQL / MySQL / SQLite,三个 Kit API 完全一致,切换数据库只改一行 import;内置审计字段自动填充(`created_*` / `updated_*` / `deleted`)与软删除查询过滤;
6
6
  - **AI 多模型服务池**:chat / embedding / rerank / image 四类能力,多厂商候选链 + 故障自动转移;
7
- - **工具集**:Redis / 文件 / 雪花 ID / 离线授权签发与校验 / 文档互转 / Word 模板填充 / MinerU 解析 / Marker 本地解析(PDF + 图片 → md + images/) / 离线 docx 解析(md + images/) / Laya 类型化决策 / 微信公众号文章抓取存储 / 通用网页文章抓取存储 / DeepSeek 搜索与抓取。
7
+ - **工具集**:Redis / 文件 / 雪花 ID / 离线授权签发与校验 / 文档互转 / Word 模板填充 / MinerU 解析 / Marker 本地解析(PDF + 图片 → md + images/) / 离线 docx 解析(md + images/) / Laya 类型化决策 / Decima 决策模型(选项乱序不变、置信度可门控) / 微信公众号文章抓取存储 / 通用网页文章抓取存储 / DeepSeek 搜索与抓取。
8
8
 
9
9
  > 要求 Python 3.13+
10
10
 
@@ -26,6 +26,7 @@ SDPY 统一工具集(Kit 体系),供 Python 项目开箱即用:
26
26
  | `kit.docs.mineru_kit` | `MinerUKit` | MinerU 文档解析(精准 + Agent 轻量) | `mineru` |
27
27
  | `kit.docs.marker_kit` | `MarkerKit` | 本地解析(PDF / 图片 → Markdown + Images) | `marker` |
28
28
  | `kit.laya_kit` | `LayaKit` | 类型化决策:choice / multi_choice 多选 / score / noul,另含 decide 多字段 / decide_batch 批量 / predict_long 长文本 | `laya` |
29
+ | `kit.decima_kit` | `DecimaKit` | 决策模型:choice / score / noul / multi_choice(原生 rank);选项乱序不变、置信度可门控;另含 predict / predict_long / brief / choice_batch(方法名与 `LayaKit` 对齐) | `decima` |
29
30
  | `kit.crawl.article_models` | `ArticleInfo` / `ArticleResult` / `SavedOutput` 等 | 文章抓取通用模型(三个 `*ArticleKit` 共用,含 `reason` 分类与非致命 `warnings`) | 无 |
30
31
  | `kit.crawl.article_io` | `resolve_stem` / `save_article` 等 | 落盘公共层(目录命名 / 图片本地化 / YAML 头 / `meta.json`),三个 `*ArticleKit` 共用 | 无 |
31
32
  | `kit.crawl.wx_article_kit` | `WxArticleKit` | 微信公众号文章 → Markdown + 图片转存 | 无 |
@@ -56,6 +57,7 @@ uv add sdpy-kit
56
57
  | `mineru` | MinerUKit 解析 API(精准解析 + Agent 轻量解析) | `pip install "sdpy-kit[mineru]"` |
57
58
  | `marker` | MarkerKit 本地解析(marker-pdf → Markdown + Images;PDF + 图片,依赖体积大) | `pip install "sdpy-kit[marker]"` |
58
59
  | `laya` | LayaKit 类型化决策(laya + torch,本地小模型 choice / multi_choice / score / noul;支持多字段 / 批量 / 长文本) | `pip install "sdpy-kit[laya]"` |
60
+ | `decima` | DecimaKit 决策模型(decima-ai + onnxruntime,**无 torch**;选项乱序不变、置信度可门控;磁盘约 137 MB) | `pip install "sdpy-kit[decima]"` |
59
61
  | `web-article` | WebArticleKit 通用网页文章抓取转存(trafilatura,纯 HTTP 无浏览器) | `pip install "sdpy-kit[web-article]"` |
60
62
  | `all` | 全部能力 | `pip install "sdpy-kit[all]"` |
61
63
 
@@ -110,6 +112,14 @@ SQLITE_DB_PATH=./data/sqlite.db
110
112
  # LAYA_HF_DISABLE_XET=true
111
113
  # LAYA_PRELOAD= # true 时只预热当前 LAYA_MODEL
112
114
  # LAYA_MAX_LOADED= # 同时驻留检查点上限,留空用 laya 默认 2
115
+
116
+ # Decima(决策模型,可选依赖 sdpy-kit[decima])
117
+ # DECIMA_MODEL=amyrmahdy/decima-small
118
+ # DECIMA_PRECISION=int8 # int8(127 MB)/ fp32(489 MB)
119
+ # DECIMA_THREADS=1 # batch-1 单线程最快,不建议调大
120
+ # DECIMA_LANG=zh # 仅 fa/ar 有实质归一化
121
+ # DECIMA_HF_ENDPOINT=https://hf-mirror.com
122
+ # DECIMA_LOCAL_ONLY=false # true = 只用本地缓存,离线部署
113
123
  ```
114
124
 
115
125
  ### 环境变量 ↔ 能力对照表
@@ -208,6 +218,21 @@ SQLITE_DB_PATH=./data/sqlite.db
208
218
  > `LAYA_HF_*` 同样由 kit 在**导入 laya 之前**经 `os.environ.setdefault` 应用,不覆盖显式配置;
209
219
  > `LAYA_MODEL` / `LAYA_DEVICE` / `LAYA_PRELOAD` / `LAYA_MAX_LOADED` 由 `Settings` 读取(进程内缓存,改后需重启进程)。
210
220
 
221
+ **Decima** → `kit.decima_kit`
222
+
223
+ | 环境变量 | 默认值 | 作用 |
224
+ |----------|--------|------|
225
+ | `DECIMA_MODEL` | `amyrmahdy/decima-small` | HF repo id 或本地 ONNX 导出目录 |
226
+ | `DECIMA_PRECISION` | `int8` | `int8`(127 MB,与 fp32 一致率 99.4%)/ `fp32`(489 MB) |
227
+ | `DECIMA_THREADS` | `1` | ONNX Runtime intra-op 线程数;batch-1 单线程最快 |
228
+ | `DECIMA_LANG` | `zh` | 传给 `Question.lang`;仅 fa/ar 有实质归一化 |
229
+ | `DECIMA_HF_ENDPOINT` | `https://hf-mirror.com` | 权重下载入口(国内镜像);置空则用官方源 |
230
+ | `DECIMA_LOCAL_ONLY` | `false` | `true` 时只用本地缓存、不联网 |
231
+
232
+ > `DECIMA_HF_ENDPOINT` 由 kit 在**导入 decima 之前**经 `os.environ.setdefault` 应用,不覆盖显式配置;
233
+ > `HF_HUB_DISABLE_XET=1` 由 kit 固定设置(国内镜像不支持 xet,会挂起)。
234
+ > 不占用上游环境变量 `DECIMA_MODELS`(短名→路径覆盖)与 `DECIMA_DECODE`(自动解码)。
235
+
211
236
  **DeepSeek Search / Fetch** → `kit.crawl.deepseek_kit`
212
237
 
213
238
  | 环境变量 | 默认值 | 作用 |
@@ -255,6 +280,7 @@ SQLITE_DB_PATH=./data/sqlite.db
255
280
  | `LOG_*`、`DB_*`、`MYSQL_*`、`REDIS_*`、`FILE_STORAGE_ROOT`、`MINERU_*`、`MARKER_MODE` | `get_settings()` 带 `lru_cache`,进程内首次读取后固定 → **重启进程**(或调用 `AI.reload_config()`,它会顺带清空该缓存) |
256
281
  | `MARKER_HF_ENDPOINT` / `MARKER_HF_DISABLE_XET` / `MARKER_LLAMA_CPP_BINARY` | 仅在 `import kit.docs.marker_kit` 时经 `os.environ.setdefault` 应用 → 改动需**重启进程**重新导入模块 |
257
282
  | `LAYA_HF_ENDPOINT` / `LAYA_HF_DISABLE_XET` | 仅在 `import kit.laya_kit` 时经 `os.environ.setdefault` 应用 → 改动需**重启进程**重新导入模块 |
283
+ | `DECIMA_HF_ENDPOINT` / `HF_HUB_DISABLE_XET` | 仅在 `import kit.decima_kit` 时经 `os.environ.setdefault` 应用 → 改动需**重启进程**重新导入模块 |
258
284
  | AI 密钥写在 `.env` 里 | `.env` 只在加载档案时注入一次 → 重启,或 `AI.reload_config(reload_file=True)` |
259
285
  | AI 密钥由自己写入 `os.environ` | `AI.reload_config()`(默认 `reload_file=False`,只重读 `os.environ`) |
260
286
  | `ai_models.json`(增删档案、换 model) | `AI.reload_config(reload_file=True)` |
@@ -1232,6 +1258,110 @@ if result.success:
1232
1258
  > 前提:中文标题依赖 `w:outlineLvl` 或「标题 N」样式(都没有时退化为正文);合并单元格表格会重建为
1233
1259
  > 等宽 GFM(覆盖位留空,不输出 HTML `<table>`);页眉页脚/脚注不提取。纯本地,无需模型或 GPU。
1234
1260
 
1261
+ ## kit.decima_kit —— 选项乱序不变、置信度可门控的决策模型
1262
+
1263
+ 122M 参数的 late-interaction 决策模型(int8 ONNX,单 CPU 线程,**不依赖 torch**)。
1264
+ 每个选项独立编码、独立与 state 打分,因此**调换选项顺序不会改变答案**。
1265
+
1266
+ ```bash
1267
+ pip install "sdpy-kit[decima]"
1268
+ ```
1269
+
1270
+ ```python
1271
+ from kit.decima_kit import DecimaKit
1272
+
1273
+ state = "我们三月份被重复扣费了两次,请把重复的那笔退款。"
1274
+
1275
+ # 单选题:选项顺序不影响结果
1276
+ r = DecimaKit.choice(
1277
+ state,
1278
+ "这封来信应该由哪个部门处理?",
1279
+ {"账务": "账单、付款、退款", "技术": "故障、报错"},
1280
+ )
1281
+ r["decision"], r["answer_confidence"], r["probabilities"]
1282
+
1283
+ # 是否题 / 评分题:第一个参数是待判断文本,第二个是命题
1284
+ DecimaKit.noul(state, "客户是否明确要求退款?")
1285
+ DecimaKit.score(state, "有多紧急?", ["不紧急", "较快处理", "紧急"])
1286
+
1287
+ # 多选:原生 rank(每项独立 sigmoid),返回 0-N 个
1288
+ DecimaKit.multi_choice(state, "涉及哪些事项?", {"账务": "账单", "技术": "故障"})
1289
+
1290
+ # 批量 / 多问题 / 长文本(方法名与 LayaKit 对齐)
1291
+ DecimaKit.choice_batch(
1292
+ [state, "系统登录后一直报错。"], "哪个部门?", {"账务": "账单", "技术": "故障"}
1293
+ )
1294
+ raw = DecimaKit.predict(
1295
+ state,
1296
+ {"dept": {"type": "choice", "instructions": "哪个部门?",
1297
+ "criteria": {"账务": "账单", "技术": "故障"}}},
1298
+ )
1299
+ DecimaKit.brief(raw)
1300
+ DecimaKit.predict_long(state * 50, {"r": {"type": "noul", "instructions": "是否要求退款?"}})
1301
+ ```
1302
+
1303
+ **返回字段**(`answer_confidence` 与 `LayaKit` 同名同口径):
1304
+
1305
+ | 字段 | 含义 |
1306
+ |------|------|
1307
+ | `decision` | 选中标签 / 序数期望值 / P(true) |
1308
+ | `answer_confidence` | top-1 概率 `max(p)`;**与 laya 同名同口径,但在本 Kit 上已标定、可做门控** |
1309
+ | `confidence` | 上游归一化置信度 `(k·max(p)−1)/(k−1)`,0~1(本 Kit 特有) |
1310
+ | `top_prob` | 原始 top-1 概率 `max(p)`,与 `answer_confidence` 相同 |
1311
+ | `probabilities` | 完整分布(`score` 的键为档位文本) |
1312
+ | `abstained` | 仅当传了 `min_confidence` 才有布尔值,否则为 `None` |
1313
+
1314
+ **从 LayaKit 迁移**:`choice` / `score` / `noul` / `multi_choice` / `predict` / `predict_long` /
1315
+ `brief` / `router` / `unload` 同名同形,**只改 import 即可**;
1316
+ `decide`(schema 多字段)、`decide_batch`、`route` 是 laya 独有能力,本 Kit 不提供
1317
+ (`decide_batch` 显式抛 `NotImplementedError`,不会静默按单选跑)。
1318
+
1319
+ ### 与 `LayaKit` 的取舍(同环境实测)
1320
+
1321
+ | 维度 | `LayaKit`(0.4.21) | `DecimaKit`(small 1.1) |
1322
+ |---|---|---|
1323
+ | 选项顺序导致的决策变化 | 中文工单 15.0%、文献 17% | **0%**(构造性) |
1324
+ | 门控覆盖率(放行准确率 ≥90%) | 25.0% | **52.5%** |
1325
+ | 中文工单分诊(40 条) | 0.700 | **0.750** |
1326
+ | 其中三个实质类(剔除「其他」,**推算**) | **0.867** | 0.667 |
1327
+ | JevBench 231 题 | 0.485 | **0.563** |
1328
+ | JevBench easy 48 题 | **0.896** | 0.812 |
1329
+ | JevBench hard 111 题 | 0.333 | **0.441** |
1330
+ | 磁盘(单检查点) | 617 MB | **137.5 MB** |
1331
+ | 依赖 | laya + torch | decima-ai + onnxruntime |
1332
+
1333
+ **怎么选**:要「动态选项 + 顺序稳定 + 低置信转人工」用 `DecimaKit`;要「固定标签 + 简单明确分诊 +
1334
+ 多字段 schema / GPU / 语言自动路由」用 `LayaKit`。两者**并列存在**,不互相替代。
1335
+ (`unload()` 两边都不该当内存回收 API:laya 实测只回落 0.4%,decima 回落约 50% 但也不回基线。)
1336
+
1337
+ ### 限制
1338
+
1339
+ - **不提供默认门控阈值**:阈值随选项数与业务分布变化(4 选项约 0.80、6 选项约 0.85、二选一约 0.54),
1340
+ 必须用自有数据标定后传入 `min_confidence`。
1341
+ - **不适用于复杂推理**:JevBench 231 题 0.563,同基准 Jev 1.13.0 为 0.986;
1342
+ `multi_hop` 0.28、`trap` 0.12。多跳策略判断请用大模型。
1343
+ - **`score` 最弱**(上游自认 benchmark 0.447):只返回分布与期望值,不承诺准确性。
1344
+ - **`multi_choice` 的 0.5 阈值偏严**:`rank` 是每选项独立 sigmoid,概率普遍低于 laya 的 `noul`
1345
+ (实测「账务 0.49 / 技术 0.12」会返回空列表);先看 `probabilities` 再定 `threshold`。
1346
+ - **无 schema 多字段能力**:`decide` / `decide_batch` 留在 laya 侧。
1347
+ - **仅 CPU**;**无真实释放接口**:上游没有 `unload`,`onnxruntime` 的 `InferenceSession`
1348
+ 也没有 `close()`,`unload()` 只能丢引用 + GC。实测 WorkingSet 882.7 → 440.2 MB(约 50%),
1349
+ 不回基线,反复加载/释放以 14~29 MB/轮 累积——长驻进程建议只加载一次。
1350
+ - **窗口 512 token 且不可调**:超长文本截断,或传 `long="chunk"` / `predict_long` 分窗。
1351
+ 分窗固定「512 token 窗口 + 64 token 重叠」,`predict_long` 的 `window` / `stride` /
1352
+ `batch_size` **只是为兼容 LayaKit 签名而接受,内部丢弃**(首次传参会告警一次);
1353
+ 需要更细粒度扫描请自行切分文本后逐段调用。长文档优先只喂标题 + 摘要。
1354
+ - **倾向选兜底类**:业务有「其他 / 无法判定」类时表现正常(实测该类 10/10 全对),
1355
+ 但代价是三个实质类低于 laya(账务 0.50 vs 0.80、销售 0.60 vs 0.80)——
1356
+ **业务没有兜底类时准确率会下降**。
1357
+
1358
+ > 实测数据来自 `research/decima/`(中文工单 40 条由 AI 标注、未人工复核,只作方向参考;
1359
+ > JevBench 为本轮独立复现,231 题实测 0.563 与模型卡自报 0.571 同量级)。
1360
+
1361
+ > **许可提示**:权重为 Apache-2.0,但 `decima-small` 训练数据含 CC BY-NC 数据集
1362
+ > (ANLI / XNLI / multilingual-NLI-26lang)。商用前请自行复核许可条款;纯商用数据版本为
1363
+ > `decima-base-clean`(体积与延迟更大,改 `DECIMA_MODEL` 后需重新验证效果)。
1364
+
1235
1365
  ## kit.crawl.wx_article_kit —— 微信公众号文章抓取存储
1236
1366
 
1237
1367
  零第三方依赖(标准库 `urllib` + 内置 HTML→Markdown 转换器),**无需 extra**,按需 import:
@@ -23,7 +23,7 @@ Author: Clark Chang
23
23
  数据库访问通过 kit.db 子包提供(基于 SQLModel)。
24
24
  """
25
25
 
26
- __version__ = "0.6.8"
26
+ __version__ = "0.6.9"
27
27
 
28
28
  from kit.db import (
29
29
  MYSQL,
@@ -18,6 +18,7 @@ Author: Clark Chang
18
18
  """
19
19
 
20
20
  import json
21
+ import logging
21
22
  import time
22
23
  from abc import ABC, abstractmethod
23
24
  from typing import ClassVar, Final, Self, cast, override
@@ -78,6 +79,12 @@ def _truncate(text: str, limit: int) -> str:
78
79
  return text
79
80
 
80
81
 
82
+ def _dump_params(payload: JSONObject) -> str:
83
+ """请求体转单行日志文本(裁剪单字段与总长;仅 DEBUG 级别使用)。"""
84
+ dumped = json.dumps(_clip(payload), ensure_ascii=False)
85
+ return _truncate(dumped, _LOG_PAYLOAD_LIMIT)
86
+
87
+
81
88
  class BaseAIService[RequestT, ResponseT](ABC):
82
89
  """AI 服务基类(模板方法 + 泛型)。
83
90
 
@@ -150,7 +157,11 @@ class BaseAIService[RequestT, ResponseT](ABC):
150
157
  return None
151
158
 
152
159
  async def _call(
153
- self, request: RequestT, *, deadline: float | None = None
160
+ self,
161
+ request: RequestT,
162
+ *,
163
+ deadline: float | None = None,
164
+ log: bool = True,
154
165
  ) -> ResponseT:
155
166
  """统一编排:构造请求体 → 发送 → 解析响应(含中文 info 日志)。
156
167
 
@@ -158,16 +169,21 @@ class BaseAIService[RequestT, ResponseT](ABC):
158
169
  request: 请求模型
159
170
  deadline: 整链预算截止时刻(``time.monotonic()`` 秒),透传给
160
171
  ``post_json`` 的重试预算检查;None 表示不限制
172
+ log: 是否记录本次调用的生命周期日志(默认 True)。``False`` 供
173
+ 批量编排层(如 ``EmbeddingService.embed_texts``)静默内层
174
+ 批次——省略 Start 与成功耗时,避免 3N+3 条重复;请求参数
175
+ 仍受级别控制(DEBUG 可见,排障时批级请求体不丢),失败仍记
176
+ ERROR 便于定位到模型与实例
161
177
  """
162
178
  desc = self._log_desc()
163
179
  payload = self._apply_request_extras(
164
180
  self._apply_extra_body(self._build_payload(request)), request
165
181
  )
166
- params = _truncate(
167
- json.dumps(_clip(payload), ensure_ascii=False), _LOG_PAYLOAD_LIMIT
168
- )
169
- logger.info(f"********** {desc} ********** Start")
170
- logger.info(f"请求参数: {params}")
182
+ if log:
183
+ logger.info(f"********** {desc} ********** Start")
184
+ # 请求体序列化开销不小(批量文本可达数千字符),非 DEBUG 不构造
185
+ if logger.isEnabledFor(logging.DEBUG):
186
+ logger.debug(f"请求参数: {_dump_params(payload)}")
171
187
  start = time.perf_counter()
172
188
  try:
173
189
  data = await self._client.post_json(payload, deadline=deadline)
@@ -179,9 +195,10 @@ class BaseAIService[RequestT, ResponseT](ABC):
179
195
  f"耗时 {time.perf_counter() - start:.1f}秒(失败:{exc})"
180
196
  )
181
197
  raise
182
- logger.info(
183
- f"********** {desc} ********** 耗时 {time.perf_counter() - start:.1f}秒"
184
- )
198
+ if log:
199
+ logger.info(
200
+ f"********** {desc} ********** 耗时 {time.perf_counter() - start:.1f}秒"
201
+ )
185
202
  return result
186
203
 
187
204
  async def close(self) -> None:
@@ -17,20 +17,14 @@ Project: sdpy
17
17
  Author: Clark Chang
18
18
  """
19
19
 
20
- import json
20
+ import logging
21
21
  import threading
22
22
  import time
23
23
  from collections.abc import AsyncIterator
24
24
  from dataclasses import dataclass
25
25
  from typing import Any, ClassVar, Final, cast, override
26
26
 
27
- from kit.ai.base_service import (
28
- _LOG_PAYLOAD_LIMIT,
29
- BaseAIService,
30
- _clip,
31
- _truncate,
32
- safe_get,
33
- )
27
+ from kit.ai.base_service import BaseAIService, _dump_params, safe_get
34
28
  from kit.ai.exceptions import AIError, AIResponseError
35
29
  from kit.ai.profiles import _RESERVED_PAYLOAD_KEYS, ModelProfile
36
30
  from kit.ai.types import JSONObject
@@ -458,9 +452,6 @@ class ChatService(BaseAIService[ChatRequest, str]):
458
452
  self._apply_extra_body(self._build_payload(request)), request
459
453
  )
460
454
  desc = self._log_desc()
461
- params = _truncate(
462
- json.dumps(_clip(payload), ensure_ascii=False), _LOG_PAYLOAD_LIMIT
463
- )
464
455
  stats = stats if stats is not None else StreamStats()
465
456
  stats.thinking_applied = _resolve_thinking(self._profile, request.thinking)[1]
466
457
  arg, value, applied, _ = _resolve_structured_output(self._profile, request)
@@ -469,7 +460,9 @@ class ChatService(BaseAIService[ChatRequest, str]):
469
460
  stats.json_mode_applied = applied
470
461
  # 惰性 Start:异步生成器体在首次 __anext__ 前不执行,不消费不打日志
471
462
  logger.info(f"********** {desc} ********** Start")
472
- logger.info(f"请求参数: {params}")
463
+ # 请求体序列化开销不小(messages 常含长 prompt),非 DEBUG 不构造
464
+ if logger.isEnabledFor(logging.DEBUG):
465
+ logger.debug(f"请求参数: {_dump_params(payload)}")
473
466
  start = time.perf_counter()
474
467
  note = ""
475
468
  failed = False
@@ -162,6 +162,11 @@ class EmbeddingService(BaseAIService[EmbeddingRequest, list[list[float]]]):
162
162
  batch_size: int | None = None,
163
163
  concurrency: int = _DEFAULT_CONCURRENCY,
164
164
  ) -> list[list[float]]:
165
+ """批量向量化(分批 + 限并发)。
166
+
167
+ 日志只由本方法的「整批汇总」记录起止:内层批次传 ``log=False`` 静默,
168
+ 避免 3N+3 条重复输出;单批失败仍由基类记 ERROR。
169
+ """
165
170
  if not texts:
166
171
  return []
167
172
 
@@ -183,7 +188,9 @@ class EmbeddingService(BaseAIService[EmbeddingRequest, list[list[float]]]):
183
188
  async def _one(batch_index: int, chunk: tuple[str, ...]) -> None:
184
189
  async with semaphore:
185
190
  try:
186
- vectors = await self._call(EmbeddingRequest(list(chunk)))
191
+ vectors = await self._call(
192
+ EmbeddingRequest(list(chunk)), log=False
193
+ )
187
194
  except AIError as exc:
188
195
  failures.append(f"批次 {batch_index}: {exc}")
189
196
  return
@@ -138,6 +138,41 @@ class Settings(BaseSettings):
138
138
  description="Router 同时驻留的检查点上限,None 沿用 laya 默认 2",
139
139
  )
140
140
 
141
+ # Decima 决策模型配置(int8 ONNX,仅 CPU;选项乱序不变、置信度已标定)
142
+ DECIMA_MODEL: str = Field(
143
+ default="amyrmahdy/decima-small",
144
+ description=(
145
+ "HF repo id 或本地 ONNX 导出目录"
146
+ "(含 encoder.onnx / scorer.onnx / decima.json / tokenizer)"
147
+ ),
148
+ )
149
+ DECIMA_PRECISION: str = Field(
150
+ default="int8",
151
+ description="ONNX 精度:int8(127 MB,与 fp32 一致率 99.4%)/ fp32(489 MB)",
152
+ )
153
+ DECIMA_THREADS: int = Field(
154
+ default=1,
155
+ ge=1,
156
+ description=(
157
+ "ONNX Runtime intra-op 线程数;上游实测 batch-1 单线程最快,不建议调大"
158
+ ),
159
+ )
160
+ DECIMA_LANG: str = Field(
161
+ default="zh",
162
+ description=(
163
+ "传入 Question.lang;仅 fa/ar 有实质文本归一化,"
164
+ "中文/英文等价但影响选项缓存键"
165
+ ),
166
+ )
167
+ DECIMA_HF_ENDPOINT: str = Field(
168
+ default="https://hf-mirror.com",
169
+ description="权重下载入口(默认国内镜像),置空则使用 HuggingFace 官方源",
170
+ )
171
+ DECIMA_LOCAL_ONLY: bool = Field(
172
+ default=False,
173
+ description="True 时只用本地缓存、不联网(离线部署)",
174
+ )
175
+
141
176
  # 文章抓取自检阈值(三个 *ArticleKit 共用):正文长度低于该值时给出非致命提示
142
177
  # 0 表示关闭该判据(正文仍按实际长度全量返回,不做截断)
143
178
  CRAWL_MIN_CONTENT_CHARS: int = Field(