feat(harness): 成本/Token 预算护栏 —— 单任务硬上限 + 单用户日预算(恒温器最后一环)

token 用量估算计量(CJK≈1/字、ASCII≈1/4字,无需分词器,护栏够用)。

单任务硬上限(dispatcher):Budget 挂 ctx 沿图透传,各 LLM 节点(对话/ReAct/compose/
报告)入口计输入 token、出口计输出,触顶即中止整图——防失控成本(死循环/超大报告)。
报告路径优雅降级:触顶跳过剩余章节出部分稿,不整体失败。预算来源 Meta.token_budget
或 env TASK_TOKEN_BUDGET(默认 20 万)。

单用户日预算(gateway):dispatcher 收尾经 NATS 回写 UsageEvent → 网关按用户按天累计
Redis(48h 过期自滚动)→ 提交前门控 USER_DAILY_TOKEN_BUDGET(0=不限,超额 402)。
/billing 升级为真实用量:当日已用 / 日预算 / 余额。

契约 UsageEvent + MetaTokenBudget + SubjectUsage;bus Publish/SubscribeUsage;
orchestrator SetUsageSink + 预算触顶 failed(不计熔断)。harness budget 5 单测,三模块全绿。
live:单任务 budget=30 → failed(已用约689);用户日 budget=200 → /billing remaining=0 → 402。

至此 harness 由「测温计」完成向「恒温器」的演进(评测闭环/纠偏/忠实度/脱敏/输入护栏/预算六项)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-26 10:00:51 +08:00
parent 2f78fc565e
commit faa1871760
16 changed files with 442 additions and 17 deletions
+23
View File
@@ -363,6 +363,29 @@ func (b *Bus) SubscribeEval(onEvent func(*contract.EvalEvent)) (unsub func() err
return sub.Unsubscribe, nil
}
// PublishUsage 广播一次任务 token 用量(dispatcher 收尾调用)。
func (b *Bus) PublishUsage(ev *contract.UsageEvent) error {
data, err := json.Marshal(ev)
if err != nil {
return err
}
return b.nc.Publish(contract.SubjectUsage, data)
}
// SubscribeUsage 订阅 token 用量(网关调用,累计到用户日预算 / 计费)。
func (b *Bus) SubscribeUsage(onEvent func(*contract.UsageEvent)) (unsub func() error, err error) {
sub, err := b.nc.Subscribe(contract.SubjectUsage, func(m *nats.Msg) {
var ev contract.UsageEvent
if json.Unmarshal(m.Data, &ev) == nil {
onEvent(&ev)
}
})
if err != nil {
return nil, fmt.Errorf("subscribe usage: %w", err)
}
return sub.Unsubscribe, nil
}
// ---- 人工审批(HITLcore NATS pub-sub----
// PublishApproval 广播一次人工审批决定(网关在收到 UI 的批准/拒绝后调用)。
+19
View File
@@ -40,8 +40,24 @@ const (
// 自动化评测结果回写:dispatcher 评完经此广播,网关订阅落 PG 并供 UI 查询。core NATS pub-sub。
SubjectEval = "sundynix.eval.task"
// Token 用量回写:dispatcher 任务收尾经此广播本轮 token 用量,网关订阅累加到用户日预算并供计费。core NATS pub-sub。
SubjectUsage = "sundynix.usage.task"
)
// UsageEvent 是一次任务的 token 用量(dispatcher 收尾经 SubjectUsage 回流给网关累计/计费)。
// dispatcher 不持有计价,只报原始 token + 模型名;真钱成本由网关据 Pricing 折算。
type UsageEvent struct {
TaskID string `json:"task_id"`
UserID string `json:"user_id,omitempty"`
Model string `json:"model,omitempty"`
PromptTok int `json:"prompt_tok"` // 输入 token(估算)
CompTok int `json:"comp_tok"` // 输出 token(估算)
TotalTok int `json:"total_tok"` // 合计
Exceeded bool `json:"exceeded"` // 是否触顶单任务预算被中止
TS int64 `json:"ts"` // unix 毫秒
}
// 评测质量分级(据综合分 + 忠实度阈值,闭环门控/告警用)。
const (
EvalOK = "ok" // 综合 ≥ 0.75 且无忠实度风险
@@ -106,6 +122,9 @@ const (
// MetaSafetyCheck 是输入护栏「灰区升级」标志:网关 Tier1(归一化+正则)判为疑似但不确定时置 true,
// Dispatcher 执行前据此调 LLM jailbreak 分类器(Tier2)裁决。明确干净/明确恶意的输入不带此标志,不付 LLM 成本。
MetaSafetyCheck = "safety_check"
// MetaTokenBudget 是单任务 token 预算上限(数字):网关按用户/套餐下发,Dispatcher 据此封顶单任务用量,
// 触顶即中止(防失控成本)。缺省用 Dispatcher 的 env TASK_TOKEN_BUDGET。
MetaTokenBudget = "token_budget"
// 配置控制面按 kind 寻址:sundynix.config.<kind>.get / .updated。
// Gateway 持有配置,消费方(Dispatcher/mcp-go)经 NATS 取用/订阅变更。