现在我们把 Main Loop 拆成三个最重要的阶段。
Reason
↓
Action
↓
Observation
↓
Reason
把三段职责拆开后,信息在循环里的流向会直观得多:Reason 负责选择,Action 负责把意图结构化,Observation 负责把环境事实带回来。
![图片[1]-小节二:Reason、Action、Observation——Agent 的三段式闭环到底在发生什么?-大赛人网](https://www.dsrw.com/wp-content/uploads/2026/11/image-1418.png)
注意图中 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、状态和耗时。
















请登录后查看评论内容