- 大模型
- AI Agent
- 模型推理服务
- 微调
- 对话系统
【免费下载链接】ChatGLM3
ChatGLM3 series: Open Bilingual Chat LLMs | 开源双语对话语言模型
ChatGLM3 引入了一套全新的对话格式,用<|role|>{metadata}形式的对话头统一承载多轮对话、工具调用(Tool & Agent)与代码解释器(Code Interpreter)三类任务的输入,从协议层面规避用户输入的注入攻击。本文将依据仓库根目录下的 PROMPT.md 完整讲解该格式的结构、角色规定与三类样例场景,并结合 composite_demo 与 tools_using_demo 中的源码实现,说明这套格式在真实工程中是如何被解析、拼装与消费的。读完本文,你将能够手写或程序化构造任何符合 ChatGLM3 规范的对话输入。
为什么 ChatGLM3 需要一套全新的对话格式
传统大模型的对话通常把历史拼成一段可读文本,把角色信息写进纯文本 prompt 中。这种做法的两个明显痛点促使 ChatGLM3 重新设计输入协议(见 PROMPT.md 开篇):
- 防范用户输入注入攻击:如果角色标记(如"用户:"/"助手:")只是普通文本,用户完全可以在自己的输入中伪造这些标记,让模型把后续内容误认为系统指令或他人对话,从而劫持模型行为。
- 统一异构任务的输入:Code Interpreter、Tool & Agent 等任务各自有截然不同的输入形态,需要一套统一的对话协议把它们表达成同一种结构化形式,方便模型在训练与推理时保持一致的理解方式。
ChatGLM3 的解法是把"角色"从文本层抽离到special token(特殊 token)层:对话头中的<|role|>部分使用模型词表内的特殊 token 表示,无法从文本形式被 tokenizer 编码,因此用户输入无论怎么写,都无法伪造出真正的角色切换标记;而紧随其后的{metadata}部分则采用纯文本表示,是可选的辅助信息。
整体结构:若干轮"对话头 + 内容"的序列
ChatGLM3 的对话格式由若干轮对话组成,每一轮对话包含一个对话头和一段内容。一个典型的多轮对话结构如下:
<|system|> You are ChatGLM3, a large language model trained by Zhipu.AI. Follow the user's instructions carefully. Respond using markdown. <|user|> Hello <|assistant|> Hello, I'm ChatGLM3. What can I assist you today?需要特别说明的是:实际中每轮对话内容并不一定以换行符结尾,上例及文档中的换行只是为了排版美观(PROMPT.md 中有明确注释,下同)。
在仓库的工程实现中,这套"角色 + 内容"的序列正是被逐段拼装成最终 prompt 的。以 composite_demo/conversation.py 的preprocess_text为例:
def preprocess_text( system: str | None, tools: list[dict] | None, history: list[Conversation], ) -> str: if tools: tools = json.dumps(tools, indent=4, ensure_ascii=False) prompt = f"{Role.SYSTEM}\n" prompt += system if not tools else TOOL_PROMPT if tools: tools = json.loads(tools) prompt += json.dumps(tools, ensure_ascii=False) for conversation in history: prompt += f'{conversation}' prompt += f'{Role.ASSISTANT}\n' return prompt可以看到:系统头永远位于最前,之后按顺序追加每一轮Conversation的序列化结果,最后预留一个<|assistant|>头等待模型续写——这正是上文"对话头 + 内容"结构的直接代码映射。其中TOOL_PROMPT('Answer the following questions as best as you can. You have access to the following tools:\n')在有工具时替代普通 system 文本,对应工具调用场景的系统提示约定。
对话头规范:<|role|>{metadata}
对话头独占完整的一行,格式为:
<|role|>{metadata}<|role|>部分使用 special token 表示,无法从文本形式被 tokenizer 编码,用于防止注入攻击;metadata部分采用纯文本表示,为可选内容,用于携带额外信息(如工具名、interpreter标记)。
四种角色的使用规定如下(PROMPT.md):
| 角色 | 含义 | 使用规定 |
|---|---|---|
<|system|> | 系统信息 | 设计上可穿插于对话中,但目前规定仅可以出现在开头 |
<|user|> | 用户 | 不会连续出现多个来自<|user|>的信息 |
<|assistant|> | AI 助手 | 在出现之前必须有一个来自<|user|>的信息 |
<|observation|> | 外部的返回结果 | 必须在<|assistant|>的信息之后 |
这些约束在源码中同样有对应体现。composite_demo/conversation.py 中的Role枚举把角色与字符串一一对应:
class Role(Enum): SYSTEM = auto() USER = auto() ASSISTANT = auto() TOOL = auto() INTERPRETER = auto() OBSERVATION = auto() def __str__(self): match self: case Role.SYSTEM: return "<|system|>" case Role.USER: return "<|user|>" case Role.ASSISTANT | Role.TOOL | Role.INTERPRETER: return "<|assistant|>" case Role.OBSERVATION: return "<|observation|>"注意这里的细节:工具调用与代码执行在模型输出侧复用的都是<|assistant|>头,区别仅在于metadata不同(分别是工具名与interpreter),这正是"对话头 = 角色 + 可选 metadata"这一设计价值的体现。另外,client.py 中stream_chat将<|user|>与<|observation|>对应的 special token 显式加入eos_token_id:
eos_token_id = [tokenizer.eos_token_id, tokenizer.get_command("<|user|>"), tokenizer.get_command("<|observation|>")]即模型生成到这些特殊 token 时会自动停止,从底层保证了"新的一轮必须由模型侧的角色头起头"这一对话约束。
样例场景一:多轮对话
普通对话场景有且仅有<|user|>、<|assistant|>、<|system|>三种 role,完整的示例输入如下:
<|system|> You are ChatGLM3, a large language model trained by Zhipu.AI. Follow the user's instructions carefully. Respond using markdown. <|user|> Hello <|assistant|> Hello, I'm ChatGLM3. What can I assist you today?为提升可读性,文档中的样例在表示角色的 special token 前额外添加了一个换行符;实际使用及 tokenizer 实现中均无需额外添加这一换行。这一点在真实推理链路里可以验证:无论 basic_demo/cli_demo.py 通过model.stream_chat(...)直接对话,还是 composite_demo/demo_chat.py 通过client.generate_stream(...)流式对话,prompt 的拼装都由 tokenizer 的build_chat_input(见 client.py)按对话记录统一完成,开发者无需手工插入换行符。
样例场景二:工具调用
工具调用场景在系统头中注入可用工具的 JSON 声明(name / description / parameters),模型在需要时输出带metadata(即工具名)的<|assistant|>头,并在内容中用一段tool_call(...)形式的 Python 代码块表达参数;外部系统执行后把结果以<|observation|>返回。完整示例输入如下:
<|system|> Answer the following questions as best as you can. You have access to the following tools: [ { "name": "get_current_weather", "description": "Get the current weather in a given location", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA", }, "unit": {"type": "string"}, }, "required": ["location"], }, } ] <|user|> 今天北京的天气怎么样? <|assistant|> 好的,让我们来查看今天的天气 <|assistant|>get_current_weather ```python tool_call(location="beijing", unit="celsius") ``` <|observation|> {"temperature": 22} <|assistant|> 根据查询结果,今天北京的气温为 22 摄氏度。这套输入协议在仓库中有多个直接对应的实现:
- 工具声明格式:composite_demo/demo_tool.py 中的
EXAMPLE_TOOL与文档示例几乎一致(额外为unit声明了enum),并支持在页面的 Manual mode 下用 YAML 编写后经yaml.safe_load解析成同样的 dict 结构;tools_using_demo/cli_demo_tool.py 则展示了多条工具的声明列表,包括track、/text-to-speech、/image_resizer、/foodimg等。 - 对话流驱动:demo_tool.py 按
<|assistant|>头识别工具调用(把output_text的首行解析为工具名),按<|observation|>头结束调用;参数通过正则extract_code取出tool_call(...)代码块,再经eval(code, {'tool_call': tool_call}, {})求值得到 dict,最终交给dispatch_tool(tool, args)执行真实工具。 - 工具注册机制:composite_demo/tool_registry.py 与 tools_using_demo/tool_register.py 提供
@register_tool装饰器:函数名即工具名、docstring 即工具说明、参数用Annotated[typ, description, required]标注类型/描述/是否必填,注册后自动生成工具描述并可通过dispatch_tool按名调用。 - CLI 侧的角色切换:cli_demo_tool.py 以
model.chat(...)的返回值是否为 dict 判断模型是否发起了工具调用,进而把下一轮输入的角色切到observation(等待用户喂入外部结果),否则保持user。
样例场景三:代码执行
代码执行场景共有<|user|>、<|assistant|>、<|system|>、<|observation|>四种 role,且<|assistant|>的 metadata 只有interpreter。模型以interpreter为 metadata 输出待执行的 Python 代码块,执行引擎返回的结果以<|observation|>形式回填(文本结果用```result包裹,图片结果用[Image]占位),模型再据此继续推理,直到任务完成。完整的示例输入如下:
<|system|> 你是一位智能AI助手,你叫ChatGLM3,你连接着一台电脑,但请注意不能联网。在使用Python解决任务时,你可以运行代码并得到结果,如果运行结果有错误,你需要尽可能对代码进行改进。你可以处理用户上传到电脑上的文件,文件默认存储路径是/mnt/data/。 <|user|> #File: /mnt/data/metadata.jsonl #Size: 35380 #File uploaded 文件中是否存在缺失值或异常值? <|assistant|> 首先,我将读取您提供的文件,并查看其内容以确定是否存在缺失值或异常值。 我们从加载文件开始: <|assistant|>interpreter ```python import json # Load the uploaded file data = [] with open("/mnt/data/metadata.jsonl", 'r') as file: for line in file: data.append(json.loads(line)) # Display the first few records to get a sense of the data data[:5] ``` <|observation|> ```result [{'file_name': 'aceinthehole.png', 'name': 'Ace in the Hole', 'type': 'survivor', 'description': 'Lady Luck always seems to be throwing something good your way.'}, {'file_name': 'adrenaline.png', 'name': 'Adrenaline', 'type': 'survivor', 'description': 'You are fuelled by unexpected energy when on the verge of escape.'}, {'file_name': 'aftercare.png', 'name': 'Aftercare', 'type': 'survivor', 'description': "Unlocks potential in one's aura reading ability."}, {'file_name': 'agitation.png', 'name': 'Agitation', 'type': 'killer', 'description': 'You get excited in anticipation of hooking your prey.'}, {'file_name': 'alert.png', 'name': 'Alert', 'type': 'survivor', 'description': 'Your acute senses are on high alert.'}] ``` <|assistant|> 该文件看起来包含有关某些条目的元数据,每个条目有以下字段: - `file_name`: 文件名称 - `name`: 名称 - `type`: 类型(例如 "survivor" 或 "killer") - `description`: 描述 接下来,我将检查这些字段是否存在缺失值。 <|assistant|>interpreter ```python # Check for missing values in each column missing_values = {key: sum(1 for item in data if key not in item) for key in data[0].keys()} missing_values ``` <|observation|> ```result {'file_name': 0, 'name': 0, 'type': 0, 'description': 0} ``` <|assistant|> 每个字段都没有缺失值。这意味着每个条目都完整。 接下来,我们可以检查是否存在任何潜在的异常值。这通常需要更深入的分析和对数据的了解,但我可以从一些基本的统计和分布开始进行检查。为了这样做,我们可以查看“type”字段的值分布,看看是否有任何意外的类别。 <|assistant|>interpreter ```python # Check the distribution of values in the 'type' column type_distribution = {key: sum(1 for item in data if item['type'] == key) for key in set(item['type'] for item in data)} type_distribution ``` <|observation|> ```result {'survivor': 116, 'killer': 103} ``` <|assistant|> “type”字段有两个值:“survivor”和“killer”。其中,“survivor”有116个条目,而“killer”有103个条目。 从这个初步的检查中,我们没有发现任何明显的异常值。 如果您有任何特定的标准或其他字段需要检查,我可以为您提供更详细的分析。您还希望我检查什么呢? <|user|> 帮我画一个爱心 <|assistant|>interpreter ```python import numpy as np import matplotlib.pyplot as plt # 爱心形状的参数方程 def heart(t): x = 16 * np.sin(t) ** 3 y = 13 * np.cos(t) - 5 * np.cos(2 * t) - 2 * np.cos(3 * t) - np.cos(4 * t) return x, y t = np.linspace(0, 2 * np.pi, 1000) x, y = heart(t) plt.figure(figsize=(6, 6)) plt.plot(x, y, color='red') plt.axis('equal') plt.axis('off') plt.show() ``` <|observation|> ```result [Image] ``` <|assistant|> 这是一个爱心形状。我使用了参数方程来描述这个形状,并使用matplotlib进行了绘制。如果您有任何其他需求或问题,请随时告诉我。 <|user|> # End这段示例还揭示了一个文件上传约定:用户在提问前用#File: <路径>、#Size: <字节数>、#File uploaded三行声明已上传文件及其默认存储位置(/mnt/data/),模型据此在代码中直接读取。仓库的 composite_demo/demo_ci.py 为 Code Interpreter 模式预置了与之完全一致的中文系统提示("你是一位智能AI助手,你叫ChatGLM3……文件默认存储路径是/mnt/data/")。
在工程实现层面,代码执行场景的对话流解析集中在 demo_ci.py:
- 遇到
<|assistant|>special token 时,把前一段文本按interpreter切分并以Role.INTERPRETER记录; - 遇到
<|observation|>时,用extract_code取出代码块,交由execute(code, get_kernel())执行; execute(demo_ci.py)内部通过jupyter_client启动的CodeKernel(即一个 Jupyter 内核,见 demo_ci.py)运行 Python,并区分text/plain文本结果与image/png图片结果——图片被转成PIL.Image展示、在对话记录中仅存[Image]占位,与文档示例的[Image]结果完全对应;- 超长结果会被
truncate_length(默认 1024)截断并追加[TRUNCATED]标记,防止观察结果淹没模型输入上下文。
从规范到工程:对话格式的完整闭环
把 PROMPT.md 的格式规范与 composite_demo 的实现对照起来,可以看到一套完整的"拼装—生成—解析—回填"闭环:
- 拼装(prompt 侧):
preprocess_text(conversation.py)按"系统头 + 逐轮对话 + 预留 assistant 头"的顺序拼出输入;工具场景用TOOL_PROMPT替换系统文本并追加工具 JSON。Conversation.__str__(conversation.py)则精确复现{角色}{metadata}\n{内容}的对话头格式,例如工具调用序列化为<|assistant|>{tool}\n{content},代码执行序列化为<|assistant|>interpreter\n{content}。 - 生成(模型侧):
stream_chat(client.py)通过tokenizer.build_chat_input编码输入,并把<|user|>、<|observation|>加入eos_token_id作为停止条件;流式输出的每个 token 以是否形如<|...|>判定special属性(client.py),供上层按特殊 token 分流。 - 解析与回填(应用侧):三种模式(Chat / Tool / Code Interpreter,见 composite_demo/main.py)在生成循环中遇到
<|assistant|>头即切换角色视角(工具调用 / interpreter 代码执行),遇到<|observation|>头即注入外部结果,并以<|user|>头作为一轮对话的终点;postprocess_text(conversation.py)在展示时剥离这些特殊 token、并把 LaTeX 分隔符\(、\[转换为$、$$以便前端渲染。
小结
ChatGLM3 对话格式的本质,是用不可被文本伪造的 special token充当角色边界,用可选的纯文本 metadata区分同一角色下的不同任务形态(普通回复、工具调用、代码执行),用<|observation|>统一承载一切外部结果。只要遵循以下三条主线,就能构造出完全合规的 ChatGLM3 输入:
- 对话序列以
<|system|>开头,<|user|>与<|assistant|>交替出现,<|observation|>紧随<|assistant|>之后; - 工具调用在系统头注入 JSON 工具声明,模型侧输出
<|assistant|>{工具名}头 +tool_call(...)代码块,外部结果以<|observation|>回填; - 代码执行在系统头声明"可运行 Python、文件默认在
/mnt/data/",模型侧输出<|assistant|>interpreter头 + Python 代码块,执行结果(文本或[Image])以<|observation|>回填。
对照 composite_demo/conversation.py、composite_demo/client.py 与 composite_demo/demo_ci.py 的源码,可以随时验证每一个格式细节在真实推理链路中的落点。
- 大模型
- AI Agent
- 模型推理服务
- 微调
- 对话系统
【免费下载链接】ChatGLM3
ChatGLM3 series: Open Bilingual Chat LLMs | 开源双语对话语言模型
相关推荐
ChatGLM3 对话格式全解析:基于 System / User / Assistant / Observation 的统一提示词规范
ChatGLM3 对话格式全解析:基于 System / User / Assistant / Observation 的统一提示词规范 本篇文章完整解读 Ch
大模型人工智能微调本地部署AI AgentRAGChatGLM3 Chat Format 对话格式规范:多轮对话、工具调用与代码执行全解析
ChatGLM3 Chat Format 对话格式规范:多轮对话、工具调用与代码执行全解析 导读 本文基于 ChatGLM3 官方 PROMPT_en.md 文
大模型人工智能微调本地部署AI AgentRAGQwopus3.6-27B-v2-GGUF训练秘籍:三阶段课程学习法全解析
Qwopus3.6 27B v2 GGUF训练秘籍:三阶段课程学习法全解析 Qwopus3.6 27B v2 GGUF是基于Qwen3.6 27B开发的推理增强
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考