1. 先统一 TaoToken 入口,再设计 Hermes Agent 日志字段
TaoToken 用户如果正在跑 Hermes Agent,第一步是到 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=hermes_log_fields_intro 拿 Key,并把客户端 Base URL 设为 https://taotoken.net/api。最近 Hermes Agent 做多子代理大规模重构、Elvis Saravia 点评自进化技能与工程复利的讨论,把很多团队的注意力拉回一个更硬核的问题:长时间运行的 Agent,日志字段应该怎样设计,才能把成本、失败和复现路径讲清楚。如果没有统一模型出口,日志里会出现多个供应商、多个 Key 别名、多个 Base URL,最后排查时只能靠记忆。本文不写热点评论,直接给一套可以跟做的方案:先把 Hermes Agent 以及同机 Claude Code、Codex、CC Switch 的模型入口改到 TaoToken,再给日志字段清单、JSONL 样例、jq 解析命令和排障顺序。
这里要区分“模型请求日志”和“Agent 运行日志”。模型请求日志通常由 SDK 或网关侧产生,能看到 status_code、usage、request_id;Agent 运行日志由 Hermes Agent 自身产生,能看到子代理、工具调用、技能加载、文件变更、测试结果。两者不打通,就会出现一种尴尬局面:你知道某次调用花了 token,但不知道属于哪个子代理;你知道某个子代理失败了,但不知道它当时用的哪个模型、哪个 Base URL、哪个 Key 别名。所以第一原则是:日志字段必须能把 run、trace、span、subagent、request_id、tool_call_id 串起来。
在配置层,先把 Base URL 固定为:
https://taotoken.net/api这个地址不加 UTM,UTM 只用于官网引导链接。Key 用占位符YOUR_API_KEY,不要写进代码仓库。拿到 Key 的入口仍然建议走官网:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=hermes_log_fields_setup 。
1.1 Claude Code、Codex、CC Switch 不要混用变量
同一台开发机上经常同时装 Claude Code、Codex 和 CC Switch。它们读取的配置不同,混用会出现“明明 Key 没问题,但工具报 401”的情况。Claude Code 走 Anthropic 兼容配置时,用settings.json或ANTHROPIC_*;Codex 走config.toml,不要把它接成ANTHROPIC_*。CC Switch 则把它当成三件套:供应商名称、Base URL、API Key。
Claude Code 的settings.json可以这样写:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "YOUR_MODEL_NAME" } }Codex 的config.toml用独立供应商名和独立环境变量:
model = "YOUR_MODEL_NAME" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat"然后在本地 shell 中导出:
export TAOTOKEN_API_KEY="YOUR_API_KEY"CC Switch 三件套按下面填:
供应商名称: TaoToken Base URL: https://taotoken.net/api API Key: YOUR_API_KEY 模型: 在模型对话页选择可用模型后填入如果 Hermes Agent 当前版本支持 OpenAI 兼容 provider,可以给它单独一组环境变量。不同版本的变量名可能不同,以它实际读取的 provider 配置为准。常见兼容写法是:
export TAOTOKEN_API_KEY="YOUR_API_KEY" export OPENAI_API_KEY="$TAOTOKEN_API_KEY" export OPENAI_BASE_URL="https://taotoken.net/api" export OPENAI_API_BASE="https://taotoken.net/api"这里再次强调:Claude Code 的ANTHROPIC_*不要抄到 Codex;Codex 的env_key也不要硬塞给 Claude Code。日志字段里应该记录api_key_alias或key_env_name,不要记录完整 Key。这样后面看到 401、429、404 时,可以快速判断是 Key 来源问题、Base URL 问题,还是模型名问题。
2. Hermes Agent 长时间多子代理运行,日志至少要覆盖六组字段
如果只记print("start")和print("done"),长时间运行结束后只能知道“跑完了”或“没跑完”。真正可复现的日志,需要覆盖 trace、LLM、工具、子代理、成本资源、安全复现六组。下面这份清单可以直接作为 Hermes Agent 的 JSONL schema 草案。
2.1 trace 与运行上下文字段
这些字段负责把一次 run 串起来。建议每条事件都带:
| 字段 | 说明 |
|---|---|
ts | ISO8601 时间戳,建议 UTC |
run_id | 一次完整运行 ID |
trace_id | 分布式追踪 ID |
span_id | 当前事件 span |
parent_span_id | 父 span,用于还原调用树 |
event_type | run_start、llm_request、llm_response、tool_call、tool_result、subagent_spawn、subagent_join、error、run_end |
level | debug、info、warn、error、fatal |
service | 固定为hermes-agent |
version | Agent 版本 |
host | 机器名 |
pid | 进程 ID |
thread | 线程或协程标识 |
session_id | 会话 ID |
task_id | 任务 ID |
subtask_id | 子任务 ID |
agent_id | 主 Agent ID |
subagent_id | 子代理 ID,主 Agent 可为空或root |
parent_subagent_id | 父级子代理 ID |
decomposition_depth | 任务拆解深度 |
concurrency_slot | 并发槽位编号 |
长时间运行时,concurrency_slot很关键。子代理数量一多,失败往往不是模型本身,而是并发抢占、队列等待、锁等待。日志里没有槽位编号,就很难判断某个时间段是否发生过拥堵。
2.2 LLM 请求与响应字段
TaoToken Key 的日志最值得记的字段集中在这里。建议每次模型调用记录:
| 字段 | 说明 |
|---|---|
provider | taotoken |
base_url_host | taotoken.net,不要记完整带 Key 的 URL |
endpoint | /api |
model | 实际请求模型名 |
request_id | 供应商或网关返回的请求 ID |
stream | 是否流式 |
temperature | 采样温度 |
max_tokens | 最大输出 token |
usage.prompt_tokens | 输入 token |
usage.completion_tokens | 输出 token |
usage.total_tokens | 总 token |
usage.cache_read_tokens | 缓存读取 token,如果有 |
usage.cache_write_tokens | 缓存写入 token,如果有 |
cost_estimate | 估算成本 |
currency | 币种 |
latency_ms | 总耗时 |
ttft_ms | 首 token 时间 |
retry_count | 重试次数 |
status_code | HTTP 状态码 |
finish_reason | 结束原因 |
error_type | 错误类型 |
error_message_digest | 错误信息摘要,不要记敏感原文 |
rate_limit_remaining | 剩余限流额度,如果有 |
rate_limit_reset | 限流重置时间,如果有 |
特别注意request_id。当用户反馈“某次请求异常”时,没有request_id就只能靠时间猜。base_url_host和endpoint用于确认请求确实走了https://taotoken.net/api,而不是被旧环境变量带到了其他地址。
2.3 工具调用字段
Hermes Agent 这类工具型 Agent,失败经常发生在工具层,而不是模型层。建议记录:
| 字段 | 说明 |
|---|---|
tool_name | 工具名或命令类型 |
tool_call_id | 工具调用 ID |
tool_args_digest | 参数摘要 |
tool_result_digest | 结果摘要 |
tool_duration_ms | 工具耗时 |
exit_code | 退出码 |
stdout_bytes | 标准输出字节数 |
stderr_bytes | 标准错误字节数 |
files_read | 读取文件列表 |
files_written | 写入文件列表 |
patch_size | 变更行数或字节数 |
diff_hash | diff 摘要 |
command_digest | 命令摘要 |
cwd | 工作目录 |
sandbox_id | 沙箱 ID |
permission_decision | 允许、拒绝或需审批 |
approval_id | 审批 ID |
不要记录完整命令里的密钥、令牌、密码。command_digest和tool_args_digest用哈希即可。需要复现时,结合本地受控的审计存储再查原文。所有解析命令都由读者在自己的本地日志上执行,不要直连生产库,也不要让 Agent 绕过沙箱。
2.4 子代理与技能字段
Elvis Saravia 的点评里提到自进化技能和工程复利,这对日志字段有直接启发:如果技能会进化,就必须记录技能版本、来源和变更。建议字段:
| 字段 | 说明 |
|---|---|
subagent_id | 子代理 ID |
role | 子代理角色 |
skill_id | 技能 ID |
skill_version | 技能版本 |
skill_source | builtin、manual、self_evolved |
self_evolved | 是否自进化产生 |
skill_patch_id | 技能补丁 ID |
step_index | 当前步数 |
max_steps | 最大步数 |
stop_reason | 停止原因 |
budget_tokens | token 预算 |
budget_time_ms | 时间预算 |
used_tokens | 已用 token |
used_time_ms | 已用时间 |
conflict_count | 冲突次数 |
merge_status | 合并状态 |
review_status | 审查状态 |
没有skill_version,你无法回答“这次成功是因为技能升级,还是因为模型变了”。没有stop_reason,你无法区分是任务完成、预算耗尽、还是子代理主动放弃。
2.5 成本与资源字段
长时间多子代理运行,成本会分散在大量请求里。建议记录:
| 字段 | 说明 |
|---|---|
token_budget | 总 token 预算 |
token_used | 已用 token |
cost_budget | 成本预算 |
cost_used | 已用成本 |
cpu_ms | CPU 时间 |
mem_peak_mb | 内存峰值 |
disk_write_bytes | 磁盘写入 |
queue_wait_ms | 排队等待 |
lock_wait_ms | 锁等待 |
这些字段不一定每条事件都有,但至少要在run_end、subagent_join和周期 checkpoint 中出现。否则只能事后估算。
2.6 安全、脱敏与复现字段
日志不能成为新的泄露面。建议:
| 字段 | 说明 |
|---|---|
secret_redaction | 是否已脱敏 |
pii_redaction | 是否已处理个人信息 |
prompt_hash | 提示词摘要 |
response_hash | 响应摘要 |
tool_args_redacted | 工具参数是否脱敏 |
api_key_alias | Key 别名 |
key_env_name | 环境变量名 |
base_url_hash | Base URL 摘要 |
tls_verify | 是否校验 TLS |
git_commit_before | 运行前 commit |
git_commit_after | 运行后 commit |
branch | 分支 |
worktree | worktree 路径 |
repo_url_hash | 仓库地址摘要 |
test_suite | 测试集 |
test_status | 测试状态 |
test_duration_ms | 测试耗时 |
coverage_delta | 覆盖率变化 |
lint_status | lint 状态 |
其中git_commit_before和git_commit_after是复现的底线。没有它们,你只能知道“代码变小了”,但无法回到运行前状态重放。
3. 一份可直接落地的 JSONL 日志样例
下面给出一组事件样例。实际写入时,每行一个 JSON 对象,文件后缀建议用.jsonl。不要把完整 Key、完整提示词、完整响应直接写进去。
运行开始:
{"ts":"2025-01-01T00:00:00.000Z","event_type":"run_start","level":"info","run_id":"run-001","trace_id":"trace-001","span_id":"span-root","parent_span_id":null,"service":"hermes-agent","version":"x.y.z","host":"dev-01","pid":12345,"session_id":"sess-001","task_id":"task-001","agent_id":"root","subagent_id":null,"parent_subagent_id":null,"decomposition_depth":0,"concurrency_slot":1,"repo_url_hash":"sha256:repo","git_commit_before":"abc123","branch":"refactor/log","worktree":"/workspace/repo"}LLM 响应:
{"ts":"2025-01-01T00:00:01.200Z","event_type":"llm_response","level":"info","run_id":"run-001","trace_id":"trace-001","span_id":"span-llm-01","parent_span_id":"span-root","subagent_id":"sub-07","model":"YOUR_MODEL_NAME","provider":"taotoken","base_url_host":"taotoken.net","endpoint":"/api","api_key_alias":"taotoken-main","key_env_name":"TAOTOKEN_API_KEY","request_id":"req_xxx","stream":true,"temperature":0.2,"max_tokens":8192,"usage":{"prompt_tokens":1200,"completion_tokens":340,"total_tokens":1540,"cache_read_tokens":0,"cache_write_tokens":0},"cost_estimate":0.0123,"currency":"USD","latency_ms":2350,"ttft_ms":410,"retry_count":0,"status_code":200,"finish_reason":"stop"}工具结果:
{"ts":"2025-01-01T00:00:03.550Z","event_type":"tool_result","level":"info","run_id":"run-001","trace_id":"trace-001","span_id":"span-tool-01","parent_span_id":"span-llm-01","subagent_id":"sub-07","tool_name":"shell","tool_call_id":"call_xxx","tool_args_digest":"sha256:args","tool_result_digest":"sha256:result","tool_duration_ms":812,"exit_code":0,"stdout_bytes":2048,"stderr_bytes":0,"files_read":["src/a.py"],"files_written":["src/a.py"],"patch_size":128,"diff_hash":"sha256:diff","command_digest":"sha256:cmd","cwd":"/workspace/repo","sandbox_id":"sbx-01","permission_decision":"allow","approval_id":null}子代理启动:
{"ts":"2025-01-01T00:00:04.000Z","event_type":"subagent_spawn","level":"info","run_id":"run-001","trace_id":"trace-001","span_id":"span-sub-07","parent_span_id":"span-root","agent_id":"root","subagent_id":"sub-07","parent_subagent_id":"sub-01","role":"python-refactor","skill_id":"skill.python.refactor","skill_version":"2025.09.1","skill_source":"self_evolved","self_evolved":true,"skill_patch_id":"patch-001","step_index":3,"max_steps":25,"budget_tokens":50000,"budget_time_ms":600000,"used_tokens":1540,"used_time_ms":2350}错误事件:
{"ts":"2025-01-01T00:00:05.000Z","event_type":"error","level":"error","run_id":"run-001","trace_id":"trace-001","span_id":"span-llm-02","subagent_id":"sub-03","request_id":"req_err","status_code":429,"error_type":"rate_limit","error_message_digest":"sha256:err","retry_count":2,"rate_limit_remaining":0,"rate_limit_reset":"2025-01-01T00:05:00Z","model":"YOUR_MODEL_NAME","base_url_host":"taotoken.net","endpoint":"/api"}运行结束:
{"ts":"2025-01-01T01:00:00.000Z","event_type":"run_end","level":"info","run_id":"run-001","trace_id":"trace-001","status":"ok","stop_reason":"completed","total_tokens":123456,"total_cost_estimate":1.234,"total_duration_ms":3600000,"git_commit_after":"def456","test_status":"pass","lint_status":"pass","coverage_delta":0.5,"merge_conflicts":0}如果 Hermes Agent 使用 Python logging,可以加一个 JSON formatter,把上述字段转成 JSONL。核心思路不是抄某个库,而是固定字段名。字段名一旦稳定,后面的 jq、Python、BI 都能复用。
4. 解析命令:用 jq 把日志拆成成本、错误、子代理和工具链
以下命令都在本地日志文件hermes-run.jsonl上执行。先确认文件存在:
ls -lh hermes-run.jsonl按模型统计调用次数、总 token、平均延迟和错误数:
jq -s ' map(select(.event_type=="llm_response")) | group_by(.model) | map({ model: .[0].model, calls: length, total_tokens: (map(.usage.total_tokens // 0) | add), avg_latency_ms: ((map(.latency_ms // 0) | add) / length), errors: (map(select((.status_code // 0) >= 400)) | length) }) | sort_by(-.total_tokens) ' hermes-run.jsonl计算 LLM 响应延迟 P95:
jq -s ' map(select(.event_type=="llm_response") | .latency_ms // 0) | sort | .[(length * 0.95 | floor)] ' hermes-run.jsonl按子代理聚合 token、成本、错误和工具调用:
jq -s ' map(select(.subagent_id != null)) | group_by(.subagent_id) | map({ subagent_id: .[0].subagent_id, events: length, tokens: (map(.usage.total_tokens // 0) | add), cost: (map(.cost_estimate // 0) | add), errors: (map(select(.level=="error")) | length), tool_calls: (map(select(.event_type=="tool_result")) | length) }) | sort_by(-.tokens) ' hermes-run.jsonl查看错误排行:
jq -r ' select(.level=="error") | [.ts, .run_id, .subagent_id, .error_type, .status_code, .request_id, .base_url_host, .endpoint] | @tsv ' hermes-run.jsonl | sort | uniq -c | sort -nr | head -30按工具统计调用次数、平均耗时、错误数和写入文件数:
jq -r ' select(.event_type=="tool_result") | [.tool_name, (.exit_code // 0), (.tool_duration_ms // 0), ((.files_written // []) | length)] | @tsv ' hermes-run.jsonl | awk '{c[$1]++; d[$1]+=$3; if($2!=0) e[$1]++; w[$1]+=$4} END {for (k in c) printf "%s\tcalls=%d\tavg_ms=%.1f\terrors=%d\tfiles_written=%d\n", k, c[k], d[k]/c[k], e[k]+0, w[k]}'追踪某个 request_id 或 tool_call_id:
REQ="req_xxx" jq -r --arg req "$REQ" ' select(.request_id==$req or .tool_call_id==$req) | [.ts, .event_type, .subagent_id, .tool_name, .status_code, .latency_ms] | @tsv ' hermes-run.jsonl按时间窗口切片:
START="2025-01-01T00:00:00Z" END="2025-01-02T00:00:00Z" jq -r --arg start "$START" --arg end "$END" ' select(.ts >= $start and .ts <= $end) | [.ts, .event_type, .level, .subagent_id, .model, .status_code] | @tsv ' hermes-run.jsonl按天和模型汇总成本:
jq -r ' select(.event_type=="llm_response" and (.cost_estimate // null) != null) | [.ts[0:10], .model, .cost_estimate] | @tsv ' hermes-run.jsonl | awk '{d[$1]+=$3; m[$2]+=$3} END{for(k in d) print "DAY",k,d[k]; for(k in m) print "MODEL",k,m[k]}'如果 jq 不够,可以用一个小的 Python 脚本聚合子代理:
#!/usr/bin/env python3 import json import sys from collections import defaultdict def iter_jsonl(path): with open(path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue try: yield json.loads(line) except json.JSONDecodeError: continue if __name__ == "__main__": path = sys.argv[1] if len(sys.argv) > 1 else "hermes-run.jsonl" sub = defaultdict(lambda: {"tokens": 0, "cost": 0.0, "errors": 0, "tools": 0}) for ev in iter_jsonl(path): sid = ev.get("subagent_id") or "root" usage = ev.get("usage") or {} sub[sid]["tokens"] += int(usage.get("total_tokens") or 0) sub[sid]["cost"] += float(ev.get("cost_estimate") or 0) if ev.get("level") == "error": sub[sid]["errors"] += 1 if ev.get("event_type") == "tool_result": sub[sid]["tools"] += 1 for sid, row in sorted(sub.items(), key=lambda kv: kv[1]["tokens"], reverse=True): print(sid, row)这些命令只读本地 JSONL,不需要连数据库,也不需要让 Agent 访问生产环境。
5. 接入 TaoToken 后的排障顺序:先看 Key 和 Base URL,再看重试和限流,最后看子代理扇出
当 Hermes Agent 长时间运行失败时,不要一上来就怀疑模型。按日志字段排障,顺序通常是:
- 看
base_url_host和endpoint。确认是taotoken.net和/api,而不是旧的环境变量残留。 - 看
api_key_alias和key_env_name。确认请求用的到底是不是YOUR_API_KEY对应的环境变量。 - 看
status_code。401、403 先查 Key 和权限;404 查模型名和 endpoint;429 查限流和并发;5xx 查服务端状态并保留request_id。 - 看
retry_count。重试很多次但最后成功,不代表没问题,可能正在放大成本。 - 看
ttft_ms和latency_ms。首 token 慢和整体慢要分开处理,前者影响交互,后者影响吞吐。 - 看
rate_limit_remaining和rate_limit_reset。如果频繁触顶,就要降低子代理并发或调整调度。 - 看
subagent_id、parent_subagent_id、decomposition_depth。定位是根代理失败,还是某个深层子代理失败。 - 看
tool_name、exit_code、stderr_bytes。很多“模型失败”其实是工具执行失败。 - 看
merge_status、conflict_count。多子代理写同一仓库时,冲突会显著拖慢运行。 - 看
token_used、cost_used、budget_tokens。确认是预算耗尽,还是预算足够但效率低。 - 看
skill_version、skill_source、self_evolved。判断是否是技能升级后行为变化。 - 看
secret_redaction、pii_redaction。确认日志没有把敏感信息写进去。
如果你需要重新检查 Key 和模型,可以从官网入口进入:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=hermes_log_fields_troubleshoot 。常见操作是:先在模型对话页确认模型可用,再创建或复制 API Key,然后把 Base URL 固定为https://taotoken.net/api。
排障时不要贴完整 Key。只贴api_key_alias、key_env_name、status_code、request_id、base_url_host、endpoint和错误摘要。这样既能定位问题,又不会扩大泄露面。
6. 最小可用字段集和扩展字段集:先跑起来,再逐层加字段
不是所有团队一开始都能打全字段。建议分两层。
最小可用字段集至少包含:
ts event_type level run_id trace_id span_id parent_span_id subagent_id parent_subagent_id model provider base_url_host endpoint api_key_alias key_env_name request_id status_code latency_ms ttft_ms retry_count usage.prompt_tokens usage.completion_tokens usage.total_tokens cost_estimate tool_name tool_call_id exit_code files_written patch_size skill_id skill_version stop_reason git_commit_before git_commit_after扩展字段集再加:
concurrency_slot decomposition_depth queue_wait_ms lock_wait_ms rate_limit_remaining rate_limit_reset cache_read_tokens cache_write_tokens budget_tokens budget_time_ms used_tokens used_time_ms conflict_count merge_status review_status permission_decision approval_id sandbox_id diff_hash command_digest prompt_hash response_hash secret_redaction pii_redaction test_suite test_status coverage_delta lint_status这份字段表的价值在于“工程复利”:同样的日志 schema,可以用于不同 harness、不同模型、不同子代理规模。Elvis Saravia 提到不同 harness 未必适用同一套方法,这个提醒同样适用于日志。字段要能回答“这次运行的成本、失败、技能版本、代码变更分别是什么”,而不是只回答“跑了多久”。
如果你想比较“更多子代理”和“更少子代理”哪个更省,也不能靠感觉。应该用subagent_id聚合 token、成本、错误、工具调用和合并冲突,再看单位有效 diff 的成本。日志字段足够细,才能把“规模”和“效率”分开看。
7. 文末 CTA:模型对话、Coding Plan、创建 Key、Claude Code 文档
如果你准备把这套日志字段落到 Hermes Agent,建议按下面顺序操作:
先在模型对话里确认目标模型可用:
https://taotoken.net/models/detail/chat?utm_source=taotoken_aicg_blog_end&utm_content=hermes_log_fields_chat如果长时间运行和子代理并发较多,先看 Coding Plan 是否匹配:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=hermes_log_fields_plan创建或复制 API Key,用
YOUR_API_KEY占位,不要硬编码:
https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=hermes_log_fields_keys如果你同时使用 Claude Code,按文档配置
ANTHROPIC_*与settings.json:
https://taotoken.net/doc/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_content=hermes_log_fields_claude_code
最后再回到日志侧:把base_url_host、endpoint、api_key_alias、request_id、subagent_id、tool_call_id、usage.total_tokens、cost_estimate、skill_version、git_commit_before、git_commit_after这些字段稳定写入 JSONL。这样下一次长时间多子代理运行结束后,你不需要翻聊天记录,也不需要猜哪个 Key 在哪个工具里生效。直接跑 jq,先把成本、错误、子代理和工具链拆开,再决定是限流、降并发、换模型,还是回滚技能版本。