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',设为true、yes或11都不会生效; - 根目录未设置时默认为相对路径
.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.thresholdReached、details.thresholdCleanup(observedIds、minRemaining等)、messageDiff(removedMessageIds、addedMessageIds、idRemap) |
post-state.json | 处理后的 OM 状态,形状同 pre-state |
observer-exchange.json | (可选)本 step 的观察者提示词与原始 LLM 响应 |
capture-error.json | (条件)任一文件序列化失败时的错误汇总 |
各字段的真实构造过程见 repro-capture.ts:
input.json:除stepNumber、threadId、resourceId外,还会记录readOnly(来自 memory request context 的memoryConfig.readOnly)、当前消息数量与全部消息 ID、state的 key 列表及净化后的 state 本体,以及回放所需的args子集(messages、steps、systemMessages、retryCount、toolChoice、activeTools、modelSettings、structuredOutput)。messageDiff:以处理前后的消息 ID 集合做差集得到removedMessageIds与addedMessageIds(repro-capture.ts)。更有价值的是idRemap:inferReproIdRemap(repro-capture.ts)用role + createdAt + content构造消息指纹,当某个指纹在前后消息集合中各自恰好唯一且 ID 发生变化时,就记录一条{ fromId, toId, fingerprint }。这让你能直接回答“这条消息是被删除了,还是被换了个新 ID 重新插入”——这正是消息 ID 轮转(rotateResponseMessageId)场景下的关键判断依据。post-state.json额外记录了处理后的messageCount、messageIds与处理后的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 的关键计数(observationTokenCount、pendingMessageTokens、isBufferingObservation等)、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.json、pre-state.json、output.json、post-state.json。
脚本的实际脱敏策略(sanitize-om-repro.mjs)比“把字符串打码”细致得多:
- 路径与邮箱正则打码(sanitize-om-repro.mjs):所有字符串先经过
redactPathLikeSegments,依次匹配/Users/<user>/...、/home/<user>/...、~/a/b/...、WindowsC:\Users\...\与 UNC\\host\share\...路径,以及邮箱地址,分别替换为<redacted-path>/<redacted-email>。若字符串整体不含可识别路径,则进一步退化为[sanitized:<label>]占位符。 - 键级语义脱敏(
sanitizeUnknown/sanitizeNode):针对 OM 特有的键做定向处理——reasoningEncryptedContent→[sanitized:reasoning-encrypted-content];observations、activeObservations、bufferedReflection(OM 的核心语义负载)→[sanitized:<label>:observations]等占位符;allowedPaths(数组)→ 逐项<redacted-path>;basePath→<redacted-path>;observedTimezone→ 统一归一为UTC,消除时区指纹;- 工具执行类的
output/stdout/stderr→ 走标量打码路径。
- 消息与部件级脱敏:
text部件内容替换为[sanitized:text:<label>],reasoning部件的text/reasoning字段打码;tool-invocation的args/result被sanitizeToolPayload压缩成结构摘要——数组变成{ redacted, type: 'array', itemCount },对象变成{ redacted, type: 'object', keys: [...] }(只留键名,不留值);其余未识别部件整体替换为{ type, value: '[sanitized:part:<label>]' }。 - 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记录cycleId、startedAt、buffered: true、候选消息 ID 列表与数量、pendingTokens、newTokens。
因此一个 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.json、post-state.json、output.json,无法解析的 step 目录会打 warning 并跳过。打印内容与 README 所列一致(analyze-om-repro.mjs):
- 总步数(
Steps: N); - 阈值激活次数(
Threshold activations: N,即details.thresholdReached为真的 step 数); - Top token drops:按
pre.contextTokenCount - post.contextTokenCount降序取前 10,每行含drop、pre、post、threshold、observed(thresholdCleanup.observedIdsCount)、minRemaining; - Activation details:对每个激活 step 输出
pre/post/drop/observed/removed/added/idRemap/waitMs/ratio,其中waitMs取自details.backpressure.waitMsApplied,ratio取自details.backpressure.ratio(无则n/a),removed/added/idRemap为messageDiff中对应数组的长度。
典型用法:若某个 step 的drop异常大且伴随大量idRemap,优先怀疑阈值清理 + ID 轮转的组合行为;若thresholdReached频繁为真但observed为 0,则应检查观察者是否真的产出。这些判断全部基于捕获文件本身,不需要重新跑线上流程。
七、录制/工具改动后的本地验证流程
README 给出针对“录制/工具链改动”的聚焦验证步骤:
- 对本地捕获执行脱敏(
pnpm sanitize:om-repro <dir> --write); - 对其运行分析器(
pnpm analyze:om-repro <dir>); - 分享前抽查脱敏后的 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.ts、idle-buffering.test.ts、activation-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),仅供参考