第 21 章

ChatML 格式与特殊 Token 设计

第21章:ChatML 格式与特殊 Token 设计

Hermes Agent 的语言理解能力,在很大程度上源于其精心设计的 Token 体系。ChatML(Chat Markup Language)不只是一种消息格式,它是 Hermes 区分"谁在说话"、"现在在做什么"的核心机制。理解 ChatML 格式和特殊 Token 的语义,是手工构造高质量 Prompt、调试 Agent 行为的必备知识。


21.1 ChatML 格式完整规范

ChatML 是由 OpenAI 最初提出、后被广泛采用的对话标记语言。Hermes 在标准 ChatML 基础上进行了扩展,增加了 Agent 专属的角色和标签。

21.1.1 标准 ChatML 结构

<|im_start|>system
{系统提示词内容}
<|im_end|>
<|im_start|>user
{用户消息内容}
<|im_end|>
<|im_start|>assistant
{助手回复内容}
<|im_end|>

21.1.2 Hermes 扩展后的完整格式

<|im_start|>system
{系统提示词}
<|im_end|>
<|im_start|>user
{用户消息}
<|im_end|>
<|im_start|>assistant
[inner_monologue]
{内部独白推理过程}
[/inner_monologue]
[tool_call]
{"name": "工具名", "arguments": {...}}
[/tool_call]
<|im_end|>
<|im_start|>tool
[tool_response]
{工具执行结果}
[/tool_response]
<|im_end|>
<|im_start|>assistant
{最终用户可见的回复}
<|im_end|>

21.2 im_start / im_end Token 的作用

<|im_start|><|im_end|> 是 ChatML 的最基础控制 Token,它们在词汇表中被分配了专属的 Token ID,不会被普通文本干扰。

21.2.1 Token ID 分配

Token 典型 ID(Llama 架构) 作用
`< im_start >`
`< im_end >`
`< endoftext >`

21.2.2 Tokenizer 层面的行为

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("NousResearch/Hermes-2-Pro-Mistral-7B")

# 验证特殊 Token 的 ID
print(tokenizer.convert_tokens_to_ids("<|im_start|>"))  # 输出: 32001
print(tokenizer.convert_tokens_to_ids("<|im_end|>"))    # 输出: 32002

# 完整 ChatML 消息的 Token 化
messages = [
    {"role": "system", "content": "你是一个有帮助的助手"},
    {"role": "user", "content": "你好"},
    {"role": "assistant", "content": "你好!有什么可以帮你的?"},
]

# 使用 apply_chat_template 自动应用 ChatML 格式
formatted = tokenizer.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=True,
)
print(formatted)
# 输出:
# <|im_start|>system
# 你是一个有帮助的助手<|im_end|>
# <|im_start|>user
# 你好<|im_end|>
# <|im_start|>assistant
# 你好!有什么可以帮你的?<|im_end|>
# <|im_start|>assistant
# (此处等待模型生成)

21.2.3 im_start / im_end 的注意力机制影响

# 在训练时,im_end 之后的 Token 注意力被遮蔽(masked)
# 这确保了模型不会"跨消息边界"错误地关注其他消息

# 示例:注意力掩码示意
# <|im_start|> user \n 你好 <|im_end|> <|im_start|> assistant \n
#     [1]      [1]  [1] [1]  [0]         [1]           [1]    [1]
#                                 ↑
#                         im_end 后掩码归零

21.3 system / user / assistant 角色标记

21.3.1 system 角色

system 消息是整个对话的"宪法",定义了 Agent 的身份、能力边界和行为准则:

<|im_start|>system
你是 Hermes,一个由 NousResearch 开发的自主 AI Agent。
你具备以下能力:
- 网络搜索和信息检索
- 代码编写、执行和调试
- 文件读写操作
- 通过工具调用完成复杂任务

## 工具列表
{tool_list_json}

## 输出格式
使用 [tool_call] 标签调用工具...
<|im_end|>

21.3.2 user 角色

user 角色代表人类输入。在多模态场景中,user 消息可以包含文本、图像或文件引用:

<|im_start|>user
请分析这张图片中的数据趋势
[IMAGE: base64_encoded_image_data]
<|im_end|>

21.3.3 assistant 角色

assistant 角色是模型生成的内容,包含:


21.4 Hermes 扩展 Token 详解

Hermes 在标准 ChatML 基础上引入了 6 个扩展角色/标签,支撑 Agent 工作流:

标签/角色 用途 是否用户可见
[inner_monologue] 内部推理,模型思考过程 否(通常过滤掉)
[tool_call] 声明要调用的工具和参数 否(由系统拦截执行)
[tool_response] 工具执行结果 否(注入到上下文)
[step] 多步推理的每一步
[scratchpad] 中间计算暂存区
tool 角色 工具结果消息

21.4.1 tool_call 标签完整示例

<|im_start|>assistant
[inner_monologue]
用户需要查询 Python asyncio 的教程。我应该先搜索网络获取最新资料。
[/inner_monologue]
[tool_call]
{"name": "web_search", "arguments": {"query": "Python asyncio best practices 2024", "max_results": 5}}
[/tool_call]
<|im_end|>
<|im_start|>tool
[tool_response]
{
  "status": "success",
  "results": [
    {"title": "AsyncIO in Python: A Complete Walkthrough", "url": "https://realpython.com/async-io-python/"},
    {"title": "Python asyncio官方文档", "url": "https://docs.python.org/3/library/asyncio.html"}
  ]
}
[/tool_response]
<|im_end|>
<|im_start|>assistant
根据搜索结果,以下是 Python asyncio 的主要学习资源:

1. **Real Python 完整教程** — https://realpython.com/async-io-python/
2. **Python 官方文档** — https://docs.python.org/3/library/asyncio.html
<|im_end|>

21.5 与 OpenAI 格式的差异对比

Hermes ChatML 与 OpenAI API 格式(以 GPT-4 为例)存在以下关键差异:

21.5.1 格式对比表

维度 OpenAI API 格式 Hermes ChatML
消息载体 JSON 对象数组 文本序列(Token 流)
角色标记 role 字段值 `<
消息分隔 JSON 结构天然分隔 `<
工具调用 tool_calls 数组字段 [tool_call] 标签
工具响应角色 role: "tool" + tool_call_id `<
内部独白 无标准支持 [inner_monologue] 标签
流式输出 SSE JSON chunks Token 流
上下文格式 API 请求体 JSON 原始文本 Token 序列

21.5.2 OpenAI 格式(API JSON)

{
  "messages": [
    {
      "role": "system",
      "content": "你是一个有帮助的助手"
    },
    {
      "role": "user",
      "content": "搜索 Python asyncio"
    },
    {
      "role": "assistant",
      "content": null,
      "tool_calls": [
        {
          "id": "call_abc123",
          "type": "function",
          "function": {
            "name": "web_search",
            "arguments": "{\"query\": \"Python asyncio\"}"
          }
        }
      ]
    },
    {
      "role": "tool",
      "tool_call_id": "call_abc123",
      "content": "{\"results\": [...]}"
    }
  ]
}

21.5.3 Hermes ChatML(原始 Token 序列)

<|im_start|>system
你是一个有帮助的助手
<|im_end|>
<|im_start|>user
搜索 Python asyncio
<|im_end|>
<|im_start|>assistant
[tool_call]
{"name": "web_search", "arguments": {"query": "Python asyncio"}}
[/tool_call]
<|im_end|>
<|im_start|>tool
[tool_response]
{"results": [...]}
[/tool_response]
<|im_end|>
<|im_start|>assistant

关键区别:Hermes ChatML 的工具调用无需 tool_call_id 进行追踪(因为是顺序 Token 流,上下文位置本身就唯一标识了调用),而 OpenAI API 的 JSON 格式需要通过 ID 关联请求和响应。


21.6 如何手工构造有效 Prompt

手工构造 Hermes Prompt 对于调试、微调数据准备和集成测试非常有价值。

21.6.1 基础 Prompt 构造函数

from transformers import AutoTokenizer

def build_hermes_prompt(
    system: str,
    messages: list[dict],
    tools: list[dict] | None = None,
    add_generation_prompt: bool = True,
) -> str:
    """
    手工构造 Hermes ChatML 格式的 Prompt
    
    Args:
        system: 系统提示词
        messages: 消息列表 [{"role": "user/assistant/tool", "content": "..."}]
        tools: 工具定义列表(JSON Schema 格式)
        add_generation_prompt: 是否在末尾添加 assistant 开始标记
    """
    parts = []
    
    # 构造系统提示词(包含工具列表)
    sys_content = system
    if tools:
        import json
        sys_content += f"\n\n## 可用工具\n```json\n{json.dumps(tools, ensure_ascii=False, indent=2)}\n```"
    
    parts.append(f"<|im_start|>system\n{sys_content}\n<|im_end|>")
    
    # 添加对话消息
    for msg in messages:
        role = msg["role"]
        content = msg["content"]
        
        if role == "tool":
            parts.append(f"<|im_start|>tool\n[tool_response]\n{content}\n[/tool_response]\n<|im_end|>")
        else:
            parts.append(f"<|im_start|>{role}\n{content}\n<|im_end|>")
    
    # 添加生成提示
    if add_generation_prompt:
        parts.append("<|im_start|>assistant\n")
    
    return "\n".join(parts)


# 使用示例
prompt = build_hermes_prompt(
    system="你是一个专业的代码助手",
    messages=[
        {"role": "user", "content": "帮我写一个快速排序算法"},
    ],
    tools=[
        {
            "name": "code_execute",
            "description": "执行 Python 代码片段",
            "parameters": {
                "type": "object",
                "properties": {
                    "code": {"type": "string"}
                }
            }
        }
    ]
)
print(prompt)

21.6.2 多轮工具调用 Prompt 示例

def build_tool_call_sequence_prompt() -> str:
    """构造包含完整工具调用序列的 Prompt(用于训练数据或测试)"""
    
    tool_call_content = '[tool_call]\n{"name": "web_search", "arguments": {"query": "Python asyncio best practices"}}\n[/tool_call]'
    
    tool_result = '{"status": "success", "results": [{"title": "Real Python AsyncIO", "url": "https://realpython.com/async-io-python/"}]}'
    
    messages = [
        {"role": "user", "content": "帮我找一些 Python asyncio 的学习资源"},
        {"role": "assistant", "content": tool_call_content},
        {"role": "tool", "content": tool_result},
    ]
    
    return build_hermes_prompt(
        system="你是一个有帮助的 AI 助手,可以使用网络搜索工具",
        messages=messages,
        add_generation_prompt=True,
    )

prompt = build_tool_call_sequence_prompt()

21.6.3 Tokenizer 级别的验证

def validate_prompt_tokens(prompt: str, model_name: str) -> dict:
    """验证 Prompt 的 Token 数量和特殊 Token 结构"""
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    
    tokens = tokenizer.encode(prompt)
    
    im_start_id = tokenizer.convert_tokens_to_ids("<|im_start|>")
    im_end_id = tokenizer.convert_tokens_to_ids("<|im_end|>")
    
    im_starts = tokens.count(im_start_id)
    im_ends = tokens.count(im_end_id)
    
    return {
        "total_tokens": len(tokens),
        "im_start_count": im_starts,
        "im_end_count": im_ends,
        "balanced": im_starts == im_ends,
        "estimated_cost_usd": len(tokens) / 1000 * 0.01,  # 估算费用
    }

# 验证
result = validate_prompt_tokens(prompt, "NousResearch/Hermes-2-Pro-Mistral-7B")
print(result)
# {'total_tokens': 312, 'im_start_count': 4, 'im_end_count': 3, 'balanced': False, ...}
# balanced=False 是正确的:最后一个 assistant 块还未闭合(等待模型生成)

21.7 小结

本章深入解析了 Hermes 的 ChatML 格式与特殊 Token 体系:

思考题

  1. <|im_start|><|im_end|> 被设计为专属 Token(不可被普通文本产生),但如果用户在消息中输入了字符串 <|im_start|>,Tokenizer 会如何处理?这会引发安全问题吗?

  2. Hermes 的 [inner_monologue] 在实际部署中通常被过滤不展示给用户。如果允许用户看到 inner_monologue,这对用户信任度和 Agent 安全性会有什么影响?

  3. 与 OpenAI 的 JSON 格式相比,ChatML Token 流格式在并行多工具调用时有什么挑战?如何在保持 Token 流顺序性的前提下实现并行工具调用?

本章评分
4.7  / 5  (12 评分)

💬 留言讨论