章末自测

题目 1:为什么普通一次 LLM 调用无法天然完成多步环境交互?

假设用户说“帮我找到项目入口文件并读取内容”,模型第一次调用时还不知道目录结构。请解释为什么一次 LLM 调用通常无法完成整个任务,并说明 Main Loop 如何通过 Tool Call 与 Observation 解决这个问题。

一次 LLM 调用只能基于调用当时已有的上下文做判断。如果模型还没看到项目目录,它无法知道真实入口文件名。Main Loop 允许模型先产生 list_files Tool Call,由 Harness 真正读取目录,再把结果作为 Observation 放回 messages。下一轮模型看到目录后,才能决定读取 src/main.py 或其他文件。也就是说,多步 Agent 不是模型一次性全知,而是通过“决策 → 执行 → 反馈 → 再决策”逐步获得真实环境信息。

题目 2:判断题——Tool Result 只要写到日志里,模型下一轮就能利用它,对吗?

请判断并解释。回答中需要明确区分“给开发者看的日志”和“给下一轮模型看的 Context”。

不对。日志只是记录系统发生了什么,主要给开发者或运维人员查看。模型下一轮只能利用真正进入其输入 Context 的信息。如果 Tool Result 只是 print() 到终端或写入日志文件,但没有加入 messages 或被 Context Builder 重新组装进模型输入,那么模型并不知道上一轮工具结果。这也是为什么很多 Agent 会重复调用同一个工具:Harness 没有把 Observation 正确回流给模型。

题目 3:代码阅读——下面 Loop 最大的问题是什么?

while True:
    response = llm.chat(messages)

    if response.tool_call:
        result = execute_tool(response.tool_call)
        messages.append(result)
    else:
        return response.answer

请指出至少三个真实工程风险,并说明应如何改进。

第一,没有最大步数,如果模型持续请求工具,可能无限循环。第二,没有 Tool Exception 处理,任意工具异常都可能让整个 Agent 直接崩溃。第三,没有重复 Tool Call 检测,模型可能持续执行同一个动作。第四,messages.append(result) 过于抽象,真实系统还需要清晰的角色、Tool Call 与 Observation 关联。最基本改进包括 MAX_STEPS、结构化 Tool Result、异常捕获、重复动作检测,并在后续加入 State、Tracing、Timeout 等机制。

题目 4:编程实现——写一个最小 Main Loop

请写一个完整可运行的 Python 程序,不调用真实大模型。要求使用模拟 LLM,实现:第一轮请求 list_files,第二轮根据文件列表读取 README.md,第三轮返回最终答案;同时必须设置最大步数。

可以用简单的“工具消息数量”模拟模型决策。重点不是模拟得有多聪明,而是把 Main Loop 的结构写完整。

from dataclasses import dataclass
from typing import Optional


@dataclass
class ToolCall:
    name: str
    arguments: dict


@dataclass
class Response:
    answer: Optional[str] = None
    tool_call: Optional[ToolCall] = None


def list_files() -> list[str]:
    return ["README.md", "app.py"]


def read_file(path: str) -> str:
    files = {
        "README.md": "Agent Harness Demo",
        "app.py": "print('hello')",
    }
    return files.get(path, "FILE_NOT_FOUND")


def fake_llm(messages: list[dict]) -> Response:
    tool_count = sum(
        1 for item in messages
        if item["role"] == "tool"
    )

    if tool_count == 0:
        return Response(
            tool_call=ToolCall(
                name="list_files",
                arguments={}
            )
        )

    if tool_count == 1:
        return Response(
            tool_call=ToolCall(
                name="read_file",
                arguments={"path": "README.md"}
            )
        )

    return Response(
        answer="已经找到并读取 README.md。"
    )


def execute_tool(call: ToolCall):
    if call.name == "list_files":
        return list_files()

    if call.name == "read_file":
        return read_file(call.arguments["path"])

    return "UNKNOWN_TOOL"


def run_agent(max_steps: int = 5) -> str:
    messages = [
        {
            "role": "user",
            "content": "读取项目 README"
        }
    ]

    for step in range(max_steps):
        response = fake_llm(messages)

        if response.tool_call is None:
            return response.answer or "任务结束"

        result = execute_tool(response.tool_call)

        messages.append({
            "role": "assistant",
            "content": (
                f"TOOL_CALL: {response.tool_call.name} "
                f"{response.tool_call.arguments}"
            )
        })

        messages.append({
            "role": "tool",
            "content": str(result)
        })

    return "达到最大执行步数"


if __name__ == "__main__":
    print(run_agent())

如果你能独立把这段结构写出来,就已经真正掌握了本节最核心的 Main Loop。

题目 5:综合应用——为什么 Tool Error 应该尽量转成 Observation?

假设 Agent 调用 read_file("app.py"),但文件不存在。请比较“直接抛异常终止进程”和“把 FILE_NOT_FOUND 作为 Observation 放回模型”两种处理方式,说明后者为什么更适合可恢复型 Agent。

如果直接抛异常终止进程,Agent 整个任务会因为一次局部失败结束,无法继续调查。而 FILE_NOT_FOUND 本身其实很有信息量:它说明模型的路径假设不成立。把它结构化为 Observation 后,模型下一轮可以改为 list_files()、search_files(),从真实目录中找到入口。因此可恢复错误更适合作为环境反馈进入 Loop。当然,并非所有错误都应该继续,例如权限拒绝或高风险动作可能需要立即停止,这属于后面的安全边界。

题目 6:对比分析——静态 Plan 与 Main Loop 的区别是什么?

一个模型在任务开始时生成五步计划,另一个系统每执行一步就根据 Observation 重新决定下一步。请分析二者在遇到“第一步假设错误”时会有什么不同,并说明为什么 Planner 不能替代 Main Loop。

静态 Plan 如果没有反馈修正机制,就会继续按照最开始的假设执行。例如第一步计划读取 app.py,但文件不存在,后面的“运行 app.py、修改 app.py”都会失去意义。Main Loop 则会把 FILE_NOT_FOUND 作为 Observation 放回模型,下一轮重新选择 list_files 等动作。因此 Planner 负责“先想一个方向”,Main Loop 负责“根据真实环境持续校正”。真实 Agent 往往可以有 Planner,但最终仍然需要反馈驱动的执行循环。

题目 7:概念理解——为什么 Main Loop 必须有最大步数?

请从概率性模型、重复 Tool Call、成本、延迟和系统稳定性几个角度说明。不要只回答“防止死循环”。

LLM 的动作选择并不是传统确定性函数,即使 Prompt 合理,也可能重复搜索、在两个动作之间来回切换、因错误 Observation 继续重试,或者始终认为“还需要更多信息”。如果没有最大步数,系统可能持续消耗 Token、API 成本、CPU、数据库和外部接口资源,同时让用户等待非常久。MAX_STEPS 是最基础的硬边界,真实系统还会叠加 Timeout、Token Budget、Tool Call 上限和成本预算。它不是对模型不信任,而是长任务系统必须具备的资源治理能力。

进一步阅读

  • ReAct: Synergizing Reasoning and Acting in Language Models:理解 Reason、Action、Observation 闭环的经典来源与思想。
  • OpenAI Tools / Function Calling 相关文档:重点观察 Tool Call 如何被结构化表达,以及 Tool Result 如何回到下一轮模型上下文。
  • LangGraph State / Execution Concepts:学完本节后再看,会更容易理解图式 Agent 运行时底层仍然需要循环与状态推进。
  • Anthropic Tool Use 相关文档:用于比较不同模型提供方如何表达工具请求与工具结果。
  • Python dataclasses 文档:后续我们会用数据类描述 ToolCall、Response、AgentState 等结构。
  • Python Exception Handling 文档:Tool Error Recovery 会大量依赖异常捕获与结构化错误。
  • Finite State Machine 相关资料:帮助理解“循环”和“状态推进”在工程控制上的关系。
  • Agent Tracing / Observability 资料:提前理解为什么 Main Loop 中每一轮都值得被完整记录。

术语表

  • 主循环(Main Loop):Agent 持续执行“模型决策、动作执行、环境反馈、再次决策”的核心循环。它让任务步骤可以在运行时动态产生,而不是全部提前写死。
  • 推理 / 决策阶段(Reason):模型根据当前任务、上下文和 Observation 判断下一步最值得做什么。本课程更关注可观察的决策结果,而不是要求展示模型内部完整思维过程。
  • 动作(Action):模型在一轮决策后提出的可执行操作,例如 read_file、run_test、query_sql。Action 通常会被结构化成 Tool Call,再交给 Harness 检查和执行。
  • 观察结果(Observation):工具真实执行后产生的环境反馈,可以是成功结果,也可以是错误、空结果或超时信息。Observation 会进入下一轮 Context,直接影响后续动作选择。
  • 工具调用(Tool Call):模型表达“希望调用哪个工具以及使用什么参数”的结构化请求。它不是工具执行本身,真正执行仍然由 Agent Harness 负责。
  • 退出条件(Termination Condition):决定 Agent Loop 什么时候停止的规则,包括模型主动返回最终答案、达到最大步数、任务失败、超时、等待人工确认等。
  • 最大步数(Max Steps):Agent 单次 Run 允许执行的最大决策轮数,是防止无限循环、成本失控和任务长期占用资源的基础安全边界。
  • 反馈驱动(Feedback-driven Execution):每一轮动作执行以后,都根据真实 Observation 修正下一步,而不是机械执行最初计划。它是 Agent 能应对开放环境的重要原因。

本章总结

这一节我们真正把 Agent 的“动态决策”从上一节的概念,变成了代码里的 Main Loop。

最开始的核心代码非常短:

while True:

    response = llm.chat(messages)

    if response.tool_call:
        result = execute_tool(response.tool_call)
        messages.append(result)
    else:
        return response.answer

但我们已经看到,这几行代码背后其实包含整个 Agent 最重要的运行逻辑。

第一,模型并不是一次性完成所有任务。

它会根据当前 Context 决定下一步。

第二,Tool Call 只是模型提出的动作意图。

真正执行 Tool 的是 Harness。

第三,Tool 执行结果必须变成 Observation,并重新进入下一轮 Context。

只有这样,模型才能看到,“我刚才做了什么,真实环境发生了什么。”

然后继续判断。

所以 Agent 能连续完成多步任务,不是因为模型一开始就知道完整路径,而是因为:

Reason
  ↓
Action
  ↓
Observation
  ↓
Reason

这个闭环不断运行。

我们还专门看了为什么失败也能推动任务前进。

FILE_NOT_FOUND、SQL 空结果、测试报错,都不一定意味着整个 Agent 失败。只要它们被结构化成 Observation,模型就可能在下一轮修正路径。

但 Main Loop 不是只要“会循环”就够。

真正工程化至少要考虑:

  • 什么时候完成;
  • 什么时候强制停止;
  • Tool 失败怎么办;
  • 怎么避免重复 Tool Call;
  • 最大执行多少步;
  • Tool Result 是否真正进入 Context。

这也是为什么一个看起来只有几行的 Main Loop,实际上会成为整个 Agent Harness 最核心的运行底座。

如果你想验证自己已经学会这一节,可以完全不使用任何 Agent 框架,自己写一个 Fake LLM:

第一轮 → list_files
第二轮 → read_file
第三轮 → finish

然后让 Python 程序真正完成:

Decision
 ↓
Tool
 ↓
Observation
 ↓
Next Decision

如果你能独立写出来,并且能解释为什么删除 Observation 回流以后 Agent 会出问题,这一节最重要的能力就已经掌握了。

下一节我们要继续解决一个 Main Loop 跑起来以后立刻出现的问题,这个 Agent 到底要记录什么?

现在我们的 messages、step、最近 Tool Call、执行状态还散落在不同变量里。

下一节我们会正式引入,Agent State

把一个“能循环的 Agent”,变成一个真正“知道自己执行到哪一步”的 Agent。

© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 抢沙发
头像-大赛人网
欢迎您留下宝贵的见解!
提交
头像-大赛人网

昵称

取消
昵称

    请登录后查看评论内容