本篇合并 s01、s02 两章。

s01:Agent Loop(主循环)

核心模式

最小智能体 = 一个循环:LLM 不断调用工具,直到自己判断任务完成。

用户提问 → LLM → 需要调用工具? → 执行工具 → 结果回传 → LLM → … → 任务完成,结束
  • 用户提问:把问题作为首条 user 消息放进 messages
  • LLM 决策:每轮把包含完整历史的 messages 传给模型,模型自己决定"再调一次工具"还是"直接回答"。
  • 结果回传:工具输出作为 tool_result 消息追加回 messages,成为下一轮的上下文。
  • 结束条件:模型不再请求 tool_use 时,循环退出。

代码实现

def agent_loop(messages: list):
    while True:
        # 1. 调用 LLM,传入当前包含历史的完整消息
        response = client.messages.create(
            model=MODEL, system=SYSTEM, messages=messages,
            tools=TOOLS, max_tokens=8000,
        )
        # 2. 将助手的回复(可能是文本或工具调用请求)加入历史
        messages.append({"role": "assistant", "content": response.content})
        # 3. 【核心判断】如果模型不再要求调用工具,退出循环
        if response.stop_reason != "tool_use":
            return
        # 4. 模型要求调用工具,执行它要求的每一个工具
        results = []
        for block in response.content:
            if block.type == "tool_use":
                output = run_bash(block.input["command"])
                results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": output,
                })
        # 5. 将工具执行结果作为新消息追加,进入下一轮循环
        messages.append({"role": "user", "content": results})

关键机制

  • stop_reason 决定去留:值为 "tool_use" 表示模型还要继续调工具;为 "end_turn" / "stop_sequence" 等其它值时表示模型认为任务已完成,循环 return 退出。
  • max_tokens=8000:限制单轮模型输出长度,防止一次生成过长把上下文撑爆;工具调用请求本身也计入这个额度。
  • 历史持续累积messages 是不断增长的列表,工具结果以 tool_result 追加进去,下一轮模型就能"看到"之前的动作和结果。
  • 不含业务约束:核心循环本身只是"调模型 → 跑工具 → 回传"的裸骨架,所有安全、权限、规划等约束都在后续模块(s02~s03 及以后)逐步叠加

本模块知识点

  • 最小智能体 = 一个循环:LLM 不断调用工具,直到自行判断任务完成。
  • 主循环while True → 调 LLM → 若 stop_reason != "tool_use" 则退出 → 否则执行工具、结果回传、进入下一轮。
  • 工具结果以 tool_result 消息追加进 messages,历史持续累积。
  • 核心循环本身不含任何业务约束,约束在后续模块(s02~s03)逐步叠加。

s02:Tool Expansion(工具扩展)

核心思路

新增了 4 个专用工具read_filewrite_fileedit_fileglob。这为模型提供了结构化的文件操作能力,让模型可以直接表达"读取这个文件"的意图,而无需绕过 shell。工具集从"一个通用接口(bash)“演变为"一套专用 API”。

工具 作用 替代的 shell 写法
read_file 读取文件内容 cat file
write_file 写入 / 覆盖文件 echo ... > file
edit_file 精确修改文件片段 sed / 手动改
glob 按模式查找文件 find / ls *.py

工具分发映射:Tool Dispatch Map

遵循开闭原则:对扩展开放,对修改封闭。新增一个工具,只需要三步,核心循环代码无需改动:

  1. 实现函数:写出工具背后的真实逻辑(如 run_read)。
  2. TOOLS 列表定义:把工具名、描述、输入 schema 注册给模型看。
  3. TOOL_HANDLERS 字典注册:把工具名映射到处理函数。
# 工具扩展
TOOLS = [
    {"name": "bash",       "description": "Run a shell command.", ...},
    {"name": "read_file",  "description": "Read file contents.",  ...},
    ...
]
# 字典:工具名称到具体处理函数的映射
TOOL_HANDLERS = {
    "bash": run_bash,
    "read_file": run_read,
    ...
}

工作区安全模型:Workspace Safety

safe_path 函数实现了一个沙箱模型:将所有文件操作强制限制在 WORKDIR(当前工作目录)之内。模型传出的任何试图访问工作区外部的路径(如 ../../etc/passwd),都会被 safe_pathis_relative_to 检查拦截并抛出异常。这是一种最小权限原则的实践。

# 文件系统的安全守卫
def safe_path(p: str) -> Path:
    # 将工作目录与用户路径拼接,形成绝对路径的雏形
    # resolve 解析所有 ..、. 符号链接等,得到最终的绝对路径
    path = (WORKDIR / p).resolve()
    # 解析后的最终路径是否还在工作目录的范围之内
    if not path.is_relative_to(WORKDIR):
        raise ValueError(f"Path escapes workspace: {p}")
    return path

TOOLSTOOL_HANDLERS 必须一一对应

  • TOOLS 列表是给 LLM 看的"菜单",定义了工具的接口长什么样(名字、描述、参数)。
response = client.messages.create(
            model=MODEL, system=SYSTEM, messages=messages,
            tools=TOOLS, max_tokens=8000,
        )
  • TOOL_HANDLERS 字典是给程序自己看的"后端逻辑",将菜单上的名字映射到真实函数。
# 核心循环中,执行工具变成了通过查表动态分发
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input)

两者必须严格同步:若某工具只在 TOOLS 注册却忘了写进 TOOL_HANDLERS,模型会去调用它,但 handler 取到 None,落到 f"Unknown: {block.name}" 分支;若只在 TOOL_HANDLERS 注册却没进 TOOLS,模型根本不知道有这个工具,永远不会调用。

本模块知识点

  • 工具集从单一 bash 演进为专用 API:read_file / write_file / edit_file / glob
  • 开闭原则:新增工具 = 实现函数 + 在 TOOLS 注册 + 在 TOOL_HANDLERS 字典注册,核心循环不变。
  • TOOLS(给 LLM 看的菜单)与 TOOL_HANDLERS(程序后端逻辑)必须严格一一对应。
  • 工作区安全safe_pathis_relative_to(WORKDIR) 实现沙箱,越界路径直接抛异常(最小权限原则)。