用 MLflow Tracing 自动观测 OpenCode 智能体会话:@mlflow/opencode 插件接入与原理全解析
【免费下载链接】mlflowThe open source AI engineering platform for agents, LLMs, and ML models. MLflow enables teams of all sizes to debug, evaluate, monitor, and optimize production-quality AI applications while controlling costs and managing access to models and data.项目地址: https://gitcode.com/GitHub_Trending/ml/mlflow
MLflow 在 libs/typescript/integrations/opencode 目录下提供了@mlflow/opencodeTypeScript 插件,可对 OpenCode 终端智能体工具的对话会话进行自动埋点:会话空闲(idle)时自动创建 MLflow Trace,完整记录用户提示词、助手回复、LLM 调用的 token 用量、工具调用及其结果和会话元数据。本文以该插件为骨架,结合其源码实现(src/index.ts)与测试用例(tests/index.test.ts)展开,读完即可完成接入、掌握其配置参数含义,并理解 trace/span 数据模型与增量去重机制,可直接在生产工作流中落地智能体可观测性。
一、插件定位:为 OpenCode 补齐智能体可观测性
@mlflow/opencode是 MLflow Tracing 体系下的 OpenCode 集成包(package.json 中描述为 "OpenCode integration package for MLflow Tracing",版本 0.4.0)。它的核心价值在于零侵入:开发者无需修改 OpenCode 内部的任何调用逻辑,只要把插件挂载进 OpenCode 的插件配置,再设定两个环境变量,后续每个会话的完整执行过程就会被自动上报到 MLflow。
插件自动捕获四类数据(源自 README):
- 用户提示词与助手响应(User prompts and assistant responses)
- 带 token 用量的 LLM 调用(LLM calls with token usage)
- 工具调用及结果(Tool invocations and results)
- 会话元数据(Session metadata)
从实现上看,它直接复用@mlflow/coreTypeScript SDK(依赖声明为"@mlflow/core": "^0.4.0",见 package.json),并通过 OpenCode 官方插件协议(peerDependency"@opencode-ai/plugin": "^1.0.0")暴露一个事件型插件。整体链路可概括为:
OpenCode 会话 → 触发 session.idle 事件 → 插件拉取会话消息 → 用 @mlflow/core 构建 AGENT/LLM/TOOL 三层 span → flushTraces() 上报 MLflow二、安装与快速接入:三步让 OpenCode 会话自动入 Trace
1. 安装插件
在项目(或全局)中安装插件及其底层 SDK:
npm install @mlflow/opencode @mlflow/core注意源码头部注释明确要求同时安装两个包(见 src/index.ts),因为插件运行期会从@mlflow/core导入init、startSpan、withSpan、updateCurrentTrace、flushTraces、SpanType、SpanAttributeKey等符号。
2. 注册到 opencode.json
在 OpenCode 的配置文件opencode.json中声明插件:
{ "plugin": ["@mlflow/opencode"] }插件导出的是MLflowTracingPlugin(默认导出同名函数,见 src/index.ts),OpenCode 加载后会调用该函数并注入插件客户端(client),插件据此订阅事件钩子。
3. 设置环境变量
export MLFLOW_TRACKING_URI=http://localhost:5000 export MLFLOW_EXPERIMENT_ID=1234. 正常运行 OpenCode
之后正常使用 OpenCode 即可,无需任何额外操作——插件只在"会话变为空闲"时触发追踪:
Run OpenCode normally - traces are created automatically when sessions become idle.(README)
三、配置参数详解:三个环境变量与底层解析逻辑
插件完全通过环境变量配置(README 原文即为 "The plugin is configured via environment variables"):
| 变量 | 必填 | 说明 |
|---|---|---|
MLFLOW_TRACKING_URI | 是 | MLflow tracking server 地址(例如http://localhost:5000) |
MLFLOW_EXPERIMENT_ID | 是 | MLflow 实验 ID |
MLFLOW_OPENCODE_DEBUG | 否 | 设为true时开启调试日志 |
结合 src/index.ts 的ensureInitialized()实现,可以进一步确认这三个变量的真实行为:
- 两个必填项缺一不可:函数按顺序检查
MLFLOW_TRACKING_URI与MLFLOW_EXPERIMENT_ID,任一缺失都会返回false,SDK 初始化被跳过,该次事件直接放弃处理。测试用例 tests/index.test.ts 也分别验证了"未设置 TRACKING_URI 时不拉取消息"和"未设置 EXPERIMENT_ID 时不拉取消息"两个分支。 - 初始化是惰性且只做一次的:
initialized标志位保证 SDK 只在首次收到session.idle事件时初始化一次,成功设置后不再重复调用init({ trackingUri, experimentId })。 - 调试日志刻意"静默":插件默认不做任何控制台输出,原因是避免污染 OpenCode 的 TUI 界面(源码注释 "Silent plugin - no console output to avoid TUI interference")。只有
MLFLOW_OPENCODE_DEBUG === 'true'时才会通过console.error输出[mlflow]前缀的调试信息,例如SDK initialized successfully、Creating trace for session: xxx、Created trace: xxx等。调试输出统一走console.error(而非console.log),同样是为减少对终端 UI 的干扰。
取值建议:MLFLOW_TRACKING_URI需要指向一个已运行的 MLflow server(本地mlflow server默认监听 5000 端口);MLFLOW_EXPERIMENT_ID可先在 MLflow UI 中创建实验后取得数字 ID,或通过mlflow experiments create创建。
四、运行原理:session.idle 事件驱动的自动追踪管线
插件不是一个持续运行的 trace 采集器,而是一个事件响应器。其完整工作流如下:
- 订阅事件钩子:
MLflowTracingPlugin返回{ event }钩子,OpenCode 每次产生事件都会回调。 - 事件过滤:只有
event.type === 'session.idle'才会继续处理,且事件属性中必须携带sessionID,否则直接返回(对应 src/index.ts)。测试明确覆盖了"非 session.idle 事件不处理"与"无 sessionID 不处理"两条路径。 - SDK 惰性初始化:调用
ensureInitialized(),环境变量不满足则放弃。 - 拉取会话消息:通过插件客户端调用
client.session.messages({ path: { id: sessionID }, query: { limit: 1000 } }),一次最多取 1000 条消息。 - 增量判定与去重:将当前消息总数与该会话上次已处理数量比对(详见下文第六节),无新消息则跳过。
- 构建 Trace 并上报:对新消息调用
processSession(),构建 trace 后调用flushTraces()刷入 MLflow。
其中processSession()(src/index.ts)是核心建链逻辑,它有几个值得注意的防御性前置校验:
- 消息列表为空 → 跳过;
- 找不到任何
user角色消息 → 跳过(无用户输入就没有对话可追踪); - 提取不到用户提示词文本 → 跳过。
时间信息贯穿整个链路:OpenCode 消息自带毫秒级时间戳(info.time.created/completed),插件通过timestampToNs()统一换算为纳秒(NANOSECONDS_PER_MS = 1e6)后写入 span 的startTimeNs/endTimeNs;trace 的起止时间分别取本批首条消息的created与末条消息的completed(缺失时回退到created)。
五、Trace 数据模型:AGENT 父 Span + LLM/TOOL 子 Span 三层结构
每个 OpenCode 会话在 MLflow 中落地为一个 trace,其内部呈"1 个 AGENT 父 span + N 个 LLM/Tool 子 span"的树状结构。
1. 父 Span:opencode_conversation
通过withSpan()创建名为opencode_conversation、spanType: SpanType.AGENT的父 span(src/index.ts):
withSpan( (parentSpan) => { /* 创建子 span、附加元数据、结束父 span */ }, { name: 'opencode_conversation', inputs: { prompt: userPrompt }, startTimeNs: createdNs, spanType: SpanType.AGENT, }, );选择withSpan而非startSpan是有意为之:它会让父 span 成为 OTel 上下文中的"当前 span",从而允许插件通过公开 APIupdateCurrentTrace(而非侵入InMemoryTraceManager内部)为整个 trace 附加元数据。父 span 结束时写入outputs:
parentSpan.setOutputs({ response: finalResponse || 'Conversation completed', status: 'completed', });2. Trace 级元数据与预览
通过updateCurrentTrace写入四类信息(src/index.ts):
| 字段 | 值 | 说明 |
|---|---|---|
metadata['mlflow.trace.session'] | sessionId | 关联 OpenCode 会话 ID |
metadata['mlflow.trace.user'] | process.env.USER | 当前系统用户 |
requestPreview | 用户提示词前 1000 字符 | 便于在 UI 快速预览 |
responsePreview | 助手最终回复前 1000 字符 | 有最终回复时才写入 |
MAX_PREVIEW_LENGTH = 1000是预览内容的上限常量。源码注释特别说明:插件刻意用 metadata 键mlflow.trace.session/mlflow.trace.user而非updateCurrentTrace的sessionId/user便捷参数,是因为后者(以及TraceMetadataKey枚举)只在更新的@mlflow/core版本中存在,而这两个 metadata 键在各发布版本中保持稳定,从而兼容更广的 SDK 版本范围。
3. LLM Span:llm_call
对每条助手消息(只要含文本、工具调用或 reasoning 中任一内容)创建一个llm_callspan(SpanType.LLM),记录(src/index.ts):
inputs.model:${providerId}/${modelId}(如anthropic/claude-3-opus);inputs.messages:重建的对话历史(见下文"会话历史重建");attributes:model、provider;- token 用量:通过
SpanAttributeKey.TOKEN_USAGE写入,由buildTokenUsage()组装。
token 用量的字段结构(buildTokenUsage):
{ input_tokens: 输入 token 数, output_tokens: 输出 token 数, total_tokens: input + output + reasoning, // reasoning 计入总量 cache_read_tokens: 缓存读取数, // 有缓存信息时才有 cache_write_tokens: 缓存写入数, // 有缓存信息时才有 }LLM span 的outputs采用OpenAI chat-completion 消息形状(choices[0].message),这是与 codex、qwen-code 等其他集成保持一致的关键设计(源码注释明确指出这一点),目的是让 MLflow 的Chat 视图能正确渲染工具调用:
{ role: 'assistant', content: 文本内容(无文本时为 null), reasoning: 推理内容(仅当存在 reasoning part 时), tool_calls: [ { id: callID, type: 'function', function: { name: 工具名, arguments: JSON 字符串化的输入 } } ] }一个细节:只有文本的工具调用消息同样会被记录为 LLM span。源码注释解释了原因——像 prometheus 这类 agent 会连续多次发出工具调用而不夹带文本,若跳过这些消息会导致大量 span 缺失、trace 空洞。对应测试 "should create LLM span for tool-call-only assistant messages" 专门复现并回归了这一场景。
4. Tool Span:tool_
对每条工具调用 part 创建tool_<工具名>span(SpanType.TOOL),记录(src/index.ts):
inputs:工具入参(state.input);attributes:tool_name、tool_id(callID)、status(completed/error等);outputs:按状态区分——completed时写result与title,error时写error信息;- 时间:
state.time.start/end换算为纳秒。
5. 会话历史重建
为了让每次 LLM 调用的inputs.messages呈现完整上下文,插件实现了reconstructConversationMessages()(src/index.ts),将 OpenCode 的消息数组转换为标准对话格式:
user消息:拼接全部文本 part;assistant消息:content拼接文本 part,reasoning拼接推理 part(存在时);tool消息:将已完成(status === 'completed')的工具调用转为{ role: 'tool', tool_call_id, content: 输出 }。
测试用例用一段"用户提问 → 助手推理并调用工具 → 助手自主续作再调工具 → 汇总 → 追问 → 回复"的 8 条消息序列,逐条断言了重建后历史中 3 条 user、4 条 assistant、2 条 tool 消息的完整结构与顺序,可作为理解该逻辑的最佳示例(见 tests/index.test.ts)。
六、增量追踪与去重机制:避免重复 Trace 的工程细节
OpenCode 的session.idle事件在一个会话中可能多次触发(每次用户停止交互都可能空闲)。若每次空闲都全量建 trace,会造成大量重复。插件的解决方案是基于"消息计数"的增量追踪:
const processedMessageCounts = new Map<string, number>();逻辑如下(src/index.ts):
- 拉取全部消息后,取其总数
messageCount与processedMessageCounts中该会话上次已处理数量lastProcessedCount比较; - 若
messageCount <= lastProcessedCount,说明没有新消息,直接返回(同一轮次不会被处理两次); - 否则用
allMessages.slice(lastProcessedCount)只取新增的消息建 trace,并更新计数; - 计数器使用LRU 语义维护:每次事件都会先
delete再set将该会话移到 Map 末尾,保证活跃会话(即便没有新消息)不会被提前淘汰; - Map 超过 50 个会话时,逐出最久未访问的条目,防止长期运行造成内存泄漏。
测试对此有直接覆盖:"should not process the same turn twice"(同一会话同消息数第二次触发不新增 span)与 "should process new messages in existing session"(追加消息后再次触发只处理增量)。值得注意的是,由于 trace 的起止时间取自本批新消息的首尾时间戳,增量上报的每个 trace 天然按对话"轮次"切分,便于在 UI 中按轮查看。
七、查看 Trace:MLflow UI 侧操作
追踪数据上报后,按 README 的说明启动 MLflow server 并打开 UI:
mlflow server # 浏览器打开 http://localhost:5000在 MLflow UI 中进入对应实验(MLFLOW_EXPERIMENT_ID指定的实验),即可看到 OpenCode 会话生成的 trace 列表。得益于第五节的 span 结构与 OpenAI 消息形状设计,可以在 Chat 视图中直接回放整个智能体对话:用户提示、assistant 的推理与回复、工具调用的入参与结果按时间线排列,同时可查看每次 LLM 调用的model、provider与token_usage明细,以及 trace 级的mlflow.trace.session/mlflow.trace.user元数据用于按会话、按用户筛选。
八、边界行为与异常处理(源码级确认)
结合 src/index.ts 与 tests/index.test.ts 的 Edge Cases 分组,插件对以下边界情况均有明确行为:
| 场景 | 行为 |
|---|---|
| 环境变量缺失 | 静默跳过(调试模式仅输出[mlflow]日志),不拉消息、不建 trace |
非session.idle事件 / 无sessionID | 直接返回 |
| 空消息数组 | 不建 trace |
| 会话中无 user 消息 | 不建 trace |
| user 消息无文本内容 | 不建 trace |
消息缺少parts字段 | 优雅降级,不建 trace |
工具调用缺少state | 仍创建 tool span,用默认值兜底(status: 'unknown'、空输出) |
拉取消息失败(data为空) | 放弃本次处理,不影响插件运行 |
工具状态为error | tool span 的 outputs 记录error字段而非 result |
此外插件还有一处兼容性设计值得留意:调试模式下即使 SDK 初始化失败,也只会输出日志而不会抛出异常打断 OpenCode 主流程;消息处理循环外包了 try/catch,任何单个会话的处理异常都不会影响后续事件。
九、测试与工程质量
@mlflow/opencode的测试体系(tests/index.test.ts)通过 mock@mlflow/core模块与 mock OpenCode 插件客户端,覆盖了以下分组:
- 插件初始化:导出函数形态、返回 Promise 及 event 钩子;
- 环境变量处理:三个分支(缺 URI / 缺 ID / 双全)下的行为;
- 事件过滤:事件类型与 sessionID 校验;
- LLM 调用追踪:文本回复、token 用量、多轮会话多 span、不同模型(
openai/gpt-4)、reasoning 内容、纯 reasoning 无文本等; - 工具调用追踪:completed / error / 多工具连续调用 / 文本+工具混合 / Edit、Write、Bash 等常见工具;
- Agent 工作流:完整"LLM+工具+回复"链路、多步 agent(Read→Edit→回复)逐条建 LLM span、纯工具调用消息不丢 span;
- 重复轮次防护:同轮不重复处理、新消息增量处理;
- Trace 元数据:
mlflow.trace.session/mlflow.trace.user/ 双 preview 断言; - 时间信息:span 与 tool span 的纳秒时间戳;
- 会话历史重建:含 reasoning、tool result 的完整历史结构。
mock 定义见 tests/mocks/@opencode-ai/plugin.ts,其中PluginClient接口给出了插件所依赖的 OpenCode 客户端能力(session.messages),印证了插件与 OpenCode 协议的最小耦合面。
十、源码路径索引
如需深入源码,可按以下路径继续探索:
- 插件主体实现:libs/typescript/integrations/opencode/src/index.ts
- 插件测试(含大量可参考的消息构造辅助函数):libs/typescript/integrations/opencode/tests/index.test.ts
- 插件元信息(版本、依赖、Node 版本要求
>=18、Apache-2.0 许可证):libs/typescript/integrations/opencode/package.json - 底层 TypeScript SDK 核心:
init位于 libs/typescript/core/src/core/config.ts,startSpan/withSpan/updateCurrentTrace位于 libs/typescript/core/src/core/api.ts,flushTraces位于 libs/typescript/core/src/core/provider.ts - SDK 使用概览:libs/typescript/core/README.md
- 同类智能体集成参考(同为 OpenAI 消息形状设计):libs/typescript/integrations/qwen-code
结语
@mlflow/opencode展示了"事件驱动 + 增量建链"的智能体可观测性集成范式:以 OpenCode 的session.idle事件为触发点,用约 600 行的单文件实现,把会话消息完整映射为 AGENT/LLM/TOOL 三层 trace 结构,并通过 OpenAI chat-completion 消息形状保持与 MLflow Chat 视图及其他集成的一致性。对于希望以低成本获得 OpenCode 开发过程全量可观测、可复盘、可评估能力的团队而言,这是一条开箱即用的路径——安装一个包、配置一行 JSON、导出两个环境变量即可。
【免费下载链接】mlflowThe open source AI engineering platform for agents, LLMs, and ML models. MLflow enables teams of all sizes to debug, evaluate, monitor, and optimize production-quality AI applications while controlling costs and managing access to models and data.项目地址: https://gitcode.com/GitHub_Trending/ml/mlflow
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考