1. 三层架构:Runtime / Framework / Harness
三层各管什么
层 | 包 | 版本 | 职责 | 一句话记法 |
Runtime |
| 1.2.13 | 把「调模型 → 执行工具 → 再调模型」编译成可执行的图,管状态流转和终止条件 | 管循环 |
Framework |
| 1.4.3 / 1.6.6 | 模型封装、消息类型、工具定义、Schema 生成 | 管零件 |
Harness |
| 0.7.22 | 在 LangGraph 之上预装文件系统、子 Agent、待办清单 | 管预装 |
关键理解
- 三层是自底向上层层构建,不是互相替代。四个包在同一个进程里共存。
create_agent和create_deep_agent不是两个框架。后者只是在前者基础上多装配了一批默认能力。
验证代码
2. 最小 Agent 的构造:create_agent的三个参
三个参数的含义
参数 | 作用 | 换掉会怎样 |
| 决定用哪个模型 | 换模型 → 工具选择能力、回复风格全变 |
| 模型能调用的工具清单 | 不传 → 模型只能靠嘴回答,无法行动 |
| 每次调用都带上的行为约束 | 不写 → 模型倾向于只聊天不调工具 |
system_prompt和用户问题的区别(重要)
system_prompt:每一次模型调用都自动带上,是持续生效的约束
- 用户问题:只在
invoke时传进去,只影响这一轮
图节点怎么读
model和tools是执行步骤。模型要工具就走到tools,工具执行完回到model,循环直到模型不再请求工具。__start__/__end__是入口和出口标记。
3. 工具:从 Python 函数到模型看得懂的 Schema
核心认知
模型看不见你的 Python 代码。它看到的是一份由框架自动生成的 JSON 说明书(Schema)。
生成依据只有三样:
你函数里的 | 变成 Schema 里的 | 作用 |
类型标注 | 参数的 | 决定模型能传什么形状的值 |
docstring 正文 | 工具的 | 模型靠这句话决定什么时候该用它 |
| 每个参数的 | 决定模型怎么填这个参数 |
默认值 | 该参数从 | 模型可以不传它 |
定义一个工具(不需要装饰器)
普通函数放进tools=[...]就行。框架读函数名、类型标注、docstring。
打印 Schema
注意required里只有两个名字——to_currency因为有默认值,自动从必填清单消失。
为什么默认值是接口设计问题
参数该不该给默认值,直接决定模型的自由度:
- 全给默认值 → 模型会偷懒不做判断
- 全不给 → 模型被必填项卡住,只能反过来追问用户
docstring 那句话更关键:它决定了模型在什么场景下想起这个工具。工具好不好用,一半看实现,一半看这句描述。
4. 一次invoke的完整生命周期
四条消息(live 实测原样输出)
消息类型对照表
# | 类型 | 角色 | 决定性字段 |
1 |
| 用户输入 |
|
2 |
| 模型请求调工具 |
|
3 |
| 工具真实执行结果 |
|
4 |
| 模型基于结果作答 |
|
两个AIMessage怎么区分
看有没有tool_calls:
- 有 → 模型在"下单",
content是空的
- 没有 → 模型在"交货",这轮结束
为什么第 2 条的content是空的
两次输出通道分离的设计:模型一次可以输出给用户看的自然语言和给框架看的结构化调用请求。塞进同一个字符串字段会让解析变成字符串匹配游戏(引号、换行、嵌套括号都能让它崩)。拆成两个字段后,判断退化成布尔检查。
为什么tool_call_id必须是随机串
一次invoke里模型可以同时请求多个工具,返回顺序不保证(并发执行、耗时差异、重试都会打乱)。按位置配对在单工具场景能蒙对,一旦并发就是错的。
更重要的:随机 ID模型伪造不了。模型能编任意content,但编不出和真实请求匹配的随机 ID。这是区分"工具真跑了"和"模型自己说了算"的物理判据。
检查函数(照搬课程实现)
三个断言对应三个独立环节:请求正确 → 执行成功 → 对话收尾。
为什么"有返回消息"不算证据:参数校验失败同样会产生ToolMessage,只是status不是success。
5. Harness 白送的东西:10 个工具从哪来
同一个echo工具,两个 API 注册后的清单:
9 个白送工具的用途分组
组 | 工具 | 干什么 |
找 |
| 列目录、按文件名匹配、按内容检索 |
读 |
| 读文件(图片/音频/PDF 走多模态打包) |
改 |
| 写入、精确替换、删除 |
派活 |
| 委派子 Agent |
执行 |
| 跑 shell 命令 |
★ 本章最重要的一句话
「工具已注册」不等于「工具已调用」。
实测数据:注册 10 个,实际调用 1 个。那 9 个在本次任务里一个都没被碰过。
工具清单只描述能力边界,描述不了实际行为。
- 判断"能做什么" → 看工具清单
- 判断"做了什么" → 看消息列表里的
ToolMessage
图节点也多了东西
多出来的是 Harness 装配的中间件节点。
6. offline 与 live:两种运行模式
offline | live | |
模型来源 | 脚本模型( | 真实模型 API |
是否读 | 不读 | 读 |
是否发网络请求 | 不发 | 发 |
工具函数与框架执行 | 真实执行 | 真实执行 |
证明了什么 | 机制成立 | 真实模型会正确选工具 |
怎么从数据上区分(这个最有价值)
两种模式都打印passed,光看日志分不出来。可靠判据是调用 ID 的形态:
模式 | 调用 ID |
offline |
|
live |
|
ID 随机与否,是"是不是真实模型"最硬的间接证据。
7. 源码里值得记住的常量与设计
工具按「只读 / 可变」分组
为什么这么分:只读工具之间没有副作用依赖,可以并行批量调用;可变工具会改状态,必须串行。分类结果直接决定并发策略。
大结果卸载(第一道防线)
工具返回值超过阈值,就从对话里卸载出去换成摘要。
排除名单
为什么文件工具被排除(源码注释给了三条理由):
- 自带截断 —— 结果本身就不会超限
- 卸载后会死循环 —— 模型看不到结果就再调一次,无限重复
- 永不超限的轻量操作
这是"框架替你做决定"的典型样本。知道它存在,能解释一类诡异现象:为什么某个工具返回了很长内容,上下文里却没被摘要掉。
8. 易错点清单
①os.environ[...]的语义
方括号语义是"取出一个名为xxx 的环境变量"。把密钥塞进去 = 让 Python 找一个名叫sk-7MA7xxxxxxxx的环境变量,必然KeyError。
② 密钥写进源码文件 = 公开
一旦文件进公开仓库,密钥就不再是秘密。正确做法:
三项配置同时提供,任何 OpenAI 兼容接口都能接入,不用改框架代码:
③ 中文 Windows 控制台编码
④ PyCharm 解释器的「名字」和「路径」不是一个东西
我看到Python 3.13 (agent-learn)这个名字,以为指向D:\agent-learn。实际它的homePath是D:/miniconda3/envs/yolo26/python.exe。
排查解释器问题,永远看路径,不看名字。
9. 自查清单
这一章学没学会,用这几个问题验:
# | 问题 | 能答出来吗 |
1 | 三层各自负责什么?为什么说不是替代关系? | |
2 |
| |
3 | 两个 | |
4 |
| |
5 | Deep Agent 白送几个工具?本次任务实际用了几个?这说明什么? | |
6 | 怎么从数据上判断一次运行是 offline 还是 live? | |
7 | 工具三要素分别变成 Schema 里的哪个字段?默认值会造成什么后果? | |
8 |
| |
9 | 为什么文件工具被排除在"大结果卸载"之外? |