Files
PlantAI-rag-new/tooling/aqua_kb/cost_model.py
adminandClaude Fable 5 730692a1f6 feat(aqua): KB build tooling, aqua configs, QA artifacts (P1)
- phase2b_converter: result.json -> neo4j.cypher + qdrant_points.jsonl
  (payload keys replicate direct_flow_retriever contract; kb_version=
  aqua_srt_v1_20260930_normalized, source_system=Aqua_canonical)
- qdrant_loader: bge-m3 embed + batched upsert + checkpoint resume
- registry_builder: paper blocks -> registry jsonl (plant format)
- batch_runner: manifest -> convert -> neo4j -> qdrant, all checkpointed
- start_aqua_kb.sh: user-space trial instances (Neo4j 19889 / Qdrant 27435)
- smoke 11/11 green incl. resume drills
- aqua configs: entity_alias_aqua.yaml (22 entries), relation_taxonomy_aqua.yaml
  (45 groups); manifest points to aqua configs
- QA artifacts: golden_aqua.jsonl (41 Qs zh/en), qc_standards.md,
  budget_template.md + cost_model.py (measured pilot tokens)
- pilot: 500 MD + 30 PDF extracted, 0 failures; QC stats computed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-12 22:19:40 +08:00

186 lines
9.2 KiB
Python
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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""cost_model.py — 水产库(aqua)建库成本校准模型(T 代理,2026-09-12)。
参数化估算 LLM 抽取成本与 MinerU GPU 耗时,输出 Markdown 预算表。
只依赖标准库;所有数值可被命令行覆盖;默认值来源见
kb-libraries/aqua/qa/budget_template.md 假设清单。
用法:
$PY tooling/aqua_kb/cost_model.py # 默认参数,打印预算表
$PY tooling/aqua_kb/cost_model.py --json # 机器可读
$PY tooling/aqua_kb/cost_model.py --out budget.md # 同时写入文件
$PY tooling/aqua_kb/cost_model.py --price-cny-m-input 1.0 --price-cny-m-output 2.0 \
--md-chars 2109 --pdf-chars 60000 --n-md 31848 --n-pdf 13175
成本公式(人民币):
in_tokens = chars / chars_per_token + overhead_tokens * calls
out_tokens = (chars / chars_per_token) * out_in_ratio
cost = (in_tokens * p_in + out_tokens * p_out) / 1e6 * docs
其中 p_in/p_out 为每百万 token 单价。
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass
@dataclass
class DocSpec:
label: str
n_docs: int
chars_per_doc: float
llm_calls_per_doc: float
chunks_per_doc: float | None = None
cost: float = 0.0
in_tokens: int = 0
out_tokens: int = 0
@dataclass
class Params:
# 单价(¥/百万 token)——官方 deepseek-v4-flash 列表价(缓存未命中口径)
price_cny_m_input: float = 1.0
price_cny_m_output: float = 2.0
# 字符/token 折算
chars_per_token: float = 4.0 # 英文约 4 字符/token(aqua 语料英文占主导)
cjk_chars_per_token: float = 1.5 # 中文约 1.5 字符/token(当前语料占比 <5%)
# LLM 调用假设
overhead_tokens_per_call: float = 350.0 # 每调用的提示词/模板开销
out_in_ratio: float = 0.35 # 输出 token / 输入正文 token(P1 试点拟合前先估值)
md_llm_calls: float = 2.0 # MD 卡:提取 + 复核 各 1 次
pdf_chunk_chars: float = 8000.0 # PDF 全文分块大小
pdf_output_per_call: float = 800.0 # PDF 每块调用输出 token(P1 实测前估值)
# 语料规模
n_md: int = 31848
n_pdf: int = 13175
n_pilot_pdf: int = 30
n_pilot_md: int = 470
# 实测/历史锚点
md_chars: float = 2109.0 # 实测:source/题目摘要_MD 200 篇抽样 mean(P50=2169)
pdf_chars: float = 60000.0 # 假设:MinerU 输出正文均值(P1 30 篇试点后回填实测)
mineru_files_per_hour: float = 700.0 # 历史实测:单卡 3 端点并流 ~9,363 篇/11h ≈ 850/h,取保守 700
mineru_unprocessable: float = 0.177 # 历史实测:1656/9363;不可处理篇不产生抽取成本
gpu_hours_per_day: float = 24.0
peak_multiplier: float = 1.0 # 高峰时段单价倍数(官方列表价口径=1.0;若按高峰 ×2 口径计费则设 2.0)
def effective_input_price(p: Params) -> float:
# 高峰时段(工作日北京时间 09-12/14-18)约占总时长 1/4;
# 列表价口径(peak_multiplier=1.0)下加权后与列表价一致。
frac_peak = 0.25
return p.price_cny_m_input * (1 - frac_peak) + p.price_cny_m_input * p.peak_multiplier * frac_peak
def model_docs(p: Params) -> tuple[list[DocSpec], list[DocSpec]]:
"""返回 (批次[P1 试点], 全量) 两组成本估算。"""
p_in_eff = effective_input_price(p)
def md_cost(chars: float, n: int) -> DocSpec:
body_tokens = chars / p.chars_per_token
in_t = body_tokens + p.overhead_tokens_per_call * p.md_llm_calls
out_t = body_tokens * p.out_in_ratio
c = (in_t * p_in_eff + out_t * p.price_cny_m_output) / 1e6 * n
return DocSpec("MD 摘要卡", n, chars, p.md_llm_calls, None, c, int(in_t * n), int(out_t * n))
def pdf_cost(chars: float, n: int) -> DocSpec:
chunks = max(1.0, round(chars / p.pdf_chunk_chars))
in_t = (chars / p.chars_per_token) + p.overhead_tokens_per_call * chunks
out_t = chunks * p.pdf_output_per_call
c = (in_t * p_in_eff + out_t * p.price_cny_m_output) / 1e6 * n
return DocSpec("PDF 全文", n, chars, chunks, chunks, c, int(in_t * n), int(out_t * n))
batch = [md_cost(p.md_chars, p.n_pilot_md), pdf_cost(p.pdf_chars, p.n_pilot_pdf)]
full = [md_cost(p.md_chars, p.n_md), pdf_cost(p.pdf_chars, p.n_pdf)]
return batch, full
def render_md(p: Params, batch: list[DocSpec], full: list[DocSpec]) -> str:
p_in_eff = effective_input_price(p)
L: list[str] = []
L.append("# 水产库建库预算表(由 cost_model.py 生成)")
L.append("")
L.append("| 参数 | 值 | 来源 |")
L.append("|---|---|---|")
L.append(f"| LLM 单价(输入/输出,¥/百万 token) | {p.price_cny_m_input:.2f} / {p.price_cny_m_output:.2f} | 官方 deepseek-v4-flash 列表价(缓存未命中) |")
L.append(f"| 高峰加权后输入单价 | {p_in_eff:.4f} | peak_multiplier={p.peak_multiplier:.1f}(1.0=列表价口径;若高峰 ×2 计费口径则设 2.0) |")
L.append(f"| 字符/token | {p.chars_per_token}(英文) / {p.cjk_chars_per_token}(中文) | 英文实测经验;中文 1.5 待试点校准 |")
L.append(f"| MD 卡平均字符数 | {p.md_chars:,.0f} | 实测:200 篇抽样(P50=2169) |")
L.append(f"| PDF 正文平均字符数 | {p.pdf_chars:,.0f} | 假设,P1 30 篇试点后回填 |")
L.append(f"| MD 篇数 / PDF 篇数(全量) | {p.n_md:,} / {p.n_pdf:,} | census.json 实测 |")
L.append(f"| MinerU 吞吐 | {p.mineru_files_per_hour:,.0f} 篇/小时(单卡 3 端点并流) | 历史实测外推(2026-08-31 批) |")
L.append("")
L.append("## P1 试点批次(30 PDF + 470 MD)")
L.append("")
L.append("| 文档类型 | 篇数 | LLM 调用/篇 | 输入 token | 输出 token | 成本(¥) |")
L.append("|---|---|---|---|---|---|")
tb = sum(s.cost for s in batch)
for s in batch:
L.append(f"| {s.label} | {s.n_docs:,} | {s.llm_calls_per_doc:g} | {s.in_tokens:,} | {s.out_tokens:,} | {s.cost:,.2f} |")
L.append(f"| **合计** | | | | | **{tb:,.2f}** |")
L.append("")
mineru_h = p.n_pilot_pdf / p.mineru_files_per_hour
L.append(f"- MinerU GPU 耗时:30 篇约 {max(mineru_h * 60, 10):.0f} 分钟(含模型加载起步开销;历史不可处理率 {p.mineru_unprocessable:.0%} → 预计成功 ~25 篇)")
L.append("- GPU:单卡 RTX 5090(32GB),MinerU 本地运行,无 API 成本")
L.append("")
L.append("## 全量批次(31,848 MD + 13,175 PDF)")
L.append("")
L.append("| 文档类型 | 篇数 | LLM 调用/篇 | 输入 token | 输出 token | 成本(¥) |")
L.append("|---|---|---|---|---|---|")
tf = sum(s.cost for s in full)
for s in full:
L.append(f"| {s.label} | {s.n_docs:,} | {s.llm_calls_per_doc:g} | {s.in_tokens:,} | {s.out_tokens:,} | {s.cost:,.2f} |")
L.append(f"| **合计(中值)** | | | | | **{tf:,.2f}** |")
lo, hi = tf * 0.7, tf * 1.3
L.append(f"| **合计(±30% 区间)** | | | | | **{lo:,.2f} – {hi:,.2f}** |")
L.append("")
days = p.n_pdf / p.mineru_files_per_hour / p.gpu_hours_per_day
L.append(f"- MinerU 全量 PDF:{p.n_pdf:,} 篇全部过一遍(其中约 {p.mineru_unprocessable:.0%} 判为不可处理),按单卡 3 端点并流历史实测吞吐(保守取 {p.mineru_files_per_hour:,.0f} 篇/时)全天跑 **{days:,.1f} 天**;加失败重试与运维余量,预算按 1–2 天排期")
L.append("- 成本区间已含 ±30% 不确定性(pdf_chars 假设、out_in_ratio、高峰价、重试等)")
L.append("- 假设清单与待回填项见 kb-libraries/aqua/qa/budget_template.md")
L.append("")
return "\n".join(L)
def main() -> int:
ap = argparse.ArgumentParser(description="aqua 库成本校准模型")
for k, v in Params().__dict__.items():
if isinstance(v, bool):
continue
ap.add_argument(f"--{k.replace('_', '-')}", type=float, default=v)
ap.add_argument("--json", action="store_true", help="输出机器可读 JSON")
ap.add_argument("--out", default="", help="同时写入预算表 md 文件")
args = ap.parse_args()
p = Params(**{k: getattr(args, k) for k in Params().__dict__})
batch, full = model_docs(p)
if args.json:
out = {
"params": p.__dict__,
"pilot_batch": {s.label: {"n_docs": s.n_docs, "in_tokens": s.in_tokens,
"out_tokens": s.out_tokens, "cost_cny": round(s.cost, 2)}
for s in batch},
"pilot_total_cny": round(sum(s.cost for s in batch), 2),
"full_batch": {s.label: {"n_docs": s.n_docs, "in_tokens": s.in_tokens,
"out_tokens": s.out_tokens, "cost_cny": round(s.cost, 2)}
for s in full},
"full_total_cny": round(sum(s.cost for s in full), 2),
"full_total_range_cny": [round(sum(s.cost for s in full) * 0.7, 2),
round(sum(s.cost for s in full) * 1.3, 2)],
"mineru_gpu_days_full": round(p.n_pdf / p.mineru_files_per_hour / 24, 2),
}
print(json.dumps(out, ensure_ascii=False, indent=2))
return 0
md = render_md(p, batch, full)
print(md)
if args.out:
with open(args.out, "w", encoding="utf-8") as f:
f.write(md)
print(f"[written] {args.out}", file=sys.stderr)
return 0
if __name__ == "__main__":
raise SystemExit(main())