模型写出命令以后,任务为什么还没有完成
设想用户提出这样一个任务:“运行 main.py,告诉我是否成功;如果失败,说明报错。”假设文件里只有一行:
print(10 / 0)模型请求使用工具执行 python main.py。到这个时刻,谁真正启动 Python?命令结束后,谁把结果交回模型?模型又凭什么知道下一步应当报告错误,还是继续检查代码?
这些问题组成第一课的主线。这条完整 Agent 执行链是思想讨论中的假设。 下文的交互图用预设步骤呈现它,再用一段独立 Python 教学代码观察循环和记录变化。原课程源码尚未逐段讲解,也尚未运行真实模型与 Shell。
学习对象是 Learn Claude Code 项目实现的 Claude Code-like 教学 Agent。第一课从一次动作的提出、执行和反馈出发,解释外围运行框架如何让模型继续工作。
模型判断下一步,外围程序执行动作
先把参与者分清:
- 模型读取用户目标与现有记录,选择回答还是请求工具,并给出工具参数。
- 外围运行框架(Harness)调用模型,接收工具请求,运行对应实现,保存请求和结果,再决定是否发起下一次模型调用。
- 工具实现及其调用的程序完成实际操作。例如工具实现启动 shell,shell 执行命令并启动 Python,Python 运行文件后产生输出或错误。
因此,在这个设想中,真正启动 python main.py 的动作由外围程序发起。模型负责提出这个动作,实际执行发生在工具实现连接的运行环境中。
main.py 这个文件名也有来源。用户已经指定了文件时,模型可以沿用户要求选择它;用户没有指定时,模型可能先请求查看目录或代码,再根据观察结果判断应该运行哪个文件。文件选择依赖任务与已有信息,不能只看最终命令猜测选择过程。
这里的“请求工具”指符合工具接口的调用信息。模型在普通文字回答里写出一条命令,并不会让外围程序自动把这段文字当作命令执行。模型响应里是否有工具调用,是循环要检查的内容。
把执行结果带回来,才能继续判断
如果没有自动连接这些环节,用户可以自己运行命令,把输出粘贴给模型,再请求它解释。每个来回都需要有人搬运动作和结果。第一课讲解把这条往返组织为 Agent Loop:
- 调用模型,让它读取目标、已有记录和可用工具定义。
- 根据模型响应执行工具请求。
- 将模型的工具调用与对应结果写回对话记录。
- 携带更新后的记录,再次调用模型。
在我们的设想中,假定工具执行后返回 ZeroDivisionError: division by zero。外围程序把这个结果与先前的请求保存在一起;第二次调用时,模型才获得关于这次执行的反馈。
点下一步,沿着一次除零反馈看记录变化;也可以点图中的对象。
用户只要求运行并报告。Harness 先把目标写进 messages,接下来的判断都沿着这个范围。
user运行 main.py,只报告结果
教学推演:图中请求与反馈是预设状态。箭头表示信息方向,记录卡片数量表示已保存条数;图不调用模型或执行 Python。
这张图沿“只报告运行结果”的假设路径展开。如果模型根据任务提出新的工具请求,外围框架还会执行、记录和回传,形成下一轮。再次调用模型这个动作仍然由外围程序发起。
messages 保存的,是这件事为什么要继续
第一次调用时,用户目标还在记录里。第二次调用如果只留下“运行 python main.py”和除零错误,模型能看到刚发生的操作,却缺少用户对回答范围和后续动作的要求。
messages 是对话记录。对于当前案例,可以用下面的概念快照理解它需要保存的信息;这不是实测日志,也没有展开 SDK 的完整消息格式。
| 顺序 | 记录 | 下一次调用需要从中知道什么 |
|---|---|---|
| 1 | 用户要求运行文件并报告结果 | 原始目标和允许的行动范围 |
| 2 | 模型请求执行 python main.py | 已经提出了哪个动作 |
| 3 | 对应工具返回除零错误 | 这个动作的执行反馈 |
外围程序维护这些记录,把相关内容随下一次调用传给模型。连续工作依靠的是持续提供的上下文:原始目标、模型已请求的动作,以及外围程序拿到的结果。
工具结果还必须对应到发起它的那次调用。否则,当记录里有多个请求时,仅有一条报错并不能说明哪个动作失败。具体怎样通过调用标识完成关联,需要在后续源码阅读中检查。
缺少目标时,为什么有时看起来也答对了
只看到命令和除零错误,模型可能仍会回答“运行失败,发生除零错误”。如果用户原本只想知道是否成功,这个回答可能碰巧覆盖需求。因此,丢失目标并不意味着每次回答都会明显出错。
问题在于,相同错误可以服务不同任务:
| 用户目标 | 看到除零错误以后需要做什么 |
|---|---|
| 只报告运行结果 | 说明本次失败和已获得的错误信息 |
| 定位错误行 | 继续获取能定位源码位置的证据 |
| 修复并验证 | 结合需求修改代码,再重新运行取得验证结果 |
除零错误本身无法区分这些目标,也不能决定正确的修复应该是什么。保留用户要求,才能让后续判断延续同一件事。若用户只要求报告,模型不应因为看到了错误就自动把任务扩大成修改文件。
没有工具结果,不等于模型不能输出
学习中容易把这个关系说成“没有输入就没有输出”。这里需要把两个条件拆开。
外围程序没有再次调用模型时,这条调用链不会产生下一次模型回复。原因是下一次调用没有发生,不能归因于模型收不到工具结果就失去了输出能力。
外围程序再次调用了模型,却没有带上执行结果时,模型仍可能回答、猜测或要求补充信息。它只是无法可靠确认这次程序实际运行的情况。如果它已经读到 print(10 / 0),也可能根据代码预测会发生除零;预测与工具真正返回的执行反馈仍是不同证据。
所以需要保存的不只是“准备执行什么”,还包括“执行后观察到了什么”。模型提出了动作,不能自动证明动作已经发生;再次生成了回答,也不能自动证明它依据了真实运行结果。
工具定义说明怎么请求,工具实现负责怎么执行
模型需要知道有哪些工具、各工具接收哪些参数。工具定义向模型描述这套接口;外围程序中还需要有能完成动作的实现。
本次查阅的 第一课源码里,TOOLS 描述 bash 工具和 command 参数,run_bash() 负责实际执行。可以先从职责上理解它们:
| 对象 | 接收或提供什么 | 职责 |
|---|---|---|
TOOLS | 工具名称、说明与参数结构 | 随模型调用提供,让模型能提出有效请求 |
| 模型的工具请求 | 所选工具与具体命令参数 | 表达模型选择的动作 |
run_bash() | 命令字符串 | 在外围运行环境执行,并返回结果 |
工具定义与用户目标提供给模型,工具实现留在外围程序中运行。知道一个工具的名称和参数,并不能代替工具实现;模型选择了有效参数,也仍要由外围程序把请求接到实际执行上。
这一节只落实已经讨论清楚的职责。run_bash() 内部怎样启动进程、收集输出,以及异常怎样回传,尚未逐段讲解。
循环停止,是一个控制条件
第一课的最小循环在模型不再请求工具时结束。这说明外围程序当前没有收到需要继续执行的工具请求;它不会自动证明用户目标已经完成。
回到同一个除零案例:如果任务只是运行并报告结果,说明执行失败可能已经满足目标;如果任务要求修复并验证,只说明除零错误就不足以证明完成。判断目标是否完成,还要对照原始需求与已经获得的证据。
本次还核对到一个版本差异。核对日期为 2026-10-12:
| 材料 | 判断是否继续的依据 |
|---|---|
| 网站第一课的教学文字 | 检查 stop_reason 是否表示工具调用 |
本次采用的仓库版本 ce8f9f1 | 从 response.content 中找实际 tool_use 块;没有这些块时返回 |
因此,记录源码行为时以本次固定版本为准。网页讲解与仓库实现都能帮助理解“有工具请求就执行并回传”的思想,但具体判断方式不能混写成同一段源码。这是对停止条件的定向核对,源码的逐段学习仍未开展。
在代码块中停住,看结果何时进入记录
图中已经走通信息方向,代码里还要找到两个不同的时刻:取得 result,以及把它写进 messages。下面用一个独立教学模型缩短这条路径。choose_action() 是固定规则:没有工具反馈时提出算式,有了反馈以后返回预设回答;Python 直接计算 10 / 0,捕获真实异常后保存结果。
这段代码只使用 Python 3 标准语法,可在浏览器中的 Pyodide 0.29.0 运行,也可保存后用 python3 执行。它没有调用真实模型、启动 Shell 或读取 main.py。运行代码块观察的是记录与循环的机制,原课程 Agent 的运行证据还要由后续实践取得。
def choose_action(messages):
# 教学用的固定规则,替代真实模型调用。
if messages[-1][0] == "tool":
return "answer", "运行失败:除零错误"
return "tool", "10 / 0"
messages = [("user", "运行 main.py,只报告结果")]
while True:
kind, value = choose_action(messages)
messages.append(("assistant", value))
if kind == "answer":
print(value)
break
try:
# 直接执行算式,替代启动 Shell 和读取文件。
result = str(10 / 0)
except ZeroDivisionError as error:
result = f"{type(error).__name__}: {error}"
messages.append(("tool", result))
print("记录条数:", len(messages))点击行号设置断点,再点“调试”;也可以直接运行。
点击行号设置断点,再点“调试”。高亮的行还没有执行,变量与输出来自这段代码真实执行一次后的轨迹回放。按下面三个位置观察:
- 在
messages.append(("assistant", value))打断点。第一次停住时,记录只有用户目标;执行这一行后,才保存提出的动作。第二次到达同一行时,kind已经是answer,将保存最终回答。 - 在
messages.append(("tool", result))打断点。此时result已经是除零错误,而messages仍只有用户目标和动作。点一次“下一步”,才看到工具结果成为第三条记录。 - 在
kind, value = choose_action(messages)打断点。第二次到达时,上一轮的工具结果已经在记录中;继续执行才能得到回答并走到break。
直接运行的预期输出是两行:
记录条数: 3
运行失败:除零错误除零异常在 except 中被接住,所以教学代码会正常结束:错误成为交给下一轮的信息,没有直接终止循环。最终 messages 有四条记录,最后一条是回答。固定规则只用于展示这个顺序;真实模型能否准确报告,还取决于它收到的目标、结果和模型响应,不能从这段代码的确定输出推断。
带着这些问题进入源码
现在可以围绕一个连续问题读源码:用户目标进入程序之后,谁保存它,谁执行动作,谁把结果送回模型,又在哪里决定继续或停止?计划中的阅读顺序是:
agent_loop():找出每次模型调用与循环的继续、停止位置。TOOLS:核对模型收到的工具接口与参数。run_bash():找到命令真正进入运行环境的位置。- 工具结果的组装与调用关联:检查结果怎样对应先前请求、怎样写回记录。
- 程序入口与
history:检查用户输入从哪里进入,记录由谁维护。
原课程的这些源码阅读步骤尚未开展。源码学习之后,再用正常输出与除零报错观察真实模型和 Shell 的执行链,比较实际记录与当前理解;代码块中的固定规则演示只证明短循环自身的行为。
| 学习层次 | 当前状态 |
|---|---|
| 思想理解 | 已讨论模型与 Harness 分工、执行反馈、记录连续性、行动范围和停止边界 |
| 资料核对(笔记整理) | 整理时已查阅课程导览和第一课讲解,定向核对源码停止条件;读者的源码逐段学习尚未开展 |
| 教学演示 | 已加入预设 SVG 分步图和可独立执行的 Python 短循环,可观察真实除零异常、记录追加和下一轮判断 |
| 原课程实践 | 尚未运行第一课 Agent,没有真实模型调用、Shell 命令执行或课程实验日志 |
开篇任务仍然是“运行文件并检查结果”。现在可以逐项追问:模型是否请求了工具,外围是否实际执行,结果是否进入记录,模型是否带着用户目标再次被调用。真实实践要为这些问题提供观察证据,而不能用一个看起来合理的最终回答替代。