扶摇AI知识笔记AI 前沿知识库
大模型基础

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

来源:掘金 行业动态 约 7808 字 token
掘金
转载

本文转载自 掘金,版权归原作者及原发布平台所有。本站仅作知识整理与转载分享,如涉版权问题请联系客服删除。

01核心要点

  • 这篇文章讲 dsh 的可观测性机制:Session 事件日志、Token 计量、遥测 Seam,OpenTelemetry

02正文全文

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

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

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

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

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

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

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

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

每个 SessionEvent 的结构:

- type:事件类型(如 'tool/call'、'turn/end'、'assistant/message')

- seq:单调递增的序号(从 0 开始)

- time:时间戳(毫秒级 Unix 时间戳)

- data:类型化的事件数据(不同 type 有不同 data 结构)

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

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

// 监听某个 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 在实时做什么,不用等它跑完。

知道"发生了什么"只是第一步。知道"花了多少钱"同样重要。

dsh 提供了 ctx.tokenMeter,可以测量当前 Session 的 Token 压力:

// 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 增加了多少。如果这个数字持续增大,说明上下文在膨胀,可能需要压缩策略。

// 在每个 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 本身不依赖任何具体的监控系统。

// 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

}

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

每个 assistant/message(含完整流数据)

每个 tool/call 和 tool/result

失败的 assistant/attempt(模型尝试了但最终没用)

所有 turn/start、turn/end 等生命周期事件

agent-error:Agent 在 Turn 之外失败了(比如初始化报错)

error:工具结果 isError: true、turn/end 带错误原因、agent-error 运营事件

这个映射让你可以在监控系统里直接过滤 severity === 'error' 来看所有异常,不用自己写判断逻辑。

dsh 提供官方的 OTel Provider 插件:dsh-session-telemetry-otel。

// 在你的 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 后端默认不实现(避免并发冲突)。如果你需要强一致性,需要自行配置。

// 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 插件,数据流向监控系统。

dsh 默认把 Session 日志持久化为 JSONL 文件(每行一个 JSON 对象,即一条 SessionEvent)。

# 查看所有工具调用(提取工具名列表)

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 做类似的分析。

实时观测(开发调试)

└─ 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 技能和工作流,不是演示级的,是用在实际项目里的。

03原文直达

本文内容转载自 掘金,如需查看原排版、配图与最新修订,请访问原始出处。

阅读原文(掘金)

正在校验阅读权限…
RELATED

相关阅读

更多 大模型基础