Mastra 观测记忆(Observational Memory)Repro 捕获工作流实战:录制、脱敏与分析记忆处理器黑盒
2026/9/13 14:39:55 网站建设 项目流程

Mastra 观测记忆(Observational Memory)Repro 捕获工作流实战:录制、脱敏与分析记忆处理器黑盒

【免费下载链接】mastraMastra is the modern TypeScript framework for AI-powered applications and agents.项目地址: https://gitcode.com/GitHub_Trending/ma/mastra

本文基于 Mastra 仓库中@mastra/memory包内 Observational Memory(下称 OM)处理器的 repro capture 工具链文档(repro-captures/README.md)展开。OM 处理器会在每一轮 agent 消息流转中对上下文做观察、阈值清理与消息 ID 重映射,其行为对使用者而言是“黑盒”;当你遇到消息丢失、token 异常波动或观察循环不触发等问题时,这套 repro capture 机制让你可以用一个环境变量把每一步的处理输入/输出完整落盘,再用配套脚本脱敏与分析。读完本文,你可以独立完成“开启录制 → 定位问题 step → 脱敏分享 → 分析器复核 → 单测回归”的完整排障闭环。

一、为什么需要 OM Repro Capture

OM 处理器位于 processor.ts,它在 agent 每个 step 的输入处理阶段介入:快照记录与消息、执行step.prepare()(激活、阈值检查、观察、过滤),再把观察上下文以 system message 形式注入消息列表。从源码结构看,这一过程中消息列表会被改写——旧消息可能因阈值清理被移除、响应消息 ID 可能轮转——这些变更对用户不可见,出问题时很难判断到底是“消息没存进去”还是“OM 主动删掉了”。

repro capture 就是为这个黑盒准备的取证工具:开启后,每个 step 处理前后的完整状态都会写入本地 JSON 文件,供事后比对。捕获实现集中在 repro-capture.ts。

二、开启录制:一个环境变量与目录约定

2.1 两个环境变量

按 README 的说明,开启捕获只需一个环境变量:

OM_REPRO_CAPTURE=1

可选地,用OM_REPRO_CAPTURE_DIR修改捕获的根目录。

对照源码可以确认这两处解析逻辑(repro-capture.ts):

function getOmReproCaptureDir(): string { return process.env.OM_REPRO_CAPTURE_DIR ?? '.mastra-om-repro'; } export function isOmReproCaptureEnabled(): boolean { return process.env.OM_REPRO_CAPTURE === '1'; }

两个注意点:

  • 开关是精确字符串匹配OM_REPRO_CAPTURE === '1',设为trueyes11都不会生效;
  • 根目录未设置时默认为相对路径.mastra-om-repro,最终会拼到process.cwd()下(见下文createOmReproCaptureDir),所以捕获位置取决于你启动应用时的工作目录。

2.2 捕获目录与文件命名

开启后,正常运行你的聊天/agent 流程,直到坏行为出现为止。每个 step 的捕获写入:

<cwd>/.mastra-om-repro/<threadId>/<timestamp>-step-<n>-<uuid>/

目录命名逻辑见 repro-capture.ts:

function createOmReproCaptureDir(threadId: string, label: string): string { const sanitizedThreadId = sanitizeCapturePathSegment(threadId); const captureDir = join( process.cwd(), getOmReproCaptureDir(), sanitizedThreadId, `${Date.now()}-${label}-${randomUUID()}`, ); mkdirSync(captureDir, { recursive: true }); return captureDir; }

<timestamp>Date.now()毫秒时间戳、<label>step-<stepNumber>(或缓冲周期的buffer-<cycleId>,见第五节)、<uuid>是随机 UUID,三者共同保证同一线程内多次捕获不互相覆盖。同时sanitizeCapturePathSegment会把 threadId 中的/\和连续的.替换为_,空值回退为unknown-thread,避免 threadId 中夹带路径分隔符导致目录逃逸或写入异常位置。

写入时机在输入处理器中:processor.ts 在开启捕获时,先对处理的记录快照、DB 消息快照和序列化后的MessageList做 deep copy(都经过safeCaptureJson净化),随后在step.prepare()完成、token 持久化之后(processor.ts)才调用writeProcessInputStepReproCapture落盘。也就是说捕获覆盖的是“本 step 处理完成后”的完整前后对比,且整段写入逻辑包在 try/catch 内、只走debug日志,捕获失败不会影响主流程

三、每个 Step 捕获了什么

每个 step 目录包含四个核心文件(另有两个条件文件),与 README 的描述对应如下:

文件内容
input.json处理输入元数据:step/readOnly/state keys + JSON 安全的state+ 捕获到的回放args
pre-state.json处理前的 OM 状态:OM 记录快照、buffered chunks、contextTokenCount、原始messages、序列化后的messageList
output.json处理输出细节:details.thresholdReacheddetails.thresholdCleanupobservedIdsminRemaining等)、messageDiffremovedMessageIdsaddedMessageIdsidRemap
post-state.json处理后的 OM 状态,形状同 pre-state
observer-exchange.json(可选)本 step 的观察者提示词与原始 LLM 响应
capture-error.json(条件)任一文件序列化失败时的错误汇总

各字段的真实构造过程见 repro-capture.ts:

  • input.json:除stepNumberthreadIdresourceId外,还会记录readOnly(来自 memory request context 的memoryConfig.readOnly)、当前消息数量与全部消息 ID、state的 key 列表及净化后的 state 本体,以及回放所需的args子集(messagesstepssystemMessagesretryCounttoolChoiceactiveToolsmodelSettingsstructuredOutput)。
  • messageDiff:以处理前后的消息 ID 集合做差集得到removedMessageIdsaddedMessageIds(repro-capture.ts)。更有价值的是idRemapinferReproIdRemap(repro-capture.ts)用role + createdAt + content构造消息指纹,当某个指纹在前后消息集合中各自恰好唯一且 ID 发生变化时,就记录一条{ fromId, toId, fingerprint }。这让你能直接回答“这条消息是被删除了,还是被换了个新 ID 重新插入”——这正是消息 ID 轮转(rotateResponseMessageId)场景下的关键判断依据。
  • post-state.json额外记录了处理后的messageCountmessageIds与处理后的messageList.serialize()结果。

3.1 序列化安全:safeCaptureJson

捕获对象来自运行时内存,可能包含普通 JSON 无法直出表示的值。safeCaptureJson(repro-capture.ts)通过JSON.stringify的 replacer 做净化:

export function safeCaptureJson(value: unknown): unknown { return JSON.parse( JSON.stringify(value, (_key, current) => { if (typeof current === 'bigint') return current.toString(); if (typeof current === 'function') return '[function]'; if (typeof current === 'symbol') return current.toString(); if (current instanceof Error) return { name: current.name, message: current.message, stack: current.stack }; if (current instanceof Set) return { __type: 'Set', values: Array.from(current.values()) }; if (current instanceof Map) return { __type: 'Map', entries: Array.from(current.entries()) }; return current; }), ); }

若某个文件整体仍无法序列化(如循环引用),safeCaptureJsonOrError会捕获异常并写一个带__captureError标记的降级文件(内含 error message、stack 与util.inspect深度摘要),同时把所有失败项汇总到capture-error.json。因此捕获永远不会因为数据形态问题而中断 agent 运行,也绝不会丢步骤——你最多得到一个带错误标记的 JSON。

另外,input.json中的state.__omTurn(一个持有活动 turn 的复杂对象)不会原样倾倒,而是被summarizeOmTurn压缩为摘要:只保留 threadId/resourceId、started/ended、record 的关键计数(observationTokenCountpendingMessageTokensisBufferingObservation等)、context 的消息数与 continuation ID、当前 step 的activated/observed/buffered/reflected/didThresholdCleanup布尔位,避免把整个活体 turn 对象写进磁盘。

四、脱敏:sanitize:om-repro

原始.mastra-om-repro捕获可能包含敏感本地路径、工具输出与对话文本。README 明确要求:原始捕获留在本机,分享前必须脱敏,且不要把这个工作流产出的完整 step 捕获提交进仓库。

脱敏命令(README 原文):

cd packages/memory pnpm sanitize:om-repro /path/to/.mastra-om-repro/<threadId> --write

该命令映射到 package.json 的sanitize:om-repro脚本,执行 sanitize-om-repro.mjs。使用方式与默认行为:

Usage: node ./scripts/sanitize-om-repro.mjs [fixture-dir] [--write]
  • 目标参数缺省时,处理内置的 fixture 目录src/processors/observational-memory/__fixtures__/repro-captures(即本文档所在目录),传参则处理你指定的 thread 捕获目录;
  • 不带--write时仅预览(打印每个被处理的文件路径),加上--write才原地重写文件;
  • 只递归收集四种标准 JSON:input.jsonpre-state.jsonoutput.jsonpost-state.json

脚本的实际脱敏策略(sanitize-om-repro.mjs)比“把字符串打码”细致得多:

  1. 路径与邮箱正则打码(sanitize-om-repro.mjs):所有字符串先经过redactPathLikeSegments,依次匹配/Users/<user>/.../home/<user>/...~/a/b/...、WindowsC:\Users\...\与 UNC\\host\share\...路径,以及邮箱地址,分别替换为<redacted-path>/<redacted-email>。若字符串整体不含可识别路径,则进一步退化为[sanitized:<label>]占位符。
  2. 键级语义脱敏sanitizeUnknown/sanitizeNode):针对 OM 特有的键做定向处理——
    • reasoningEncryptedContent[sanitized:reasoning-encrypted-content]
    • observationsactiveObservationsbufferedReflection(OM 的核心语义负载)→[sanitized:<label>:observations]等占位符;
    • allowedPaths(数组)→ 逐项<redacted-path>basePath<redacted-path>
    • observedTimezone→ 统一归一为UTC,消除时区指纹;
    • 工具执行类的output/stdout/stderr→ 走标量打码路径。
  3. 消息与部件级脱敏text部件内容替换为[sanitized:text:<label>]reasoning部件的text/reasoning字段打码;tool-invocationargs/resultsanitizeToolPayload压缩成结构摘要——数组变成{ redacted, type: 'array', itemCount },对象变成{ redacted, type: 'object', keys: [...] }(只留键名,不留值);其余未识别部件整体替换为{ type, value: '[sanitized:part:<label>]' }
  4. token 计数保真:脱敏会把原始消息的 token 估计写入providerMetadata.mastra.tokenEstimate(或content.metadata.mastra.tokenEstimate),带版本号v: 6、来源(默认v6:tokenx,即 tokenx 库的estimateTokenCount)与内容指纹(kind + sha1)。这保证脱敏后仍可按“原始 token 分布”做分析——分析器依赖的正是contextTokenCount与阈值信息,而不是明文内容。同时metadata.mastra.sealed等结构标记会被刻意保留。

如果复制捕获目录到别处做分析,README 建议给本地目录起一个有描述性的名字,方便回溯对应事件。

五、第二个捕获点:观察者缓冲周期

除 step 级捕获外,还有一个条件捕获:observer-exchange.json(观察者提示词 + 原始 LLM 响应)。它出现在两处:

  • step 捕获中,若本 step 触发了观察者调用,会把ctx.observerExchange一并写入(processor.ts);
  • 异步缓冲观察周期结束后,observational-memory.ts 调用writeObserverExchangeReproCapture,label 为buffer-<cycleId>details记录cycleIdstartedAtbuffered: true、候选消息 ID 列表与数量、pendingTokensnewTokens

因此一个 thread 目录下的子目录可能混有...-step-<n>-<uuid>...-buffer-<cycleId>-<uuid>两类捕获,前者含完整四文件,后者只含input.json/output.json/observer-exchange.json(repro-capture.ts)。排查“为什么这次观察的结论是 X”时,缓冲捕获里的提示词与模型原文是最直接的证据。

六、分析:analyze:om-repro

脱敏或自检之后,用分析器复核(README 原文):

cd packages/memory pnpm analyze:om-repro /path/to/.mastra-om-repro/<threadId>

对应 package.json 中analyze:om-repro→ analyze-om-repro.mjs。分析器逐个子目录读取pre-state.jsonpost-state.jsonoutput.json,无法解析的 step 目录会打 warning 并跳过。打印内容与 README 所列一致(analyze-om-repro.mjs):

  • 总步数Steps: N);
  • 阈值激活次数Threshold activations: N,即details.thresholdReached为真的 step 数);
  • Top token drops:按pre.contextTokenCount - post.contextTokenCount降序取前 10,每行含dropprepostthresholdobservedthresholdCleanup.observedIdsCount)、minRemaining
  • Activation details:对每个激活 step 输出pre/post/drop/observed/removed/added/idRemap/waitMs/ratio,其中waitMs取自details.backpressure.waitMsAppliedratio取自details.backpressure.ratio(无则n/a),removed/added/idRemapmessageDiff中对应数组的长度。

典型用法:若某个 step 的drop异常大且伴随大量idRemap,优先怀疑阈值清理 + ID 轮转的组合行为;若thresholdReached频繁为真但observed为 0,则应检查观察者是否真的产出。这些判断全部基于捕获文件本身,不需要重新跑线上流程。

七、录制/工具改动后的本地验证流程

README 给出针对“录制/工具链改动”的聚焦验证步骤:

  1. 对本地捕获执行脱敏(pnpm sanitize:om-repro <dir> --write);
  2. 对其运行分析器(pnpm analyze:om-repro <dir>);
  3. 分享前抽查脱敏后的 JSON,确认本地路径与工具输出确实已被移除。

配套的建议单测命令(原文照录):

cd packages/memory pnpm vitest run src/processors/observational-memory/__tests__/observational-memory.test.ts -t "<test name>" -- --bail 1 --reporter=dot

该测试文件位于 observational-memory.test.ts,与捕获机制同属tests目录(同目录还有thresholds.test.tsidle-buffering.test.tsactivation-ttl.test.ts等针对阈值与缓冲行为的用例),改动回放/分析逻辑后可用它做快速回归。

八、Fixture 命名约定

README 末尾还留了一条给后续贡献者的约定:新增 fixture 时,在 fixture 目录名或 README 更新中附一句简短说明,让后来的贡献者能把 fixture 快速映射回对应的捕获事件。当前该 fixture 目录(fixtures/repro-captures)中除本 README 外没有提交任何完整 step 捕获——这正是第一节“不要提交完整 step 捕获”原则的体现:敏感原始数据只留在贡献者本机,仓库中只沉淀工作流文档。

附:本工作流涉及的文件速查

用途相对路径
工作流文档(本文主体)packages/memory/src/processors/observational-memory/fixtures/repro-captures/README.md
捕获核心实现packages/memory/src/processors/observational-memory/repro-capture.ts
step 捕获调用点packages/memory/src/processors/observational-memory/processor.ts
缓冲观察捕获调用点packages/memory/src/processors/observational-memory/observational-memory.ts
脱敏脚本packages/memory/scripts/sanitize-om-repro.mjs
分析脚本packages/memory/scripts/analyze-om-repro.mjs
脚本入口定义packages/memory/package.json
回归测试packages/memory/src/processors/observational-memory/tests/observational-memory.test.ts

适用前提:本工作流属于@mastra/memory(当前版本见 package.json)的开发/排障工具链,捕获文件仅写入本地磁盘,不经过任何网络或云端通道;所有脱敏与分析命令均需先cd packages/memory后以 pnpm 脚本形式执行。

【免费下载链接】mastraMastra is the modern TypeScript framework for AI-powered applications and agents.项目地址: https://gitcode.com/GitHub_Trending/ma/mastra

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询