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

快速开始

守护进程跑起来,智能体接入好,trace 出现在屏幕上。不用注册,旁边也不用再装别的东西。

1. 运行守护进程

尚未发布任何版本。

本站命令中提到的镜像、Helm chart 和软件包都属于计划中的分发方式,目前一个都没有发布。现在可行的做法是从源码构建,这需要 Rust 和 Node:

构建并运行

git clone https://github.com/mcpd/mcpd && cd mcpd && make build && ./target/release/mcpd demo

mcpd 是一个进程,存储已经编译在里面。没有数据库要搭,也没有配置文件要写。

docker run -p 4317:4317 -v mcpd-data:/data ghcr.io/mcpd/mcpd

数据卷可选,但值得加上:没有它,容器停掉时 trace 也一起没了。更想用二进制文件?直接安装它。

你应该看到的输出
$ docker run -p 4317:4317 ghcr.io/mcpd/mcpdWARN listening on 0.0.0.0:4317 with open ingest. Anyone who can reach this port can write to it. Set --ingest-key (or MCPD_INGEST_KEYS) before exposing it.WARN listening on 0.0.0.0:4317 with an open read API. Stored prompts and completions are readable by anyone who can reach this port. Set --read-key to require a key.WARN listening on 0.0.0.0:4317 with an open run surface. Anyone who can reach this port can start runs and claim steps. Set --execute-key and --worker-key before exposing it.INFO mcpd 0.1.0 listening on http://0.0.0.0:4317INFO   interface   http://0.0.0.0:4317/INFO   OTLP        http://0.0.0.0:4317/v1/tracesINFO   native      http://0.0.0.0:4317/v1/ingestINFO   runs        http://0.0.0.0:4317/v1/api/runsINFO   storage     sqlite at /data/mcpd.db

警告排在最前面,这是有意的。在你自己的机器上它们无关紧要。在这个端口能被别的东西访问到之前,先读部署。

2. 给它发点东西

有两条路进来,而且你不必选我们这条。如果你的智能体已经在输出 OpenTelemetry,把它指向 mcpd,一行新代码都不用写:

export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317

否则就装上你所用语言的 SDK。它没有必需的依赖,记录一个 span 不会阻塞,也不会抛异常。

连接

import { Mcpd } from '@mcpd/sdk'

// Every field here also reads an environment variable, so the same code runs
// unchanged in development and in production. With no arguments at all this
// talks to a daemon on http://localhost:4317.
const mcpd = new Mcpd({
  project: 'support',
  environment: 'development',
  serviceName: 'support-agent',
  serviceVersion: '1.4.0',
})

3. 接入一次运行

把智能体做的工作包起来。一个 span 会记录进去了什么、出来了什么、花了多久、花了多少钱;子 span 会自动嵌套进去,所以在这段代码里发出的工具调用,不用告诉它归属,就会落在树里正确的位置。

记录一次运行

/**
 * One ticket, one trace.
 *
 * `withSpan` opens the span, keeps it active for everything the callback
 * starts, and ends it when the callback settles. Nothing inside has to be
 * passed a handle to be nested under it.
 */
async function handleTicket(ticket: Ticket): Promise<{ answer: string; traceId: string }> {
  let traceId = ''
  const answer = await mcpd.withSpan('support_agent', async (run) => {
    // Keep the id: a score or a piece of feedback that arrives later attaches
    // to this run by it.
    traceId = run.traceId
    // Tags label the whole run, so the trace list can filter on them.
    run.setTags(['support', 'tier-1'])
    run.setCorrelation({ sessionId: ticket.id, userId: ticket.customerId })
    run.setInput({ question: ticket.question })

    const order = await lookUpOrder(ticket)
    const answer = await draftAnswer(ticket, order)

    run.setOutput({ answer })
    return answer
  })
  return { answer, traceId }
}

4. 读它

打开 localhost:4317。你这次运行就在列表里。点开它就能看到整棵树:每次模型调用连同它的提示词和补全,每次工具调用连同它的参数和结果,还有耗时、token 和成本。

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.

5. 找到值得看的那一次

你的时间不会花在列表上,而会花在搜索上。mcpd 会为每一条提示词和补全建索引,所以你可以直接找到模型说出那句被客户投诉的话的那次运行:

搜索与分析

// The same client reads. This is the query that finds bad answers: every run
// that a scorer put below the bar.
const suspect = await mcpd.listTraces({
  hasScore: 'answer_relevance',
  scoreBelow: 0.5,
  limit: 10,
})
console.log(`runs scoring under 0.5: ${suspect.traces.length}`)

const recent = await mcpd.listTraces({ limit: 5, sort: 'start_time', order: 'desc' })
for (const trace of recent.traces) {
  const seconds = ((trace.durationNs ?? 0) / 1e9).toFixed(2)
  console.log(
    `${trace.traceId.slice(0, 8)}  ${trace.name.padEnd(16)} ${trace.status.padEnd(6)} ` +
      `${seconds}s  ${trace.spanCount} spans  $${(trace.cost?.totalCost ?? 0).toFixed(6)}`,
  )
}

// One run, with every span in it, ordered so a waterfall renders directly.
const detail = await mcpd.getTrace(traceId, { include: ['scores', 'feedback'] })
for (const span of detail.spans) {
  console.log(`  ${span.kind.padEnd(18)} ${span.name}`)
}

接下来读什么

整个闭环你已经走过一遍了。接下来值得读哪几页,取决于你在做什么: