Python 字典详解:从消息结构到 Agent 开发

QuibblerAgentQuibblerAgent 2026-11-12 约 17 分钟 3 次阅读

翻开任何一个 agent 的 Python 代码,最先撞见的八成是这一行:messages.append({"role": "assistant", "content": reply})。再往下看,工具定义是字典,usage 统计是字典,配置合并还是字典——agent 开发的数据骨架,几乎全由字典搭成。它不是 Python 里可有可无的一个类型,而是这门语言里最接近 JSON、最擅长"结构固定但字段常变"的数据结构。

本文从这行代码出发,把字典从创建、访问、增删改、遍历讲到 JSON 互转与 agent 实战,最后过一遍高频陷阱。例子全部可以在本地直接运行。


1、字典是什么

字典(dict)是"键到值"的映射:一个键(key)唯一对应一个值(value),像一本通讯录——名字是键,电话是值,按名字直接查号,不用从头翻到尾。{"role": "assistant", "content": reply} 里,"role" 和 "content" 是键,"assistant" 和变量 reply 的内容是值。

为什么消息偏用字典而不是别的结构?两个原因。其一,字段名即文档:role、content 这些键本身就是语义,读代码等于读协议。其二,与 JSON 天然同构:API 收发的 JSON 对象,进到 Python 就是字典,序列化反序列化零转换成本。创建字典的常见方式如下:

msg = {"role": "assistant", "content": reply}   # 字面量,最常用
empty = {}                                       # 空字典
pairs = dict(role="user", content="你好")        # 关键字参数式
from_lists = dict(zip(["role", "content"], ["user", "你好"]))
scores = dict.fromkeys(["input", "output"], 0)   # 批量同值:{'input': 0, 'output': 0}

一条约束要记牢:键必须是可哈希的不可变对象——字符串、数字、元组都行,列表不行(列表可变,无法做键);值则没有任何限制,嵌套字典、列表、函数都可以。

2、访问与嵌套

2.1、三种取值方式

取值是字典最高频的操作,三种方式各有分工,对比如下:

方式 键不存在时 典型场景
d[key] 抛 KeyError 确定存在,缺了就是 bug
d.get(key) 返回 None 可选字段,缺了正常
d.get(key, default) 返回默认值 带兜底的读取
d.setdefault(key, default) 写入默认值并返回 分组、计数

四种形态的行为了然于同一个示例:

usage = {"input_tokens": 22, "output_tokens": 195}

usage["input_tokens"]              # 22;确定存在,直接取
usage.get("cache_read_input_tokens")          # None;可选字段没给
usage.get("cache_read", 0)                    # 0;缓存读取默认按 0 计
groups = {}
groups.setdefault("text", []).append("1\n2")  # 键不存在先建空列表,再追加

最后那行是 setdefault 的经典用法:一步完成"没有就建、有了就用",省掉先 in 判断再赋值的两步。

2.2、嵌套结构的取值

agent 里真正的消息很少只有一层:一条 assistant 消息的 content 不是字符串而是块列表,每个块又是字典。取嵌套值就是层层下标:

message = {
    "role": "assistant",
    "content": [
        {"type": "thinking", "thinking": "用户要数数……"},
        {"type": "text", "text": "1\n2\n3"},
    ],
    "usage": {"input_tokens": 22, "output_tokens": 195},
}

first_block = message["content"][0]        # 先键后下标:第一个块
text_body = message["content"][1]["text"]  # 层层深入:正文块的 text
in_tokens = message["usage"].get("input_tokens", 0)

嵌套取值的纪律只有一条:外层结构确定用中括号,内层字段可选用 get。content 一定存在且一定是列表,用 [] 让它出错时报错;usage 的某个计数字段可能没给,用 get 兜底。

3、增删改与合并

字典可变,增删改都是原地操作。赋值即新增或覆盖:msg["stop_reason"] = "end_turn"——键不存在就新增,存在就覆盖,一个语法两种效果。删除有三种:del d[key] 直接删,d.pop(key) 删并返回值,d.popitem() 弹出最后插入的键值对。合并的形态更多,对比如下:

形态 效果 备注
d.update(other) 原地合并,同键覆盖 最古老,返回 None
{**a, **b} 生成新字典,b 覆盖 a 3.5 起可用
a | b 生成新字典,b 覆盖 a 3.9 起可用
a |= b 原地合并 3.9 起可用

agent 开发最常见的合并场景是配置叠加:默认配置在下、用户配置在上,同名键以用户为准:

defaults = {"model": "claude-opus-5-5", "max_tokens": 4096, "temperature": 0.7}
user_cfg = {"max_tokens": 128000}

merged = defaults | user_cfg
# {'model': 'claude-opus-5-5', 'max_tokens': 128000, 'temperature': 0.7}

注意合并是浅覆盖:同键直接整体替换,不会递归深入合并嵌套字典——多层嵌套配置的深度合并要自己写递归或用专门库。

4、遍历与推导式

遍历字典的标准姿势是 items() 同时拿键和值;只要键时直接 for k in d,等价于 d.keys()。字典推导式则是一行完成"筛选、变换、重组"的利器:

usage_by_block = {"thinking": 175, "text": 20, "tool_use": 50}

total = sum(v for v in usage_by_block.values())        # 245:只累加值
big = {k: v for k, v in usage_by_block.items() if v >= 50}
# {'thinking': 175, 'tool_use': 50}:过滤出大项
by_cost = dict(sorted(usage_by_block.items(), key=lambda kv: -kv[1]))
# {'thinking': 175, 'tool_use': 50, 'text': 20}:按值降序

三行展示了 values、items 过滤、按值排序三个惯用法。反转字典(键值互换)也是推导式一行:{v: k for k, v in d.items()}——前提是值也唯一,否则后写的会覆盖先写的。

5、字典与 JSON

agent 与 API 的边界上,字典与 JSON 互转是家常便饭:json.dumps 把字典序列化成字符串发出去,json.loads 把响应字符串解析回字典。一轮完整的往返如下:

import json

msg = {"role": "assistant", "content": "1\n2\n3"}
wire = json.dumps(msg, ensure_ascii=False)   # 中文不转义,便于日志可读
# '{"role": "assistant", "content": "1\n2\n3"}'
back = json.loads(wire)                      # 解析回字典
print(back == msg)                           # True:内容完好往返

两个边界差异要心里有数。其一,JSON 的键只能是字符串:Python 字典的整型键 1 序列化后会变成 "1",往返之后类型变了。其二,JSON 没有元组也没有 Python 对象:元组序列化成列表,集合与自定义对象会直接报错。完整规则见 json 模块文档。

6、Agent 开发里的典型用法

6.1、消息列表的组装

对话历史就是"字典的列表"。一轮工具调用的历史组装,把本文前半的内容全部用上了:

messages = [{"role": "user", "content": "上海今天天气"}]
messages.append({"role": "assistant", "content": [
    {"type": "text", "text": "我查一下天气。"},
    {"type": "tool_use", "id": "t1", "name": "weather", "input": {"city": "上海"}},
]})

result = fetch_weather("上海")
messages.append({"role": "user", "content": [
    {"type": "tool_result", "tool_use_id": "t1", "content": "晴,22 度"},
]})
messages.append({"role": "assistant", "content": reply})   # 回到开头那行

四段追加对应一个完整的工具调用回合:提问、模型发起调用、回传结果、拿到最终回答——agent 的对话历史,就是这样一个个字典轮转累加出来的。一条消息字典在 agent 循环里的完整生命周期如下:

flowchart LR A[创建字典] --> B[追加进列表] B --> C[序列化为 JSON] C --> D[发给模型] D --> E[解析回字典] E --> F[读字段做决策] F --> B

图中"组装、序列化、发送、解析、读取"每一步的操作对象都是字典——这就是说字典是 agent 数据骨架的完整含义。

6.2、工具定义与配置注入

工具定义是嵌套字典:外层 name、description、input_schema,内层 JSON Schema 又是字典套字典。往消息里注入缓存标记这类"可选增强"时,setdefault 与 | 合并让代码保持声明式:

tool = {"name": "weather", "description": "查询城市天气",
        "input_schema": {"type": "object",
                         "properties": {"city": {"type": "string"}}}}

system_block = {"type": "text", "text": SYSTEM_PROMPT} | {
    "cache_control": {"type": "ephemeral"}}   # 原字典不动,附加缓存标记

竖线合并生成的是新字典:SYSTEM_PROMPT 的原始定义不被污染,需要附加增强字段时零副作用——比原地 update 更适合"基础定义复用、按需增强"的模式。

6.3、统计与分组

usage 累计与按类型分组是 harness 的日常。defaultdict 把"取值前先确保键存在"这一步彻底省掉:

from collections import defaultdict

totals = defaultdict(int)          # 取不存在的键时自动建 0
for resp in responses:
    totals["input"] += resp["usage"]["input_tokens"]
    totals["output"] += resp["usage"]["output_tokens"]
# {'input': 66, 'output': 585}:三条流累计

by_type = defaultdict(list)        # 自动建空列表,setdefault 的替身
for block in all_blocks:
    by_type[block["type"]].append(block)

defaultdict(int) 之于计数、defaultdict(list) 之于分组,是 collections 文档 里的两个标准姿势,比手写判断简洁得多。

7、陷阱与边界

浅拷贝是第一大坑。d.copy() 与 dict(d) 都只复制第一层,嵌套的子字典仍被新旧两个变量共享,改一个动两个:

cfg = {"model": "opus", "limits": {"max_tokens": 4096}}
copy1 = cfg.copy()
copy1["limits"]["max_tokens"] = 128000
print(cfg["limits"]["max_tokens"])    # 128000:原字典也被改了

import copy
deep = copy.deepcopy(cfg)             # 深拷贝才彻底独立

其次是键类型混用:d[1] 与 d["1"] 是两个不同的键,从 JSON 读回来的配置全是字符串键,拿整型去取必然扑空——排查"明明有值却取不到"时先查键类型。顺带两个版本事实:Python 3.7 起字典保证保持插入顺序(顺序是语言特性,不是巧合);字面量里写 d = {} 而不是 d = dict() 的惯例,纯粹是因为短。

最后是选型边界——字典不是唯一选择:

方案 优点 适合
字典 零成本、JSON 同构、字段可变 消息、API 载荷、配置
dataclass 类型检查、属性访问、IDE 补全 内部领域对象
Pydantic 模型 自动校验、序列化一体 边界入参校验

经验法则:进出 API 的用字典,程序内部传递的用类型化结构,两者以序列化点为界各管一段。字典的完整方法清单见 官方文档。

8、总结

回顾全文:字典是键值映射,消息、工具、配置、统计全靠它承载;取值三式按"确定存在用中括号、可选字段用 get"分工,合并用竖线、分组用 defaultdict、JSON 互转用 dumps 与 loads;浅拷贝共享子结构与键类型混用是最常见的两个坑。对于写 agent 的工程师,建议把本文 6.1 的消息组装示例敲一遍——读懂那几行字典操作,主流 harness 的源码就通了大半;内部对象多到字典键开始散落各处时,及时换 dataclass 或 Pydantic,让字典留在它最擅长的边界上。

相关推荐

获取系统SDK版本、判断手机ROM
精选Code

获取系统SDK版本、判断手机ROM

Build获取系统SDK版本Android中部分API的使用,需要在特定的SDK版本之后才能使用,因此在兼容老版本SDK的时候,经常需要判断API的版本。各种Android版本的对应关系参考《Android各版本对应的SDK版本》判断手机ROM有时候需要判断手机系统的ROM,检测ROM是MIUI、EMUI还是Flyme,可以使用getprop命令,去系统build.prop文件(关于build.p

4.2k
AndroidStudio中各种中文乱码问题
精选Code

AndroidStudio中各种中文乱码问题

AndroidStudio中各种中文乱码问题1、编译Java错误信息乱码在出现这个Annotation processors must be explicitly declared now...问题的时候,正好也发现AndroidStudio Build Output错误信息都乱码。经常遇到各种问题,习惯了,现在遇到的问题,都是以后的答案。 1.1、修改项目build.gradle(无效)在整个p

2.8k
PydanticAI 详解:类型安全的 Python AI SDK
精选AI

PydanticAI 详解:类型安全的 Python AI SDK

PydanticAI 详解:类型安全的 Python AI SDK写 LLM 应用最疼的两件事:模型返回的 JSON 不合预期、换一家模型商要改一串代码。pydantic/pydantic-ai(PydanticAI)用 Pydantic 团队的老本行回答了第一件——输出用 BaseModel 定义、运行前先校验,错误从运行时提前到编码时;第二件则用"模型即字符串"回答:换个厂商只改一个参数。如今

110