跳转到内容

LLM 结构化输出 XML 解析器

LLM 结构化输出 XML 解析器:Claude-Mem 的具体实现

学习目标

本章聚焦 Claude-Mem 如何解析 LLM 生成的 XML 格式输出。建议先阅读前置知识,下文直接聚焦具体落地方案。

前置知识

本章涉及结构化输出策略的通用知识,建议先阅读:

项目实践

解析器入口

src/sdk/parser.ts 提供了完整的 XML 解析能力,入口函数为 parseAgentXml(raw)

XML 字段提取

解析器使用正则提取每个 XML 标签的内容:

// 伪代码:单字段提取
function extractField(content, fieldName):
regex = /<fieldName>([\s\S]*?)<\/fieldName>/
match = regex.exec(content)
return match ? match[1].trim() : null
// 伪代码:数组字段提取
function extractArrayElements(content, arrayName, elementName):
// 先提取 <facts>...</facts> 块
// 再用 <fact>...</fact> 正则提取每个元素
// 过滤空字符串

概念去重

解析器在提取 concepts 数组后,自动移除与 type 相同的值:

const cleanedConcepts = concepts.filter(c => c !== finalType)

这是因为 type 字段已经是观察的分类,将其同时放入 concepts 会导致冗余标签。

模式感知的类型验证

ModeManager 维护当前活动模式的有效观察类型列表。解析器在提取到 type 字段后:

  1. 检查 type 是否在有效列表中
  2. 若有效 → 使用
  3. 若无效 → 记录错误日志,回退到默认类型(有效列表的第一个)
  4. 若缺失 → 记录错误日志,使用默认类型

这种设计使模式可以定义自己支持的观察类型集合,而无需修改解析器代码。

空观察过滤

解析器检测以下情况:

if (!title && !narrative && facts.length === 0 && cleanedConcepts.length === 0) {
→ 跳过该观察(所有字段为空,无实际内容)
}

这避免了 LLM 生成空观察时浪费存储空间。

问题与规避

LLM 输出格式不稳定

问题:不同 LLM、不同温度设置下,XML 输出格式可能不一致。

规避

  1. 代码围栏剥离:处理 xml ... 包裹
  2. 宽松正则:[\s\S]*? 匹配任意字符(包括换行)
  3. 字段缺失容忍:缺少非必需字段(如 subtitle)不会导致解析失败
  4. 空内容过滤:跳过全空的观察块

嵌套 XML 冲突

问题:如果观察的 narrative 字段中包含 <observation> 字样,正则可能误匹配。

规避:使用非贪婪匹配 *? 和逐块提取模式,优先匹配最近的一对闭合标签。

设计取舍

正则解析 vs DOM 解析

Claude-Mem 使用正则而非 DOM 解析器(如 xml2js):

维度正则DOM 解析器
依赖无(内建)需要第三方库
容错高(部分匹配也有效)低(格式错误会抛异常)
性能快(纯字符串操作)慢(需要构建 DOM 树)
维护需要维护正则表达式标准 API

Claude-Mem 选择正则是因为 LLM 生成的 XML 格式不完全可靠,正则比 DOM 解析器更容错。代码注释中标注了 TODO,计划迁移到 Anthropic tool-use API 的结构化输出,届时将不再需要 XML 解析。

参考来源