
消息、角色与 token聊天的数据结构解剖你以为你在跟模型聊天其实你每次都在往 API 里塞一个 JSON 数组。这个数组里的四个角色、和那个叫 token 的计量单位撑起了 Agent 世界的全部数据结构。这一节我们把它拆开看个明白。本文导航messages对话的原子结构四大角色一场严格的舞台剧tokenAgent 世界的通用货币中英文 token 密度实测用 pydantic 打造强类型消息模型这套结构如何撑起 Agent小结下节预告上一节我们看懂了 OpenAI 兼容协议这个壳端点、请求、响应三件套。今天把壳切开看里面最核心的数据结构——messages列表和token计量。别小看这节的内容。说实话我见过不少写了半年 LLM 应用的人仍然分不清tool角色和assistant角色的消息谁负责存 tool_calls、谁负责存执行结果。这个概念要是糊的后面写 Agent Loop 时你一定会晕——因为整个 Agent 的状态本质上就是一个不断增长的 messages 数组。messages对话的原子结构回看请求体messages是一个数组每个元素是一条消息{messages:[{role:system,content:你是一个 Python 导师},{role:user,content:什么是 Agent},{role:assistant,content:Agent 是……},{role:user,content:再举个例子}]}关键认知来了敲黑板模型没有会话概念。所谓多轮对话就是你每轮把之前的全部历史重新发一遍。上一节第 6 节讲过模型是金鱼这里的机制就是根源——历史不长在模型身上长在你发的数组里。也就是说messages 数组 模型的全部世界。它看得见的就是数组里有的数组之外的上一轮发过但这次没带的对它来说等于没发生过。这个认知直接导出 Agent 的核心编程模型调用模型工具结果也追加循环直到完成任务输入messages 数组模型输出追加到数组最终 messages 这次任务的完整黑匣子你在第 4 章会手写的 Agent Loop本质就是往这个数组里追加内容的循环。而第 11 章的上下文工程本质就是管理这个数组别撑爆窗口。整个 Harness 的一半工作都在伺候这个数组。四大角色一场严格的舞台剧messages 里每个元素都带一个role。四个角色各司其职规矩比你想的严角色谁在说话内容是什么什么时候出现system你开发者行为准则、人设、约束每次请求的头部用户不可见user终端用户任务、问题、指令用户每说一句话assistant模型模型的回复文本或tool_calls 请求每次模型输出tool你的程序某个工具的真实执行结果紧跟在带 tool_calls 的 assistant 消息后几个容易搞混的点我逐个说清system 是宪法不是聊天记录。它定义模型是什么、守什么规矩优先级高于 user 指令。所以你的 Agent 人设“你是 DeepPilot一个谨慎的编程助手”、安全约束“禁止执行删除命令”都放这里。为什么权限提示要放 system 而不是 user因为 user 消息在模型眼里是可协商的请求system 才是不可动摇的设定。assistant 不只是说话还会点菜。当模型决定调用工具时它的输出长这样{role:assistant,content:null,tool_calls:[{id:call_001,type:function,function:{name:read_file,arguments:{\path\: \app.py\}}}]}注意两个细节content可能为空arguments是字符串形式的 JSON不是对象——这是新手解析时最常见的坑。tool 消息必须带上工具 ID 回填{role:tool,tool_call_id:call_001,content:print(hi)...文件内容...}tool_call_id是关键——模型可能一口气点多个菜并行工具调用你得靠 ID 把每道菜端到对应的桌位。端错了模型当场懵圈轻则答非所问重则陷入重试循环。顺序铁律也记一下tool消息必须紧跟对应的assistant(tool_calls)消息。数组里出现assistant 点了菜下一条却是 user 消息很多网关会直接报 400。tokenAgent 世界的通用货币第二个主角登场。token 是模型处理文本的最小单位介于字和词之间。模型不认识汉字也不认识单词它眼里只有 token 序列。为什么你要关心 token因为在 Agent 世界里它是一切资源的计量单位什么在用 token 计量对你的意义上下文窗口DeepSeek 1M超了就报错或被截断API 账单按百万 token 计价你的真金白银响应长度上限最大 384K 输出单次能写多长的代码/文档缓存计费命中 0.02 元省钱的大头第 13 节细讲一句话总结token 之于 Agent就像字节之于内存、money 之于云服务。你后面做成本核算第 13 节、上下文管理第 11 章、留痕分析第 7 章全都在跟 token 数打交道。那 token 到底怎么切给你建立直觉tokenizer分词器会把常见词切成一个 token生僻词切成多个中文平均一个汉字约 0.6 个 token英文平均一个单词约 1.3 个 token代码因为符号多会更贵。中英文 token 密度实测空谈没感觉实测一把。最靠谱的方法是用 API 返回的usage字段——那是服务端 tokenizer 算出来的真账比任何本地估算都准token_density.py —— 用 usage 实测不同内容的 token 密度需 DEEPSEEK_API_KEY。importosfromopenaiimportOpenAI clientOpenAI(base_urlhttps://api.deepseek.com,api_keyos.environ[DEEPSEEK_API_KEY])SAMPLES{中文(100字):大模型通过预测下一个词元来生成文本*5整个过程类似于接龙。,英文(约100词):(Large language models generate text by predicting the next token. The process works like a word chain. *4).strip(),Python代码:def fibonacci(n: int) - int:\n a, b 0, 1\n for _ in range(n):\n a, b b, a b\n return a\n,}forname,textinSAMPLES.items():respclient.chat.completions.create(modeldeepseek-flash,messages[{role:user,content:text}],max_tokens1,# 我们只要 usage回答本身没意义)print(f{name:12s}字符数{len(text):4d}tokens{resp.usage.prompt_tokens})跑出来的量级大概长这样具体数字随 tokenizer 版本浮动但比例关系很稳定中文(100字) 字符数 110 tokens73 英文(约100词) 字符数 376 tokens101 Python代码 字符数 112 tokens87看出规律没中文更值钱一个汉字约 0.6~0.7 token一个英文字符约 0.27 token。同样信息量中文账单更贵一点代码最贵符号、缩进、驼峰命名都是 token 大户同样字符数的代码 token 数明显高于散文字符数估不出 token 数这就是为什么留痕要记usage.prompt_tokens而不是自己拿字符数除以 4 了事这个实测顺便示范了一个工程技巧用max_tokens1做只称重不生成把测试成本压到最低。后面你要做提示词优化时这个技巧能帮你白嫖式地测量 prompt 长度。用 pydantic 打造强类型消息模型到这里五条铁律里的 pydantic 该出场了。messages 是纯 JSON裸写容易出错role 拼错、tool_call_id 忘带、顺序搞反。而 pydantic 能把这些规矩固化成类型让错误在编码阶段就被抓住而不是等 API 返回 400 才发现。messages_model.py —— 用 pydantic 定义强类型消息体系离线可跑。fromtypingimportLiteral,UnionfrompydanticimportBaseModel,FieldclassSystemMessage(BaseModel):role:Literal[system]systemcontent:strclassUserMessage(BaseModel):role:Literal[user]usercontent:strclassFunctionCall(BaseModel):name:strarguments:str# 注意协议里这是字符串化的 JSONclassToolCall(BaseModel):id:str# call_xxx回填时靠它对号入座type:Literal[function]functionfunction:FunctionCallclassAssistantMessage(BaseModel):role:Literal[assistant]assistantcontent:str|NoneNone# 点菜时可以为空tool_calls:list[ToolCall]|NoneNone# 点的菜在这里classToolMessage(BaseModel):role:Literal[tool]tooltool_call_id:str# 必须与 ToolCall.id 对应content:strMessageUnion[SystemMessage,UserMessage,AssistantMessage,ToolMessage]# 用法校验一条消息role 拼错当场就炸而不是等到 API 400try:msgAssistantMessage.model_validate({role:assistnt,content:hi})# 故意拼错exceptExceptionase:print(校验拦截:,e.errors()[0][type])$ uv run python messages_model.py 校验拦截: literal_error这段代码就是 DeepPilot 消息体系的地基。后面所有模块Agent Loop 追加历史、上下文引擎裁剪窗口、留痕系统序列化存档操作的都是这套类型而不是裸 dict。带来的好处很实在拼写错误编译期暴露如上literal_error当场拦截序列化/反序列化统一model_dump()出去就是合法 API 载荷任务存档、回放、调试都靠它字段即文档看到tool_call_id: str就知道回填必须带 ID不用翻协议文档这套结构如何撑起 Agent把本节内容串进大局。你现在知道了Agent 的全部状态 一个 messages 数组模型无状态第 6 节讲过的金鱼理论这里看到了数据结构层面的根源Agent Loop 每转一圈就是数组经历一次assistant 点菜 → tool 上菜的追加这个数组的 token 数就是你的窗口压力和账单金额一次Agent任务的生命周期还有工具调用任务完成整个数组system人设安全约束user任务指令assistanttool_calls 点菜toolread_file 结果assistant继续点菜或给出答案assistant最终回答finish_reasonstop这就是第4章你要手写的Agent Loop 的全部状态机第 4 章第 16 节开始写 ReAct 循环时你会发现循环维护的就是这么个数组——没有更多魔法了。小结messages 数组 模型的全部世界模型无会话概念多轮对话靠每轮重发全部历史。四大角色分工system 是宪法人设/安全约束、user 是任务、assistant 既能说话也能用 tool_calls 点菜、tool 必须带 tool_call_id 回填执行结果。token 是通用货币窗口、账单、输出上限、缓存全用它计量中文约 0.6 token/字、代码更贵实测要用usage字段字符估算不可靠。pydantic 强类型消息模型把协议规矩固化成类型错误在编码期拦截序列化统一是 DeepPilot 消息体系的地基。Agent Loop 本质 维护这个数组的循环上下文工程 管理它的 token 预算。下节预告数据结构搞定了下一节我们把 DeepSeek API 的能力盘个底朝天——DeepSeek API 能力全景工具调用、思考模式与 Anthropic 格式。Tool Calls 协议的完整细节、reasoning_content思考模式怎么开怎么用、Anthropic 格式端点又是给谁用的这些能力各自适合什么场景、怎么组合一次讲完。这一节是第 4 章 Agent Loop 实战前的最后一块理论拼图。如果觉得本文对你有帮助欢迎点赞、收藏、关注三连本系列持续更新中80篇硬核实战关注不迷路~