开源 Agent 套件 ECC 怎么管住 LLM 成本:从模型路由到账单审计的几处闸门
本文基于 ECC 仓库 commit 591ab5c(2026-07-29)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/affaan-m/ECC 最新代码与文档为准。
LLM 的钱不是在”调用次数”上花掉的,是在几个没人守门的位置漏掉的——选错模型的那一行、没检查预算就发出去的那一次请求、对着一个永远不会成功的错误重试三遍、以及每轮都完整重发的那段系统提示。ECC 这套装在编码 Agent 之上的增强件(MIT 许可,agents 目录 67 个 agent、skills 目录 281 个技能、commands 目录 94 个命令)里,与成本直接相关的东西被拆成了三块:一块教你写应用时把闸门放在哪,一块记录你自己用掉了多少,还有一块是事情已经失控之后的排查顺序。这三块的分界很清楚,值得单独看。
先说清本篇和站内已有内容的分工。/learn/agent-chengben-shikong/ 讲的是成本失控的通用识别与止血思路,/learn/moxing-luyou-celve/ 讲模型路由该按什么维度分层,/learn/token-chengben-youhua/ 讲 token 层面的通用优化手法——那三篇是方法论。本篇不重复方法论,只看一个任何人都能当场 clone 下来对照的开源项目,把这些原则具体落到了哪些文件、哪些字段、哪几行判断上,以及它为此付出了什么代价。
一、写应用时的四个闸门,位置比手法重要
skills/cost-aware-llm-pipeline/SKILL.md 这个技能只干一件事:把成本控制归纳成四个可组合的点,然后给出每个点的最小实现。它自己的定位写得很直白——model routing、budget tracking、retry logic、prompt caching,四者组合成一条流水线。
第一个闸门是按任务复杂度选模型。技能里给的判断函数结构是这样的:
def select_model(
text_length: int,
item_count: int,
force_model: str | None = None,
) -> str:
"""Select model based on task complexity."""
if force_model is not None:
return force_model
if text_length >= _SONNET_TEXT_THRESHOLD or item_count >= _SONNET_ITEM_THRESHOLD:
return MODEL_SONNET # Complex task
return MODEL_HAIKU # Simple task
真正要看的不是阈值本身(_SONNET_TEXT_THRESHOLD 按字符数、_SONNET_ITEM_THRESHOLD 按条目数,都是文件里给的示例常量,你得按自己的数据调),而是三个设计选择:默认走便宜档、复杂才升档;force_model 作为逃生口保留人工覆盖;模型名收敛成模块级常量而不是散在各处字符串。技能的反模式清单里就明确点了”把模型名硬编码到代码各处”这一条。这三点决定了你日后调阈值时改一处还是改二十处。
第二个闸门是预算检查要发生在请求之前。技能给的成本追踪器是 frozen dataclass,add() 返回一个新的 tracker 而不是就地修改:
@dataclass(frozen=True, slots=True)
class CostTracker:
budget_limit: float = 1.00
records: tuple[CostRecord, ...] = ()
def add(self, record: CostRecord) -> "CostTracker":
"""Return new tracker with added record (never mutates self)."""
return CostTracker(
budget_limit=self.budget_limit,
records=(*self.records, record),
)
budget_limit 那个默认值只是文件里随手写的占位,真用起来必须由调用方按批次规模显式传入,别把它当成推荐值。为什么非要不可变?技能自己给了答案:可变的成本状态让调试和审计变得困难。你排查一条超支的批处理时,需要的是”每一步之后账面是什么样”,而不是一个被反复覆写的最终数字。组合示例里,tracker.over_budget 的检查排在模型路由之后、API 调用之前,超了直接抛 BudgetExceededError——闸门在门外,不在门里。
第三个闸门是把重试收窄。技能里可重试的错误只有连接错误、限流错误、服务端内部错误三类,认证错误和参数错误一律立即抛出。这条看着像可靠性问题,其实是纯成本问题:一个永远不会成功的请求重试三次,就是三份钱换零份结果。
第四个闸门是提示缓存。技能给的写法是在消息内容块上打 cache_control 标记,把长而稳定的系统提示和易变的用户输入拆成两个 text 块,只给前者打标:
{
"type": "text",
"text": system_prompt,
"cache_control": {"type": "ephemeral"},
}
这里有个容易被忽略的顺序要求:稳定内容必须在前、可变内容在后,缓存才有意义。各家服务商的缓存计费与生效条件规则不同且会调整,以官方最新说明为准,技能里只写机制不写数额。
二、自己的账怎么记:钩子、日志、报表
前面四点是你写应用时用的。但你用编码 Agent 干活本身也在花钱,这部分归 skills/cost-tracking/SKILL.md 和它背后的钩子管。
数据来源是 scripts/hooks/cost-tracker.js,注册在 hooks/hooks.json 里、id 为 stop:cost-tracker 的 Stop 钩子。它的工作方式是:从 Stop 事件的输入里拿到 transcript_path,扫描那份会话 JSONL,把所有 assistant 轮次的 token 用量加起来,然后往 ~/.claude/metrics/costs.jsonl 追加一行。行的结构是固定的:timestamp、session_id、transcript_path、model、input_tokens、output_tokens、cache_write_tokens、cache_read_tokens、estimated_cost_usd。
这个脚本的文件头注释比脚本本身更有教学价值,因为它老老实实记了两次翻车。第一次:早期版本以为 Stop 事件的载荷里直接带 usage 和 model,实际并没有,于是长期静默写出全零的行——日志天天在长,数据全是废的。修法是改去读 Claude Code 本来就传过来的 transcript 文件。第二次:Claude Code 对每个内容块写一行 JSONL,同一次 API 响应会跨多行、每行重复同一份 message.usage,逐行求和会把总额放大数倍。修法是按 message.id 去重、每个 id 只留最后一行的用量。
const key = (typeof msg.id === 'string' && msg.id)
? msg.id
: `__line_${++syntheticKey}`;
usageById.set(key, msg.usage);
还有一层:脚本里定义了一个可选的”harness-cost 契约”。如果你的状态栏(它能直接从 Claude Code 拿到 cost.total_cost_usd)把 {ts, cost_usd} 写进系统临时目录下的 harness-cost-<session_id>.json,钩子会在这份缓存足够新鲜时优先采用它,否则退回自己按 transcript 估算。注释给的理由很实在:脚本内置的费率表表达不了分层计费和长缓存的倍率,会低估长会话;而且跨 --resume 边界求和会重复计算,cost.total_cost_usd 是按进程算的。没有写入方时行为不变。
配套的读法是 commands/cost-report.md(/cost-report,带 csv 参数可导出)。它和 cost-tracking 技能反复强调同一件事:每行是该会话到那一刻的累计快照,不是增量。所以统计总额要先按 session_id 归约到最新一行再求和,逐行相加会重复计数。另外两条纪律也一致:有 estimated_cost_usd 就别自己拿 token 数重算;日志不存在就直说没有,不要编数据。命令里选 node 而不是 sqlite3 或 jq,理由是三个操作系统上行为一致。
关于钩子这个机制本身怎么挂、怎么调试,可以参考 /learn/claude-code-hooks/;关于长期的账单趋势监控该怎么建,/learn/api-chengben-jiankong/ 讲得更系统。
三、已经烧起来了:审计的排查顺序
第三块是 skills/ecc-tools-cost-audit/SKILL.md,定位很窄——它明说自己不是通用账单技能,也不是全仓代码评审,只服务于一个场景:怀疑某个 GitHub App 在疯狂建 PR、绕过用量限制、把免费用户漏进付费模型路径。
它值得学的地方是排查顺序被写死了。第一步冻结范围,先确认审的是哪个面:webhook 路由、队列生产者、队列消费者、PR 创建路径、用量预留/计费路径、模型路由路径。第二步追入口,先看主入口文件,把所有入队路径画出来,确认哪些 GitHub 事件共用同一种队列任务、push/pull_request/synchronize/评论/手动重跑会不会汇到同一条昂贵路径。第三步追 worker 和副作用,重点判断一次排队的分析是否必然产出结果——如果它可能花完 token 之后、在结果落盘前失败,就归类为”烧钱但输出坏掉”。
第四步才是逐条审高信号路径,四类:PR 增殖、配额绕过、付费模型泄漏、重试烧钱。其中配额绕过那条讲得最具体:如果配额在入队前检查、而用量只在 worker 里增加,并发请求就能一起通过前门,这是真实的竞态。PR 增殖那条则把”App 自己生成的分支能再次进入分析”标为最高优先级的递归风险。
第五步是修复的优先级——技能规定按烧钱量排,不按代码整洁度排:先止住自动 PR 增殖,再堵配额绕过,再堵付费模型泄漏,再消除重复任务扇出和无谓重试,最后补重跑安全性。并且限定一轮只做一到三处直接修复。第六步验证,要求跑最小的证明步骤,并且把最终状态说清楚是”本地已改""本地已验证""已推送""已部署”还是”仍被阻塞”——不许混为一谈。技能默认只读,除非用户明确要求修改。
这套顺序对你的价值不在于 GitHub App 这个具体场景,而在于它把”哪里最可能漏钱”排出了次序。/learn/agent-chengben-shikong/ 里的通用止血思路,在这里被压成了一张可以照着走的检查表。
四、四块东西各管什么
| 组成部分 | 它负责什么 | 仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
| cost-aware-llm-pipeline | 应用侧四个闸门:模型路由、预算门、窄重试、提示缓存 | skills/cost-aware-llm-pipeline/SKILL.md | 自己写要批量调 LLM 的服务时 |
| cost-tracker 钩子 | Stop 时扫 transcript、按 message.id 去重、追加一行到本地日志 | scripts/hooks/cost-tracker.js,注册于 hooks/hooks.json | 想知道自己用编码 Agent 花了多少 |
| cost-tracking 技能 | 教怎么正确读那份 JSONL:按会话取最新行再汇总 | skills/cost-tracking/SKILL.md | 问”这个会话花了多少""按模型拆一下” |
| /cost-report 命令 | 命令形态的报表与 CSV 导出,走同一份日志 | commands/cost-report.md | 想要今天/昨天/近七天的汇总 |
| ecc-tools-cost-audit | 烧钱事故的定范围、追链路、按烧钱量排序修复 | skills/ecc-tools-cost-audit/SKILL.md | 怀疑某条自动化路径在失控消耗 |
审计技能还列了一份”技能栈”,说明它期望和哪些同仓技能配合使用:autonomous-loops、agentic-engineering、customer-billing-ops、search-first、security-review、verification-loop、tdd-workflow。这些目录在 skills 下都实际存在,说明这套东西的组合方式是”技能引用技能”,而不是每个技能各自把所有背景知识抄一遍。
五、边界与代价:它明确不管什么
第一,本地统计只是估算,不是账单。 钩子内置了一张按模型档位的近似费率表,它自己在注释里承认表达不了分层计费和长缓存倍率。harness-cost 契约是个改善,但那要求你的状态栏主动写缓存文件,属于选择性加入,默认没有。所以这份数字适合做趋势和结构分析,不适合拿去和服务商对账。
第二,它要往你的机器里写东西。 这是必须如实说的代价:启用后钩子会在每次 Stop 时执行一段 Node 脚本、读取你的会话 transcript(里面有你所有的对话内容和代码)、在你的家目录下持续追加日志、并可能读取系统临时目录里的缓存文件。日志会一直长,没有自动轮转。cost-tracking 技能自己的反模式清单里就有一条”不要建议安装未经审查、会执行任意代码的钩子或插件”——这句话同样适用于你审查它。装之前把 scripts/hooks/cost-tracker.js 通读一遍,这是两百多行的单文件,一次能读完,没有隐藏依赖树。
第三,应用侧那四个闸门是模式不是库。 skills/cost-aware-llm-pipeline/SKILL.md 给的是可读的最小实现片段,没有打包成可安装的包,阈值、费率、模型常量都得你自己接。它也没提供任何跨进程的预算共享——那个 CostTracker 是进程内的、按值传递的,多副本并发跑同一个预算时,你会遇到和审计技能里”配额在前门检查、用量在 worker 里增加”完全同构的竞态。技能里没有解决这个问题。
第四,审计技能的适用面很窄。 它开头就限定了服务对象是同级的 ECC-Tools 仓库,并且反复强调不要在追查烧钱时顺手改动无关的计费、结账、UI 流程。你把它当通用检查表用可以,但别指望它覆盖你自己架构里的路径。
第五,它不管质量回归。 路由到便宜模型省下的钱,和因此产生的返工成本,这套东西不做权衡计算。技能只给了”记录模型选择的决策日志,好让你根据真实数据调阈值”这一条建议——判断留给你。
六、上手与避坑清单
逐行求和会把总额放大好几倍。 会踩是因为 JSONL 日志看起来就该逐行累加,而每行确实都带着完整的成本字段。避法:先按 session_id 归约到时间戳最新的那一行,再跨会话求和;commands/cost-report.md 里的脚本就是这么做的,直接照它的归约逻辑写。
看到全零的行别急着以为自己没花钱。 会踩是因为钩子设计成永不阻断 Stop,出任何问题都静默吞掉,坏数据和好数据长得一模一样。避法:启用后先手动打开 ~/.claude/metrics/costs.jsonl 看几行,确认 token 字段不是零、model 不是 unknown,再开始信任它。
跨 --resume 的会话统计会重复计算。 会踩是因为 transcript 求和是按文件算的,而恢复出来的会话会把之前的内容一起带上。避法:要么接上 harness-cost 契约让状态栏写入按进程计算的权威数字,要么在解读长会话数据时心里有数。
把预算检查放在 API 调用之后等于没放。 会踩是因为”记账”这个动作天然发生在调用之后,顺手就写成了调完再看超没超。避法:照组合示例的顺序来——先选模型,再判 over_budget 并抛错,再发请求,最后记账。
重试逻辑里 catch 了太宽的异常。 会踩是因为写 except Exception 最省事、也最像”健壮”。避法:把可重试异常显式列成一个元组常量,认证类和参数类错误直接向上抛。
提示缓存标错位置。 会踩是因为把整个消息内容当成一个 text 块,或者把可变部分放在了稳定部分之前。避法:拆成两个 text 块,稳定的系统提示在前并打 cache_control,可变输入在后不打。
审计时一上来就全仓乱翻。 会踩是因为”烧钱”是个模糊报警,没有明确入口。避法:按技能给的顺序先把 webhook → 队列 → worker 这条链定下来,再谈假设;修复按烧钱量排序,不按代码好看程度排序;没跑那个最小证明步骤之前,不要说已经修好了。
最后
这套东西的取向挺明确:不追求一个全自动的省钱开关,而是把成本决策点一个个显式化,然后要求你在每个点上做出可被审计的选择。你会发现三块内容其实是同一个思路的三个时态——事前设闸门、事中记账、事后追责。
拿它做自检,问自己五个问题就够了:模型选择是不是一个可以单独测试的函数?预算判断有没有排在请求之前?重试的异常清单是不是显式列出来的?你能不能说出上周花在哪个模型上最多?如果明天某条自动化路径开始失控烧钱,你知不知道第一个该打开的文件是哪个?
想继续读源码的话,顺序建议是:先 skills/cost-aware-llm-pipeline/SKILL.md 建立框架,再 scripts/hooks/cost-tracker.js 的文件头注释(那段注释把两次翻车的现象、原因和修法都写在了明面上,比正文更值得读),最后 skills/ecc-tools-cost-audit/SKILL.md 的 Workflow 和 High-Signal Failure Patterns 两节。
本文属于 ECC 开源 Agent 套件专题(共 40 篇,从选择性安装一直拆到内部机制)。同一批还写了另一种取向的项目——不给你资产、只给你纪律的 superpowers 方法论专题。