程云来 / 杭州
返回文章·Agent 与 Harness
·约 8 分钟

思想|Agent Loop,一句请求怎样变成执行结果

从运行 main.py 的一句请求出发,跟着工具请求、执行反馈和再次调用,理解模型与 Harness 怎样一起完成任务。


它说“我来运行”,然后呢?

你把一句话交给编程助手:

运行 main.py,只报告结果。

假设文件里只有一行:

python
print(10 / 0)

这段代码会触发除零错误。不过,你让助手做的是运行文件,再报告这次运行的结果。它要怎样调用程序、拿到结果,再给出回答?

模型可以提出执行 python main.py,实际启动程序还需要工具实现。在编程助手里,负责调用模型、执行工具请求、保存结果的外围运行框架称为 Harness。模型决定请求什么动作,Harness 调用相应的工具,再把结果交回模型。

下面沿用 Learn Claude Code 第一课的循环思想,用这个假设案例追踪一次完整的执行过程。

先跟着这句话走一圈

Harness 首先把你的问题交给模型,同时提供工作约定和工具说明。工作约定告诉模型在哪里工作;工具说明告诉它可以请求哪些操作、参数怎样填写。在这个案例里,工具名是 bash,参数 command 接收要执行的命令。

点“下一步”,顺着亮起的箭头看:这次是谁交给谁,交了什么。

你:运行 main.py,只报告结果。
1 / 6
把问题交给模型
Harness→模型

Harness 将你的问题、工作约定和工具说明一起送给模型。

随问题一起提供
工作约定
在当前目录使用工具。
工具说明
bash(command):执行命令,返回输出或错误。
LOOPGOALMODELINPUTPYTHONREADY模型Python 程序Harness
  1. 把问题交给模型
    Harness→模型

    Harness 将你的问题、工作约定和工具说明一起送给模型。

    随问题一起提供
    工作约定
    在当前目录使用工具。
    工具说明
    bash(command):执行命令,返回输出或错误。
  2. 模型请求运行文件
    模型→Harness

    模型选择 bash 工具,并给出参数。Harness 保存这次请求;命令还没有执行。

    工具请求
    工具
    bash
    参数
    command: "python main.py"
  3. Harness 真正执行命令
    Harness→Python 程序

    Harness 把参数交给 bash 的实现,启动 Python,运行当前目录的 main.py。

    实际执行
    命令
    python main.py
  4. 程序把错误带回来
    Python 程序→Harness

    10 / 0 触发除零异常。执行工具收集错误并返回给 Harness;模型此时还没看到它。

    返回的错误末行
    错误
    ZeroDivisionError: division by zero
  5. 带着结果再问模型
    Harness→模型

    Harness 将错误关联到刚才的请求并保存,再带着完整对话调用同一个模型。

    第二次调用带上
    对话
    你的原问题 → bash 请求 → 除零错误
    调用约定
    工作约定与工具说明仍然提供。
  6. 模型回答你的问题
    模型→Harness

    你只要求报告结果,因此本例回复运行失败。Harness 把回复显示给你,这轮结束。

    你看到的回答
    回答
    运行失败:除零错误。

教学推演,未连接真实模型与 Shell。

这次往返中,模型被调用了两次:第一次提出工具请求,第二次读到执行结果后回答你。两次调用之间,Harness 完成了命令执行和结果保存。

每当模型请求工具,Harness 就执行工具、保存结果,再调用模型判断下一步。这种反复执行和判断的过程称为 Agent Loop。本例只需要执行一次;如果模型提出新的工具请求,Harness 就继续同样的过程。

工具请求怎样变成真正的执行

先看图中的第二、三步。两步里都有 python main.py,但第二步只是模型提出请求,第三步才真正执行命令。

模型返回的工具请求包含两项信息:工具名 bash,以及参数 command: "python main.py"。Harness 读取这份请求,找到对应的工具实现,再把命令传给它。因此,普通回复里写一句“你可以运行 python main.py”,并不会自动触发执行。

在 第一课的固定版本源码中,TOOLS 是提供给模型的工具说明,run_bash() 是 Harness 调用的执行函数。前者描述工具名称和参数,后者启动命令、收集输出并返回结果。

运行到 10 / 0 时,Python 产生除零错误。工具把错误返回给 Harness,图中的第四步就走完了。可是,模型不会自动收到这个结果:Harness 还需要把它加入对话记录,并发起第二次调用。

为什么第二次还要带上原问题?

既然已经拿到了 ZeroDivisionError,第二次调用只发送这一行错误,够不够?

试着换一下最初的要求,文件和错误都保持不变:

你最初说的话看到同一个错误以后,该做什么
只报告结果告诉你运行失败,发生除零错误
找到报错位置继续查看能定位错误行的代码或输出
修好它,再运行一次查清预期行为,修改代码并取得新的运行结果

错误相同,接下来该做的事却不同。除零错误说明这次运行发生了什么,原问题说明你希望助手做到哪一步。本例要求“只报告结果”,如实报告失败就可以了;看到错误并不意味着需要修改文件。

所以,Harness 在第二次调用时,会一起提供原问题、模型先前的工具请求,以及对应的执行结果。这些内容保存在对话记录 messages 中,工作约定和工具说明则随调用继续提供。模型再次判断时,就能同时看到你要它运行什么、它刚才请求了什么,以及那次请求得到了什么结果。

执行结果也要关联到发起它的请求。只有一条命令时,这个对应关系很直观;若一次请求了多个工具,就需要明确每份结果属于哪次调用。课程源码通过工具调用标识完成关联。

把这次往返缩成一段短代码,就可以停在结果写入记录的位置,观察 messages 的变化。

在代码里停住,看看结果何时写入记录

为了只观察这段循环,下面用 choose_action() 的固定规则代替模型判断,用直接计算 10 / 0 代替 Shell 运行文件。除零异常由 Python 真实产生,最终回答由规则返回。

代码可以直接运行,也可以保存后用 python3 执行。浏览器代码块使用 Pyodide 0.29.0;这个短例子只用 Python 3 标准语法,不读取 main.py。

PyPython · 浏览器本地运行
未运行
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))
输出

点击行号设置断点,再点“调试”;也可以直接运行。

代码只读;执行发生在浏览器中,不会发送到博客服务器。

先点“运行”,会看到两行输出:

text
记录条数: 3
运行失败:除零错误

第一行在工具结果写入后输出:此时三条记录分别是用户问题、工具请求和执行结果。循环随后再次调用 choose_action(messages),固定规则读到工具结果,返回回答,才有第二行输出。

要看清这两个时刻,可以设置两个断点:

  1. 在 messages.append(("tool", result)) 这一行点击行号,再点“调试”。停住时,result 已经保存除零错误,messages 里还只有用户问题和工具请求。点一次“下一步”,结果才成为第三条记录。
  2. 再给 kind, value = choose_action(messages) 设置断点。第二次停在这里时,上一轮的工具结果已经在记录中;继续执行,固定规则会返回回答,循环随后走到 break。

调试时,高亮的是尚未执行的那一行;变量和输出来自这段代码真实执行轨迹的回放。

除零异常被 except 捕获后,变成了可以保存和读取的信息,所以循环能继续到第二次判断。回答也会写入记录,最终 messages 共有四条。你可以把这两次判断对照图中的第一次模型调用和第二次模型调用,观察执行结果怎样影响下一步。

运行失败了,这一轮却可以结束

短代码最后走到了 break,因为固定规则读到工具结果后返回了 answer。真实 Agent Loop 也需要一个停止条件:第一课的最小循环在模型响应中有工具调用时执行并回传,没有新的 tool_use 块时返回。

循环停止,只说明当前没有新的工具请求。是否完成任务,还要对照用户的原始要求和执行证据。本例要求“运行 main.py,只报告结果”,执行失败也是运行结果,助手如实报告错误,就回答了你的问题。

如果要求改成“修好并验证”,只报告错误显然还不够。模型可能需要继续请求查看代码、修改文件和重新运行;每次请求都要经过 Harness 执行,再把结果交回模型判断。

回看开头那句话,一次有依据的回答需要这几次交接完整发生:模型提出请求,Harness 实际执行,结果进入对话,模型再根据结果和原问题回答。读 agent_loop() 时,可以沿着这条顺序追踪模型调用、run_bash() 和工具结果的保存位置。

图中的执行过程是教学推演,短代码验证的是固定规则下的记录和循环。要把这些交接接到真实程序上,可以继续读实践|写出第一个可控的循环。实践篇沿用 main.py,补上模型接口、工具结果和停止控制;想先理解后续思想,也可以之后再回来动手。