目录

v1: 模型即代理 - 4 个工具覆盖 90% 场景

从 v0 到 v1

四个核心工具

工具定义详解

bash - 通往一切的大门

read_file - 理解现有代码

write_file - 创建新文件

edit_file - 精确修改

安全机制

路径安全检查

危险命令阻止

输出截断

超时控制

Agent 循环详解

为什么工具结果是 "user" 消息?

消息历史结构

工具分发器

关键洞察

1. 模型即代理

2. 工具即接口

3. 上下文即记忆

运行示例

从 v1 到 v2

总结


上一篇

v1: 模型即代理 - 4 个工具覆盖 90% 场景

本文是 Learn Claude Code (C# 版) 系列的第二篇,对应代码文件 v1_basic_agent.cs

从 v0 到 v1

v0 用一个 bash 工具证明了最简可行性。但在实际使用中:

  • 用 cat 读文件需要额外的 tokens 来构造命令

  • 用 echo 写文件容易出错(引号转义、换行处理)

  • 缺乏安全检查,可能逃逸工作目录

v1 引入 4 个专用工具,这是 Claude Code 20+ 工具的精华提炼。

四个核心工具

工具

用途

覆盖场景

bash

运行任何命令

git, npm, dotnet, curl...

read_file

读取文件内容

理解代码

write_file

创建/覆盖文件

生成新文件

edit_file

精确文本替换

修改现有代码

为什么是这 4 个?因为编程任务本质上就是:探索 → 理解 → 修改 → 验证

工具定义详解

bash - 通往一切的大门

new Tool
{
    Name = "bash",
    Description = "运行 shell 命令。用于: ls, find, grep, git, npm, dotnet 等。",
    InputSchema = new InputSchema
    {
        Type = "object",
        Properties = new Dictionary<string, JsonElement>
        {
            ["command"] = JsonDocument.Parse("""{"type": "string", "description": "要执行的命令"}""").RootElement
        },
        Required = ["command"]
    }
}

bash 仍然是最重要的工具——它是模型与外部世界交互的通道。

read_file - 理解现有代码

new Tool
{
    Name = "read_file",
    Description = "读取文件内容。返回 UTF-8 文本。",
    InputSchema = new InputSchema
    {
        Type = "object",
        Properties = new Dictionary<string, JsonElement>
        {
            ["path"] = JsonDocument.Parse("""{"type": "string", "description": "文件的相对路径"}""").RootElement,
            ["limit"] = JsonDocument.Parse("""{"type": "integer", "description": "最大读取行数"}""").RootElement
        },
        Required = ["path"]
    }
}

limit 参数很重要——大文件只需要看前 N 行,避免上下文溢出。

write_file - 创建新文件

new Tool
{
    Name = "write_file",
    Description = "将内容写入文件。如果需要会创建父目录。",
    InputSchema = new InputSchema
    {
        Type = "object",
        Properties = new Dictionary<string, JsonElement>
        {
            ["path"] = JsonDocument.Parse("""{"type": "string"}""").RootElement,
            ["content"] = JsonDocument.Parse("""{"type": "string"}""").RootElement
        },
        Required = ["path", "content"]
    }
}

自动创建父目录是个贴心的设计——模型不需要先 mkdir -p

edit_file - 精确修改

new Tool
{
    Name = "edit_file",
    Description = "替换文件中的精确文本。用于局部编辑。",
    InputSchema = new InputSchema
    {
        Type = "object",
        Properties = new Dictionary<string, JsonElement>
        {
            ["path"] = JsonDocument.Parse("""{"type": "string"}""").RootElement,
            ["old_text"] = JsonDocument.Parse("""{"type": "string", "description": "要查找的精确文本"}""").RootElement,
            ["new_text"] = JsonDocument.Parse("""{"type": "string", "description": "替换文本"}""").RootElement
        },
        Required = ["path", "old_text", "new_text"]
    }
}

精确文本匹配是关键——模型必须提供要替换的确切内容,这避免了意外修改。

安全机制

路径安全检查

string SafePath(string p)
{
    var fullPath = Path.GetFullPath(Path.Combine(workDir, p));
    if (!fullPath.StartsWith(workDir))
        throw new InvalidOperationException($"路径逃逸工作区: {p}");
    return fullPath;
}

防止 ../../../etc/passwd 这样的路径遍历攻击。

危险命令阻止

string[] dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"];
if (dangerous.Any(d => command.Contains(d)))
    return "Error: 危险命令已阻止";

基本的安全护栏,阻止显然危险的命令。

输出截断

return output[..Math.Min(output.Length, 50000)];

50KB 上限防止一个巨大的输出撑爆上下文。

超时控制

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60));
await process.WaitForExitAsync(cts.Token);

60 秒超时防止命令挂起。

Agent 循环详解

async Task AgentLoopAsync(List<Message> messages)
{
    while (true)
    {
        // 步骤 1: 调用模型
        var response = await client.CreateMessageAsync(
            modelId, messages,
            new MessageParameters { System = systemPrompt, Tools = tools, MaxTokens = 8000 });

        // 步骤 2: 收集工具调用并打印文本输出
        var toolCalls = new List<ToolUseBlock>();
        foreach (var block in response.Content)
        {
            if (block.Text is not null)
                Console.WriteLine(block.Text.Text);  // 模型的思考
            if (block.ToolUse is not null)
                toolCalls.Add(block.ToolUse);        // 工具调用
        }

        // 步骤 3: 如果没有工具调用,任务完成
        if (response.StopReason != StopReason.ToolUse)
        {
            messages.Add(response.AsRequestMessage());
            return;
        }

        // 步骤 4: 执行每个工具并收集结果
        var results = new List<ContentBlock>();
        foreach (var tc in toolCalls)
        {
            var output = await ExecuteToolAsync(tc.Name, tc.Input);
            results.Add(ContentBlock.CreateToolResult(tc.Id, output));
        }

        // 步骤 5: 添加到对话并继续
        messages.Add(response.AsRequestMessage());
        messages.Add(new Message { Role = "user", Content = [.. results] });
    }
}

为什么工具结果是 "user" 消息?

Anthropic API 的设计:工具结果作为 user 角色的消息发送,包含 tool_result 类型的内容块。这维持了 user/assistant 的交替模式。

消息历史结构

[user] "创建一个 hello world 程序"
[assistant] "我来创建..." + tool_use(write_file, {path: "hello.cs", content: "..."})
[user] tool_result(id, "Wrote 50 bytes to hello.cs")
[assistant] "文件已创建。让我运行它..." + tool_use(bash, {command: "dotnet run hello.cs"})
[user] tool_result(id, "Hello, World!")
[assistant] "程序运行成功,输出了 'Hello, World!'"

工具分发器

async Task<string> ExecuteToolAsync(string name, JsonElement args)
{
    return name switch
    {
        "bash" => await RunBashAsync(args.GetProperty("command").GetString()!),
        "read_file" => RunRead(
            args.GetProperty("path").GetString()!,
            args.TryGetProperty("limit", outvar limit) ? limit.GetInt32() : null),
        "write_file" => RunWrite(
            args.GetProperty("path").GetString()!,
            args.GetProperty("content").GetString()!),
        "edit_file" => RunEdit(
            args.GetProperty("path").GetString()!,
            args.GetProperty("old_text").GetString()!,
            args.GetProperty("new_text").GetString()!),
        _ => $"Unknown tool: {name}"
    };
}

C# 的 switch expression 让分发器代码非常简洁。

关键洞察

1. 模型即代理

传统编程:程序员写逻辑,代码执行。 Agent 编程:程序员提供工具,模型决定逻辑

v1 的代码只做了两件事:

  1. 定义工具(模型能做什么)

  2. 运行循环(让模型持续决策)

2. 工具即接口

工具定义就是给模型的 API 文档:

  • Name - 函数名

  • Description - 函数说明

  • InputSchema - 参数类型

模型根据这些信息决定调用什么、传什么参数。

3. 上下文即记忆

messages 列表就是 Agent 的记忆。它累积:

  • 用户的请求

  • 模型的思考和工具调用

  • 工具的执行结果

模型通过阅读历史来理解当前状态。

运行示例

Mini Claude Code v1 (C#) - /path/to/project
输入 'exit' 退出。

You: 创建一个计算斐波那契数列的函数

我来创建一个计算斐波那契数列的 C# 文件。

> write_file: {"path":"Fibonacci.cs","content":"..."}
  Wrote 450 bytes to Fibonacci.cs

文件已创建。让我运行测试一下:

> bash: {"command":"dotnet run Fibonacci.cs"}
  1, 1, 2, 3, 5, 8, 13, 21, 34, 55

成功!Fibonacci.cs 已创建并测试通过。

从 v1 到 v2

v1 解决了工具效率问题,但对于复杂任务:

You: 重构 auth 模块、添加测试、更新文档

模型可能会:

  • 随机跳转任务

  • 忘记已完成的步骤

  • 中途失焦

v2 将引入 TodoManager——让计划显式化,给模型一个"外部记忆"来追踪进度。

总结

v1 的哲学:

模型即代理。代码提供工具,模型做决策。

4 个工具覆盖了 90% 的编程场景。核心循环只有 30 行。这就是 Agent 的本质——简单到令人惊讶。

下一篇

引入地址

更多推荐