C# 实现简版 Claude Code | 4 个工具覆盖 90% 场景(2)
目录
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 的代码只做了两件事:
-
定义工具(模型能做什么)
-
运行循环(让模型持续决策)
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 的本质——简单到令人惊讶。
更多推荐

所有评论(0)