UNPKG

@tencentdb-agent-memory/memory-tencentdb

Version:

Four-layer local memory system plugin for OpenClaw — auto-captures, structures, and profiles conversational knowledge using local LLM + SQLite vector search (L0→L1→L2→L3 pipeline)

363 lines (329 loc) 13.5 kB
/** * MetricTrackingRunner / MetricTrackingRunnerFactory — LLMRunner 装饰器, * 在 LLM 调用完成后非侵入式上报 credit 消耗到 Kafka。 * * 设计原则(与 MetricTrackingStore 完全同构): * 1. 先执行原方法,拿到结果 * 2. 原方法成功后,try-catch 做上报(静默失败) * 3. 无论上报成功失败,都返回原方法的结果 * 4. 原方法抛异常时直接 re-throw,不执行任何上报 * 5. 不改变 LLMRunner / LLMRunnerFactory 接口签名 * * taskId → 指标名映射: * - "l1-extraction" → "l1_extraction_credit_rate" * - "l1-conflict-detection" → "l1_dedup_credit_rate" * - "scene-extract-*" → "l2_extraction_credit_rate" * - "persona-generation" → "l3_generation_credit_rate" * * Token Usage 获取策略: * - 优先从 inner runner 的 lastUsage side-channel 读取精确值(区分 input/output) * - 如果不可用(OpenClaw 路径),基于字符长度估算 * * Credit 计算(Producer 侧完成,Consumer 侧只做 ÷窗口周期得速率): * 公式:Credit = (input_tokens/10000 × INPUT_RATE + output_tokens/10000 × OUTPUT_RATE) × multiplier * 1 Credit = 10000 个标准 Input Tokens(以 M2.7 为锚点) * 基础费率: * - INPUT_RATE = 1.0 Credit / 10k tokens * - CACHE_RATE = 0.2 Credit / 10k tokens(一期暂不区分 cache,全按 input 计) * - OUTPUT_RATE = 4.0 Credit / 10k tokens * 模型系数:M2.7 = 1.0,旗舰型 = 15.0,极速型 = 0.8(一期默认 1.0) * 降级策略:无法区分 input/output 时,全按 input 费率(1.0)保守估算 */ import type { LLMRunner, LLMRunParams, LLMRunnerFactory, LLMRunnerCreateOptions, } from "../types.js"; import { metricProducer } from "./kafka-metric-producer.js"; // ============================ // taskId → 指标名映射 // ============================ /** LLM Runner 的 token usage 信息(side-channel) */ export interface LLMUsage { promptTokens: number; completionTokens: number; totalTokens: number; } /** 带 lastUsage side-channel 的 LLMRunner(可选扩展) */ export interface LLMRunnerWithUsage extends LLMRunner { lastUsage?: LLMUsage; } /** * 根据 taskId 映射到 Kafka 指标名。 * 返回 undefined 表示该 taskId 不需要上报 credit。 */ export function taskIdToMetricName(taskId: string): string | undefined { if (taskId === "l1-extraction") return "l1_extraction_credit_rate"; if (taskId === "l1-conflict-detection") return "l1_dedup_credit_rate"; if (taskId.startsWith("scene-extract")) return "l2_extraction_credit_rate"; if (taskId === "persona-generation") return "l3_generation_credit_rate"; return undefined; } /** * 根据 taskId 映射到评测 token 指标前缀。 * 返回 undefined 表示该 taskId 不需要按环节上报 token。 * * 产出指标名举例: * l1_extraction_input_tokens / l1_extraction_output_tokens * l1_dedup_input_tokens / l1_dedup_output_tokens * l2_extraction_input_tokens / l2_extraction_output_tokens * l3_generation_input_tokens / l3_generation_output_tokens */ export function taskIdToTokenMetricPrefix(taskId: string): string | undefined { if (taskId === "l1-extraction") return "l1_extraction"; if (taskId === "l1-conflict-detection") return "l1_dedup"; if (taskId.startsWith("scene-extract")) return "l2_extraction"; if (taskId === "persona-generation") return "l3_generation"; return undefined; } // ============================ // Credit 计算常量与函数 // ============================ /** 1 Credit 对应的 token 基数(10000 tokens = 1 Credit) */ export const TOKENS_PER_CREDIT = 10000; /** 基础费率:标准输入 1.0 Credit / 10k tokens(M2.7 锚点) */ export const INPUT_RATE = 1.0; /** 基础费率:缓存输入 0.2 Credit / 10k tokens(一期暂不区分 cache,全按 input 计) */ export const CACHE_RATE = 0.2; /** 基础费率:模型输出 4.0 Credit / 10k tokens */ export const OUTPUT_RATE = 4.0; /** 默认模型系数(M2.7 标准型) */ export const DEFAULT_MULTIPLIER = 1.0; /** * 将 Credit 值四舍五入到 5 位小数。 * 所有上报统一使用此函数,保证精度一致。 */ export function roundCredit(value: number): number { return Math.round(value * 100000) / 100000; } /** * 根据 taskId 映射到记忆层级(用于用量上报)。 * 返回 undefined 表示该 taskId 不需要上报。 */ export function taskIdToLevel(taskId: string): "L1" | "L2" | "L3" | undefined { if (taskId === "l1-extraction" || taskId === "l1-conflict-detection") return "L1"; if (taskId.startsWith("scene-extract")) return "L2"; if (taskId === "persona-generation") return "L3"; return undefined; } /** onCreditConsumed 回调参数 */ export interface CreditConsumedEvent { instanceId: string; credit: number; level: "L1" | "L2" | "L3"; taskId: string; } /** onCreditConsumed 回调类型 */ export type OnCreditConsumed = (event: CreditConsumedEvent) => void; /** * 根据精确的 input/output token 数计算 Credit 值。 * 公式:Credit = (input/10000 × INPUT_RATE + output/10000 × OUTPUT_RATE) × multiplier * * 1 Credit = 10000 个标准 Input Tokens。 * 一期简化:不区分 cache tokens,全按 input 费率计算。 * 后续 LLM SDK 支持返回 cache hit 数后再精细化。 */ export function computeCredit( inputTokens: number, outputTokens: number, multiplier: number = DEFAULT_MULTIPLIER, ): number { return ((inputTokens / TOKENS_PER_CREDIT) * INPUT_RATE + (outputTokens / TOKENS_PER_CREDIT) * OUTPUT_RATE) * multiplier; } /** * 基于字符长度粗略估算 token 数量,然后计算 Credit。 * 英文约 4 字符/token,中文约 2 字符/token,取折中值 3 字符/token。 * * 降级策略:无法区分 input/output 时,全按 input 费率(1.0)保守估算。 * 公式:Credit = estimatedTotalTokens / 10000 × INPUT_RATE × multiplier */ export function estimateCreditFromChars( inputCharLength: number, outputCharLength: number, multiplier: number = DEFAULT_MULTIPLIER, ): number { const estimatedTokens = Math.ceil((inputCharLength + outputCharLength) / 3); return (estimatedTokens / TOKENS_PER_CREDIT) * INPUT_RATE * multiplier; } // ============================ // MetricTrackingRunner(装饰器) // ============================ /** * 包装 LLMRunner,在 run() 完成后异步上报 credit 消耗到 Kafka。 * * 上报的 value 是 **Credit 值**(已完成 Token → Credit 转换), * Consumer 侧只需 ÷ 窗口周期得速率,不需要再做 Token → Credit 换算。 * * 安全保证: * - 上报失败静默忽略,绝不影响 run() 的返回值 * - 原方法抛异常时不上报,异常正常传播 * - 不改变 run() 的签名和返回值 */ export class MetricTrackingRunner implements LLMRunner { private readonly inner: LLMRunner; private readonly getInstanceId: () => string | undefined; private readonly multiplier: number; private readonly onCreditConsumed?: OnCreditConsumed; /** Accumulated credit consumed across all run() calls on this runner instance. */ accumulatedCredit = 0; constructor( inner: LLMRunner, getInstanceId: () => string | undefined, multiplier: number = DEFAULT_MULTIPLIER, onCreditConsumed?: OnCreditConsumed, ) { this.inner = inner; this.getInstanceId = getInstanceId; this.multiplier = multiplier; this.onCreditConsumed = onCreditConsumed; } async run(params: LLMRunParams): Promise<string> { // 0. 注入 instanceId(如果调用方没传,从 getInstanceId 回调获取) const enrichedParams = params.instanceId ? params : { ...params, instanceId: this.getInstanceId() }; // 1. 先执行原方法,拿到结果(异常直接 re-throw) const text = await this.inner.run(enrichedParams); // 2. 原方法成功后,try-catch 做上报(静默失败) try { const metricName = taskIdToMetricName(params.taskId); if (metricName) { const instanceId = enrichedParams.instanceId ?? this.getInstanceId(); if (instanceId) { // 优先从 inner runner 的 lastUsage side-channel 读取精确 token 数 const innerWithUsage = this.inner as LLMRunnerWithUsage; let creditValue: number; let inputTokens: number; let outputTokens: number; if (innerWithUsage.lastUsage && innerWithUsage.lastUsage.totalTokens > 0) { inputTokens = innerWithUsage.lastUsage.promptTokens; outputTokens = innerWithUsage.lastUsage.completionTokens; creditValue = computeCredit(inputTokens, outputTokens, this.multiplier); } else { // 无精确 token 数时,基于字符长度估算 const inputChars = (params.prompt?.length ?? 0) + (params.systemPrompt?.length ?? 0); const outputChars = text.length; inputTokens = Math.ceil(inputChars / 3); outputTokens = Math.ceil(outputChars / 3); creditValue = estimateCreditFromChars(inputChars, outputChars, this.multiplier); } // 统一 round 到 5 位小数(所有上报使用相同数据) const roundedCredit = roundCredit(creditValue); // Accumulate credit for caller to read this.accumulatedCredit += roundedCredit; if (roundedCredit > 0) { // 上报聚合指标(5 位小数,静默失败) try { metricProducer.send({ metric: metricName, instanceId, value: roundedCredit, source: "core", }); } catch { // 指标发送失败静默忽略 } // 上报用量回调(同样 5 位小数,静默失败) if (this.onCreditConsumed) { const level = taskIdToLevel(params.taskId); if (level) { try { this.onCreditConsumed({ instanceId, credit: roundedCredit, level, taskId: params.taskId, }); } catch { // 静默失败,绝不影响业务 } } } } // 上报原始 Token 指标(用于聚合侧计算 TPM) // 只有 > 0 的指标才上报,静默失败 try { if (inputTokens > 0) { metricProducer.send({ metric: "llm_input_tokens", instanceId, value: inputTokens, source: "core", }); } if (outputTokens > 0) { metricProducer.send({ metric: "llm_output_tokens", instanceId, value: outputTokens, source: "core", }); } } catch { // Token 指标上报失败静默忽略,绝不影响业务 } // 上报按环节区分的 Token 指标(评测用,带 traceId) try { const tokenPrefix = taskIdToTokenMetricPrefix(params.taskId); if (tokenPrefix && inputTokens > 0) { metricProducer.send({ metric: `${tokenPrefix}_input_tokens`, instanceId, value: inputTokens, source: "core", }); } if (tokenPrefix && outputTokens > 0) { metricProducer.send({ metric: `${tokenPrefix}_output_tokens`, instanceId, value: outputTokens, source: "core", }); } } catch { // 静默失败 } } } } catch { // 静默失败,绝不影响业务 } // 3. 无论上报成功失败,都返回原方法的结果 return text; } } // ============================ // MetricTrackingRunnerFactory(装饰器) // ============================ /** * 包装 LLMRunnerFactory,创建出的 Runner 自带 credit 上报能力。 * * 注入点:在 tdai-core.ts 的 wirePipelineRunners() 中包装 factory。 * 这是唯一的"改动"——属于可观测性代码的注入点,不是业务逻辑的修改。 * * @param multiplier 模型系数(默认 1.0 = M2.7 标准型)。 * 后续可从配置中读取,支持多模型动态切换。 */ export class MetricTrackingRunnerFactory implements LLMRunnerFactory { private readonly inner: LLMRunnerFactory; private readonly getInstanceId: () => string | undefined; private readonly multiplier: number; private readonly onCreditConsumed?: OnCreditConsumed; constructor( inner: LLMRunnerFactory, getInstanceId: () => string | undefined, multiplier: number = DEFAULT_MULTIPLIER, onCreditConsumed?: OnCreditConsumed, ) { this.inner = inner; this.getInstanceId = getInstanceId; this.multiplier = multiplier; this.onCreditConsumed = onCreditConsumed; } createRunner(opts?: LLMRunnerCreateOptions): LLMRunner { const innerRunner = this.inner.createRunner(opts); return new MetricTrackingRunner(innerRunner, this.getInstanceId, this.multiplier, this.onCreditConsumed); } }