learn-claude-code/s01_agent_loop at main · shareAI-lab/learn-claude-code

Agent归根到底就是一个循环

  • 阅读query
  • thinking……
  • do something……/stop
  • observation

Agent通过工具调用进行外部交互

While True的停止条件实际上就是

  • stop_reason == "tool_use":执行,返回工具结果,继续循环
  • stop_reason != "tool_use":退出循环

Antropic SDK

和Openai风格的SDK不太一样,似乎大家喜欢用A社的做工具调用

 1#!/usr/bin/env python3
 2
 3import os
 4from dotenv import load_dotenv
 5from anthropic import Anthropic
 6
 7load_dotenv(override=True)
 8
 9client = Anthropic(
10    base_url=os.getenv("ANTHROPIC_BASE_URL"),
11    api_key=os.getenv("ANTHROPIC_API_KEY"),
12)
13
14MODEL = os.getenv("MODEL_ID", "claude-3-5-sonnet-latest")
15
16TOOLS = [
17    {
18        "name": "bash",
19        "description": "Run a shell command.",
20        "input_schema": {
21            "type": "object",
22            "properties": {
23                "command": {"type": "string"}
24            },
25            "required": ["command"],
26        },
27    }
28]
29
30response = client.messages.create(
31    model=MODEL,
32    max_tokens=1024,
33    messages=[
34        {
35            "role": "user",
36            "content": (
37                "请先用一句话说明你将要做什么,"
38                "然后调用 bash 工具查看当前目录下的文件列表。"
39            ),
40        }
41    ],
42    tools=TOOLS
43)
44
45print(response)

Tool

工具定义,其实就是定义一个函数

 1{
 2        "name": "bash",
 3        "description": "Run a shell command.",
 4        "input_schema": {
 5            "type": "object",
 6            "properties": {
 7                "command": {"type": "string"}
 8            },
 9            "required": ["command"],
10        },
11}

相当于注册了一个函数bash(command:str)

  • name:工具名,模型调用工具必须名字一致
  • description:告诉模型工具的说明
  • input_schema:工具输入参数定义

定义了一个command参数(required,必须存在)

参数值是字符串,且传参方式是是字典对象

例如:{"command": "ls -la"}

Return

上述代码的返回是:

 1Message(
 2    id='ec45b38f-7416-4b74-b501-e03448af1633',
 3    container=None,
 4    content=[
 5        ThinkingBlock(
 6            signature='ec45b38f-7416-4b74-b501-e03448af1633',
 7            thinking="....",
 8            type='thinking'
 9        ),
10        TextBlock(
11            citations=None,
12            text='我将使用 bash 工具查看当前目录下的文件列表。',
13            type='text'
14        ),
15        ToolUseBlock(
16            id='call_00_9mk6ZlmzqJtCPY0Epd165520',
17            caller=None,
18            input={
19                'command': 'ls -la'
20            },
21            name='bash',
22            type='tool_use'
23        )
24    ],
25    model='deepseek-v4-pro',
26    role='assistant',
27    stop_details=None,
28    stop_reason='tool_use',
29    stop_sequence=None,
30    type='message',
31    usage=Usage(
32        cache_creation=None,
33        cache_creation_input_tokens=0,
34        cache_read_input_tokens=256,
35        inference_geo=None,
36        input_tokens=33,
37        output_tokens=86,
38        output_tokens_details=None,
39        server_tool_use=None,
40        service_tier='standard'
41    )
42)
  • content:按Block进行切分,有专门的ToolUseBlock方便解析
  • stop_reason:这里是tool_use,说明需要调用工具

Code

完整代码:

  1#!/usr/bin/env python3
  2
  3import os
  4from dotenv import load_dotenv
  5from anthropic import Anthropic
  6import subprocess
  7
  8load_dotenv(override=True)
  9
 10client = Anthropic(
 11    base_url=os.getenv("ANTHROPIC_BASE_URL"),
 12    api_key=os.getenv("ANTHROPIC_API_KEY"),
 13)
 14
 15MODEL = os.getenv("MODEL_ID", "claude-3-5-sonnet-latest")
 16
 17TOOLS = [
 18    {
 19        "name": "bash",
 20        "description": "Run a shell command.",
 21        "input_schema": {
 22            "type": "object",
 23            "properties": {
 24                "command": {"type": "string"}
 25            },
 26            "required": ["command"],
 27        },
 28    }
 29]
 30
 31
 32
 33
 34SYSTEM = f"You are a coding agent at {os.getcwd()}. "
 35
 36# ── Tool execution ────────────────────────────────────────
 37def run_bash(command: str) -> str:
 38    dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
 39    if any(d in command for d in dangerous):
 40        return "Error: Dangerous command blocked"
 41    try:
 42        r = subprocess.run(command, shell=True, cwd=os.getcwd(),
 43                           capture_output=True, text=True, timeout=120)
 44        out = (r.stdout + r.stderr).strip()
 45        return out[:50000] if out else "(no output)"
 46    except subprocess.TimeoutExpired:
 47        return "Error: Timeout (120s)"
 48    except (FileNotFoundError, OSError) as e:
 49        return f"Error: {e}"
 50
 51def agent_loop(history):
 52    while True:
 53        
 54        response = client.messages.create(
 55            model=MODEL,
 56            max_tokens=1024,
 57            messages=history,
 58            tools=TOOLS,
 59            system=SYSTEM,        
 60        )
 61
 62        # 保存模型记录
 63        history.append({"role": "assistant", "content": response.content})
 64        
 65        # 如果不再需要工具调用
 66        if response.stop_reason != "tool_use":
 67            return 
 68        
 69        tool_results = []
 70        for block in response.content:
 71            if block.type == 'tool_use':
 72                print(f"Tool call: {block.name} with input {block.input}")
 73                
 74                run_bash_result = run_bash(block.input['command'])
 75                tool_results.append({
 76                    "type": "tool_result",
 77                    "tool_use_id": block.id, # 对应同block的工具调用id
 78                    "content": run_bash_result
 79                })                 
 80
 81        # 将工具调用结果添加到历史记录中,供模型后续生成使用
 82        history.append({"role": "user", "content": tool_results})        
 83
 84
 85
 86
 87if __name__ == "__main__":
 88
 89    history = []
 90    
 91    # 这里是多轮对话的循环
 92    while True:
 93        query = input("Enter your query: ")
 94
 95        if query.lower() in ["exit", "quit"]:
 96            print("Exiting the agent loop.")
 97            break
 98
 99        history.append({"role": "user", "content": query})
100        agent_loop(history) # Agent内部循环,完成工具调用和响应生成
101
102        final_output = history[-1]["content"]
103        print(final_output)

Claude Code

以上是一个AgentLoop最小实现

但对于Claude Code的实现也值得去看一下

循环结构

CC并不是等待一个响应完成,检测最后的stop_reason

而是根据当前流式输出内容,如果出现ToolUseBlock

设置needsFollowUp = True,表示需要下一轮循环

这里只是说明如何判断Loop中断

stop_reason事实上也有其他逻辑使用,暂时不提

同时为了性能,工具调用也会在流式时进行

但是会做concurrency-safe的检测,决定并行or独占

同时针对费用超限、结构化输出失败等做了非常多保护

退出

CC事实上对blocking limit、prompt too long、model error、abort、hook stop、max turns、token budget continuation、reactive compact retry 等场景,都准备了退出、恢复机制

以上代码只有模型不调用工具就结束的逻辑