Skip to content

01 - 智能体循环

核心路径 · 第一站

"One loop & Bash is all you need" —— 一个工具 + 一个循环 = 一个智能体。

Harness 层: 循环 —— 模型与真实世界的第一道连接。

学习目标

  • 理解 Agent Loop 的模式:消息累积 → LLM 推理 → 工具执行 → 回到推理
  • 掌握 stop_reason 作为循环退出条件的机制
  • 用不到 30 行代码实现一个最小 Agent
  • 认识 Harness 工程师的视角:你构建模型栖居的世界,而不是编写智能

核心概念

问题

语言模型能推理代码,但碰不到真实世界 —— 不能读文件、跑测试、看报错。没有循环,每次工具调用都得手动把结果粘回去。你自己就是那个循环。

最小循环

+--------+      +-------+      +---------+
|  User  | ---> |  LLM  | ---> |  Tool   |
| prompt |      |       |      | execute |
+--------+      +---+---+      +----+----+
                    ^                |
                    |   tool_result  |
                    +----------------+
                    (loop until stop_reason != "tool_use")

一个退出条件控制整个流程。循环持续运行,直到模型不再调用工具。

Agent vs. Harness

概念含义
Agent模型本身 —— 一个通过训练学会推理和行动的神经网络,无法直接被修改
Harness模型栖居的世界 —— 工具、知识、上下文管理、权限边界,这些是工程师可以构建和优化的
Agent Loop连接 Agent 和 Harness 的核心循环,让模型能看到工具结果并决定下一步

三行核心逻辑

整个 Agent Loop 可以浓缩为三句话:

  1. 发消息给 LLM(携带消息历史和工具定义)
  2. 检查 stop_reason —— 如果模型没调用工具,结束
  3. 执行工具调用,把结果追加到消息列表,回到第 1 步
python
while True:
    response = client.messages.create(model=MODEL, system=SYSTEM, messages=messages, tools=TOOLS)
    messages.append({"role": "assistant", "content": response.content})
    if response.stop_reason != "tool_use":
        return
    for tool_call in response.content:
        result = execute(tool_call)
        messages.append({"role": "user", "content": result})

stop_reason 详解

stop_reason 是 API 返回的枚举值,指示模型为什么停止生成:

stop_reason含义循环行为
"tool_use"模型要调用工具继续循环
"end_turn"模型给出最终回答退出循环
"max_tokens"达到 token 上限退出循环(需额外处理)
"stop_sequence"命中停止序列退出循环

安全约束

最小 Harness 只做一件事:阻止危险命令。这不是心智负担,而是 Agent 工程的第一条规则 —— 永远假设模型可能出错。

python
def run_bash(command: str) -> str:
    dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
    if any(d in command for d in dangerous):
        return "Error: Dangerous command blocked"
    ...

动手实践:最小 Agent

前置准备

sh
cd learn-claude-code
python agents/s01_agent_loop.py

完整代码

python
import os, subprocess
from anthropic import Anthropic
from dotenv import load_dotenv

load_dotenv(override=True)
if os.getenv("ANTHROPIC_BASE_URL"):
    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)

client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
MODEL = os.environ["MODEL_ID"]
SYSTEM = f"You are a coding agent at {os.getcwd()}. Use bash to solve tasks. Act, don't explain."

TOOLS = [{
    "name": "bash",
    "description": "Run a shell command.",
    "input_schema": {
        "type": "object",
        "properties": {"command": {"type": "string"}},
        "required": ["command"],
    },
}]

def run_bash(command: str) -> str:
    dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
    if any(d in command for d in dangerous):
        return "Error: Dangerous command blocked"
    try:
        r = subprocess.run(command, shell=True, cwd=os.getcwd(),
                           capture_output=True, text=True, timeout=120)
        out = (r.stdout + r.stderr).strip()
        return out[:50000] if out else "(no output)"
    except subprocess.TimeoutExpired:
        return "Error: Timeout (120s)"

def agent_loop(messages: list):
    while True:
        response = client.messages.create(
            model=MODEL, system=SYSTEM, messages=messages,
            tools=TOOLS, max_tokens=8000,
        )
        messages.append({"role": "assistant", "content": response.content})
        if response.stop_reason != "tool_use":
            return
        results = []
        for block in response.content:
            if block.type == "tool_use":
                print(f"\033[33m$ {block.input['command']}\033[0m")
                output = run_bash(block.input["command"])
                print(output[:200])
                results.append({"type": "tool_result",
                                "tool_use_id": block.id, "content": output})
        messages.append({"role": "user", "content": results})

if __name__ == "__main__":
    history = []
    while True:
        try:
            query = input("\033[36ms01 >> \033[0m")
        except (EOFError, KeyboardInterrupt):
            break
        if query.strip().lower() in ("q", "exit", ""):
            break
        history.append({"role": "user", "content": query})
        agent_loop(history)
        response_content = history[-1]["content"]
        if isinstance(response_content, list):
            for block in response_content:
                if hasattr(block, "text"):
                    print(block.text)
        print()

尝试这些 Prompt

英文 prompt 对 LLM 效果更好,也可以用中文:

  1. Create a file called hello.py that prints "Hello, World!"
  2. List all Python files in this directory
  3. What is the current git branch?
  4. Create a directory called test_output and write 3 files in it

观察点

  • 每次工具调用后,模型都会"看到"结果并决定下一步
  • 多步任务(如创建目录再写文件)需要多次循环
  • 模型会自动决定何时停止 —— 不需要你判断任务是否完成

关键概念

组件作用备注
Agent Loopwhile True + stop_reason循环在模型放弃使用工具时退出
Toolsbash(单一工具)最小集合,一个工具足以完成任务
Messages累积式消息列表每次迭代追加 assistant + tool_result 消息
Control Flowstop_reason != "tool_use"唯一退出条件
Tool executesubprocess.run()同步执行,结果截断至 50K chars
System Prompt"Act, don't explain."引导模型直接行动而非讨论

为什么 S01 是 ⭐ 核心路径第一站

后续所有课程都是在此循环上叠加机制:

  • [[../02-tool-use/02-工具使用|s02 工具使用]] —— 加一个工具,只加一个 handler
  • [[../03-todo-write/03-待办写入|s03 待办写入]] —— 先列步骤再动手
  • [[../04-subagent/04-子智能体|s04 子智能体]] —— 大任务拆小,每个小任务干净的上下文
  • [[../05-skill-loading/05-技能加载|s05 技能加载]] —— 用到什么知识,临时加载什么知识
  • [[../06-context-compact/06-上下文压缩|s06 上下文压缩]] —— 上下文总会满,要有办法腾地方
  • [[../07-task-system/07-任务系统|s07 任务系统]] —— 大目标要拆成小任务,排好序,记在磁盘上
  • [[../09-agent-teams/09-智能体团队|s09 智能体团队]] —— 任务太大一个人干不完,要能分给队友

循环本身永远不变。理解了这个循环,你就理解了所有 Agent 的本质。

变化一览

组件之前之后
Agent loop(不存在)while True + stop_reason
Tools(不存在)bash(单一工具)
Messages(不存在)累积式消息列表
Control flow(不存在)stop_reason != "tool_use"
Safety(不存在)危险命令过滤

讨论问题

  1. 如果模型在循环中无限调用工具怎么办?有哪些策略可以防止这种情况?
  2. 为什么 s01 只暴露 bash 一个工具?如果加一个 read_file 工具会有什么不同?
  3. stop_reason 的局限性在哪里?考虑 max_tokens 被截断的场景 —— 模型还没完成工作但被迫停止了。
  4. "Agent 是模型,不是框架" —— 这句话在实践中意味着什么?如果你改不了模型,你能改什么?
  5. System prompt 说 "Act, don't explain" —— 删除这句话模型的行为会怎样变化?试试看。

延伸阅读

基于 Learn Claude Code 项目改编