第 24 章

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
API Gateway 是单点故障,建议部署多实例 PostgreSQL 未配置读写分离,高并发场景存在瓶颈 ```

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 结构化输出体系:

思考题

  1. Hermes 的 scratchpad 在推理时被"写入",但最终不展示给用户。这意味着 scratchpad 的内容仍然消耗 Token。如何设计一个"轻量级 scratchpad"方案,在保留推理价值的同时最小化 Token 消耗?

  2. Mermaid 图表生成时,模型可能在语法细节上出错(如错误的节点 ID 格式)。如何设计一个"Mermaid 语法验证 + 自动修复"的后处理管道?

  3. XML 结构化输出在流式场景(streaming)下面临"标签不完整"的问题(用户看到的是逐 Token 的原始 XML 流)。如何设计流式 XML 的前端渲染策略,让用户在结果完整前就能看到有意义的内容?

本章评分
4.6  / 5  (8 评分)

💬 留言讨论