Claude理财降低账单AI 评分 · 待评分AI 辅助编辑·未发现明显提示词注入风险n8n.io

用 n8n 账本追踪 AI Agent 成本与决策

通过 Webhook 接收 Agent 事件 → 计算 token 成本 → 持久化账本 → WhatsApp 预算告警与日报

有一点经验步骤流程提示词贴士
给谁
运行多 Agent 或 Supervisor 工作流、需要追踪每次 LLM/工具调用成本的技术人员
你会得到
获得按 run、agent、model、tool 汇总的实时成本账本,超预算时 WhatsApp 告警,每日定时摘要
你需要
n8n · whatsapp

硬约束

需要可访问的 n8n 实例;WhatsApp Business 账号与凭证;无需 Anthropic 凭证。账本数据存储在 n8n workflow static data 中,仅在工作流激活后持久化。

他们做了什么

作者 oneclick-ai 在 n8n 上发布了一个模板工作流,通过两个 Webhook 端点接收 AI Agent 的事件(llm_call、tool_call、decision、error),利用 Code 节点验证字段、按模型定价计算成本,将结果追加到 n8n workflow static data 的持久账本中,并在单 run 超预算时发送一次性 WhatsApp 告警。另设 GET 端点与每日定时触发(cron 09:00),用于聚合账本并返回 JSON 报告或通过 WhatsApp 发送成本/活动摘要。

效果如何

来源报告该模板已发布在 n8n.io 模板库(author_report)。截至采集时 views=2,未提供实测 ROI 或生产规模数据。

值得知道

账本依赖 n8n workflow static data,仅在激活(active)状态下持久化;若实例重启或工作流重新导入,历史数据可能丢失。pricingPerMillionUsd 使用前缀匹配模型名(如 claude-sonnet 匹配 claude-sonnet-4-6),需确保模型前缀与定价键一致。来源未声明支持多实例共享账本;如需跨实例汇总,需自行外接数据库。本站未实测(reproduction=not_tested),搭建后建议先用样例事件验证定价与告警逻辑。

n8n 工作流模板

结构已检查 · 未实际运行

下载后在 n8n 选择“Import from File”,或复制 JSON 后粘贴到编辑器。先配置自己的账号,再用测试数据运行;模板默认未启用。

配置与测试清单

需要绑定的凭证

  • 导入后按各需鉴权节点绑定你自己的凭证;本附件不含原作者密钥。

需要替换的配置

  • 源模板来自 n8n 公开库 #20257(约 19 个节点)。导入后先保持未启用,核对触发器、表达式与外部 API 端点。
  • Sticky Note 中的说明仅供参考;按自己的账号、表结构或频道 ID 替换占位配置。

测试输入与预期结果

  • 在测试环境手动执行一次主路径,确认每个需凭证的节点已连接且无鉴权错误。
  • 用一条合成/脱敏样例数据跑通,核对输出字段与下游写入目标,再考虑生产启用。

模板处理说明

  • 已移除原作者凭证绑定、webhookId 与执行数据,工作流设为未启用;节点、连线与原文提示词保持原样。未在 n8n 中实际运行。
  • 未改业务逻辑;运行前请自行核对密钥、配额与隐私合规。

模板获取日期: 2026-10-25

步骤

  1. 导入模板

    在 n8n 中打开模板页面,导入「Track AI agent costs and decisions via webhooks and WhatsApp digests」工作流。导入后可见两个主要分支:Record Events(事件记录)与 Report & Daily Digest(报告与日报)。

  2. 配置凭证与常量

    1) 在 WhatsApp Business Cloud 节点关联你的 WhatsApp Business 凭证,填写 whatsappBusinessPhoneId(WhatsApp 电话号码 ID)与 whatsappRecipientPhone(接收告警的手机号)。 2) 编辑 Ledger Config 节点:设置 per-run 预算(runBudgetUsd,默认 0.5)、单条 run 最大条目数(maxEntries,默认 5000)、最大保留 run 数(maxRuns,默认 200)、每百万 token 定价表(pricingPerMillionUsd,JSON 格式,默认含 default/claude-haiku/claude-sonnet/claude-opus 四档)。 3) 编辑 Report Config 节点:设置日报时间窗口(digestWindowHours,默认 24)、最近条目限制(recentLimit,默认 20)、以及 WhatsApp 发送目标 ID。

  3. 激活并获取 Webhook URL

    激活工作流(Active)。只有激活后,static data 才会在每次执行后持久化保存。复制两个 Webhook 路径的完整 URL: - POST /webhook/agent-ledger-event(接收事件) - GET /webhook/agent-ledger-report(查询报告) 将 POST URL 配置到你的 Agent 或 Supervisor 工作流中,在每次 LLM 调用、工具调用或决策后发送事件。

  4. 发送 Agent 事件到账本

    向 POST 端点发送事件。支持单条对象或 events 数组批量发送。每条事件需包含 runId、agentName、eventType(llm_call/tool_call/decision/error)。对于 llm_call,提供 model、inputTokens、outputTokens;对于 tool_call,提供 toolName,可选 toolCostUsd;对于 decision,提供 decision 字段。来源中的 Validate and Price Events 节点会校验字段、按 pricingPerMillionUsd 计算成本(或接受显式 costUsd),随后 Append to Ledger 节点将事件写入 static data,更新 per-run 汇总,并在超预算时触发一次性 WhatsApp 告警。

    运行提示词 · 来源原文。Validate and Price Events 节点的 JS Code 字段

    const cfg = $input.first().json;
    const body = cfg.body || {};
    
    let pricing;
    try { pricing = JSON.parse(cfg.pricingPerMillionUsd); } catch (e) { pricing = { default: { input: 3, output: 15 } }; }
    
    const TYPES = ['tool_call', 'decision', 'llm_call', 'error'];
    const raw = Array.isArray(body.events) ? body.events : (body.eventType ? [body] : []);
    const errors = [];
    const events = [];
    const num = v => (Number(v) > 0 ? Number(v) : 0);
    const preview = v => v === undefined || v === null ? null : (typeof v === 'string' ? v : JSON.stringify(v)).slice(0, 500);
    
    function rateFor(model) {
      const keys = Object.keys(pricing)
        .filter(k => k !== 'default' && String(model || '').startsWith(k))
        .sort((a, b) => b.length - a.length);
      return pricing[keys[0]] || pricing.default || { input: 0, output: 0 };
    }
    
    if (raw.length === 0) errors.push('Body must contain an events array or a single event with eventType');
    
    raw.forEach((e, i) => {
      const p = 'events[' + i + ']';
      if (!e || typeof e !== 'object') { errors.push(p + ' must be an object'); return; }
      if (!e.runId) errors.push(p + ' is missing runId');
      if (!TYPES.includes(e.eventType)) errors.push(p + ' eventType must be one of ' + TYPES.join(', '));
      if (e.eventType === 'tool_call' && !e.toolName) errors.push(p + ' tool_call requires toolName');
      if (e.eventType === 'decision' && !e.decision) errors.push(p + ' decision requires decision');
    
      const inTok = num(e.inputTokens), outTok = num(e.outputTokens), cacheTok = num(e.cacheReadTokens);
      const r = rateFor(e.model);
      const cacheRate = r.cacheRead !== undefined ? r.cacheRead : (r.input || 0) * 0.1;
      const tokenCost = inTok / 1e6 * (r.input || 0) + outTok / 1e6 * (r.output || 0) + cacheTok / 1e6 * cacheRate;
      const explicit = e.costUsd !== undefined && Number(e.costUsd) >= 0 ? Number(e.costUsd) : null;
      const costUsd = explicit !== null ? explicit : tokenCost + num(e.toolCostUsd);
    
      events.push({
        runId: e.runId,
        taskId: e.taskId || null,
        agentName: e.agentName || 'unknown',
        eventType: e.eventType,
        eventTimestamp: e.timestamp || new Date().toISOString(),
        toolName: e.toolName || null,
        toolInput: preview(e.toolInput),
        toolOutput: preview(e.toolOutput),
        model: e.model || null,
        decision: e.decision || null,
        reasoning: e.reasoning ? String(e.reasoning).slice(0, 1000) : null,
        confidence: typeof e.confidence === 'number' ? e.confidence : null,
        status: e.status || (e.eventType === 'error' ? 'failed' : 'ok'),
        errorMessage: e.errorMessage ? String(e.errorMessage).slice(0, 500) : null,
        inputTokens: inTok,
        outputTokens: outTok,
        cacheReadTokens: cacheTok,
        latencyMs: num(e.latencyMs),
        costUsd: Math.round(costUsd * 1e6) / 1e6,
        metadata: e.metadata && typeof e.metadata === 'object' ? e.metadata : {}
      });
    });
    
    return [{
      json: {
        valid: errors.length === 0,
        validationError: errors.join('; '),
        events,
        runBudgetUsd: Number(cfg.runBudgetUsd) || 0,
        maxEntries: Number(cfg.maxEntries) || 5000,
        maxRuns: Number(cfg.maxRuns) || 200,
        whatsappBusinessPhoneId: cfg.whatsappBusinessPhoneId,
        whatsappRecipientPhone: cfg.whatsappRecipientPhone
      }
    }];

    运行提示词 · 来源原文。Append to Ledger 节点的 JS Code 字段

    const s = $input.first().json;
    const sd = $getWorkflowStaticData('global');
    if (!sd.ledger) sd.ledger = { seq: 0, entries: [], runs: {} };
    const L = sd.ledger;
    const now = new Date().toISOString();
    const touched = [];
    const alerts = [];
    let seqFrom = null;
    
    for (const ev of s.events) {
      L.seq += 1;
      if (seqFrom === null) seqFrom = L.seq;
      L.entries.push({ seq: L.seq, id: 'led-' + L.seq, recordedAt: now, ...ev });
    
      const r = L.runs[ev.runId] || (L.runs[ev.runId] = {
        runId: ev.runId, taskId: ev.taskId, agents: {}, totalCostUsd: 0,
        inputTokens: 0, outputTokens: 0, llmCalls: 0, toolCalls: 0, decisions: 0, errors: 0,
        firstEventAt: ev.eventTimestamp, lastEventAt: ev.eventTimestamp, budgetAlerted: false
      });
      r.agents[ev.agentName] = (r.agents[ev.agentName] || 0) + 1;
      r.totalCostUsd = Math.round((r.totalCostUsd + ev.costUsd) * 1e6) / 1e6;
      r.inputTokens += ev.inputTokens;
      r.outputTokens += ev.outputTokens;
      if (ev.eventType === 'llm_call') r.llmCalls += 1;
      if (ev.eventType === 'tool_call') r.toolCalls += 1;
      if (ev.eventType === 'decision') r.decisions += 1;
      if (ev.eventType === 'error' || ev.status !== 'ok') r.errors += 1;
      r.lastEventAt = ev.eventTimestamp;
      if (!touched.includes(ev.runId)) touched.push(ev.runId);
    
      if (s.runBudgetUsd > 0 && r.totalCostUsd > s.runBudgetUsd && !r.budgetAlerted) {
        r.budgetAlerted = true;
        alerts.push('Run ' + ev.runId + ' spent $' + r.totalCostUsd.toFixed(4) + ' (budget $' + s.runBudgetUsd.toFixed(2) + ') after ' + (r.llmCalls + r.toolCalls) + ' calls. Last agent: ' + ev.agentName);
      }
    }
    
    // Retention
    if (L.entries.length > s.maxEntries) L.entries = L.entries.slice(-s.maxEntries);
    const ids = Object.keys(L.runs);
    if (ids.length > s.maxRuns) {
      ids.sort((a, b) => String(L.runs[a].lastEventAt).localeCompare(String(L.runs[b].lastEventAt)))
         .slice(0, ids.length - s.maxRuns)
         .forEach(id => { delete L.runs[id]; });
    }
    
    const runs = touched.filter(id => L.runs[id]).map(id => {
      const r = L.runs[id];
      return {
        runId: r.runId, totalCostUsd: r.totalCostUsd, inputTokens: r.inputTokens, outputTokens: r.outputTokens,
        llmCalls: r.llmCalls, toolCalls: r.toolCalls, decisions: r.decisions, errors: r.errors,
        budgetUsd: s.runBudgetUsd || null, budgetExceeded: s.runBudgetUsd > 0 && r.totalCostUsd > s.runBudgetUsd
      };
    });
    
    return [{
      json: {
        budgetAlert: alerts.length > 0,
        whatsappText: '\uD83D\uDEA8 Agent Ledger budget alert\n\n' + alerts.join('\n'),
        whatsappBusinessPhoneId: s.whatsappBusinessPhoneId,
        whatsappRecipientPhone: s.whatsappRecipientPhone,
        webhookResponse: { recorded: s.events.length, seqFrom, seqTo: L.seq, ledgerSize: L.entries.length, runs }
      }
    }];
  5. 查询报告与接收日报

    通过 GET /webhook/agent-ledger-report 查询账本,可选参数:?runId=、?agent=、?since=<ISO date>、?limit=。Build Ledger Report 节点会按 agent、tool、model、decision 聚合,返回 totals、byAgent、byTool、byModel、byDecision、topRuns、recentEntries。若请求来自 Webhook,直接返回 JSON;若由 Daily Digest Schedule(默认每天 09:00 cron)触发,则生成 WhatsApp 文本摘要并发送。

    运行提示词 · 来源原文。Build Ledger Report 节点的 JS Code 字段

    const c = $input.first().json;
    const isWebhook = !!c.headers;
    const q = c.query || {};
    const sd = $getWorkflowStaticData('global');
    const L = sd.ledger || { seq: 0, entries: [], runs: {} };
    const round = n => Math.round(n * 1e6) / 1e6;
    
    let entries = L.entries;
    let windowLabel = 'all recorded history';
    if (!isWebhook) {
      const hours = Number(c.digestWindowHours) || 24;
      const cutoff = Date.now() - hours * 3600000;
      entries = entries.filter(e => new Date(e.recordedAt).getTime() >= cutoff);
      windowLabel = 'last ' + hours + 'h';
    }
    if (q.runId) entries = entries.filter(e => e.runId === q.runId);
    if (q.agent) entries = entries.filter(e => e.agentName === q.agent);
    if (q.since) entries = entries.filter(e => e.recordedAt >= q.since);
    
    const group = keyFn => {
      const m = {};
      for (const e of entries) {
        const k = keyFn(e);
        if (k === null || k === undefined) continue;
        const g = m[k] || (m[k] = { name: k, events: 0, costUsd: 0, inputTokens: 0, outputTokens: 0, errors: 0, totalLatencyMs: 0 });
        g.events += 1;
        g.costUsd += e.costUsd;
        g.inputTokens += e.inputTokens;
        g.outputTokens += e.outputTokens;
        g.totalLatencyMs += e.latencyMs;
        if (e.eventType === 'error' || e.status !== 'ok') g.errors += 1;
      }
      return Object.values(m)
        .map(({ totalLatencyMs, ...g }) => ({ ...g, costUsd: round(g.costUsd), avgLatencyMs: g.events ? Math.round(totalLatencyMs / g.events) : 0 }))
        .sort((a, b) => b.costUsd - a.costUsd);
    };
    
    const byAgent = group(e => e.agentName);
    const byTool = group(e => e.eventType === 'tool_call' ? e.toolName : null);
    const byModel = group(e => e.model);
    const topRuns = group(e => e.runId).slice(0, 5);
    
    const dm = {};
    for (const e of entries.filter(x => x.eventType === 'decision')) {
      const d = dm[e.decision] || (dm[e.decision] = { decision: e.decision, count: 0, confidenceSum: 0, confidenceN: 0 });
      d.count += 1;
      if (typeof e.confidence === 'number') { d.confidenceSum += e.confidence; d.confidenceN += 1; }
    }
    const byDecision = Object.values(dm)
      .map(d => ({ decision: d.decision, count: d.count, avgConfidence: d.confidenceN ? Math.round(d.confidenceSum / d.confidenceN * 100) / 100 : null }))
      .sort((a, b) => b.count - a.count);
    
    const totals = {
      events: entries.length,
      costUsd: round(entries.reduce((a, e) => a + e.costUsd, 0)),
      inputTokens: entries.reduce((a, e) => a + e.inputTokens, 0),
      outputTokens: entries.reduce((a, e) => a + e.outputTokens, 0),
      llmCalls: entries.filter(e => e.eventType === 'llm_call').length,
      toolCalls: entries.filter(e => e.eventType === 'tool_call').length,
      decisions: entries.filter(e => e.eventType === 'decision').length,
      errors: entries.filter(e => e.eventType === 'error' || e.status !== 'ok').length
    };
    totals.errorRate = totals.events ? Math.round(totals.errors / totals.events * 1000) / 1000 : 0;
    
    const limit = Number(q.limit) || Number(c.recentLimit) || 20;
    const recentEntries = entries.slice(-limit).reverse();
    
    const report = {
      generatedAt: new Date().toISOString(),
      window: windowLabel,
      filters: { runId: q.runId || null, agent: q.agent || null, since: q.since || null },
      totals, byAgent, byTool, byModel, byDecision, topRuns, recentEntries,
      runSummaries: q.runId && L.runs[q.runId] ? L.runs[q.runId] : undefined
    };
    
    const top = arr => arr.length ? arr[0].name + ' ($' + arr[0].costUsd.toFixed(4) + ')' : 'n/a';
    const whatsappText = '\uD83D\uDCD2 Agent Ledger digest (' + windowLabel + ')\n'
      + 'Events: ' + totals.events + ' | LLM calls: ' + totals.llmCalls + ' | Tool calls: ' + totals.toolCalls + ' | Decisions: ' + totals.decisions + '\n'
      + 'Total cost: $' + totals.costUsd.toFixed(4) + ' | Errors: ' + totals.errors + '\n'
      + 'Top agent: ' + top(byAgent) + '\n'
      + 'Top tool: ' + top(byTool) + '\n'
      + 'Top model: ' + top(byModel) + '\n'
      + 'Costliest run: ' + top(topRuns);
    
    return [{ json: { isWebhook, report, whatsappText, whatsappBusinessPhoneId: c.whatsappBusinessPhoneId, whatsappRecipientPhone: c.whatsappRecipientPhone } }];

可复制提示词

搭建指导 · 本站建议版本。交给搭建助手,先确认输入和权限

这一段是提示词,页面不会在这里插入广告。

你是一个 n8n 模板搭建助手。用户希望复现「Track AI agent costs and decisions via webhooks and WhatsApp digests」工作流。请按以下顺序协助:
1. 确认用户已有 n8n 实例和 WhatsApp Business 凭证;
2. 指导导入模板并检查 Ledger Config / Report Config 中的预算、定价、保留限制;
3. 提醒激活工作流后 static data 才会持久化;
4. 提供 POST / GET webhook URL 的复制位置;
5. 给出事件 JSON 样例供测试。

粘贴到对话助手以获取分步搭建指导;节点内代码已在步骤中提供,无需额外提示词。

来源

Track AI agent costs and decisions via webhooks and WhatsApp digests · oneclick-ai · n8n.io · 2026-10-01

留言与反馈

试过这个方法吗?分享效果、遇到的问题或改进建议。留言审核后公开,请勿填写敏感信息。

正在加载…

    相关用例