DeepSeek Harness 系列(09):可观测性——怎么知道 Agent 在干什么
英文标题:DeepSeek Harness 系列(09):可观测性——怎么知道 Agent 在干什么
Token 消耗多少?哪步最慢?出错了在哪里?这篇文章讲 dsh 的可观测性机制:Session 事件日志、Token 计量、遥测 Seam,OpenTelemetry
6.3 KB
中文
先说一个让人头疼的场景
你的 Agent 跑了三分钟,最后报了一个错误。
然后你盯着日志——什么都没有。你不知道它调用了哪些工具、哪一步卡住了、Token 到底花在哪里,更不知道为什么失败。
这就是可观测性缺失的典型后果。
为什么可观测性对 Agent 尤其重要
传统服务出了问题,你可以加断点、看堆栈。Agent 不一样。
Agent 是非确定性的。 相同的输入,模型可能产生完全不同的工具调用序列。你没办法在"模型决策"这一步加断点——那是一个黑盒。
调试只能靠日志反推。 模型的"想法"只体现在它输出的文字和工具调用里。你需要把这些全部记下来,事后才能重建它的推理链。
Token 费用不透明。 一个多轮 Agent 对话,到底哪一步最贵?是第三轮工具结果太长?还是 system prompt 占了大头?没有计量,你连优化方向都找不到。
生产环境出问题,你得有证据。 用户说"它给了我一个错误答案",你需要还原当时的完整执行链——用了哪些工具、返回了什么、模型看到了什么。
Session 日志:最完整的观测数据
回顾第 05 篇讲过的内容:dsh Session 是一份仅追加的类型化事件日志 。
这个设计不只是为了持久化,它本身就是最完整的观测数据源。
每个 Session 事件都包含:
每个 SessionEvent 的结构:
- type:事件类型(如 'tool/call'、'turn/end'、'assistant/message')
- seq:单调递增的序号(从 0 开始)
- time:时间戳(毫秒级 Unix 时间戳)
- data:类型化的事件数据(不同 type 有不同 data 结构)
这意味着:只要你能读取 Session 日志,你就能重建整个执行过程——每一步做了什么、花了多少时间、有没有出错。
实时监听:session/event
不需要等日志写完再分析。dsh 提供了 session/event 事件,让你实时监听 Session 的每一条记录:
ctx.on('session/event', (session, event) => {
console.log(`[${event.type}] seq=${event.seq} time=${event.time}`)
if (event.type === 'tool/call') {
console.log(` Tool: ${event.data.name}`)
console.log(` Args: ${event.data.arguments}`)
}
if (event.type === 'tool/result') {
const isError = event.data.message.content.some(
b => b.type === 'tool_result' && b.isError
)
console.log(` Result: ${isError ? 'ERROR' : 'OK'}`)
}
})
这个监听器在开发调试时非常有用——你能看到 Agent 在实时做什么,不用等它跑完。
Token 计量:ctx.tokenMeter
知道"发生了什么"只是第一步。知道"花了多少钱"同样重要。
dsh 提供了 ctx.tokenMeter,可以测量当前 Session 的 Token 压力:
interface TokenMeasurement {
readonly logRevision: SessionLogOffset
readonly totalTokens: number
readonly surfaceTokens: number
readonly surfaceDeltaTokens: number
readonly nodes: readonly TokenSurfaceNode[]
}
几个关键概念:
- surface tokens :模型这次请求实际看到的历史内容有多少 token。这决定了你的 API 费用中"输入 token"这部分。
- total tokens :输入 + 输出的总和,反映这次请求的完整费用。
- surfaceDeltaTokens :和上一次请求相比,surface 增加了多少。如果这个数字持续增大,说明上下文在膨胀,可能需要压缩策略。
使用示例:
ctx.on('session/event', (session, event) => {
if (event.type !== 'turn/end') return
const measurement = ctx.tokenMeter.measure(session)
console.log(`Turn ${event.data.turn} ended:`)
console.log(` Surface tokens: ${measurement.surfaceTokens}`)
console.log(` Total tokens: ${measurement.totalTokens}`)
const delta = measurement.surfaceDeltaTokens
const sign = delta > 0 ? '+' : ''
console.log(` Delta: ${sign}${delta}`)
if (delta > 2000) {
console.warn(' ⚠ Context growing fast, consider compression')
}
})
遥测 Seam:ctx.sessionTelemetry
session/event 监听适合开发调试,但生产环境你需要把数据发到外部系统——比如 Grafana、Datadog、CloudWatch。
dsh 为此设计了一个"遥测 Seam":ctx.sessionTelemetry。
Seam(接缝)这个词用得很精准——它是一个标准化的接口,让你把遥测数据接入任意后端,同时 harness 本身不依赖任何具体的监控系统。
每条遥测记录的结构:
interface SessionTelemetryRecord {
channel: 'ledger' | 'ops'
time: number
severity: 'info' | 'warn' | 'error'
attributes: Record<string, string | number>
body: unknown
}
两种 channel 分别做什么
ledger channel:Session 日志的完整镜像。每一条 Session 事件都会产生一条对应的 ledger 记录。这是审计和回放的数据来源。
包括:
- 每个
assistant/message(含完整流数据) - 每个
tool/call和tool/result - 失败的
assistant/attempt(模型尝试了但最终没用) - 所有
turn/start、turn/end等生命周期事件
ops channel:运营信号,只有两种:
agent-error:Agent 在 Turn 之外失败了(比如初始化报错)shutdown:Agent 正常关闭
严重程度如何判定
error:工具结果isError: true、turn/end 带错误原因、agent-error 运营事件- 其他情况:
info
这个映射让你可以在监控系统里直接过滤 severity === 'error' 来看所有异常,不用自己写判断逻辑。
OpenTelemetry 接入
dsh 提供官方的 OTel Provider 插件:dsh-session-telemetry-otel。
接入方式(概念性):
'@deepseek-ai/dsh-session-telemetry-otel'
几个设计原则值得了解:
边界公理 :harness 只负责调用 emit(),批处理、重试、排队这些属于 OTel SDK 的职责,harness 不插手。这样两边都可以独立演化。
尽力而为 :遥测记录可能重复也可能丢失。接收端应该基于 (session.id, format_version, event.seq) 组合来去重 ledger 记录,而不是假设每条记录恰好到达一次。
flush 是可选的 :每次 Turn 结束后可以调用 flush(),但 OTel 后端默认不实现(避免并发冲突)。如果你需要强一致性,需要自行配置。
实战:写一个简单的调试插件
把上面的内容整合成一个完整的调试观测插件:
export const name = 'debug-observer'
export const inject = ['tokenMeter']
export function apply(ctx: Context): void {
ctx.on('session/event', (session, event) => {
if (event.type !== 'tool/call') return
console.log(`[Tool Call] ${event.data.name}`)
console.log(` Call ID: ${event.data.callId}`)
console.log(` Args: ${event.data.arguments}`)
})
ctx.on('session/event', (session, event) => {
if (event.type !== 'tool/result') return
const blocks = event.data.message.content
const isError = blocks.some(b => b.type === 'tool_result' && b.isError)
const icon = isError ? '✗' : '✓'
const toolUseId = blocks[0]?.toolUseId ?? 'unknown'
console.log(`[Tool Result] ${icon} (call: ${toolUseId})`)
})
ctx.on('session/event', (session, event) => {
if (event.type !== 'turn/end') return
const reason = event.data.reason.kind
const measurement = ctx.tokenMeter.measure(session)
console.log(`\n[Turn ${event.data.turn}] ended: ${reason}`)
console.log(` Surface: ${measurement.surfaceTokens} tokens`)
console.log(` Total: ${measurement.totalTokens} tokens`)
const delta = measurement.surfaceDeltaTokens
const sign = delta > 0 ? '+' : ''
console.log(` Delta: ${sign}${delta}`)
if (reason === 'error') {
console.error(` Error: ${JSON.stringify(event.data.reason)}`)
}
})
ctx.on('session/created', (session) => {
console.log(`\n[Session] created: ${session.id}`)
})
ctx.on('session/disposed', (session) => {
console.log(`[Session] disposed: ${session.id}`)
})
}
这个插件在开发时可以快速加入 Bundle,看到完整的运行轨迹。生产环境则换成 dsh-session-telemetry-otel 插件,数据流向监控系统。
调试技巧:读 JSONL 日志文件
dsh 默认把 Session 日志持久化为 JSONL 文件(每行一个 JSON 对象,即一条 SessionEvent)。
以下是一些常用的命令行分析技巧:
cat session.jsonl | grep '"type":"tool/call"' | jq '.data.name'
cat session.jsonl | grep '"type":"assistant/attempt"' | jq '.'
cat session.jsonl | grep '"type":"assistant/message"' | jq '.data.usage'
cat session.jsonl | grep '"type":"turn/end"' | jq '.data.reason.kind'
cat session.jsonl | grep '"type":"tool/result"' | jq 'select(.data.message.content[].isError == true)'
这些命令假设你有 jq 工具。如果是 Windows 环境,可以用 PowerShell 的 ConvertFrom-Json 做类似的分析。
可观测性层次总结
四个层次,覆盖从开发到生产:
实时观测(开发调试)
└─ session/event 监听器 → 每条事件即时打印到控制台
审计与回放(事后分析)
└─ JSONL 日志文件 → 完整重建执行链,配合 jq 分析
Token 用量分析
└─ ctx.tokenMeter.measure(session) → 每个 surface 节点的 token 计量
→ 找到上下文膨胀的罪魁祸首
生产监控(系统级)
└─ ctx.sessionTelemetry + OTel 插件 → 接入 Grafana / Datadog / CloudWatch
→ 告警、看板、错误追踪全部打通
小结
可观测性不是"有了更好"的附加项,对 Agent 来说它是调试的唯一手段 。
dsh 在设计上就考虑到了这一点:Session 本身是事件日志,事件日志天然就是审计数据;ctx.tokenMeter 让 Token 消耗不再是黑盒;ctx.sessionTelemetry 提供标准化接缝,让你自由选择后端。
核心模式很简单:Session 事件监听 → JSONL 持久化 → 遥测 Seam → OTel 后端 。你用哪一层取决于你的场景,但这几层可以同时运行,互不干扰。
下一篇是系列的最后一篇,我们会把前面学到的所有机制整合起来,完整地写一个生产级插件——从工具注册、Session 管理、错误处理,到可观测性,一起落地。
在PrimeSkills 可以找到已在真实企业场景验证过的 AI Agent 技能和工作流,不是演示级的,是用在实际项目里的。
更多内容见我的个人主页
English / 原文
先说一个让人头疼的场景
你的 Agent 跑了三分钟,最后报了一个错误。
然后你盯着日志——什么都没有。你不知道它调用了哪些工具、哪一步卡住了、Token 到底花在哪里,更不知道为什么失败。
这就是可观测性缺失的典型后果。
为什么可观测性对 Agent 尤其重要
传统服务出了问题,你可以加断点、看堆栈。Agent 不一样。
Agent 是非确定性的。 相同的输入,模型可能产生完全不同的工具调用序列。你没办法在"模型决策"这一步加断点——那是一个黑盒。
调试只能靠日志反推。 模型的"想法"只体现在它输出的文字和工具调用里。你需要把这些全部记下来,事后才能重建它的推理链。
Token 费用不透明。 一个多轮 Agent 对话,到底哪一步最贵?是第三轮工具结果太长?还是 system prompt 占了大头?没有计量,你连优化方向都找不到。
生产环境出问题,你得有证据。 用户说"它给了我一个错误答案",你需要还原当时的完整执行链——用了哪些工具、返回了什么、模型看到了什么。
Session 日志:最完整的观测数据
回顾第 05 篇讲过的内容:dsh Session 是一份仅追加的类型化事件日志 。
这个设计不只是为了持久化,它本身就是最完整的观测数据源。
每个 Session 事件都包含:
每个 SessionEvent 的结构:
- type:事件类型(如 'tool/call'、'turn/end'、'assistant/message')
- seq:单调递增的序号(从 0 开始)
- time:时间戳(毫秒级 Unix 时间戳)
- data:类型化的事件数据(不同 type 有不同 data 结构)
这意味着:只要你能读取 Session 日志,你就能重建整个执行过程——每一步做了什么、花了多少时间、有没有出错。
实时监听:session/event
不需要等日志写完再分析。dsh 提供了 session/event 事件,让你实时监听 Session 的每一条记录:
ctx.on('session/event', (session, event) => {
console.log(`[${event.type}] seq=${event.seq} time=${event.time}`)
if (event.type === 'tool/call') {
console.log(` Tool: ${event.data.name}`)
console.log(` Args: ${event.data.arguments}`)
}
if (event.type === 'tool/result') {
const isError = event.data.message.content.some(
b => b.type === 'tool_result' && b.isError
)
console.log(` Result: ${isError ? 'ERROR' : 'OK'}`)
}
})
这个监听器在开发调试时非常有用——你能看到 Agent 在实时做什么,不用等它跑完。
Token 计量:ctx.tokenMeter
知道"发生了什么"只是第一步。知道"花了多少钱"同样重要。
dsh 提供了 ctx.tokenMeter,可以测量当前 Session 的 Token 压力:
interface TokenMeasurement {
readonly logRevision: SessionLogOffset
readonly totalTokens: number
readonly surfaceTokens: number
readonly surfaceDeltaTokens: number
readonly nodes: readonly TokenSurfaceNode[]
}
几个关键概念:
- surface tokens :模型这次请求实际看到的历史内容有多少 token。这决定了你的 API 费用中"输入 token"这部分。
- total tokens :输入 + 输出的总和,反映这次请求的完整费用。
- surfaceDeltaTokens :和上一次请求相比,surface 增加了多少。如果这个数字持续增大,说明上下文在膨胀,可能需要压缩策略。
使用示例:
ctx.on('session/event', (session, event) => {
if (event.type !== 'turn/end') return
const measurement = ctx.tokenMeter.measure(session)
console.log(`Turn ${event.data.turn} ended:`)
console.log(` Surface tokens: ${measurement.surfaceTokens}`)
console.log(` Total tokens: ${measurement.totalTokens}`)
const delta = measurement.surfaceDeltaTokens
const sign = delta > 0 ? '+' : ''
console.log(` Delta: ${sign}${delta}`)
if (delta > 2000) {
console.warn(' ⚠ Context growing fast, consider compression')
}
})
遥测 Seam:ctx.sessionTelemetry
session/event 监听适合开发调试,但生产环境你需要把数据发到外部系统——比如 Grafana、Datadog、CloudWatch。
dsh 为此设计了一个"遥测 Seam":ctx.sessionTelemetry。
Seam(接缝)这个词用得很精准——它是一个标准化的接口,让你把遥测数据接入任意后端,同时 harness 本身不依赖任何具体的监控系统。
每条遥测记录的结构:
interface SessionTelemetryRecord {
channel: 'ledger' | 'ops'
time: number
severity: 'info' | 'warn' | 'error'
attributes: Record<string, string | number>
body: unknown
}
两种 channel 分别做什么
ledger channel:Session 日志的完整镜像。每一条 Session 事件都会产生一条对应的 ledger 记录。这是审计和回放的数据来源。
包括:
- 每个
assistant/message(含完整流数据) - 每个
tool/call和tool/result - 失败的
assistant/attempt(模型尝试了但最终没用) - 所有
turn/start、turn/end等生命周期事件
ops channel:运营信号,只有两种:
agent-error:Agent 在 Turn 之外失败了(比如初始化报错)shutdown:Agent 正常关闭
严重程度如何判定
error:工具结果isError: true、turn/end 带错误原因、agent-error 运营事件- 其他情况:
info
这个映射让你可以在监控系统里直接过滤 severity === 'error' 来看所有异常,不用自己写判断逻辑。
OpenTelemetry 接入
dsh 提供官方的 OTel Provider 插件:dsh-session-telemetry-otel。
接入方式(概念性):
'@deepseek-ai/dsh-session-telemetry-otel'
几个设计原则值得了解:
边界公理 :harness 只负责调用 emit(),批处理、重试、排队这些属于 OTel SDK 的职责,harness 不插手。这样两边都可以独立演化。
尽力而为 :遥测记录可能重复也可能丢失。接收端应该基于 (session.id, format_version, event.seq) 组合来去重 ledger 记录,而不是假设每条记录恰好到达一次。
flush 是可选的 :每次 Turn 结束后可以调用 flush(),但 OTel 后端默认不实现(避免并发冲突)。如果你需要强一致性,需要自行配置。
实战:写一个简单的调试插件
把上面的内容整合成一个完整的调试观测插件:
export const name = 'debug-observer'
export const inject = ['tokenMeter']
export function apply(ctx: Context): void {
ctx.on('session/event', (session, event) => {
if (event.type !== 'tool/call') return
console.log(`[Tool Call] ${event.data.name}`)
console.log(` Call ID: ${event.data.callId}`)
console.log(` Args: ${event.data.arguments}`)
})
ctx.on('session/event', (session, event) => {
if (event.type !== 'tool/result') return
const blocks = event.data.message.content
const isError = blocks.some(b => b.type === 'tool_result' && b.isError)
const icon = isError ? '✗' : '✓'
const toolUseId = blocks[0]?.toolUseId ?? 'unknown'
console.log(`[Tool Result] ${icon} (call: ${toolUseId})`)
})
ctx.on('session/event', (session, event) => {
if (event.type !== 'turn/end') return
const reason = event.data.reason.kind
const measurement = ctx.tokenMeter.measure(session)
console.log(`\n[Turn ${event.data.turn}] ended: ${reason}`)
console.log(` Surface: ${measurement.surfaceTokens} tokens`)
console.log(` Total: ${measurement.totalTokens} tokens`)
const delta = measurement.surfaceDeltaTokens
const sign = delta > 0 ? '+' : ''
console.log(` Delta: ${sign}${delta}`)
if (reason === 'error') {
console.error(` Error: ${JSON.stringify(event.data.reason)}`)
}
})
ctx.on('session/created', (session) => {
console.log(`\n[Session] created: ${session.id}`)
})
ctx.on('session/disposed', (session) => {
console.log(`[Session] disposed: ${session.id}`)
})
}
这个插件在开发时可以快速加入 Bundle,看到完整的运行轨迹。生产环境则换成 dsh-session-telemetry-otel 插件,数据流向监控系统。
调试技巧:读 JSONL 日志文件
dsh 默认把 Session 日志持久化为 JSONL 文件(每行一个 JSON 对象,即一条 SessionEvent)。
以下是一些常用的命令行分析技巧:
cat session.jsonl | grep '"type":"tool/call"' | jq '.data.name'
cat session.jsonl | grep '"type":"assistant/attempt"' | jq '.'
cat session.jsonl | grep '"type":"assistant/message"' | jq '.data.usage'
cat session.jsonl | grep '"type":"turn/end"' | jq '.data.reason.kind'
cat session.jsonl | grep '"type":"tool/result"' | jq 'select(.data.message.content[].isError == true)'
这些命令假设你有 jq 工具。如果是 Windows 环境,可以用 PowerShell 的 ConvertFrom-Json 做类似的分析。
可观测性层次总结
四个层次,覆盖从开发到生产:
实时观测(开发调试)
└─ session/event 监听器 → 每条事件即时打印到控制台
审计与回放(事后分析)
└─ JSONL 日志文件 → 完整重建执行链,配合 jq 分析
Token 用量分析
└─ ctx.tokenMeter.measure(session) → 每个 surface 节点的 token 计量
→ 找到上下文膨胀的罪魁祸首
生产监控(系统级)
└─ ctx.sessionTelemetry + OTel 插件 → 接入 Grafana / Datadog / CloudWatch
→ 告警、看板、错误追踪全部打通
小结
可观测性不是"有了更好"的附加项,对 Agent 来说它是调试的唯一手段 。
dsh 在设计上就考虑到了这一点:Session 本身是事件日志,事件日志天然就是审计数据;ctx.tokenMeter 让 Token 消耗不再是黑盒;ctx.sessionTelemetry 提供标准化接缝,让你自由选择后端。
核心模式很简单:Session 事件监听 → JSONL 持久化 → 遥测 Seam → OTel 后端 。你用哪一层取决于你的场景,但这几层可以同时运行,互不干扰。
下一篇是系列的最后一篇,我们会把前面学到的所有机制整合起来,完整地写一个生产级插件——从工具注册、Session 管理、错误处理,到可观测性,一起落地。
在PrimeSkills 可以找到已在真实企业场景验证过的 AI Agent 技能和工作流,不是演示级的,是用在实际项目里的。
更多内容见我的个人主页