翻开任何一个 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 循环里的完整生命周期如下:
图中"组装、序列化、发送、解析、读取"每一步的操作对象都是字典——这就是说字典是 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,让字典留在它最擅长的边界上。

