
1. 工具侧失败的锅别让模型请求背一次 DSH 任务的事后复盘DSH 任务失败告警弹出来的时候第一反应往往是「模型又抽风了」。可真去翻记录十次里有六七次是工具侧的问题文件路径拼错、命令退出码非零、依赖没装、沙箱权限不够、超时被强杀。剩下三四次才是模型侧上下文超限、流式响应被截断、工具参数 JSON 解析失败、重试次数耗尽。麻烦在于这两类失败在终端里长得几乎一样——都是红色 stderr 加一段堆栈都发生在同一个 turn 里时间戳还挨得很近。要区分它们必须让模型请求和工具调用各自留下可对照的记录。本文的做法是把 DSH 的模型请求统一收敛到 TaoTokenhttps://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentdsh_tool_failure把 Base URL 设成https://taotoken.net/api再配合调用链里的 tool Span把一次失败拆成「模型说了什么」和「工具做了什么」两条独立可查的线。本文的视角是故障归因不聊框架源码只给一套能落地的对照方法从配置怎么改到 tool Span 的错误明细怎么读再到模型请求记录与 Token 消耗怎么和工具轮次对齐最后给一张归因决策表。2. 让模型请求落到可查的 Base URLTaoToken 配置清单归因的前提是「模型侧有记录」。如果模型请求打到一个没有日志、没有用量明细的端点上那故障发生时你只能看到 Agent 最后一句「任务失败」中间发生了什么全靠猜。TaoToken 在这套流程里的角色是统一的模型入口所有 DSH 会话、所有 Coding Agent 的模型调用都走同一个 Base URL请求记录和 Token 消耗自然就汇聚到一处和工具侧调用链的时间轴能对齐。第一步是拿 Key——打开 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentcreate_key 创建一把 API Key下文所有配置里的YOUR_API_KEY都替换成它不要写进代码仓库用环境变量或本地密钥文件注入。2.1 DSH 侧把模型适配器指向 TaoTokenDSH 的模型适配器是插件配置分布在 profile 目录下。最小改动是先落环境变量再让适配器读到它# ~/.zshrc 或 ~/.bashrc export TAOTOKEN_BASE_URLhttps://taotoken.net/api export TAOTOKEN_API_KEYYOUR_API_KEY然后在当前 profile 的补丁配置里显式声明未设置DSH_HOME时默认为~/.dsh# ~/.dsh/profiles/default/cordis.patch.yml model: provider: openai-compatible baseURL: https://taotoken.net/api apiKey: ${TAOTOKEN_API_KEY} defaultModel: deepseek-chat stream: true timeoutMs: 120000timeoutMs这一项在归因里很关键它决定模型侧超时是「我这里主动断开」还是「对端先返回错误」两者的错误码完全不同。2.2 Claude Code 侧settings.json 与 ANTHROPIC_*如果同一个项目里还跑着 Claude Code把它也指向同一个入口这样一次跨工具的任务失败可以在同一套用量视图里排查。{ 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 }, permissions: { allow: [Bash(git status), Bash(git diff:*)], deny: [Bash(rm -rf:*)] } }写入~/.claude/settings.json或项目级.claude/settings.json。注意ANTHROPIC_SMALL_FAST_MODEL也要一起改否则后台的小模型请求会走另一条路用量对不上账。2.3 Codex 侧config.toml不要套 ANTHROPIC_*Codex 用的是自己的 provider 表把 Claude Code 那套环境变量搬过去不会生效只会静默走默认端点。# ~/.codex/config.toml model gpt-5-codex model_provider taotoken model_reasoning_effort medium [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api/v1 env_key TAOTOKEN_API_KEY wire_api responses request_max_retries 3 stream_idle_timeout_ms 300000request_max_retries要和调用链里的dsh.llm.attempt或 Codex 自身的重试计数对上否则你在 Trace 里看到的「一次调用」可能实际上是三次。2.4 CC Switch 三件套Base URL、API Key、Model用 CC Switch 这类配置切换工具时本质上只解决三件事改完记得三处都改Base URL统一https://taotoken.net/api别留旧的官方地址。API Key换成 TaoToken 创建的 Key切换配置时不要残留上一家。Model主模型与快速模型分开填避免小模型请求跑到别的供应商。三件套不一致是「模型请求记录缺失」最常见的原因URL 改了、Key 没改请求直接 401而工具侧还在正常跑看日志就以为模型没问题。3. tool Span 的错误明细五层调用树里该抓哪些字段DSH 的执行结构是 ReAct 循环一个 turn 是一次用户任务turn 内每一轮「推理—调用工具—观察结果」是一个 step。把这条循环还原成调用树就是五层entry会话入口 └── agent一次 turn └── step一轮推理 ├── chat一次真实模型调用 └── tool一次工具执行工具侧失败一定落在最底层的toolSpan 上。定位时按顺序看这几个字段{ name: tool.exec_command, span.kind: internal, gen_ai.tool.name: exec_command, gen_ai.tool.call.id: call_01H..., dsh.turn.id: turn_7f3a, dsh.step.index: 4, dsh.llm.attempt: 1, status.code: ERROR, error.type: tool_execution_failed, error.message: Process exited with code 127, tool.duration_ms: 318, tool.exit_code: 127, tool.args.truncated: false, tool.result.size_bytes: 812 }几个读法上的坑error.type才是归因入口error.message只是证据。退出码 127 表示命令找不到是环境问题退出码 1 往往是脚本自身逻辑没有退出码却有超时字段是沙箱或超时策略拦的。gen_ai.tool.call.id是连接模型与工具的钥匙。模型在chatSpan 里发出的 tool_calls 带这个 id工具执行返回时也带同一个 id两边靠它对上。dsh.step.index指向那一轮推理。同一个 step 下如果chat成功、tool失败基本可以直接判定为工具侧如果chat本身就异常工具根本没跑起来。tool.result.size_bytes和tool.args.truncated要看。输出被截断会导致模型下一轮拿到残缺上下文报错出现在很后面根因却在工具侧。如果工具是被中断的用户 CtrlC、流没收尾、step 先于工具结束这一层 Span 依然会被补发出来并带错误码。中断场景下status.code是ERRORerror.type通常是cancelled或interrupted不要误判成工具逻辑失败。4. 模型请求记录chat Span 与 TaoToken 用量怎么对齐chatSpan 承载的是模型侧的全部事实。和工具 Span 对照时重点是下面这几组{ name: chat claude-sonnet-4-5, gen_ai.system: anthropic, gen_ai.request.model: claude-sonnet-4-5, gen_ai.response.model: claude-sonnet-4-5-20250929, gen_ai.usage.input_tokens: 18420, gen_ai.usage.output_tokens: 642, gen_ai.response.finish_reasons: [tool_use], dsh.llm.attempt: 1, status.code: OK, chat.first_token_ms: 780, chat.total_ms: 4210 }对齐规则有三条第一条finish_reasons决定下一步走向。如果是tool_use说明模型正常要求调用工具此时 tool Span 失败就是工具侧责任如果是length说明输出被长度上限截断模型可能根本没生成完整的工具参数后面的工具报「参数解析失败」根因在模型侧如果是stop模型认为任务结束但外部还报错那多半是模型对环境的判断有误。第二条gen_ai.usage.*_tokens与 TaoToken 侧的用量记录要能对上号。同一个gen_ai.tool.call.id对应的那一轮TaoToken 后台显示的输入/输出 Token 应该和 Span 上的数字一致。对不上通常意味着有重试被合并了或者有请求没经过统一入口。第三条dsh.llm.attempt大于 1 就要警惕。每次真实调用生成独立 Span重试不合并。如果你看到 attempt 是 3而工具只执行了一次说明前两次模型请求失败后重试成功了失败原因需要单独看那两个 Span 的error.type。把这两层放在同一时间轴上一次失败就能被切成三段模型决定调什么 → 工具实际执行了什么 → 模型如何消化工具结果。5. 归因决策表把 tool Span 与模型请求逐条对上下面这张表是本文的核心按「chat 状态 tool 状态 用量特征」三列组合直接给结论。chat Spantool Span用量/耗时特征归因结论OKfinish_reasonstool_useERRORexit_code非 0chat 正常工具侧命令/脚本/环境问题OKfinish_reasonstool_useERRORerror.typetool_execution_timeouttool 耗时接近上限工具侧超时策略或依赖阻塞OKfinish_reasonstool_useERROR参数解析失败output_tokens 接近上限模型侧输出被截断参数不完整OKfinish_reasonslengthERROR 或未执行output_tokens 顶到 max模型侧长度上限太小或提示词过长ERRORerror.typellm_call_failed无 Span无 token 记录模型侧请求未成功工具未启动ERRORerror.typellm_call_failedattempt3无 Span只有最后一次有 token模型侧重试耗尽看前两次错误OKOK但tool.result.truncatedtrue后续 step 报错工具侧结果截断导致上下文残缺OKERRORerror.typecancelled用户中断交互侧非故障OKfinish_reasonsstop未执行token 正常模型侧过早判定任务完成用法是先在调用链里定位到红色的那层 Span取dsh.step.index找到同 step 的chatSpan再按上表落格。超过七成的情况落到第一、二行也就是工具侧。6. 实操从一份失败 trace 读出结论假设你把 DSH 的会话事件流落在本地可以直接做一次离线核对。事件流是 zstd 压缩的 JSONL先解压再挑工具结果DSH_HOME${DSH_HOME:-$HOME/.dsh} # 列出最近修改的会话文件 ls -lt $DSH_HOME/sessions/ | head -20 # 解出某次会话里的工具执行结果 zstd -dc $DSH_HOME/sessions/session-id.jsonl.zst \ | jq -c select(.kindtool_result) | {tool:.toolName, ok:.ok, code:.exitCode, err:.errorType, ms:.durationMs, callId:.toolCallId, step:.stepIndex}字段名随版本可能微调按你本地实际出现的键调整jq的先做keys探测即可。拿到callId之后回到调用链控制台按这个 id 检索能直接跳到对应的toolSpan再用stepIndex找同一 step 的chatSpan对比两边的finish_reasons与usage。一次真实的排障过程大致是这样告警显示某仓库的定时 Agent 任务失败率上升。按 Trace 检索找到失败的那条链路agent层是 ERROR。展开step层第 4 步和第 7 步红。看第 4 步chat是 OKfinish_reasonstool_use输出 Token 只有 300 多没有截断迹象。看同 step 的toolSpangen_ai.tool.nameexec_commandexit_code127tool.duration_ms318。命令找不到环境问题。交叉验证把callId在 TaoToken 的模型请求记录里检索确认这一轮是一次正常请求输入 18k、输出 0.3k和 Span 数字一致模型侧没有异常重试。结论落到工具侧去检查那个 profile 的 PATH 与容器镜像。第 7 步的失败同理属于同一个根因的第二次暴露。整条链路走下来十分钟以内。如果没有模型请求记录做交叉验证第 4 步就没法排除「模型偷偷重试」这种干扰项结论会站不稳。7. Token 消耗对照把成本按 step 拆开看归因清楚之后成本问题自然浮出来。Token 消耗对照的价值在于回答两个问题失败的那几轮到底烧了多少重试和长上下文分别贡献了多少。做法是把同一 trace 内所有chatSpan 的用量按dsh.step.index排开[ {step: 1, model: claude-sonnet-4-5, in: 3120, out: 210, attempt: 1, status: OK}, {step: 2, model: claude-sonnet-4-5, in: 5210, out: 388, attempt: 1, status: OK}, {step: 3, model: claude-sonnet-4-5, in: 9040, out: 512, attempt: 2, status: ERROR}, {step: 4, model: claude-sonnet-4-5, in: 18420, out: 642, attempt: 1, status: OK} ]读这张表的经验输入 Token 是阶梯式增长的因为每多一轮历史消息都在累积。第 4 步的 18k 里大部分是前四步的对话与工具结果。工具返回大段日志时这一步会陡增。失败轮次如果发生在 chat 层通常不产生输出 Token但输入已计入。所以模型侧失败看起来「没花钱」实际上输入成本已经发生。attempt2的那一轮要单独看如果两次都提交了完整输入成本是双倍。把重试上限从 3 降到 2 之前先确认失败是瞬时的还是稳定的。工具侧失败会间接推高后续轮次的输入 Token因为错误信息被塞进上下文模型带着一肚子报错继续跑。所以工具侧失败越多整体成本涨得越快这条因果链经常被忽略。按会话、按模型聚合之后可以直接做排行哪几个会话的 Token 最高、哪个模型的平均单步输入最大、哪类工具失败之后平均多跑了几轮。这些聚合值在控制台的 Token 与成本视图里能直接看到不用自己写脚本。8. 把一次排障变成日常观测单次归因做完还有一步把结论沉淀成可持续的检查项否则下次换个同事还得从头查一遍。建议至少落三条给工具侧失败率配告警。按gen_ai.tool.name分组统计status.codeERROR的比例超过阈值直接告警。工具侧失败往往成簇出现镜像换了、依赖升级了、权限策略改了单看总失败率会被模型侧噪声淹没。给模型侧重试次数配告警。dsh.llm.attempt 1的比例上升通常先于整体失败率上升出现是更早的信号。保留会话轨迹与用量记录的可追溯窗口。事件流落盘在本机是有损的换机器就丢把 Span 上报到统一后端才能做跨机、跨会话、跨时间的对比。另外建议把prompts、responses、tool arguments/results的内容捕获按环境区分开发环境开启方便复现生产环境关闭只留结构、耗时、状态与用量避免命令输出里的敏感信息进入链路数据。这个开关一般在插件配置里用captureContent: false之类的方式控制。再补一句配置纪律模型入口、Key、模型名三处要保持一致改完做一次冒烟——发起一次至少触发一次模型调用和一次工具调用的测试任务确认调用链里chat和tool两层都出现了再上生产。9. 小结与下一步回到最初的问题DSH 工具侧失败和模型侧失败怎么区分。答案不是看终端输出而是看两条记录能不能对上——toolSpan 上的错误类型、退出码、耗时、调用 id和chatSpan 上的结束原因、重试序号、输入输出 Token。两边的gen_ai.tool.call.id对上step.index对上归因就能落到唯一结论上。要让这套对照成立模型请求必须走一个可查的入口。把 DSH、Claude Code、Codex 的 Base URL 统一成https://taotoken.net/apiKey 用YOUR_API_KEY占位、通过环境变量注入请求记录和 Token 消耗就会自然沉淀下来和调用链的时间轴严丝合缝。接下来可以按这个顺序往下走先在 模型对话 里跑一次真实任务观察模型请求记录长什么样。需要长期跑 Agent 任务的话看看 Coding Plan 的额度与并发安排。然后去 创建 API Key把上面三份配置里的占位符换掉。需要细调 Claude Code 的行为参考 Claude Code 文档 里的环境变量与权限说明。下一篇文章会讲怎么把工具侧失败率、重试比例和单步 Token 消耗做成一张面板让归因从「事后翻记录」变成「看板上一眼可见」。