功能都有了,为什么接起来还是会出错?
用户只说了一句话:“按项目检查规范读main.py,查docs运行规则,运行前问我;核对输出是否42。”
到了这里,我们已经能加载规范、选记忆、压缩历史、查询MCP服务,也能在运行前确认。把这些函数逐个调用一遍,似乎就能得到一个完整Agent。
可试着问三个更具体的问题:记忆该加进哪份messages?模型请求收到529以后,重试的范围包不包括已经运行过的main.py?模型说“没有获准运行”然后结束,这项任务算成功了吗?
这些问题都发生在函数之间。第二十课的整合(Comprehensive Agent Turn),就是让每个机制在正确的边界读写状态:原循环管理交接与停止,Harness准备请求、守住工具入口,并管理资源。
跟同一个问题走到底
先在图里看一次完整任务。每次“下一步”只追踪当前交接;切换拒绝路径,看它在哪里失去执行证据。
用户要按项目规范读main.py,查运行规则,运行前确认,并核对输出42。任务调度或队友入口先交出这项工作;当前会话只处理它。
- 先接住一个完整任务:用户要按项目规范读main.py,查运行规则,运行前确认,并核对输出42。任务调度或队友入口先交出这项工作;当前会话只处理它。
- Harness 准备本轮请求:压缩候选历史,另开请求副本附相关记忆;system放规范目录,tools同时列出本地和MCP能力。完整规范由load_skill按需读取。
- 模型提出下一项动作:先读规范与源码,再查询规则。每个响应固定自己的能力快照;模型只提出带ID的调用,不能越过当前定义临时增加实现。
- 共同入口检查,再进入对应工具:本地读取、MCP查询、程序运行都先经过Hook。查询和运行各自询问本次许可,获准后只进入对应实现。
- 工具结果回到原历史:run_main返回exit_code=0和42。后置Hook记录结果,原循环保存同ID反馈;下一次模型请求读这条证据。
- 只恢复请求,不重做工具:如果这次模型请求收到529,重发同一份已含反馈的请求。工具入口在恢复范围之外,查询与运行不会因此再来一次。
- 停止原因与任务证据分开:模型回答后status=answered;宿主还要核对本次实际运行证据。若四次额度先用完,即使输出42,状态仍为call_limit。
- 收尾只处理资源,不偷开下一轮:finally执行一次宿主收尾,外层SDK上下文关闭连接。收尾故障成为独立警告;不覆盖原停止原因,也不再调用模型。
- 先接住一个完整任务:用户要按项目规范读main.py,查运行规则,运行前确认,并核对输出42。任务调度或队友入口先交出这项工作;当前会话只处理它。
- Harness 准备本轮请求:压缩候选历史,另开请求副本附相关记忆;system放规范目录,tools同时列出本地和MCP能力。完整规范由load_skill按需读取。
- 模型提出下一项动作:先读规范与源码,再查询规则。每个响应固定自己的能力快照;模型只提出带ID的调用,不能越过当前定义临时增加实现。
- 宿主拒绝,生成未执行反馈:外部查询没有本次许可,停在宿主。Harness仍生成step_3对应的错误反馈;没有查询服务,也不会继续运行程序。
- 工具结果回到原历史:未执行原因也由原循环保存。模型接下来只能据实回答缺少证据,不能把拒绝读成成功。
- 只恢复请求,不重做工具:如果这次模型请求收到529,重发同一份已含反馈的请求。工具入口在恢复范围之外,查询与运行不会因此再来一次。
- 停止原因与任务证据分开:合法回答仍可得到answered,但verified=False,因为缺少实际查询与运行证据。answered不是任务成功标志。
- 收尾只处理资源,不偷开下一轮:finally执行一次宿主收尾,外层SDK上下文关闭连接。收尾故障成为独立警告;不覆盖原停止原因,也不再调用模型。
看见模型的工具请求,还不能马上运行。Hook先检查,路由再进入原来的许可函数;外部查询和本地程序分别拿到本次决定。模型收到的是tool_result,既可能是实际输出,也可能是“未执行”。
这条任务里不需要创建队友或定时表。会用这些机制,与每轮都要启动它们,是两件不同的事。我们先把当前任务走通,再看其他入口怎样复用同一个会话。
一份历史,两种用途
历史是这次任务的交接记录。模型提出了哪项调用、Harness保存了哪条同ID结果,都要在这里对应起来。压缩可以生成候选,但必须保留近期完整请求与结果;候选通过检查后,原循环才提交它。
记忆和宿主通知的用途不同:它们帮助这次判断,不是刚发生的工具执行。若每轮都把记忆追加进原历史,第五次请求就可能带五份相同正文。我们在压缩后的历史上另开请求副本,只在副本附一次记忆。
history = [{"role": "user", "content": "核对本次输出。"}]
for turn in range(1, 4):
request = [dict(item) for item in history]
request[0]["content"] += " [项目记忆]"
print(turn, request[0]["content"].count("[项目记忆]"))
print("原历史:", history[0]["content"])点击行号设置断点,再点“调试”;也可以直接运行。
三轮都输出1,原历史仍只有用户问题。这里只演示两个列表的用途;实践里的消息含嵌套内容块,因此会用deepcopy,并验证最终请求的字节预算。
还有一个更隐蔽的兼容问题:第八课的压缩器只接受成对的工具交接。直接把后台通知插成新的user消息,再交给它压缩,会打破输入契约。本课的通知接口只读宿主已保存的文本,在请求副本附上;通知的持久保存、确认和去重,仍需要事件账本负责,不能靠清空队列假装完成。
重试画多大圈,决定了什么会再做一次
假设main.py已经返回42,结果也保存了。随后模型服务返回529。此时需要的是“再发送这份含结果的模型请求”,并不需要“重新运行用户程序”。
所以恢复函数只包住send。prepare、工具分发和最后收尾都在它之外。恢复层返回完整模型响应后,原循环才判断是工具请求、合法回答,还是要停下检查的异常响应。既有工具反馈不会因为请求恢复而被再次执行。
同样地,一份模型响应要使用同一份能力快照。模型看到的工具定义、Harness查到的路由和对应的本地实现必须一致。元数据可以复制,MCP连接继续由宿主持有;不把连接对象整个deepcopy一遍,也不让本轮里刚新增的工具提前获得执行资格。
前十九课分别接在哪里?
这些机制有不同的作用范围。把它们归位以后,调用链反而比较容易读。
| 边界 | 已有机制 | 当前要交出的东西 |
|---|---|---|
| 决定开始哪项工作 | 任务图、Cron、自主认领(12、14、17课) | 当前问题与负责人,随后调用会话入口 |
| 决定谁在哪个目录工作 | 子Agent、团队、协议、Worktree(6、15、16、18课) | 独立上下文、工作目录、协作状态 |
| 准备模型请求 | 技能、压缩、记忆、system(7—10课) | 历史候选、带附件的请求、实际能力说明 |
| 发起模型请求 | 错误恢复(11课) | 完整响应或有界失败 |
| 处理模型动作 | 工具、许可、Hook、计划、后台、MCP(2—5、13、19课) | 同调用ID的结果;后台启动与完成分别记录 |
| 保存与停止 | 第一课原循环 | 完整交接、停止原因、留给宿主的证据 |
表里“接入”表示职责位置,不意味着本课每条路径都运行了所有机制。公开例子实际启用规范、记忆、压缩、系统说明、恢复、本地工具、MCP、权限与Hook;任务调度和成员生命周期保留在各自入口,没有用空函数冒充整合完成。
例如定时器命中,只说明有一项待交付工作;它不该在当前工具执行到一半时改messages。后台程序仍在运行,也不意味着模型必须不停空转。宿主可以在空闲时交付新问题,成员可以在自己的目录里调用同一个run_session。
停止与成功,最后再分一次
默认五次逻辑请求依次是读规范、读源码、查询规则、请求运行、回答。若获准并得到完整的42,模型最终可回答;宿主再核对实际run_main反馈,得到verified=True。
现在把额度改成四次。程序已经运行并输出42,但循环来不及再请求一次回答,返回call_limit。这时“已有正确运行证据”和“会话正常答完”同时有不同结果。反过来,宿主拒绝查询后,模型如实说明缺少证据,状态可以是answered,verified却是False。
收尾因此只处理它负责的资源。finally执行一次宿主回调,SDK连接由外层上下文关闭;收尾报错单独留警告,不覆盖原停止原因,更不在退出时偷偷开启新任务。这个分工让调用者知道该继续检查输出、补许可,还是处理连接关闭故障。
实践篇把三个边界展开成完整函数,可以用同一个断点游标观察代码和图怎样变化。
本课以参考课程s20的机制归位为学习方向(2026-10-20核对);代码是本册独立的有限教学实现,不据社区示例推断Claude Code内部源码或生产实现。