小节二:Reason、Action、Observation——Agent 的三段式闭环到底在发生什么?

现在我们把 Main Loop 拆成三个最重要的阶段。

Reason
  ↓
Action
  ↓
Observation
  ↓
Reason

把三段职责拆开后,信息在循环里的流向会直观得多:Reason 负责选择,Action 负责把意图结构化,Observation 负责把环境事实带回来。

图片[1]-小节二:Reason、Action、Observation——Agent 的三段式闭环到底在发生什么?-大赛人网

注意图中 Observation 并不只代表“成功结果”。错误、空结果和超时同样属于可用反馈,它们都会进入 Context,影响下一轮 Reason。

这里的 Reason 并不一定意味着我们要把模型的内部推理过程全部展示出来。工程上我们更关心的是,模型基于当前上下文,做出了什么可执行决策。

所以在我们的 Harness 里,可以把 Reason 理解为“决策阶段”。

Reason:现在最值得做什么?

假设当前上下文是:

Goal:
修复项目启动失败

已有信息:
- 项目存在 src/main.py
- 运行时报 ModuleNotFoundError: requests

模型可能决定,下一步应该检查依赖文件。

于是产生:

Action:
read_file("requirements.txt")

这就是 Reason 到 Action 的转换。

真正重要的是,模型没有直接输出“最终答案”,而是意识到信息还不够,因此选择继续行动。

Action:把模型意图变成一个结构化动作

Action 最好不是一段自然语言:

“我想看看 requirements.txt”

而是结构化:

{
  "name": "read_file",
  "arguments": {
    "path": "requirements.txt"
  }
}

为什么结构化很重要?

因为程序才能稳定处理。

我们可以校验:

  • 工具名是否存在;
  • 参数是否齐全;
  • 路径是否合法;
  • 是否需要审批;
  • 是否超权限。

如果模型只说,“我建议你看看依赖文件。”

程序不知道该不该执行,也不知道参数是什么。

Tool Calling 本质上就是把“自然语言意图”变成“机器可执行动作描述”。

Observation:环境真正发生了什么?

Harness 执行:

read_file("requirements.txt")

得到:

Flask==3.0.0

这个结果就是 Observation。

接着把它放回上下文:

tool:
requirements.txt 内容:
Flask==3.0.0

下一轮 Reason 时,模型会把:

main.py 中 import requests

和:

requirements.txt 中没有 requests

结合起来。

于是它可能得出,依赖未声明。

这就是为什么 Agent 能“连续完成任务”。

它不是因为第一轮就知道全部答案,而是通过 Observation 一步步获得新证据。

Observation 不是只有成功结果

真实世界里,Observation 可能是:

FILE_NOT_FOUND

也可能是:

PermissionDenied

还可能是:

Process timeout after 30 seconds

甚至:

SQL returned 0 rows

这些失败同样是信息。

Agent 应该能够看到:

Action:
read_file("config.py")

Observation:
FILE_NOT_FOUND

然后下一轮:

Reason:
文件不存在,应该先搜索项目

Action:
search_files("config")

这才是真正的错误恢复基础。

做一个“能被失败改变路径”的 Loop

我们写一个完整例子。

from dataclasses import dataclass
from typing import Optional


@dataclass
class ToolCall:
    name: str
    arguments: dict


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


FILES = {
    "src/main.py": "print('hello agent')",
    "README.md": "Project entry: src/main.py",
}


def list_files() -> list[str]:
    return list(FILES.keys())


def read_file(path: str) -> str:
    return FILES.get(path, "FILE_NOT_FOUND")


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

    if call.name == "list_files":
        return list_files()

    return "UNKNOWN_TOOL"


def fake_llm_chat(messages: list[dict]) -> LLMResponse:
    tool_results = [
        message["content"]
        for message in messages
        if message["role"] == "tool"
    ]

    if not tool_results:
        return LLMResponse(
            tool_call=ToolCall(
                name="read_file",
                arguments={"path": "app.py"}
            )
        )

    last_result = tool_results[-1]

    if last_result == "FILE_NOT_FOUND":
        return LLMResponse(
            tool_call=ToolCall(
                name="list_files",
                arguments={}
            )
        )

    if "src/main.py" in last_result:
        return LLMResponse(
            tool_call=ToolCall(
                name="read_file",
                arguments={"path": "src/main.py"}
            )
        )

    return LLMResponse(
        answer="已经找到真实入口 src/main.py,并成功读取。"
    )


def run_agent(user_message: str, max_steps: int = 10) -> str:
    messages = [
        {
            "role": "user",
            "content": user_message
        }
    ]

    for step in range(max_steps):
        response = fake_llm_chat(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("找到项目入口文件"))

这段代码第一次读取:

app.py

会失败。

但失败不是结束。

它变成下一轮的 Observation。

然后系统:

read app.py
 ↓
FILE_NOT_FOUND
 ↓
list_files
 ↓
["src/main.py", "README.md"]
 ↓
read src/main.py
 ↓
成功

这就是 Agent 与普通固定 Chain 很不一样的地方。

Reason 不等于“永远要生成一段思维文字”

从工程角度,我们更希望模型输出:

{
  "action": "read_file",
  "arguments": {
    "path": "src/main.py"
  }
}

而不是先输出几百字“我为什么这么想”。

真正有价值的是:

  • 当前决策;
  • 当前动作;
  • 执行结果;
  • 下一步是否改变。

因此本课程里,我们讲 Reason 时,会关注“决策逻辑”,而不是要求把模型内部思考全文记录下来。

后面做 Tracing 时,我们也会重点记录可观察的输入、输出、Tool Call、Tool Result、状态和耗时。

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

昵称

取消
昵称

    请登录后查看评论内容