DeepSeek Harness 系列(09):可观测性——怎么知道 Agent 在干什么

先说一个让人头疼的场景

你的 Agent 跑了三分钟,最后报了一个错误。

然后你盯着日志------什么都没有。你不知道它调用了哪些工具、哪一步卡住了、Token 到底花在哪里,更不知道为什么失败。

这就是可观测性缺失的典型后果。


为什么可观测性对 Agent 尤其重要

传统服务出了问题,你可以加断点、看堆栈。Agent 不一样。

Agent 是非确定性的。 相同的输入,模型可能产生完全不同的工具调用序列。你没办法在"模型决策"这一步加断点------那是一个黑盒。

调试只能靠日志反推。 模型的"想法"只体现在它输出的文字和工具调用里。你需要把这些全部记下来,事后才能重建它的推理链。

Token 费用不透明。 一个多轮 Agent 对话,到底哪一步最贵?是第三轮工具结果太长?还是 system prompt 占了大头?没有计量,你连优化方向都找不到。

生产环境出问题,你得有证据。 用户说"它给了我一个错误答案",你需要还原当时的完整执行链------用了哪些工具、返回了什么、模型看到了什么。


Session 日志:最完整的观测数据

回顾第 05 篇讲过的内容:dsh Session 是一份仅追加的类型化事件日志

这个设计不只是为了持久化,它本身就是最完整的观测数据源。

每个 Session 事件都包含:

bash 复制代码
每个 SessionEvent 的结构:
  - type:事件类型(如 'tool/call'、'turn/end'、'assistant/message')
  - seq:单调递增的序号(从 0 开始)
  - time:时间戳(毫秒级 Unix 时间戳)
  - data:类型化的事件数据(不同 type 有不同 data 结构)

这意味着:只要你能读取 Session 日志,你就能重建整个执行过程------每一步做了什么、花了多少时间、有没有出错。


实时监听:session/event

不需要等日志写完再分析。dsh 提供了 session/event 事件,让你实时监听 Session 的每一条记录:

typescript 复制代码
// 监听某个 Session 的所有事件(实时)
ctx.on('session/event', (session, event) => {
  // 每条事件都会触发这个回调
  console.log(`[${event.type}] seq=${event.seq} time=${event.time}`)
  
  // 检查是否是工具调用
  if (event.type === 'tool/call') {
    // event.data.name 是工具名
    // event.data.arguments 是原始 JSON 字符串(模型输出的,未解析)
    // event.data.callId 是这次调用的唯一 ID
    console.log(`  Tool: ${event.data.name}`)
    console.log(`  Args: ${event.data.arguments}`)
  }
  
  // 检查是否是工具执行结果
  if (event.type === 'tool/result') {
    // 通过检查 content block 里有没有 isError: true 判断是否失败
    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 压力:

typescript 复制代码
// TokenMeasurement 接口(来自 packages/llm/token-meter/src/types.ts)
interface TokenMeasurement {
  // 这次计量消费了多少事件(用于缓存,避免重复计算)
  readonly logRevision: SessionLogOffset
  
  // 当前请求的总 token 压力(输入 + 输出之和)
  readonly totalTokens: number
  
  // 当前 surface(模型可见的历史消息)的 token 数量
  readonly surfaceTokens: number
  
  // surface 相对于最后一次成功请求的 token 变化量(有符号,可以是负数)
  readonly surfaceDeltaTokens: number
  
  // 按位置排列的 surface 节点及其 token 数(可以看每条消息占多少)
  readonly nodes: readonly TokenSurfaceNode[]
}

几个关键概念:

  • surface tokens:模型这次请求实际看到的历史内容有多少 token。这决定了你的 API 费用中"输入 token"这部分。
  • total tokens:输入 + 输出的总和,反映这次请求的完整费用。
  • surfaceDeltaTokens:和上一次请求相比,surface 增加了多少。如果这个数字持续增大,说明上下文在膨胀,可能需要压缩策略。

使用示例:

typescript 复制代码
// 在每个 Turn 结束时打印 Token 使用摘要
ctx.on('session/event', (session, event) => {
  // 只关心 Turn 结束事件
  if (event.type !== 'turn/end') return
  
  // 调用 measure 获取当前 Session 的 Token 测量结果
  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}`)
  
  // 显示 delta,正数表示上下文在增长
  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 本身不依赖任何具体的监控系统。

每条遥测记录的结构:

typescript 复制代码
// SessionTelemetryRecord(来自 packages/session/session-telemetry/src)
interface SessionTelemetryRecord {
  // 两种 channel:
  //   'ledger':Session 日志事件的完整镜像,和事件一一对应
  //   'ops':运营信号,只有特殊情况才产生
  channel: 'ledger' | 'ops'
  
  // 时间戳(毫秒)
  time: number
  
  // 严重程度
  severity: 'info' | 'warn' | 'error'
  
  // 标识属性(用于查询和过滤)
  // 例如:session.id、event.type、event.seq 等
  attributes: Record<string, string | number>
  
  // 完整 payload:event.data 的深拷贝
  body: unknown
}

两种 channel 分别做什么

ledger channel:Session 日志的完整镜像。每一条 Session 事件都会产生一条对应的 ledger 记录。这是审计和回放的数据来源。

包括:

  • 每个 assistant/message(含完整流数据)
  • 每个 tool/calltool/result
  • 失败的 assistant/attempt(模型尝试了但最终没用)
  • 所有 turn/startturn/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

接入方式(概念性):

typescript 复制代码
// 在你的 Bundle 配置里加入这个插件(伪代码)
// 这会把 ctx.sessionTelemetry 接到 OTel 后端
'@deepseek-ai/dsh-session-telemetry-otel'

// 该插件内部会:
// 1. 注册 ctx.sessionTelemetry 的 OTel 后端实现
// 2. 每条 SessionTelemetryRecord 通过 OTel JS SDK 的 Logger API 发送
// 3. 支持配置不同的 Exporter(OTLP、Console、File 等)

几个设计原则值得了解:

边界公理 :harness 只负责调用 emit(),批处理、重试、排队这些属于 OTel SDK 的职责,harness 不插手。这样两边都可以独立演化。

尽力而为 :遥测记录可能重复也可能丢失。接收端应该基于 (session.id, format_version, event.seq) 组合来去重 ledger 记录,而不是假设每条记录恰好到达一次。

flush 是可选的 :每次 Turn 结束后可以调用 flush(),但 OTel 后端默认不实现(避免并发冲突)。如果你需要强一致性,需要自行配置。


实战:写一个简单的调试插件

把上面的内容整合成一个完整的调试观测插件:

typescript 复制代码
// debug-observer.ts --- 调试用的可观测性插件
// 用法:在开发时加入 Bundle,生产时替换为真正的遥测后端

export const name = 'debug-observer'

// 声明依赖注入 tokenMeter
export const inject = ['tokenMeter']

export function apply(ctx: Context): void {
  // ── 1. 监听工具调用 ────────────────────────────────────────
  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}`)
    // arguments 是原始 JSON 字符串(模型直接输出的,还没有被解析)
    console.log(`  Args: ${event.data.arguments}`)
  })
  
  // ── 2. 监听工具执行结果 ────────────────────────────────────
  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 ? '✗' : '✓'
    
    // 从第一个 block 里拿到对应的 toolUseId(关联 tool/call 事件)
    const toolUseId = blocks[0]?.toolUseId ?? 'unknown'
    console.log(`[Tool Result] ${icon} (call: ${toolUseId})`)
  })
  
  // ── 3. 每个 Turn 结束时打印 Token 摘要 ────────────────────
  ctx.on('session/event', (session, event) => {
    if (event.type !== 'turn/end') return
    
    const reason = event.data.reason.kind  // 'complete' | 'error' | 'interrupted' 等
    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)}`)
    }
  })
  
  // ── 4. 监听 Session 生命周期 ───────────────────────────────
  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)。

以下是一些常用的命令行分析技巧:

bash 复制代码
# 查看所有工具调用(提取工具名列表)
cat session.jsonl | grep '"type":"tool/call"' | jq '.data.name'

# 查看失败的助手尝试(模型生成了但最终没用到的内容)
cat session.jsonl | grep '"type":"assistant/attempt"' | jq '.'

# 统计每轮的 token 用量(从 assistant/message 里的 usage 字段)
cat session.jsonl | grep '"type":"assistant/message"' | jq '.data.usage'

# 查看所有 Turn 的结束原因(是正常完成还是出错)
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 做类似的分析。


可观测性层次总结

四个层次,覆盖从开发到生产:

markdown 复制代码
实时观测(开发调试)
  └─ 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 技能和工作流,不是演示级的,是用在实际项目里的。

更多内容见我的个人主页

相关推荐
jzshmyt3 小时前
我用 Python 从零“生成“了一个宇宙,然后让它观察自己(v14)
人工智能·pytorch·python·numpy·matplotlib·空间计算·scipy
LuTshoes4 小时前
spring ai 实战 手搓 PlaneExecuteAgent
java·人工智能·spring·ai
火山引擎开发者社区4 小时前
火山引擎 AgentKit 获评中国信通院 2026 智能原生软件“银弹”标杆实践
人工智能
米小虾4 小时前
RSI 走到哪一步了:拆开递归自我改进的三个可写面、五条定律,和那个没人做的对照实验
人工智能·agent
ACP广源盛139246256734 小时前
GSV9001E 国产 4K 视频处理器,AI 多模态可视化大屏多路画面合成方案解析
人工智能·硬件架构·国产芯片·ai服务器
长谷深风1114 小时前
评测AI Agent:三种裁判各司其职
java·大数据·开发语言·人工智能·ai agent
火山引擎开发者社区4 小时前
火山方舟Agent Plan上线最新生图生视频模型
人工智能
明志数科4 小时前
具身智能产业基础设施换挡:从算力本体到数据层的技术逻辑
人工智能·机器学习
gongfeng30004 小时前
福州企业级AI自动化获客解决方案品牌梳理与适用场景分析
运维·人工智能·自动化·g-claw ai员工