
1. 群聊 Agent 的账单为什么总是对不上群聊里放进三个以上的 Agent最先暴露的问题通常不是回答质量而是账单归属谁被点名、谁该回话、谁在没人叫它的时候偷偷轮询空转日志里全糊成一团。本文把「默认静默、点名发言」这套群聊 Agent 编排落到可核账的层面接入点在 TaoTokenhttps://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentgroup_agent_ledger_introBase URL 统一填https://taotoken.net/api。先给群里的 Bot 申请独立的 Key再去群里开静默守门两件事顺序不能反。事情起因很具体。群里两个机器人用了同一个 Key日志里只有一串请求记录月底打开用量页面看到总量翻了三倍却完全分不清是「翻译 Bot 被动接话」还是「日报 Bot 定时空转」造成的。进一步排查发现那个日报 Bot 每 60 秒轮询一次消息接口发现没有点名就顺手把整段聊天记录丢给模型做「上下文理解」一次几百 Token一天两万多次量就是这么堆出来的。类似的经验在一场 Agent 实操分享里被总结成三条动作能用 API 就别让 Agent 去模拟点击屏幕同时配一个巡检机器人清理空转任务把终稿和草稿做差分对比把纠正逻辑固化成长期技能群聊里多个 Agent 默认静默、只在点名时发言登录 Cookie 按最小权限下发。这三条里第一条治的是「无意义消耗」第二条治的是「重复踩坑」第三条治的是「权限越界」。而支撑这三条能长期跑下去的前提是成本审计和权限隔离这两套制度——自动化系统垮掉常常不是模型不够聪明而是没人说得清钱花在哪、谁的权限太大。这篇的落点很窄也很实把群聊 Agent 的点名发言机制做成一个带账本的实现产出一张「群聊点名发言 Token 统计表」能明确标注哪个 Agent 消耗了多少 Token并且把 Claude Code、Codex 这两条命令行链路也纳入同一套记账口径。2. 先分账再编排给每个群聊 Agent 一个独立 Key多 Agent 共享一个 Key是所有成本统计失效的根源。请求头里只带了同一串凭证服务端用量视角看到的只是一个整体你拿不到「谁花的」这个维度。所以第一件事是分账一个 Agent 一个 Key。拿到 Key 的入口在官网控制台注册后进入 API Keys 页面创建。为了避免「同一个 Bot 在测试环境和群里混用」建议按下面的命名规范来Key 别名绑定的 Agent用途预算上限grp-router-01路由/守门 Agent只做点名判断不生成正文低grp-writer-01写作 Agent被点名后出正文中grp-review-01审校 Agent只在被写作 Agent 点名时触发中grp-cron-01定时/巡检 Agent只做清理与告警不调大模型极低命名里的grp-前缀是关键后面做日志聚合时可以直接按前缀切分。Key 创建后只在服务端环境变量里保存群聊前端、共享配置文件、Git 仓库里都不出现明文。这一步做完你在用量页面上看到的就不再是一条总曲线而是四条可以单独归因的曲线。后面所有的统计表、熔断、告警全都建立在这个前提上。如果你还没建 Key可以从官网入口进https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentgroup_agent_key_setup创建完再回来继续。有一点要在团队里说清楚分账不是把 Key 当作权限边界。Key 负责「钱算得清」权限负责「事干得对」两者要分开设计。群聊 Agent 该拿到的会话读取权限、该访问的频道范围属于后半段要处理的问题。3. 静默守门让「被点名」成为唯一的调用入口把「默认静默」落到实处就是一条守门规则收到群消息 → 判断是否点名 → 未点名则直接丢弃连模型都不碰 → 点名则调模型并把用量写进账本。这个顺序很重要判断必须放在模型调用之前否则「静默」只是名义上的。下面是一段可以直接跑的 Python 示例把守门、调用、记账三步串起来。它读环境变量里的 Key请求发往固定的 Base URL记账信息写进本地 JSONL 文件方便后面用 SQL 聚合。import json import os import time from openai import OpenAI # 每个 Agent 用自己的 Key通过环境变量注入不写在代码里 KEY_ALIAS os.environ[AGENT_KEY_ALIAS] # 例如 grp-writer-01 client OpenAI( api_keyos.environ[TAOTOKEN_API_KEY], # 占位YOUR_API_KEY base_urlhttps://taotoken.net/api, ) LEDGER group_agent_ledger.jsonl # 点名规则别名 或 请 别名 回答 MENTION_PREFIX def is_mentioned(text: str, alias: str) - bool: 只有显式点名才返回 True其余一律视为静默 if not text: return False if text.startswith(MENTION_PREFIX alias): return True return f请 {alias} in text def ask_model(prompt: str, model: str claude-sonnet-4-5) - dict: resp client.chat.completions.create( modelmodel, messages[{role: user, content: prompt}], max_tokens800, ) usage resp.usage return { text: resp.choices[0].message.content, prompt_tokens: usage.prompt_tokens, completion_tokens: usage.completion_tokens, total_tokens: usage.total_tokens, } def handle_message(msg: dict, alias: str) - dict | None: msg 结构{room_id, msg_id, sender, text, ts} trigger mention if is_mentioned(msg[text], alias) else silent if trigger silent: # 关键未点名直接返回不产生任何模型调用 return None started time.time() result ask_model(msg[text]) record { ts: int(time.time()), room_id: msg[room_id], msg_id: msg[msg_id], mentioned_agent: alias, # 被点名者 responder_key: KEY_ALIAS, # 实际出钱的 Key trigger: trigger, latency_ms: int((time.time() - started) * 1000), prompt_tokens: result[prompt_tokens], completion_tokens: result[completion_tokens], total_tokens: result[total_tokens], cost_estimate: round(result[total_tokens] / 1000 * 0.003, 6), } with open(LEDGER, a, encodingutf-8) as f: f.write(json.dumps(record, ensure_asciiFalse) \n) return {reply: result[text], record: record} if __name__ __main__: demo { room_id: room-42, msg_id: m-1001, sender: human, text: writer-bot 把今天的需求评审结论整理成三条, ts: int(time.time()), } print(handle_message(demo, writer-bot))这段代码里有三个设计点值得单独拎出来。第一mentioned_agent和responder_key是两个字段不能合并。被点名的可能是「写作 Bot」但实际发请求的可能是「审校 Bot 转发过来」的调用。归因要看responder_key因为它才是真正花 Token 的那个凭证mentioned_agent记录的是业务语义。两个字段都留着统计表才能既回答「谁被叫得多」也回答「谁花得多」。第二未点名时直接return None不走任何网络请求。巡检机器人后面就是靠这个字段来判断有没有 Agent 在被静默期偷偷调用——账本里不会出现triggersilent的请求记录一旦出现就说明守门被绕过了。第三regular的定时任务不要走这条链路。定时 Agent 用自己独立的 Key比如grp-cron-01且只做本地清理和告警不调用模型。能用 API 直接完成的事不要让 Agent 去模拟点击屏幕这条原则在群聊场景里尤其重要——模拟点击本身产生的中间请求几乎无法纳入 Token 账本。4. Claude Code 侧用 settings.json 把用量口径固定下来群聊 Bot 跑在服务端但很多团队会用 Claude Code 在本地做同一套 Agent 逻辑的调试。这时候容易出现一个隐患本地调试用的是一个 Key线上跑的是另一个 Key两边的模型名、超时、重试策略还不一样最后统计表里的数字根本不可比。Claude Code 的配置写在settings.json里通过env字段注入 Anthropic 相关的环境变量。下面是一份把 Base URL 固定指向 TaoToken 的示例注意ANTHROPIC_BASE_URL不要带尾斜杠{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_AUTH_TOKEN: YOUR_API_KEY, ANTHROPIC_MODEL: claude-sonnet-4-5, ANTHROPIC_SMALL_FAST_MODEL: claude-haiku-4-5, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1, AGENT_KEY_ALIAS: grp-writer-01 }, permissions: { allow: [], deny: [] } }几点说明ANTHROPIC_AUTH_TOKEN里直接填占位符YOUR_API_KEY或者更稳妥的做法是留空从系统环境变量里读避免把 Key 写进会同步到 Git 的文件。ANTHROPIC_SMALL_FAST_MODEL指向一个更便宜的模型它承担的是标题生成、命令补全这类高频低价值请求。这部分用量往往是「看不见的支出」单独指定小模型之后主模型的账单会干净很多。AGENT_KEY_ALIAS是自定义变量Claude Code 本身不读它但你在同一台机器上跑的记账脚本可以读用来判断当前这个会话应该归到哪个 Agent 名下。调试完成后把本地产生的账本和线上账本按同一个responder_key维度合并统计表才完整。5. Codex 侧config.toml 是另一条链路别混用变量名Codex 的配置体系和 Claude Code 完全不同最容易犯的错误就是把ANTHROPIC_*这套变量名套到 Codex 上。Codex 读的是config.toml走的是模型提供方provider配置两者不能互相顶替。一份可用的config.toml长这样model gpt-5 model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY wire_api chat [profiles.group-writer] model gpt-5 model_provider taotoken [profiles.group-cron] model gpt-5-mini model_provider taotoken这里用env_key指定环境变量名Key 本身仍然放在环境变量里export TAOTOKEN_API_KEYYOUR_API_KEY export AGENT_KEY_ALIASgrp-cron-01需要注意部分 OpenAI 兼容客户端会在base_url后面自动追加版本路径。如果调用返回 404先把base_url换成带上版本路径的形式试一次再回退到https://taotoken.net/api不要凭猜测反复改。判断依据是错误体里给出的路径提示而不是「试到能跑为止」。Claude Code 和 Codex 两条链路并存时用 CC Switch 一类的切换器管理会省事很多。它的三件套其实就是Base URL、API Key、模型名。切换前先确认当前 profile 用的是哪套变量名——Claude Code 走ANTHROPIC_*Codex 走model_providersenv_key。把这两套搞混最典型的症状是「配置看起来没错但请求根本没到网关」。6. 产出物群聊点名发言 Token 统计表账本落成 JSONL 之后用 SQLite 建一张表做聚合。下面这段 SQL 由你在本地执行不涉及任何线上数据源CREATE TABLE agent_usage ( ts INTEGER, -- 时间戳 room_id TEXT, -- 群标识 msg_id TEXT, -- 消息标识 mentioned_agent TEXT, -- 被点名的 Agent responder_key TEXT, -- 实际发起调用的 Key 别名 trigger TEXT, -- mention / interval / manual latency_ms INTEGER, prompt_tokens INTEGER, completion_tokens INTEGER, total_tokens INTEGER, cost_estimate REAL ); -- 按 Key 别名汇总谁是消耗大户 SELECT responder_key, COUNT(*) AS calls, SUM(prompt_tokens) AS in_tokens, SUM(completion_tokens) AS out_tokens, SUM(total_tokens) AS total, ROUND(SUM(cost_estimate), 4) AS cost FROM agent_usage WHERE trigger mention GROUP BY responder_key ORDER BY total DESC; -- 被点名次数 vs 实际消耗看是否存在「叫得多但不贵」或反过来的 Agent SELECT mentioned_agent, COUNT(*) AS mentioned_times, SUM(total_tokens) AS total, ROUND(AVG(total_tokens), 1) AS avg_per_call FROM agent_usage WHERE trigger mention GROUP BY mentioned_agent ORDER BY total DESC;导入 JSONL 可以用一行命令完成sqlite3 group_agent.db SQL .mode json .import --skip 1 /dev/stdin agent_usage SQL jq -c . group_agent_ledger.jsonl | sqlite3 group_agent.db \ .import /dev/stdin agent_usage跑完之后你会得到一张可以直接贴进周报的表形如日期群被点名 Agent出账 Key调用次数输入 Token输出 Token合计估算成本03-11room-42writer-botgrp-writer-011824,3006,12030,4200.09103-11room-42review-botgrp-review-0169,8002,05011,8500.03603-11room-42cron-botgrp-cron-0100000第三行是这张表最有价值的地方grp-cron-01的调用次数是 0。如果某天它突然变成 40说明定时 Agent 开始调模型了要么是守门坏了要么是新加的巡检逻辑偷偷接了模型。能明确回答「哪个 Agent 消耗 Token」靠的不是总量而是这张表里按 Key 别名分组后的零值。再看两个衍生指标。第一个是「人均单次消耗」写作 Agent 和审校 Agent 的上下文长度天然不同如果写作 Agent 的单次消耗突然从 1.7k 涨到 8k多半是有人把整段聊天记录都塞进了 prompt。第二个是「点名响应率」被点名次数和实际调用次数应该接近 1:1差得太多说明有的点名被漏掉了或者一个点名触发了多个 Agent 同时抢答。7. 巡检与熔断把空转任务在花钱之前掐掉有了账本就能做巡检。巡检 Agent 本身的定位是「不调大模型」所以它的实现应当全是本地查询和规则判断import sqlite3 import time DAILY_BUDGET 200_000 # 单个 Key 每日 Token 上限 SILENT_GRACE 3600 # 静默期容忍窗口秒 conn sqlite3.connect(group_agent.db) cur conn.cursor() today int(time.time()) - 86400 # 规则一超过日预算的 Key 直接告警 cur.execute( SELECT responder_key, SUM(total_tokens) AS total FROM agent_usage WHERE ts ? GROUP BY responder_key HAVING total ? ORDER BY total DESC; , (today, DAILY_BUDGET)) for key, total in cur.fetchall(): print(f[BUDGET] {key} 已用 {total} tokens建议降级到小模型或暂停) # 规则二非点名触发却产生了消耗说明守门被绕过 cur.execute( SELECT responder_key, trigger, COUNT(*) AS n FROM agent_usage WHERE ts ? AND trigger mention GROUP BY responder_key, trigger; , (today,)) rows cur.fetchall() if not rows: print([OK] 无越权调用) else: for key, trigger, n in rows: print(f[LEAK] {key} 在 trigger{trigger} 下产生了 {n} 次调用请检查守门逻辑) conn.close()规则二的告警阈值定得很死非mention触发就应该为零。真出现正值无非三种原因——有人直接调了 Bot 的 HTTP 接口绕过了群聊守门某个定时任务被改成了调模型或者守门判断里的别名和目标 Bot 的实际昵称不一致导致点名识别失败后走了兜底分支。三种都能顺着responder_key直接定位到具体 Agent。熔断的粒度建议按 Key 来而不是按群。按群熔断会误伤一个群里有五个 Agent其中一个失控不该让其余四个也停下来。按 Key 熔断之后把超限的 Key 对应的 Agent 降级到小模型或者直接让它进入只读模式等人工确认再恢复。这套机制也回应了权限隔离的思路——降级和熔断的开关不应该握在 Agent 自己手里。8. 权限与常见报错排查权限那部分遵循最小下发原则。群聊 Agent 需要的登录态按「只读消息、只发指定频道」的范围授予不要把管理员的完整 Cookie 复制给每个 Bot。上面第一条建议里提到「登录 Cookie 按最小权限下发」落到操作上就是每个 Agent 一套独立凭证权限范围写清楚过期时间短于业务周期。接入过程中高频出现的几类报错可以先按这张表自查现象常见原因处理方向401Key 拼错、环境变量未加载、Key 已停用打印变量前缀确认非空重新从控制台取 Key404base_url路径不匹配先用https://taotoken.net/api再按返回体提示补版本路径429单 Key 并发过高按 Agent 拆 Key或对同一点名做合并去重超时单次 prompt 过大限制携带的上下文条数把长历史做摘要消耗异常守门被绕过、定时任务调模型查账本中trigger mention的记录其中 429 在群聊场景下出现得比较隐蔽一个点名消息被三个 Agent 同时看到三个都认为该自己回答于是并发三个请求。解决办法是在路由层加一个短窗口去重同一条msg_id在 2 秒内只允许一个 Agent 出账。还有一个细节容易被忽略会话历史。群聊 Agent 如果每次都把最近 50 条消息带上输入 Token 会随群活跃度线性增长。比较稳的做法是只带被点名消息及之前若干条与当前话题相关的记录其余用摘要替代。这部分优化不需要改模型改的是 prompt 组装逻辑效果直接体现在统计表的输入 Token 列上。如果你想先把单 Agent 的链路跑通、观察一次完整调用的用量结构可以从模型对话入口试https://taotoken.net/models/detail/chat?utm_sourcetaotoken_aicg_blog_endutm_contentgroup_agent_trial拿到真实返回里的 usage 字段再对照账本里的字段做映射比凭空设计字段靠谱得多。9. 把成本审计变成默认设置回到最初的问题群聊 Agent 只点名发言只是把调用次数压下来它本身并不产生账单。真正让成本可控的是三件事同时成立——每个 Agent 有独立 Key所以消耗可归因守门在模型调用之前所以静默是真的静默账本每次调用都写所以统计表可以复现。这三件事都不复杂难的是让它们成为默认值而不是临时措施。实践顺序可以这样排先给已在群里的每个 Bot 拆出独立 Key把别名写进配置文件再在消息入口加一层点名判断让未点名的消息在调用之前就被拦截接着把每次调用的 usage 落盘最后把上面那段巡检脚本挂成定时任务按日检查预算与非点名调用。四步走完统计表就是日常产物而不是出问题之后回头补的作业。命令行方向如果要继续扩展Codex 那条链路照着config.toml把 provider 固定住即可关键是别把两套环境变量混用。需要更细的接入说明和参数含义时可以直接看 Claude Code 文档https://taotoken.net/doc/ClaudeCodeAnthropic?utm_sourcetaotoken_aicg_blog_endutm_contentgroup_agent_doc里面按要求配置 base_url 与鉴权项就能跑通。按量用、按需扩先把 Key 拆开再把守门加上最后让巡检脚本每天替你问一句今天哪个 Agent 花的 Token值不值。