LLM 结构化输出 XML 解析器
LLM 结构化输出 XML 解析器:Claude-Mem 的具体实现
学习目标
本章聚焦 Claude-Mem 如何解析 LLM 生成的 XML 格式输出。建议先阅读前置知识,下文直接聚焦具体落地方案。
前置知识
本章涉及结构化输出策略的通用知识,建议先阅读:
- 结构化输出策略 — LLM 结构化输出的常见模式
项目实践
解析器入口
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 字段后:
- 检查 type 是否在有效列表中
- 若有效 → 使用
- 若无效 → 记录错误日志,回退到默认类型(有效列表的第一个)
- 若缺失 → 记录错误日志,使用默认类型
这种设计使模式可以定义自己支持的观察类型集合,而无需修改解析器代码。
空观察过滤
解析器检测以下情况:
if (!title && !narrative && facts.length === 0 && cleanedConcepts.length === 0) { → 跳过该观察(所有字段为空,无实际内容)}这避免了 LLM 生成空观察时浪费存储空间。
问题与规避
LLM 输出格式不稳定
问题:不同 LLM、不同温度设置下,XML 输出格式可能不一致。
规避:
- 代码围栏剥离:处理
xml ...包裹 - 宽松正则:
[\s\S]*?匹配任意字符(包括换行) - 字段缺失容忍:缺少非必需字段(如 subtitle)不会导致解析失败
- 空内容过滤:跳过全空的观察块
嵌套 XML 冲突
问题:如果观察的 narrative 字段中包含 <observation> 字样,正则可能误匹配。
规避:使用非贪婪匹配 *? 和逐块提取模式,优先匹配最近的一对闭合标签。
设计取舍
正则解析 vs DOM 解析
Claude-Mem 使用正则而非 DOM 解析器(如 xml2js):
| 维度 | 正则 | DOM 解析器 |
|---|---|---|
| 依赖 | 无(内建) | 需要第三方库 |
| 容错 | 高(部分匹配也有效) | 低(格式错误会抛异常) |
| 性能 | 快(纯字符串操作) | 慢(需要构建 DOM 树) |
| 维护 | 需要维护正则表达式 | 标准 API |
Claude-Mem 选择正则是因为 LLM 生成的 XML 格式不完全可靠,正则比 DOM 解析器更容错。代码注释中标注了 TODO,计划迁移到 Anthropic tool-use API 的结构化输出,届时将不再需要 XML 解析。