跳至内容

v1:模型即代理

Claude Code 的秘密?没有秘密。

剥去 CLI 外观、进度条、权限系统,剩下的出奇简单:一个让模型持续调用工具直到任务完成的循环。

核心洞察

传统助手:

用户 -> 模型 -> 文本回复

Agent 系统:

用户 -> 模型 -> [工具 -> 结果]* -> 回复
                   ^_________|

星号很重要。模型反复调用工具,直到它决定任务完成。这将聊天机器人转变为自主代理。

    flowchart LR
    U[用户] --> M[模型]
    M -->|需要工具| T[工具调用]
    T --> E[执行结果]
    E --> M
    M -->|任务完成| R[回复]
  

核心洞察:模型是决策者,代码只提供工具并运行循环。

四个核心工具

Claude Code 有约 20 个工具,但 4 个覆盖 90% 的场景:

工具用途示例
bash运行命令npm install, git status
read_file读取内容查看 src/index.ts
write_file创建/覆盖创建 README.md
edit_file精确修改替换一个函数

有了这 4 个工具,模型可以:

  • 探索代码库(bash: find, grep, ls)
  • 理解代码(read_file)
  • 做出修改(write_file, edit_file)
  • 运行任何东西(bash: python, npm, make)

完整代码

#!/usr/bin/env python3
"""
v1_basic_agent.py - Mini Claude Code: Model as Agent (~200 lines)

Core Philosophy: "The Model IS the Agent"
=========================================
What is the core pattern behind this generation of agent coding tools? There is no secret.

Strip away the CLI polish, progress bars, permission systems. What remains
is surprisingly simple: a LOOP that lets the model call tools until done.

Traditional Assistant:
    User -> Model -> Text Response

Agent System:
    User -> Model -> [Tool -> Result]* -> Response
                  ^________|

The asterisk (*) matters! The model calls tools REPEATEDLY until it decides
the task is complete. This transforms a chatbot into an autonomous agent.

KEY INSIGHT: The model is the decision-maker. Code just provides tools and
runs the loop. The model decides:
  - Which tools to call
  - In what order
  - When to stop

The Four Essential Tools:
------------------------
Claude Code has ~20 tools. But these 4 cover 90% of use cases:

    | Tool       | Purpose              | Example                    |
    |------------|----------------------|----------------------------|
    | bash       | Run any command      | npm install, git status    |
    | read_file  | Read file contents   | View src/index.ts          |
    | write_file | Create/overwrite     | Create README.md           |
    | edit_file  | Surgical changes     | Replace a function         |

With just these 4 tools, the model can:
  - Explore codebases (bash: find, grep, ls)
  - Understand code (read_file)
  - Make changes (write_file, edit_file)
  - Run anything (bash: python, npm, make)

Usage:
    python v1_basic_agent.py
"""

import os
import subprocess
import sys
from pathlib import Path

from anthropic import Anthropic
from dotenv import load_dotenv

load_dotenv(override=True)


# =============================================================================
# Configuration
# =============================================================================

WORKDIR = Path.cwd()
MODEL = os.getenv("MODEL_ID", "claude-sonnet-4-5-20250929")
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))


# =============================================================================
# System Prompt - The only "configuration" the model needs
# =============================================================================

SYSTEM = f"""You are a coding agent at {WORKDIR}.

Loop: think briefly -> use tools -> report results.

Rules:
- Prefer tools over prose. Act, don't just explain.
- Never invent file paths. Use bash ls/find first if unsure.
- Make minimal changes. Don't over-engineer.
- After finishing, summarize what changed."""


# =============================================================================
# Tool Definitions - 4 tools cover 90% of coding tasks
# =============================================================================

TOOLS = [
    # Tool 1: Bash - The gateway to everything
    # Can run any command: git, npm, python, curl, etc.
    {
        "name": "bash",
        "description": "Run a shell command. Use for: ls, find, grep, git, npm, python, etc.",
        "input_schema": {
            "type": "object",
            "properties": {
                "command": {
                    "type": "string",
                    "description": "The shell command to execute"
                }
            },
            "required": ["command"],
        },
    },

    # Tool 2: Read File - For understanding existing code
    # Returns file content with optional line limit for large files
    {
        "name": "read_file",
        "description": "Read file contents. Returns UTF-8 text.",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Relative path to the file"
                },
                "limit": {
                    "type": "integer",
                    "description": "Max lines to read (default: all)"
                },
            },
            "required": ["path"],
        },
    },

    # Tool 3: Write File - For creating new files or complete rewrites
    # Creates parent directories automatically
    {
        "name": "write_file",
        "description": "Write content to a file. Creates parent directories if needed.",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Relative path for the file"
                },
                "content": {
                    "type": "string",
                    "description": "Content to write"
                },
            },
            "required": ["path", "content"],
        },
    },

    # Tool 4: Edit File - For surgical changes to existing code
    # Uses exact string matching for precise edits
    {
        "name": "edit_file",
        "description": "Replace exact text in a file. Use for surgical edits.",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Relative path to the file"
                },
                "old_text": {
                    "type": "string",
                    "description": "Exact text to find (must match precisely)"
                },
                "new_text": {
                    "type": "string",
                    "description": "Replacement text"
                },
            },
            "required": ["path", "old_text", "new_text"],
        },
    },
]


# =============================================================================
# Tool Implementations
# =============================================================================

def safe_path(p: str) -> Path:
    """
    Ensure path stays within workspace (security measure).

    Prevents the model from accessing files outside the project directory.
    Resolves relative paths and checks they don't escape via '../'.
    """
    path = (WORKDIR / p).resolve()
    if not path.is_relative_to(WORKDIR):
        raise ValueError(f"Path escapes workspace: {p}")
    return path


def run_bash(command: str) -> str:
    """
    Execute shell command with safety checks.

    Security: Blocks obviously dangerous commands.
    Timeout: 60 seconds to prevent hanging.
    Output: Truncated to 50KB to prevent context overflow.
    """
    # Basic safety - block dangerous patterns
    dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
    if any(d in command for d in dangerous):
        return "Error: Dangerous command blocked"

    try:
        result = subprocess.run(
            command,
            shell=True,
            cwd=WORKDIR,
            capture_output=True,
            text=True,
            timeout=60
        )
        output = (result.stdout + result.stderr).strip()
        return output[:50000] if output else "(no output)"

    except subprocess.TimeoutExpired:
        return "Error: Command timed out (60s)"
    except Exception as e:
        return f"Error: {e}"


def run_read(path: str, limit: int = None) -> str:
    """
    Read file contents with optional line limit.

    For large files, use limit to read just the first N lines.
    Output truncated to 50KB to prevent context overflow.
    """
    try:
        text = safe_path(path).read_text()
        lines = text.splitlines()

        if limit and limit < len(lines):
            lines = lines[:limit]
            lines.append(f"... ({len(text.splitlines()) - limit} more lines)")

        return "\n".join(lines)[:50000]

    except Exception as e:
        return f"Error: {e}"


def run_write(path: str, content: str) -> str:
    """
    Write content to file, creating parent directories if needed.

    This is for complete file creation/overwrite.
    For partial edits, use edit_file instead.
    """
    try:
        fp = safe_path(path)
        fp.parent.mkdir(parents=True, exist_ok=True)
        fp.write_text(content)
        return f"Wrote {len(content)} bytes to {path}"

    except Exception as e:
        return f"Error: {e}"


def run_edit(path: str, old_text: str, new_text: str) -> str:
    """
    Replace exact text in a file (surgical edit).

    Uses exact string matching - the old_text must appear verbatim.
    Only replaces the first occurrence to prevent accidental mass changes.
    """
    try:
        fp = safe_path(path)
        content = fp.read_text()

        if old_text not in content:
            return f"Error: Text not found in {path}"

        # Replace only first occurrence for safety
        new_content = content.replace(old_text, new_text, 1)
        fp.write_text(new_content)
        return f"Edited {path}"

    except Exception as e:
        return f"Error: {e}"


def execute_tool(name: str, args: dict) -> str:
    """
    Dispatch tool call to the appropriate implementation.

    This is the bridge between the model's tool calls and actual execution.
    Each tool returns a string result that goes back to the model.
    """
    if name == "bash":
        return run_bash(args["command"])
    if name == "read_file":
        return run_read(args["path"], args.get("limit"))
    if name == "write_file":
        return run_write(args["path"], args["content"])
    if name == "edit_file":
        return run_edit(args["path"], args["old_text"], args["new_text"])
    return f"Unknown tool: {name}"


# =============================================================================
# The Agent Loop - This is the CORE of everything
# =============================================================================

def agent_loop(messages: list) -> list:
    """
    The complete agent in one function.

    This is the pattern that ALL coding agents share:

        while True:
            response = model(messages, tools)
            if no tool calls: return
            execute tools, append results, continue

    The model controls the loop:
      - Keeps calling tools until stop_reason != "tool_use"
      - Results become context (fed back as "user" messages)
      - Memory is automatic (messages list accumulates history)

    Why this works:
      1. Model decides which tools, in what order, when to stop
      2. Tool results provide feedback for next decision
      3. Conversation history maintains context across turns
    """
    while True:
        # Step 1: Call the model
        response = client.messages.create(
            model=MODEL,
            system=SYSTEM,
            messages=messages,
            tools=TOOLS,
            max_tokens=8000,
        )

        # Step 2: Collect any tool calls and print text output
        tool_calls = []
        for block in response.content:
            if hasattr(block, "text"):
                print(block.text)
            if block.type == "tool_use":
                tool_calls.append(block)

        # Step 3: If no tool calls, task is complete
        if response.stop_reason != "tool_use":
            messages.append({"role": "assistant", "content": response.content})
            return messages

        # Step 4: Execute each tool and collect results
        results = []
        for tc in tool_calls:
            # Display what's being executed
            print(f"\n> {tc.name}: {tc.input}")

            # Execute and show result preview
            output = execute_tool(tc.name, tc.input)
            preview = output[:200] + "..." if len(output) > 200 else output
            print(f"  {preview}")

            # Collect result for the model
            results.append({
                "type": "tool_result",
                "tool_use_id": tc.id,
                "content": output,
            })

        # Step 5: Append to conversation and continue
        # Note: We append assistant's response, then user's tool results
        # This maintains the alternating user/assistant pattern
        messages.append({"role": "assistant", "content": response.content})
        messages.append({"role": "user", "content": results})


# =============================================================================
# Main REPL
# =============================================================================

def main():
    """
    Simple Read-Eval-Print Loop for interactive use.

    The history list maintains conversation context across turns,
    allowing multi-turn conversations with memory.
    """
    print(f"Mini Claude Code v1 - {WORKDIR}")
    print("Type 'exit' to quit.\n")

    history = []

    while True:
        try:
            user_input = input("You: ").strip()
        except (EOFError, KeyboardInterrupt):
            break

        if not user_input or user_input.lower() in ("exit", "quit", "q"):
            break

        # Add user message to history
        history.append({"role": "user", "content": user_input})

        try:
            # Run the agent loop
            agent_loop(history)
        except Exception as e:
            print(f"Error: {e}")

        print()  # Blank line between turns


if __name__ == "__main__":
    main()

Agent 循环

整个 Agent 在一个函数里:

def agent_loop(messages):
    while True:
        # 1. 询问模型
        response = client.messages.create(
            model=MODEL, system=SYSTEM,
            messages=messages, tools=TOOLS
        )

        # 2. 打印文本输出
        for block in response.content:
            if hasattr(block, "text"):
                print(block.text)

        # 3. 如果没有工具调用,完成
        if response.stop_reason != "tool_use":
            return messages

        # 4. 执行工具,继续循环
        results = []
        for tc in response.tool_calls:
            output = execute_tool(tc.name, tc.input)
            results.append({"type": "tool_result", "tool_use_id": tc.id, "content": output})

        messages.append({"role": "assistant", "content": response.content})
        messages.append({"role": "user", "content": results})

为什么这能工作:

  1. 模型控制循环(持续调用工具直到 stop_reason != "tool_use")
  2. 结果成为上下文(作为 “user” 消息反馈)
  3. 记忆自动累积(messages 列表保存历史)
    flowchart TD
    A[开始: 用户消息] --> B[调用模型 API]
    B --> C{有工具调用?}
    C -->|是| D[执行工具]
    D --> E[收集结果]
    E --> F[追加到对话历史]
    F --> B
    C -->|否| G[返回文本回复]
    G --> H[结束]
  

系统提示词

唯一需要的"配置":

SYSTEM = f"""You are a coding agent at {WORKDIR}.

Loop: think briefly -> use tools -> report results.

Rules:
- Prefer tools over prose. Act, don't just explain.
- Never invent file paths. Use ls/find first if unsure.
- Make minimal changes. Don't over-engineer.
- After finishing, summarize what changed."""

没有复杂逻辑,只有清晰的指令。

为什么这个设计有效

1. 简单 没有状态机,没有规划模块,没有框架。

2. 模型负责思考 模型决定用哪些工具、什么顺序、何时停止。

3. 透明 每个工具调用可见,每个结果在对话中。

4. 可扩展 添加工具 = 一个函数 + 一个 JSON schema。

缺少什么

特性为什么省略添加于
待办追踪非必需v2
子代理复杂度v3
权限学习目的信任模型生产版

关键点:核心是微小的,其他都是精化。

更大的图景

这一代 agent coding 系统都共享这个模式:

while not done:
    response = model(conversation, tools)
    results = execute(response.tool_calls)
    conversation.append(results)

差异在于工具、显示、安全性。但本质始终是:给模型工具,让它工作。

运行示例

# 安装依赖
pip install anthropic python-dotenv

# 配置环境变量
echo "ANTHROPIC_API_KEY=sk-ant-xxx" > .env

# 运行
python v1_basic_agent.py

# 示例对话
You: 列出当前目录的 Python 文件
> bash: {'command': 'ls *.py'}
  (no output)

You: 用 find 再试一次
> bash: {'command': 'find . -name "*.py"'}
  ./v0_bash_agent.py
  ./v1_basic_agent.py

模型即代理。这就是全部秘密。