news 2026/5/5 7:23:28

C# 实现简版 Claude Code | 4 个工具覆盖 90% 场景(2)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C# 实现简版 Claude Code | 4 个工具覆盖 90% 场景(2)

该系列文章基于github.com/shareAI-lab/learn-claude-code写就,该仓库以大道至简的风格剖析了Claude Code的核心原理,值得大家学习。由于该仓库是基于Python语言,为方便.NET开发者学习,我已经将代码基于.NET 10的dotnet file重写,源码已上传至github,源码地址见文末。

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 的本质——简单到令人惊讶。

点击阅读原文,获取仓库地址:👇👇👇

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/2 12:06:11

【异常】Antigravity IDE 登录异常与网络连接错误排查指南

在使用 Google 推出的 AI 原生 IDE Antigravity 时,部分用户可能会遇到身份验证失效与网络环境冲突导致的叠加错误。本文将针对此类典型故障提供完整的解决思路。 一、 报错内容 在 IDE 的通知栏或输出面板中,通常会同时出现以下两条错误提示: 网络连接错误: Check your i…

作者头像 李华
网站建设 2026/4/19 11:55:46

库克反击中国手机,大量安卓用户转买iPhone,真是风水轮流转!

在以往中国手机品牌都喜欢说苹果用户舍弃iPhone购买他们的手机&#xff0c;苹果往往都不会回应&#xff0c;而这次苹果CEO库克专门提到中国市场“从安卓阵营转入的用户数量创下新高”&#xff0c;这无疑就是在回敬中国手机。库克当然有理由高兴&#xff0c;因为2025年四季度苹果…

作者头像 李华
网站建设 2026/5/2 16:33:10

【2026】 LLM 大模型系统学习指南 (42)

生成式 AI 的进化&#xff1a;从「工具」到「工具人」—— 核心能力与应用逻辑 生成式 AI 的真正突破&#xff0c;不在于能生成文本、图像等内容&#xff0c;而在于完成了从「被动工具」到「主动工具人」的跨越。所谓「工具」&#xff0c;是需要人精准指令、一步一操作的辅助载…

作者头像 李华
网站建设 2026/5/3 21:19:23

$.ajax参数传递详解:GET与POST请求示例

$.ajax是jQuery中用于发起异步HTTP请求的核心方法&#xff0c;正确传递参数是实现前后端数据交互的关键。无论是获取数据、提交表单还是与API交互&#xff0c;掌握参数传递的技巧都能显著提升开发效率和代码质量。 $.ajax参数传递的基本格式是什么 $.ajax方法的参数是一个Jav…

作者头像 李华