XML 结构化输出与 Scratchpad
第24章:XML 结构化输出与 Scratchpad
Hermes Agent 不仅能输出自然语言,还能生成精确的 XML 结构化数据——这是将 AI 能力深度集成到系统工程中的关键能力。从 Scratchpad 中间推理暂存,到 Mermaid 架构图生成,再到 Python 解析代码,本章完整呈现 Hermes XML 输出体系的设计哲学与工程实践。
24.1 Hermes 的 XML 输出规范
Hermes 的 XML 输出体系由四个核心标签构成,每个标签有明确的语义边界:
<!-- 完整 XML 输出结构示例 -->
<response>
<scratchpad>
<!-- 中间推理与计算暂存(对用户隐藏) -->
</scratchpad>
<thinking>
<!-- 轻量级推理(可选择性展示) -->
</thinking>
<result>
<!-- 最终结构化输出 -->
</result>
<metadata>
<!-- 元信息(执行时间、置信度等) -->
</metadata>
</response>
24.1.1 各标签用途对比
| 标签 | 用途 | 用户可见 | 典型内容 |
|---|---|---|---|
scratchpad |
中间计算暂存 | 否 | 草稿计算、临时变量、候选方案 |
thinking |
高层推理过程 | 可选 | 决策逻辑、权衡分析 |
result |
最终输出 | 是 | 结构化答案、报告、数据 |
metadata |
元信息 | 可选 | 置信度、来源、执行统计 |
24.1.2 XML 输出的格式规范
<!-- 规范示例:代码审查输出 -->
<code_review>
<summary severity="high">
发现 3 个高危漏洞,7 个中等风险,12 个低风险问题
</summary>
<findings>
<issue id="001" severity="high" line="42" file="auth.py">
<type>SQL Injection</type>
<description>用户输入未经过参数化查询直接拼接到 SQL 语句</description>
<code_snippet>query = f"SELECT * FROM users WHERE id = {user_id}"</code_snippet>
<fix>
<description>使用参数化查询</description>
<code>query = "SELECT * FROM users WHERE id = %s"; cursor.execute(query, (user_id,))</code>
</fix>
<references>
<ref>OWASP A03:2021 – Injection</ref>
<ref>CWE-89: SQL Injection</ref>
</references>
</issue>
<issue id="002" severity="medium" line="78" file="auth.py">
<type>Hardcoded Secret</type>
<description>API 密钥硬编码在源代码中</description>
<code_snippet>API_KEY = "sk-abc123xyz..."</code_snippet>
<fix>
<description>从环境变量读取</description>
<code>API_KEY = os.environ.get("API_KEY")</code>
</fix>
</issue>
</findings>
<metrics>
<files_analyzed>15</files_analyzed>
<lines_analyzed>2847</lines_analyzed>
<scan_time_seconds>3.2</scan_time_seconds>
</metrics>
</code_review>
24.2 Scratchpad 标签的用途与机制
scratchpad 是 Hermes 最具特色的输出组件之一。它为模型提供了一个"草稿纸"空间,用于进行中间计算、记录临时变量、探索多个候选方案,最终从 scratchpad 中提炼出高质量的最终答案。
24.2.1 Scratchpad 的三大价值
1. 减少工作记忆压力
语言模型的"工作记忆"就是上下文窗口。复杂计算时,scratchpad 允许模型将中间结果"写在纸上"而不是强行记忆:
<scratchpad>
计算任务:找出100以内所有质数的和
候选数字: 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97
逐步求和:
2 + 3 = 5
5 + 5 = 10
10 + 7 = 17
17 + 11 = 28
28 + 13 = 41
41 + 17 = 58
58 + 19 = 77
77 + 23 = 100
100 + 29 = 129
129 + 31 = 160
160 + 37 = 197
... (继续)
最终结果: 1060
</scratchpad>
<result>100以内所有质数的和为 **1060**。</result>
2. 支持多方案探索(Draft-then-Select)
<scratchpad>
用户需要一个函数名,候选方案:
方案A: process_user_data() — 太宽泛,不够描述性
方案B: sanitize_and_validate_user_input() — 太长
方案C: validate_user_profile() — 准确且适中
方案D: check_user_data() — 动词选择不精确
综合评估:方案C最佳
</scratchpad>
<result>建议函数名:`validate_user_profile()`</result>
3. 提升结构化输出的准确率
在生成复杂 XML 结构之前,在 scratchpad 中先规划结构:
<scratchpad>
需要输出的 XML 结构:
- 根节点: report
- summary (string)
- findings (list)
- finding (object): id, severity, description, fix
- statistics (object): count, scan_time
必须检查:
- severity 枚举: high/medium/low 三选一
- id 格式: "FIND-{三位数字}"
- fix 必须包含 description 和 code_example 两个子节点
</scratchpad>
24.3 结构化输出 vs JSON Mode 对比
Hermes XML 结构化输出与 LLM 常见的 JSON Mode 有本质区别:
| 维度 | JSON Mode | Hermes XML 结构化输出 |
|---|---|---|
| 格式灵活性 | 严格 JSON Schema 约束 | XML + 自然语言混合,更灵活 |
| 可读性 | 机器友好,人类一般 | 人类和机器都易读 |
| 嵌套复杂度 | 深层嵌套时可读性差 | 缩进标签天然可读 |
| 注释支持 | 不支持注释 | XML 注释(<!-- -->)支持 |
| 流式输出 | 需要积累完整 JSON 再解析 | 可按标签流式解析 |
| 模型训练需求 | 需要 JSON Schema 遵从训练 | Hermes 原生支持 |
| Scratchpad | 无标准支持 | 原生 scratchpad 标签 |
| 混合内容 | 不支持(value 只能是 JSON 类型) | 支持 XML 内嵌代码块、表格等 |
24.3.1 JSON Mode 示例
# OpenAI JSON Mode
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4-turbo",
response_format={"type": "json_object"},
messages=[
{"role": "user", "content": "分析这段代码的质量,以JSON格式返回"}
]
)
# 输出必须是合法 JSON,但无法包含中间推理过程
24.3.2 Hermes XML 输出示例
# Hermes XML 结构化输出
from hermes import HermesAgent
agent = HermesAgent()
result = await agent.run("""
分析以下代码的质量,使用以下 XML 格式输出:
<code_analysis>
<scratchpad>(你的中间分析过程)</scratchpad>
<overall_score>(0-100分)</overall_score>
<issues>(问题列表)</issues>
<recommendations>(改进建议)</recommendations>
</code_analysis>
""")
24.4 Mermaid 图表生成示例
Hermes 能够在 XML 结构化输出中生成 Mermaid 格式的图表,适用于架构图、流程图、时序图等多种场景。
24.4.1 系统架构图
<architecture_analysis>
<summary>这是一个标准的三层 Web 架构</summary>
<diagram type="mermaid">
```mermaid
graph TB
subgraph "前端层"
A[React SPA] --> B[Nginx]
B --> C[CDN]
end
subgraph "应用层"
D[API Gateway]
E[Auth Service]
F[Business Service]
G[File Service]
D --> E
D --> F
D --> G
end
subgraph "数据层"
H[(PostgreSQL)]
I[(Redis Cache)]
J[(S3 Storage)]
F --> H
F --> I
G --> J
end
B --> D
style A fill:#61AFEF
style H fill:#E06C75
style I fill:#E5C07B
24.4.2 工作流时序图
<workflow_diagram>
<diagram type="mermaid">
```mermaid
sequenceDiagram
participant U as 用户
participant A as Hermes Agent
participant T as 工具层
participant M as 记忆层
U->>A: 发送任务请求
A->>M: 检索相关历史上下文
M-->>A: 返回相关记忆
loop 推理循环
A->>A: 内部独白(CoT 推理)
A->>T: 工具调用请求
T-->>A: 工具执行结果
end
A->>M: 存储本轮对话
A-->>U: 返回最终回复
```
24.4.3 状态机图(Agent 状态转换)
<state_diagram>
<diagram type="mermaid">
```mermaid
stateDiagram-v2
[*] --> Idle: 初始化完成
Idle --> Thinking: 收到用户请求
Thinking --> Planning: 分析任务
Planning --> Executing: 制定计划
Executing --> ToolCalling: 需要工具
ToolCalling --> Executing: 工具完成
Executing --> Responding: 所有步骤完成
Executing --> ErrorHandling: 工具失败
ErrorHandling --> Planning: 重新规划
ErrorHandling --> Responding: 无法恢复
Responding --> Idle: 回复完成
```
24.5 Python 解析 Hermes XML 输出
以下是完整的 Hermes XML 解析工具库:
24.5.1 基础解析器
import xml.etree.ElementTree as ET
from dataclasses import dataclass, field
from typing import Any
import re
@dataclass
class HermesXMLOutput:
"""解析后的 Hermes XML 输出结构"""
raw: str # 原始 XML 字符串
scratchpad: str | None = None # 中间推理内容
thinking: str | None = None # 思考过程
result: Any = None # 最终结果(可以是字符串或嵌套字典)
metadata: dict = field(default_factory=dict)
custom_tags: dict = field(default_factory=dict) # 自定义标签内容
class HermesXMLParser:
"""Hermes XML 输出解析器"""
KNOWN_TAGS = {"scratchpad", "thinking", "result", "metadata"}
def parse(self, response: str) -> HermesXMLOutput:
"""
解析 Hermes 的 XML 格式响应
支持:
- 带或不带外层根标签的 XML
- 混合了自然语言的 XML(自动提取 XML 部分)
- 包含代码块的 XML(防止代码干扰解析)
"""
# 1. 预处理:提取 XML 内容
xml_content = self._extract_xml(response)
if not xml_content:
# 没有找到 XML 标签,返回纯文本结果
return HermesXMLOutput(raw=response, result=response)
# 2. 规范化:添加根标签(如果缺失)
if not xml_content.strip().startswith("<response>"):
xml_content = f"<response>{xml_content}</response>"
# 3. 解析 XML
try:
root = ET.fromstring(xml_content)
except ET.ParseError as e:
# XML 格式错误时降级处理
return self._fallback_parse(response, str(e))
output = HermesXMLOutput(raw=response)
# 4. 提取各标签内容
for child in root:
tag = child.tag
text = self._get_text_content(child)
if tag == "scratchpad":
output.scratchpad = text
elif tag == "thinking":
output.thinking = text
elif tag == "result":
# result 可能包含复杂嵌套结构
output.result = self._parse_result(child)
elif tag == "metadata":
output.metadata = self._parse_metadata(child)
else:
# 自定义标签
output.custom_tags[tag] = self._parse_element(child)
return output
def _extract_xml(self, text: str) -> str | None:
"""从混合文本中提取 XML 内容"""
# 查找第一个 XML 标签开始位置
xml_start = re.search(r'<(?!--)\w+[\s>]', text)
if not xml_start:
return None
# 提取从第一个标签开始到末尾的内容
return text[xml_start.start():]
def _get_text_content(self, element: ET.Element) -> str:
"""获取元素的完整文本内容(包括子标签的文本)"""
parts = []
if element.text:
parts.append(element.text.strip())
for child in element:
parts.append(ET.tostring(child, encoding="unicode"))
if child.tail:
parts.append(child.tail.strip())
return "\n".join(filter(None, parts))
def _parse_result(self, element: ET.Element) -> dict | str:
"""递归解析 result 标签"""
children = list(element)
if not children:
return element.text or ""
result = {}
for child in children:
result[child.tag] = self._parse_element(child)
return result
def _parse_element(self, element: ET.Element) -> Any:
"""递归解析任意 XML 元素"""
children = list(element)
if not children:
# 叶节点:返回文本内容
return element.text or ""
# 有子节点:递归解析
result = {}
for child in children:
result[child.tag] = self._parse_element(child)
# 保留属性
if element.attrib:
result["_attributes"] = element.attrib
return result
def _parse_metadata(self, element: ET.Element) -> dict:
metadata = {}
for child in element:
value = child.text or ""
# 尝试类型转换
try:
value = int(value)
except ValueError:
try:
value = float(value)
except ValueError:
pass
metadata[child.tag] = value
return metadata
def _fallback_parse(self, text: str, error: str) -> HermesXMLOutput:
"""XML 解析失败时的降级处理"""
# 使用正则表达式提取关键标签
scratchpad = self._regex_extract(text, "scratchpad")
result = self._regex_extract(text, "result")
return HermesXMLOutput(
raw=text,
scratchpad=scratchpad,
result=result or text,
metadata={"parse_error": error, "parse_method": "regex_fallback"},
)
def _regex_extract(self, text: str, tag: str) -> str | None:
pattern = rf'<{tag}[^>]*>(.*?)</{tag}>'
match = re.search(pattern, text, re.DOTALL)
return match.group(1).strip() if match else None
24.5.2 高级解析:Mermaid 图表提取
class MermaidExtractor:
"""从 Hermes XML 输出中提取 Mermaid 图表"""
def extract_all(self, xml_output: HermesXMLOutput) -> list[dict]:
"""提取所有 Mermaid 图表"""
diagrams = []
# 在所有自定义标签中搜索
all_content = xml_output.raw
# 匹配 ```mermaid ... ``` 代码块
pattern = r'```mermaid\s*\n(.*?)```'
matches = re.findall(pattern, all_content, re.DOTALL)
for i, code in enumerate(matches):
diagram_type = self._detect_type(code)
diagrams.append({
"index": i,
"type": diagram_type,
"code": code.strip(),
"rendered_url": self._render_url(code),
})
return diagrams
def _detect_type(self, code: str) -> str:
first_line = code.strip().split("\n")[0].lower()
type_map = {
"graph": "flowchart",
"sequencediagram": "sequence",
"statediagram": "state",
"classDiagram": "class",
"erdiagram": "er",
"gantt": "gantt",
}
for key, value in type_map.items():
if key in first_line:
return value
return "unknown"
def _render_url(self, code: str) -> str:
"""生成 Mermaid Live Editor 的 URL"""
import base64
import json
payload = json.dumps({
"code": code,
"mermaid": {"theme": "default"},
})
encoded = base64.urlsafe_b64encode(payload.encode()).decode()
return f"https://mermaid.live/edit#{encoded}"
24.5.3 完整使用示例
async def main():
from hermes import HermesAgent
agent = HermesAgent()
# 请求结构化分析
response = await agent.run("""
请分析以下 Python 函数的代码质量:
```python
def get_user(id):
db = connect("localhost:5432/prod")
result = db.execute(f"SELECT * FROM users WHERE id = {id}")
return result
```
使用 XML 格式输出,包含:
- scratchpad: 你的分析过程
- result/issues: 发现的问题列表(每个包含 severity 和 description)
- result/overall_score: 0-100 的综合评分
- result/refactored_code: 重构后的代码
""")
# 解析 XML 输出
parser = HermesXMLParser()
parsed = parser.parse(response.content)
# 访问各部分
print("=== 中间分析过程(不展示给用户)===")
print(parsed.scratchpad)
print("\n=== 最终结果 ===")
if isinstance(parsed.result, dict):
issues = parsed.result.get("issues", {})
score = parsed.result.get("overall_score", "N/A")
print(f"综合评分: {score}")
print(f"发现问题: {issues}")
# 提取 Mermaid 图表(如果有)
extractor = MermaidExtractor()
diagrams = extractor.extract_all(parsed)
for diagram in diagrams:
print(f"\n图表类型: {diagram['type']}")
print(f"Mermaid Live 链接: {diagram['rendered_url']}")
import asyncio
asyncio.run(main())
24.6 小结
本章系统讲解了 Hermes 的 XML 结构化输出体系:
- 四大核心标签:scratchpad(暂存)/ thinking(推理)/ result(结果)/ metadata(元信息)
- Scratchpad 价值:减少工作记忆压力、支持多方案探索、提升结构化输出准确率
- vs JSON Mode:XML 支持混合内容、注释、流式解析,更适合 Agent 复杂输出场景
- Mermaid 图表:在 XML 中内嵌架构图、时序图、状态机图,实现"文档即代码"
- Python 解析:
HermesXMLParser支持优雅降级,MermaidExtractor提取可渲染图表
思考题
-
Hermes 的 scratchpad 在推理时被"写入",但最终不展示给用户。这意味着 scratchpad 的内容仍然消耗 Token。如何设计一个"轻量级 scratchpad"方案,在保留推理价值的同时最小化 Token 消耗?
-
Mermaid 图表生成时,模型可能在语法细节上出错(如错误的节点 ID 格式)。如何设计一个"Mermaid 语法验证 + 自动修复"的后处理管道?
-
XML 结构化输出在流式场景(streaming)下面临"标签不完整"的问题(用户看到的是逐 Token 的原始 XML 流)。如何设计流式 XML 的前端渲染策略,让用户在结果完整前就能看到有意义的内容?