DeepSeek Harness 可观测架构实战
作者:c4t2026.11.06 21:26浏览量:1简介:Agent集群规模扩大后,运营者会被三个问题反复追问:慢在哪、贵在哪、错在哪。本文围绕可观测架构的落地实践,讲清如何把一次任务还原成带父子关系与时间区间的调用树,并让数据跨会话、跨机器汇聚留存,从而支撑延迟归因、成本拆解与失败回溯,帮助团队把Agent运营从"翻日志"升级为可量化、可追责的工程体系。
DeepSeek Harness 可观测架构实战
一、从”结果对不对”到”三个追问”
Agent 在试验阶段,关注点只有一个:任务结果是否正确。这个阶段样本量小、调用链短,人肉核对会话记录完全够用。但一旦 Agent 从单点试验走向集群化运营,问题的性质就发生了根本变化。
运营一个 Agent 集群,本质上与运营一套微服务系统没有区别:同样存在调用链路、同样存在资源成本、同样存在故障定位。区别只在于,微服务的调用是确定性的函数跳转,而 Agent 的调用是模型推理与工具执行交替驱动的动态循环,链路更长、分支更多、耗时分布更偏斜。于是你会被三个问题反复追问:
第一问:慢在哪。 一次任务跑了两分钟,这两分钟究竟花在模型推理上,还是工具执行上?首 Token 延迟(TTFT)是否拉高了整体体感?ReAct 循环里到底是哪一轮拖了后腿?是某一轮工具调用超时,还是上下文膨胀导致推理变慢?
第二问:贵在哪。 Token 账单持续上涨,消耗集中在哪些会话、哪个模型、哪类任务上?缓存命中率是否在悄悄下滑?输入与输出的比例是否健康?是否存在大量无效重试在重复计费?
第三问:错在哪。 任务失败了,是模型侧的问题还是工具侧的问题?中间重试了几次?中断发生在第几步?几天前的那次失败,还能不能完整回溯现场?
这三个问题有一个共同点:它们都无法通过”翻会话记录”回答。会话记录是文本流,缺少结构化的父子关系、缺少精确的时间区间、缺少跨会话的聚合维度。你真正需要的是:把每一次任务还原成一棵带父子关系和时间区间的调用树,并让这些数据能够跨会话、跨机器汇聚,长期留存下来。
这正是本文要拆解的可观测架构所做的事情。
二、核心抽象:调用树是唯一的一等公民
2.1 为什么是树,而不是日志流
日志是扁平的、按时间追加的文本序列。它适合记录”发生了什么”,但不适合回答”谁导致了谁”。Agent 的执行天然是嵌套结构:一次任务(Task)包含若干轮循环(Turn),每一轮循环包含一次模型推理(LLM Call)和若干次工具执行(Tool Call),工具执行内部还可能触发子任务。这种嵌套关系用日志表达会迅速退化为难以解析的缩进文本,而用树表达则是原生结构。
因此,可观测架构的第一条设计原则是:以调用树(Trace Tree)为核心数据模型,日志、指标、事件都作为树的附属属性挂载,而不是反过来。
2.2 调用树的最小节点模型
一个可用的节点模型至少应包含以下字段:
@dataclassclass SpanNode:span_id: str # 节点唯一标识parent_span_id: str | None # 父节点标识,根节点为空trace_id: str # 所属调用树标识,贯穿整次任务session_id: str # 会话标识,用于跨任务聚合agent_id: str # Agent 实例标识,用于跨机器聚合span_type: str # task / turn / llm_call / tool_call / retryname: str # 节点名称,如具体工具名、模型类别start_time_ns: int # 起始时间戳(纳秒)end_time_ns: int # 结束时间戳(纳秒)status: str # ok / error / timeout / cancelledattributes: dict # 结构化属性:token 数、缓存命中、重试次数等events: list # 节点内发生的关键事件(如中断、降级)
这个模型的关键在于三点:父子关系支撑链路归因,时间区间支撑延迟拆解,结构化属性支撑成本核算。三者缺一,三个追问中就至少有一个无法回答。
2.3 节点类型与语义约定
节点类型必须语义明确,否则聚合时会产生歧义。建议的最小集合:
task:一次完整任务的根节点,对应一个用户请求。turn:ReAct 循环中的一轮,是延迟分析的基本单位。llm_call:一次模型推理调用,携带输入输出 token 数、TTFT、缓存命中标记。tool_call:一次工具执行,携带工具类别、调用耗时、返回状态。retry:一次重试尝试,作为对应llm_call或tool_call的子节点,避免重试污染原始耗时统计。
把 retry 单独建模而不是简单覆盖原节点,是很多实现容易忽略的细节。重试是成本与延迟的重要来源,必须能被独立统计,否则”贵在哪”永远算不清。
三、采集层:低侵入埋点与上下文传播
3.1 埋点位置的选择
采集的关键不是”记多少”,而是”记在哪”。对于 Agent 运行时,建议在三个边界埋点:
- 调度边界:任务被接收、被分配、被取消的位置。这里生成
trace_id与根task节点。 - 推理边界:每次模型调用的发起与返回。这里能拿到 TTFT、token 计数、缓存命中状态。
- 工具边界:每次工具执行的进入与退出。这里能拿到执行耗时、异常类型、重试次数。
这三个边界恰好覆盖了三个追问所需的数据源:调度边界回答”任务整体发生了什么”,推理边界回答”慢与贵”,工具边界回答”错”。
3.2 上下文传播机制
跨节点、跨进程的父子关系依赖上下文传播。通用做法是把 trace_id 与当前 span_id 放入一个上下文载体(context),随调用链传递。伪代码如下:
def with_span(span_type, name, parent_ctx, fn):span = SpanNode(span_id=gen_id(),parent_span_id=parent_ctx.span_id,trace_id=parent_ctx.trace_id,session_id=parent_ctx.session_id,agent_id=parent_ctx.agent_id,span_type=span_type,name=name,start_time_ns=now_ns(),)try:result = fn(new_ctx(parent_ctx, span))span.status = "ok"return resultexcept TimeoutError:span.status = "timeout"raiseexcept Exception:span.status = "error"raisefinally:span.end_time_ns = now_ns()emit(span) # 异步上报,不阻塞主链路
要点有二:上报必须异步,可观测性不能成为新的延迟来源;异常路径必须完整记录,失败现场的 value 恰恰在异常分支上。
3.3 采样策略
全量采集在高吞吐场景下成本不可接受,但盲目采样会让”错在哪”失去样本。建议采用分层采样:
- 所有状态为
error、timeout的调用树 100% 保留。 - 高延迟调用树(如 P99 以上)100% 保留。
- 正常调用树按固定比例采样,但保证每个
agent_id、每个模型类别都有最低样本量。
这样既控制了存储成本,又保证了故障与长尾场景的可回溯性——而这恰恰是最需要回溯的场景。
四、存储层:让数据跨会话、跨机器长期留存
4.1 分离式存储
调用树数据有两类访问模式:按 trace_id 点查(回溯现场)和按时间范围与维度聚合(统计分析)。这两类需求对存储的要求不同,建议分离:
- 调用树明细写入日志型存储或列式存储,按时间分区,支持按
trace_id快速定位与按时间范围扫描。 - 聚合指标(按会话、按模型、按工具类别的耗时与 token 汇总)写入时序数据库,支撑看板与告警。
- 原始会话内容(输入输出文本)单独存对象存储,仅在需要回溯时按引用拉取,避免明细表膨胀。
4.2 维度设计决定聚合能力
存储 schema 的维度设计直接决定了三个追问能否被回答。至少要保证以下维度可聚合:
| 维度 | 支撑的问题 |
|---|---|
session_id |
成本集中在哪些会话 |
agent_id / 机器标识 |
跨机器对比、定位异常实例 |
| 模型类别 | 不同模型的延迟与成本对比 |
| 工具类别 | 工具侧耗时与失败率 |
span_type + 状态 |
失败分布、重试分布 |
| 时间分区 | 趋势分析与回归定位 |
缺少任一维度,相应的追问就会退化为”大概看看”。
4.3 留存策略
“几天前的那次失败还能不能回溯”是一个硬性指标。建议对错误调用树与高延迟调用树设置显著长于正常样本的留存周期,并对原始会话内容设置冷热分层:近期热存、远期冷存,引用关系保持有效。留存周期的设定应基于业务复盘的实际节奏,而非拍脑袋决定。
五、分析层:三个追问的落地解法
5.1 慢在哪:关键路径分析
拿到调用树后,”慢”不再是一个标量,而是一条路径。做法是:在树上计算关键路径——从根节点到叶子节点中累计耗时最长的链路。关键路径上的节点即延迟主因。
进一步拆解:
- TTFT 与生成耗时的分离:
llm_call节点内分别记录首 Token 时间与总生成时间。若 TTFT 占比高,问题在排队与预填充;若生成占比高,问题在输出长度或解码效率。 - 轮次定位:按
turn节点排序,找出耗时最长的轮次,再下钻到该轮内的llm_call与tool_call。 - 工具与推理的占比:对整棵树按
span_type汇总耗时,得到推理总耗时与工具总耗时的比例,一眼看出瓶颈在哪一侧。
def critical_path(root):# 自底向上计算每个节点的累计耗时代价def cost(node):own = node.end_time_ns - node.start_time_nsif not node.children:return own, [node]child_cost, child_path = max((cost(c) for c in node.children),key=lambda x: x[0])return own + child_cost, [node] + child_pathreturn cost(root)
5.2 贵在哪:成本归因树
成本归因的本质是把 token 消耗沿调用树向下分摊。每个 llm_call 节点记录输入 token、输出 token、缓存命中 token,再乘以对应模型类别的单价(单价作为配置项维护,不硬编码)。汇总维度包括:
- 按
session_id汇总,找出高消耗会话; - 按模型类别汇总,判断是否存在”大模型干小事”的错配;
- 按
turn序号汇总,观察上下文膨胀带来的输入 token 增长曲线; - 单独统计
retry子树的 token 消耗,量化重试成本。
缓存命中率的下滑往往不会体现在单次调用上,而是体现在趋势上。因此缓存命中率应作为时序指标持续采集,并与其对应的输入 token 量联动观察:命中率下降叠加输入量上升,通常意味着上下文组织方式退化。
5.3 错在哪:失败现场的结构化回溯
失败分析依赖三个信息:失败节点的类型、失败发生的位置、失败前的重试历史。
- 若失败集中在
tool_call且状态为timeout,问题在工具侧或依赖服务; - 若失败集中在
llm_call且伴随格式解析异常,问题在输出约束或提示组织; - 若失败节点前存在多个
retry子节点,说明该环节稳定性不足,需优先治理。
由于调用树长期留存,任一历史失败都可以通过 trace_id 完整还原:树结构、每轮输入输出、每次重试的返回、中断发生的具体步骤。这正是”翻会话记录”永远做不到的事情。
5.4 从可观测到可行动
分析的价值在于驱动改进。建议把上述分析固化为几条常规动作:
- 按周输出关键路径耗时分布与成本归因报告,定位 Top N 高耗会话与高耗模型;
- 对失败率与重试率设置阈值告警,告警信息直接附带可点击的
trace_id; - 将缓存命中率、TTFT、单位任务 token 成本纳入持续跟踪的核心指标集;
- 每次故障复盘都以调用树为唯一事实来源,避免口头描述失真。
六、工程实践中的几个坑
坑一:时间戳精度不足。 用毫秒时间戳在嵌套较深的树上会产生重叠与负耗时。统一使用纳秒时间戳,并在节点退出时统一打点,避免中途多次取时导致区间漂移。
坑二:异步上报阻塞主链路。 上报队列必须设置容量上限与丢弃策略,宁可丢样本也不能拖慢任务。丢弃时应记录丢弃计数,保证采样率的可解释性。
坑三:重试被合并统计。 若重试直接覆盖原节点耗时,延迟与成本都会被低估。必须把每次尝试建模为独立子节点。
坑四:维度缺失导致无法下钻。 采集时图省事只记 trace_id,事后想看”哪个模型贵”就只能重跑。维度必须在埋点阶段就确定,事后补采成本极高。
坑五:留存策略与复盘节奏脱节。 错误样本过早过期,等于把最有价值的数据删掉。留存周期应显式对齐业务复盘周期。
七、小结
Agent 集群的可观测性,不是给日志加个检索框,而是把每一次任务还原成一棵带父子关系与时间区间的调用树,并让这棵树跨会话、跨机器、长期地存下来。有了这棵树,”慢在哪、贵在哪、错在哪”才从主观感受变成可计算、可下钻、可追责的工程问题。
落地路径可以归纳为四步:先定节点模型,明确 task / turn / llm_call / tool_call / retry 的语义;再定埋点边界,在调度、推理、工具三处低侵入采集并保证上下文传播;然后定存储维度,让会话、实例、模型、工具四类维度都可聚合;最后定分析动作,把关键路径、成本归因、失败回溯固化为常规流程。四步走完,Agent 运营才算真正具备了工程化的底座。

登录后可评论,请前往 登录 或 注册