news 2026/9/18 15:17:57

任务 DSH 执行结束后,TaoToken 的模型调用 Span 怎么查完整

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
任务 DSH 执行结束后,TaoToken 的模型调用 Span 怎么查完整

dsh一次任务跑完,$DSH_HOME/sessions/里只留下一份 zstd 压缩的 JSONL:事件都在,但哪一轮推理烧掉了多少 Token、哪个工具把墙钟时间吃掉了、这次失败是模型侧还是工具侧,仍然要人工翻流。这篇把复盘拆成三段可跟做的动作:先去 TaoToken(https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=dsh_trace_intro)取 Key、把模型出口的 Base URL 设为https://taotoken.net/api,再从会话事件流里还原出任务 Trace 树与模型调用 Span 列表,最后做一张 Token 消耗节点对照表,把"花了多少"钉到具体节点上。

先说清定位差异。DSH 自带的轨迹视图解决的是本机、单会话、实时看;它按轮次列出执行记录,也能看到单条记录的用量与耗时。可一旦要回答"昨天这批任务的 P95 首 Token 延迟是多少""这条长会话的 Token 集中在第几轮""三天前那次中断是在哪一步断的",本机视图就不够了——事件流是按时间排的序列,没有父子关系,也没有各层的时间占用统计。本文不引入额外平台,只做一件事:把这批本地数据加工成带层级、带区间、带用量归属的复盘材料,而模型侧的统一出口交给 TaoToken。

1. 先把模型出口切到 TaoToken:Base URL 与 Key 的最小改动

复盘的前提是链路数据里能读到稳定的modelusage字段。如果一次任务里模型请求散落在多个供应商、多个 Key 上,Span 列表里的模型名会对不上账。所以第一步是收敛出口。

1.1 取 Key 与端点约定

到 TaoToken 控制台创建 API Key,key 形如YOUR_API_KEY的占位形式换成你自己的真实值即可。控制台入口:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=dsh_trace_key 。统一的基础地址是:

https://taotoken.net/api

注意这个地址是给工具配置用的,不要在后面拼接额外路径再写进配置项。OpenAI 兼容客户端一般会自己在 Base URL 后补/v1/chat/completions之类的路径;如果你手写 curl,建议先确认控制台文档里给出的完整端点形状,再决定是否补/v1

1.2 DSH 侧:环境变量优先

DSH 的模型适配器、工具集、沙箱策略都是按 profile 装配的插件,模型出口通常由某个适配器插件负责。多数 OpenAI 兼容适配器会读取标准环境变量,因此最省事的做法是先把下面三个变量注入到启动dsh的那个 shell 里:

# ~/.zshrc 或 ~/.bashrc,写入后重开终端 export TAOTOKEN_API_KEY="YOUR_API_KEY" export OPENAI_API_KEY="$TAOTOKEN_API_KEY" export OPENAI_BASE_URL="https://taotoken.net/api"

这样做的价值在于:无论你后面用dsh的 web、tui 还是 headless 形态,只要它们共享同一个内核与环境,模型出口就是同一个。

1.3 DSH 侧:profile 显式配置

如果本机的适配器插件不认OPENAI_*环境变量,或者你需要在同一个 profile 里挂多套模型,就得落到 profile 的 patch 文件里。路径与命名沿用 DSH 的约定:$DSH_HOME/profiles/<profile>/cordis.patch.yml,未设置DSH_HOME时默认在~/.dsh/profiles/<profile>/cordis.patch.yml

# $DSH_HOME/profiles/default/cordis.patch.yml # 说明:插件 id 与字段名请以本机已启用的模型适配器为准, # 下面给出的是 OpenAI 兼容适配器最常见的三段式形态。 - id: model-adapter-openai config: baseURL: https://taotoken.net/api apiKey: ${TAOTOKEN_API_KEY} # 若该插件不支持变量插值,改用环境变量注入 models: - id: deepseek-chat alias: fast - id: deepseek-reasoner alias: deep

改完重启 DSH 服务使配置生效。显式配置的优先级高于环境变量,所以两处都写了的情况下,以 patch 文件为准——排查"我明明改了环境变量还是不生效"时,先看这里。

1.4 十秒钟验证出口是否切换成功

在正式跑任务之前,先用一条最小的对话请求确认 Key 与端点可用:

curl -sS "https://taotoken.net/api/v1/chat/completions" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "deepseek-chat", "messages": [{"role": "user", "content": "ping"}], "max_tokens": 16 }'

返回体里只要能看到choicesusage两段,就说明出口通了。特别留意usage.prompt_tokensusage.completion_tokens——这两个字段正是后面 Span 列表里 Token 归属的原始来源。若返回 401/403,先检查 Authorization 头有没有带Bearer前缀;若返回 404,多半是端点路径补错了层级。

2. 复盘第一层:把一次 turn 还原成任务 Trace 树

DSH 的执行模型是 ReAct 循环:一次用户任务叫一个 turn,turn 内的每一轮"推理 → 调工具 → 观察结果"叫一个 step。循环轮数和调用哪些工具由模型运行时决定,所以执行结构事前不可知。这也正是为什么不能靠静态代码去推链路,只能从事件流里重建。

2.1 先摸清事件类型分布

别急着写解析逻辑。第一步是确认你本机这份事件流里,各类型事件到底叫什么名字——不同版本的事件字段可能叫typekindevent

export DSH_HOME="${DSH_HOME:-$HOME/.dsh}" ls -lt "$DSH_HOME/sessions/" | head # 任取一个会话文件,看前若干行结构(不落地,直接管道) zstd -d -c "$DSH_HOME/sessions/<session-id>.jsonl.zst" | head -n 5

你会看到一行一个 JSON 对象,字段里有时间戳、事件类型和一些载荷。记住几个关键点:时间戳字段名turn 开始/结束事件名step 开始/结束事件名模型请求与响应事件名usage 挂在哪一层

2.2 一个可直接跑的树重建脚本

下面这个脚本做三件事:打印事件类型直方图(用于确认字段名)、按 turn/step 分组、输出带 Token 归属的 Span 列表。存成dsh-trace.mjs,用 Node.js 22 以上跑。

// dsh-trace.mjs // 用法: node dsh-trace.mjs ~/.dsh/sessions/<session-id>.jsonl.zst import { createInterface } from "node:readline"; import { spawn } from "node:child_process"; const file = process.argv[2]; if (!file) { console.error("用法: node dsh-trace.mjs <session.jsonl.zst>"); process.exit(1); } const raw = []; const proc = spawn("zstd", ["-d", "-c", file]); createInterface({ input: proc.stdout, crlfDelay: Infinity }) .on("line", (line) => { const s = line.trim(); if (!s) return; try { raw.push(JSON.parse(s)); } catch { /* 截断行直接跳过 */ } }) .on("close", main); const get = (o, keys, dflt) => { for (const k of keys) { const v = o?.[k]; if (v !== undefined && v !== null) return v; } return dflt; }; const tsOf = (e) => Number(get(e, ["ts", "timestamp", "time", "created_at"], 0)); const typeOf = (e) => String(get(e, ["type", "kind", "event"], "unknown")); const usageOf = (e) => { const u = get(e, ["usage", "token_usage", "tokens"], {}) || {}; return { input: Number(get(u, ["prompt_tokens", "input_tokens"], 0)), output: Number(get(u, ["completion_tokens", "output_tokens"], 0)), }; }; function main() { raw.sort((a, b) => tsOf(a) - tsOf(b)); // 1) 事件类型直方图:字段名对不上时,先看这里再改正则 const hist = new Map(); for (const e of raw) hist.set(typeOf(e), (hist.get(typeOf(e)) || 0) + 1); console.log("== 事件类型分布 =="); for (const [k, v] of [...hist].sort((a, b) => b[1] - a[1])) { console.log(`${k.padEnd(38)} ${v}`); } // 2) 重建层级并累加用量 const spans = []; let turnIdx = -1; let stepIdx = 0; let turnTok = { input: 0, output: 0 }; let stepTok = { input: 0, output: 0 }; const flushStep = () => { if (stepIdx > 0) { spans.push({ level: "step", turn: turnIdx, step: stepIdx, tokens: { ...stepTok } }); stepTok = { input: 0, output: 0 }; } }; const flushTurn = () => { if (turnIdx >= 0) { spans.push({ level: "turn", turn: turnIdx, tokens: { ...turnTok } }); turnTok = { input: 0, output: 0 }; } }; for (const e of raw) { const t = typeOf(e); const u = usageOf(e); if (/turn.*(start|begin)/i.test(t)) { flushStep(); flushTurn(); turnIdx += 1; stepIdx = 0; } else if (/step.*(start|begin)/i.test(t)) { flushStep(); stepIdx += 1; } else if (/(chat|completion|llm|response)/i.test(t) && (u.input || u.output)) { turnTok.input += u.input; turnTok.output += u.output; stepTok.input += u.input; stepTok.output += u.output; spans.push({ level: "chat", turn: turnIdx, step: stepIdx, ts: tsOf(e), model: get(e, ["model", "model_name"], "unknown"), finish: get(e, ["finish_reason", "stop_reason"], "-"), attempt: get(e, ["attempt", "retry_count", "retry"], 1), input: u.input, output: u.output, }); } else if (/(tool).*(end|finish|result|done)/i.test(t)) { spans.push({ level: "tool", turn: turnIdx, step: stepIdx, ts: tsOf(e), name: get(e, ["tool", "tool_name", "name"], "unknown"), ms: get(e, ["duration_ms", "elapsed_ms", "ms"], 0), error: get(e, ["error", "error_code"], null), }); } } flushStep(); flushTurn(); // 3) 打印:先树、后 Span 列表 console.log("\n== Trace 树(缩进即父子关系) =="); for (const s of spans.filter((x) => x.level !== "chat" && x.level !== "tool")) { if (s.level === "turn") { console.log(`turn#${s.turn} tokens in/out = ${s.tokens.input}/${s.tokens.output}`); } else { console.log(` └─ step#${s.step} tokens in/out = ${s.tokens.input}/${s.tokens.output}`); } } console.log("\n== 模型调用 Span 列表 =="); for (const c of spans.filter((x) => x.level === "chat")) { console.log( `turn#${c.turn} step#${c.step} attempt=${c.attempt} model=${c.model} ` + `finish=${c.finish} in=${c.input} out=${c.output}` ); } }

跑起来:

node dsh-trace.mjs "$DSH_HOME/sessions/<session-id>.jsonl.zst"

输出里你会得到三块东西:事件类型分布、带 Token 累加的树、以及模型调用明细。如果事件类型分布里出现了你正则没覆盖的名字,直接改那几个正则即可——脚本刻意把"字段名探测"和"层级重建"分开,就是为了让这一步可调。

2.3 树要满足的两个硬条件

好的复盘材料必须满足两个条件,否则后面所有聚合都是错的:

父子关系明确。一次 turn 对应一条链路,turn 内的 step 是它的子节点,step 内的模型调用与工具调用再往下一层。多轮对话之间用会话 ID 横向关联,而不是硬塞进同一条无限膨胀的链路里——长会话一旦塞进单链路,任何按耗时的排序都会失真。

时间区间完整。每个节点都要有开始与结束。流式响应没正常收尾、step 先于工具结束、用户手动 Ctrl+C,这些场景都必须在数据里留痕。如果你在事件流里看到"有 chat 开始、没有 chat 结束",那就要在解析时补一个带错误状态的节点,而不是静默丢弃——丢掉的正是复盘时最想看的那个失败。

3. 复盘第二层:模型调用 Span 列表该有哪些字段

树负责"看得清结构",Span 列表负责"查得到细节"。下面这张表可以直接当核对清单用:跑完一次任务,逐行确认字段有没有值。

字段含义常见来源缺失时怎么办
gen_ai.session.id会话标识,用于跨 turn 关联会话文件元信息用文件名兜底
dsh.turn.index第几轮任务turn 开始事件序号按时间排序自增
dsh.step.index轮内第几步step 开始事件序号按时间排序自增
dsh.llm.attempt第几次真实调用(重试计数)请求事件或重试标记默认 1,重试场景必修
gen_ai.request.model请求的模型名请求体model从 profile 配置回填
gen_ai.response.model实际返回的模型名响应体model与请求名不一致时以响应为准
gen_ai.usage.input_tokens输入 Tokenusage.prompt_tokens流式场景下看末块
gen_ai.usage.output_tokens输出 Tokenusage.completion_tokens同上
gen_ai.response.finish_reasons结束原因choices[].finish_reason缺失即视为中断
调用耗时单次调用墙钟请求与响应时间戳差用首末事件时间差
首 Token 延迟TTFT首个流式分块时间戳非流式调用可留空

有两个坑值得单独说。

重试不要合并。一次任务里模型调用失败重试是常态。每次真实发生的请求都要生成独立节点,并用dsh.llm.attempt这类序号标记。如果你把三次重试合并成一个节点、耗时取总和、用量取末次,那"这次任务为什么慢"的答案就永远找不出来了——慢的往往正是前两次失败的等待。

结束原因要原样保留。finish_reasonstoplength还是缺失,直接决定这次是正常结束、被截断,还是流没走完。很多"Token 消耗异常"的案子,本质是length截断导致模型没输出完整结果,Agent 又发起了一轮补救调用。

4. 复盘第三层:Token 消耗节点对照表

有了树和 Span 列表,最后一步是把用量钉到节点上。做法很朴素:自底向上累加。chat 节点上的输入/输出 Token,向上分别归入所属 step 和所属 turn。

层级节点类型Token 归属规则复盘时回答的问题
L0会话该会话全部 turn 之和这段会话总共花了多少
L1turn该 turn 内所有 chat 之和哪一轮任务最贵
L2step该 step 内所有 chat 之和贵在推理还是贵在反复试错
L3chat单次请求的usage原值是不是某次重试烧掉的
L4tool不计 Token,只计耗时与状态慢是慢在工具还是慢在模型

实际复盘时,这张表最常用的三种看法:

  1. 按 turn 排序看头部。把 turn 按输出 Token 降序排,通常前 10% 的 turn 吃掉一半以上的量。看看它们是不是同一个任务类型、同一个模型。
  2. 按 step 看分布。如果一个 turn 里 step 数量明显偏多,而单次 chat 用量很小,典型症状是"模型在反复调工具但没收敛",问题在工具描述或提示词,不在模型规格。
  3. 按 chat 看重试。attempt > 1的节点单独拉出来统计。重试率高说明上游不稳定或请求体触发限流,这时候要看的不是用量,而是错误码分布。

如果你还想把这份数据补齐成一个可长期留存、可跨机聚合、可配告警的形态,思路就是:把上述字段映射到 OpenTelemetry GenAI 语义约定(gen_ai.*命名空间),各层节点带上独立状态与错误类型,再统一上报到支持 Traces / Spans / Sessions 多视角检索的后端。这样"三天前那次的 Trace ID"才真的能被搜出来,而不是靠翻本机文件。

5. 三类高频异常的 Span 长相

复盘的价值在于把"感觉慢"变成"知道哪一层慢"。下面三类是最常遇到的。

第一类:401 / 403,一个 Span 都没有。说明请求根本没出网关。检查顺序:环境变量有没有注入到启动dsh的那个 shell;profile patch 里的apiKey有没有被正确插值;Authorization 头的前缀是否完整。这类问题不会在 DSH 的会话轨迹里留下明显痕迹,最容易被误判成"框架不工作"。

第二类:有 chat 节点但没有结束原因,用量为 0。典型的流式中断。表现为:树上有节点,节点上output_tokens是 0 或缺失,finish_reason为空。排查动作是在解析脚本里把这一类显式标成status = error,而不是让它们混进成功节点里拉低平均耗时。

第三类:step 数量爆炸,Token 反而正常。单次调用都很小,但一个 turn 里几十个 step。这种时候耗时的大头在工具执行和往返等待上。做法是把 tool 节点的耗时单独汇总,和 chat 节点的耗时做占比对照。工具耗时占比超过一半,就该去看工具本身的实现或调用频率,而不是加模型预算。

6. 顺手把 Claude Code 与 Codex 也指向同一个出口

既然已经在 TaoToken 上统一了模型出口,同一台机器上的其他编码工具也建议一并收敛,避免复盘时口径不一致。

Claude Code(settings.json):

{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "claude-sonnet-4-5" } }

注意 Claude Code 走的是ANTHROPIC_*系列变量,这三个不要和下面的 Codex 配置混用。

Codex(config.toml):

# ~/.codex/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"

Codex 用的是OPENAI体系与model_providers表,把ANTHROPIC_*变量写进这里不会生效——这是最常见的配置串台问题。

CC Switch 三件套:如果你用 CC Switch 做多套配置切换,每个 profile 只需要填齐三项即可快速切走:Base URL、API Key、Model(供应商名可自定义)。三件套填完,切换供应商就不用改任何工具源码。

7. 一次完整的复盘走法

把前面的东西串成一条可执行的路径:

  1. 注入TAOTOKEN_API_KEYOPENAI_BASE_URL,重启 DSH 服务。
  2. 用一条 curl 确认usage字段正常返回。
  3. 跑一个真实任务,让 Agent 至少完成一次模型调用与一次工具调用。
  4. 找到对应的会话文件,跑dsh-trace.mjs,先看事件类型分布。
  5. 对不上的事件名,调整脚本里的正则,重跑。
  6. 拿到 Trace 树,确认每一层都有开始与结束;异常节点手动补状态。
  7. 导出模型调用 Span 列表,逐个核对第 3 节表格里的字段。
  8. 生成 Token 消耗节点对照表,按 turn 排序看头部,按 step 看分布,按 attempt 看重试。
  9. 把结论写回任务记录:这次慢在哪一层、贵在哪个节点、失败属于模型侧还是工具侧。

做到第 9 步,你手上就不再是一份"事件很多但说不清楚"的 JSONL,而是一份能直接拿去过评审的复盘材料。需要实际对照模型返回字段时,可以打开模型对话页(https://taotoken.net/models/detail/chat?utm_source=taotoken_aicg_blog_end&utm_content=dsh_trace_chat )发一轮同样的提示词,看usagefinish_reason的真实取值。

如果团队里跑 Agent 的机器不止一台,或者需要把用量做成按周的成本报表,那就该考虑把出口和额度一起管起来。Coding Plan(https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=dsh_trace_plan )适合把编码类任务集中到一套额度下,避免多台机器各拿一把 Key 导致对账困难。新增或轮换 Key 在 API Keys 页面(https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=dsh_trace_keys )完成,建议按机器或按项目拆 Key,这样复盘时"哪个项目的用量异常"可以直接从 Key 维度切出来。Claude Code 侧的完整环境变量说明在文档页(https://taotoken.net/doc/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_content=dsh_trace_cc ),配好之后再和 DSH 一起跑同一批任务,两边的 Trace 口径就能对齐了。

最后提醒一句:本文所有解析脚本与配置都在本地执行、本地读取,不要把它们接到生产库或线上环境上跑;YOUR_API_KEY也不要提交进代码仓库,用环境变量或密钥管理工具注入。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 15:16:52

Trae 跑 Builder/Chat 智能体:Key 用 TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 15:16:39

Docker运行Oracle 11g:helowin镜像SID修改实战指南

1. 项目概述&#xff1a;为什么非得用 Docker 跑 Oracle 11g&#xff1f;又为何偏偏选 helowin 镜像&#xff1f;Docker 安装 Oracle 11g —— 这句话在 DBA 和 Java 开发者圈子里&#xff0c;几乎等同于“既要马儿跑&#xff0c;又要马儿不吃草”的现实版。Oracle 11g 是个典型…

作者头像 李华
网站建设 2026/9/18 15:16:23

pgx v5 pgconn 指南:基于 Go 实现 libpq 同级的低层 PostgreSQL 驱动

pgx v5 pgconn 指南&#xff1a;基于 Go 实现 libpq 同级的低层 PostgreSQL 驱动 【免费下载链接】inngest The leading workflow orchestration platform. Run stateful step functions and AI workflows on serverless, servers, or the edge. 项目地址: https://gitcode.c…

作者头像 李华
网站建设 2026/9/18 15:12:53

中小券商研报自动生成:DeepSeek私有化部署架构与落地实践

简介&#xff1a;《财务分析智能化&#xff1a;中小券商部署DeepSeek实现研报自动生成的架构设计》是一份面向券商数字化转型场景的技术方案文档&#xff0c;适合金融IT架构师、数据分析师及关注大模型落地的读者&#xff0c;主要解决中小券商在财务分析效率、研报生成质量与人…

作者头像 李华