跳到主要内容
mcpd
无守护进程
源码

概念

整个数据模型由少数几个词撑起来。文档其余部分默认你已经认识它们,所以这一页逐个定义,并说明每个词是做什么用的。

Span

一个 span 是一个工作单元。它有名称、类型、开始时间、通常还有结束时间,以及一个状态。它可以带上进去了什么、出来了什么、用掉多少 token、花了多少钱,以及失败时的错误。

名称要保持低基数。search_docs 是一个名称;search_docs(query=hello) 是名称和取值粘在一起,只要有两次运行,按名称分组就没用了。

Trace

一条 trace 是一次运行的全部 span,由一个 128 位的 trace id 串起来。在 trace 内部,parent_span_id 决定树的形状:在一个模型步骤里发出的工具调用,是那个步骤的子节点,所以屏幕上的嵌套就是真实发生过的嵌套。

id 是 OpenTelemetry 的形状,trace 是 32 个小写十六进制字符,span 是 16 个。这不是装饰。正是它让一条从 Web 层开始、在智能体里结束的 trace 在两边仍然是同一条,也正是它让 mcpd 能和已经在读你遥测数据的其他东西并存。

如果一个 span 的父节点来自外部的追踪系统,它改用 external_parent_span_id 携带。mcpd 保留这个值用于关联,但绝不用它来建树,因为一个指向 mcpd 从未收到过的 span 的父 id,会让整棵子树挂在空处。

Trace b3d9e71a Fault
0ms 2.1s 4.2s 6.3s 8.4s
  1. agent support_agent fail
  2. model chat
  3. tool returns_policy
  4. model chat
  5. tool returns_policy
  6. model chat
  7. tool returns_policy

The model called one tool three times and never answered. That is visible in four seconds here, and invisible in a log file.

mcpd 还为每条 trace 存一份投影:span 数、错误数、token 总量、总成本、模型调用次数和工具调用次数。这一行在每次写入时都从 span 重新算出来,而不是累加,因为 span 会乱序到达、会被重试,也会被就地更新。计数器在这三种情况下都会漂移,而列表里一个明显不对的 span 数,会让读者不再相信这一页上的其他任何数字。

根 span 与事件 span

根 span 是在这条 trace 里没有父节点的那个 span。它通常是一个 agent_run 或 workflow_run,trace 的名称、标签和关联都从它来。标签只在根上有意义,在那里它们标注的是整次运行。

事件 span 是一个时间点而不是一段时长,所以它没有结束时间,也没有持续时间。流式返回的分片是最常见的例子。这个区分是存下来的,而不是从缺失的结束时间推断出来的,因为“它本来就没有结束”和“它还没有结束”是两个不同的事实,其中只有一个是问题。

当一条 trace 的根已经结束、并且没有 span 还开着时,它就完整了。在那之前它照样出现在列表里,用当下能拿到的最好名称,而不是等它结束才显示。那次永远没有完成的运行,往往正是你要看的那一次。

Span 类型

类型说明一个操作是什么。它决定这个 span 怎么画、哪些属性在它上面有意义,以及成本核算会不会看它。mcpd 定义了 33 种类型,分成几个家族:

家族类型
智能体 agent_run
模型 model_generation、model_step、model_inference、model_chunk
工具 tool_call、client_tool_call、provider_tool_call、mcp_tool_call
工作流 workflow_run、workflow_step、workflow_conditional、workflow_parallel、workflow_loop,以及其余几种
检索 rag_ingestion、rag_embedding、rag_vector_operation、rag_action、retrieval、memory_operation、graph_action
评估 scorer_run、scorer_step、guardrail、processor_run
其他 mapping、skill_resolution、workspace_action、http_request、generic

有四种类型计 token:model_generation、model_step、model_inference 和 rag_embedding。成本核算读的就是这些 span。generic 永远合法,也永远不会被推断出来,所以 mcpd 分不出类别的 span 会被如实标注,而不是靠猜。数据模型一页列出了每一种类型及其含义。

模型层级

其中四种类型互相嵌套,而这个嵌套是整个模型里最有用的部分:

  • model_generation 是一次逻辑上的生成:从第一个请求到最终回答的全部内容。
  • model_step 是其中的一个步骤。一次调用了两回工具的生成,有三个步骤。
  • model_inference 只是与供应商的一次往返,里面不含处理器,也不含工具执行。
  • model_chunk 是流式返回的一个分片,作为事件记录。

分成四层而不是一层,是因为“模型调用很慢”这句话按字面几乎从来都不成立。一旦牵扯到工具,你代码里的一次调用就不是一次 HTTP 往返:它是一个请求、一个工具、又一个请求、一道护栏、再一个请求。把这些压进一个 span,一次在你自己的重试循环里耗掉六秒的运行,就和一次等供应商等了六秒的运行长得一模一样。把 inference、step 和 generation 分开,才让这个差别读得出来,而第一个分片事件给你的是首个 token 的时间。

token 用量只记一次,记在真正发出供应商调用的那个 span 上。trace 投影会把这条 trace 里每个 span 的输入和输出 token 加起来,所以同一份计数既挂在一个 generation 上、又挂在它的每个 step 上,屏幕上的 token 总量和成本都会翻倍。层级是用来看时间的;token 属于真正和供应商说过话的那一层。

六种信号

span 是 mcpd 存的六类东西之一。日志是一次运行输出的行。指标是数值观测,形式为计数器、量表或直方图。评分是机器对一个 span 或一条 trace 的评估。反馈是人的评估。方面注释是一次运行结束后写下的、关于它的一句话。大多数由模型写。outcome 注释由守护进程根据运行的状态和它已经掌握的其他事实写下。

  • Spans

    What ran, nested, with timings

  • Logs

    Lines the run emitted

  • Metrics

    Numbers, with exemplars back to a run

  • Scores

    Machine evaluation

  • Feedback

    What a person thought

  • Aspect notes

    A sentence a model wrote about the run

trace_id Store Search Export

六种信号都带着它们所属的 trace id,多数还带着 span id,而且写入任何东西之前,六种都会经过同一条脱敏流水线。一条日志泄露密钥的本事,和一条提示词一样大。

评分总要声明自己的量程:取值,以及它所处的最小值和最大值。每个 SDK 都在调用处要求这两个值,因为一个不带量程的 0.7 没法和任何东西比较,包括同一个评分器上周的输出。

反馈和评分分开存放,也从不与它们平均。它有五种形态:thumbs、rating、correction、tag 和 comment。correction 是五种里最有价值的,因为它是唯一带着正确答案的信号。

关联

关联是一个信号所属的上下文。它决定了你手上是一堆运行,还是一组可以切分的数据。

字段它装的是什么
project租户键。每个信号都恰好属于一个。
environment它在哪里运行,例如 production 或 staging。
service_name发出它的服务。
service_version该服务的版本,这样一次回归可以定位到某个发布。
session_id面向用户的会话。把多条 trace 归成一次对话。
thread_id该会话内部的一个线程。
user_id这次运行所服务的最终用户。
resource_id你自己应用内部的租户或工作区。
run_id你自己给这次运行起的标识。
request_id引发这份工作的入站请求。
experiment_id当这次运行由一次实验产生时设置。
release一个发布名称或一个 git sha。

project 是唯一必填的字段。发送方没有指定时,守护进程会把这个信号归到它的 --project 默认值下,所以不会有任何东西存下来却无处归属。

即使看起来多余,也请设置 session_id。一次智能体运行是一条 trace,但投诉几乎总是关于一次对话的,而没有 session id,就没有什么能把十一条分开的 trace 还原成那个人真正经历的那次交流。

trace 列表可以直接按 environment、session_id、thread_id 和 user_id 过滤,同时还有状态、类型、名称、标签、模型、时间范围、最小时长、最低成本和评分。查找一次运行一页把它们组合到了一起。

链接

一个 span 有一个父节点,和任意数量的链接。一个链接指向另一个 span 或另一条 trace,并说明为什么:follows_from、retry_of、replay_of,或者你自己起的名字。

链接之所以存在,是因为父子关系表达不了扇入。一个由二十个请求引发的批处理任务,有二十个链接和一个父节点。硬把它塞进树里,要么凭空造出十九棵重复的子树,要么丢掉十九个起因。

接下来

  • 每个实体和字段的完整说明:数据模型。
  • 这些词在代码里长什么样:SDK。
  • 当这些 span 以 OTLP 到达时 mcpd 会推断什么:OpenTelemetry。
  • 把这套词汇变成一次查询:查找一次运行。