☰
C# 实现简版 Claude Code | 4 个工具覆盖 90% 场景(2):用 TaoToken 统一 Key 打通 dotnet Agent 工具链
2026/10/17 14:17:24 网站建设 项目流程

1. 从 v0 到 v1:为什么 bash 一把梭不够用

如果你已经跟着上一篇把 v0 跑起来了,会发现一个尴尬的现实:一个 bash 工具确实能让模型"动起来",但真拿它写代码,体验相当别扭。我试过让模型用cat读一个 300 行的 C# 文件,它得先构造cat -n xxx.cs这样的命令,再解析带行号的输出;写文件时用echo "..." > file.cs,只要内容里出现双引号、反引号或者换行,转义就开始打架,生成十次能错三次。

这就是 Claude Code 这类工具真正聪明的地方:它没有把"文件读写"这种高频操作塞进 shell,而是拆成了独立工具。模型不需要学 shell 转义规则,只需要填 JSON 参数。本篇要做的,就是把 v0 的单个 bash 工具,升级成 4 个专用工具——bash、read_file、write_file、edit_file,覆盖"探索 → 理解 → 修改 → 验证"这条编程任务的主链路。

同时,多模型调用会带来一个新的工程问题:Key 和 Base URL 散落在代码里,换一个模型就要改一次配置。这里我用 TaoToken 做统一入口,把 Anthropic 兼容协议的请求都收敛到一个 Base URL 和一把 Key 上,appsettings.json里改个 Model ID 就能切换模型,不用动业务代码。

这篇的目标很明确:给你一份能直接dotnet run起来的 C# Agent 骨架,4 个工具全部可复制,配置片段和排错清单都带上。适合已经会写 C#、想搞明白 Agent 工具链到底怎么落地的人。下面从项目结构和配置开始。

2. TaoToken 前置:统一 Key 与 Base URL 的 appsettings 配置

在写工具之前,先把模型通道打通。C# 这边我推荐用官方Anthropic.SDK这个 NuGet 包,它直接支持 Messages API 和 Tool Use,省得自己拼 HTTP 请求。但默认它指向官方地址,我们要做的是把 Base URL 换成 TaoToken 的兼容端点,这样一把 Key 就能管多个模型。

先建项目:

dotnet new console -n MiniClaudeCode cd MiniClaudeCode dotnet add package Anthropic.SDK dotnet add package Microsoft.Extensions.Configuration.Json dotnet add package Microsoft.Extensions.Configuration.Binder

然后在项目根目录建appsettings.json。这里有个坑:dotnet run默认的工作目录是项目目录,但发布后appsettings.json不一定被复制到输出目录,所以 csproj 里要加一条CopyToOutputDirectory。配置内容如下:

{ "TaoToken": { "BaseUrl": "https://taotoken.net/api", "ApiKey": "sk-你的TaoToken密钥", "ModelId": "claude-sonnet-4-5-20250929", "MaxTokens": 8000 }, "Agent": { "WorkDir": ".", "CommandTimeoutSeconds": 60, "MaxOutputBytes": 50000 } }

对应的 csproj 片段,确保配置文件跟着走:

<ItemGroup> <None Update="appsettings.json"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup>

读取配置用一个简单的 POCO 类,别用魔法字符串:

public sealed class TaoTokenOptions { public string BaseUrl { get; set; } = ""; public string ApiKey { get; set; } = ""; public string ModelId { get; set; } = ""; public int MaxTokens { get; set; } = 8000; } public sealed class AgentOptions { public string WorkDir { get; set; } = "."; public int CommandTimeoutSeconds { get; set; } = 60; public int MaxOutputBytes { get; set; } = 50000; }

初始化客户端的代码,关键是BaseUrl要指向 TaoToken 的 API 端点,而不是官方地址:

var config = new ConfigurationBuilder() .SetBasePath(AppContext.BaseDirectory) .AddJsonFile("appsettings.json", optional: false) .Build(); var tao = config.GetSection("TaoToken").Get<TaoTokenOptions>()!; var agentOpts = config.GetSection("Agent").Get<AgentOptions>()!; var client = new AnthropicClient(new APIAuthentication(tao.ApiKey)) { // 关键:把请求指向 TaoToken 兼容端点 };

注意:Anthropic.SDK不同版本对自定义 Base URL 的支持方式不一样。如果你的版本没有直接暴露 BaseUrl 属性,就在HttpClient层做一次BaseAddress替换,或者用HttpClientHandler拦截。核心是让所有 Messages 请求都走https://taotoken.net/api,Key 用 TaoToken 签发的那把。

配置好之后,先别急着写工具,用一段最小代码验证通道是否通:

var resp = await client.Messages.GetClaudeMessageAsync(new MessageParameters { Model = tao.ModelId, MaxTokens = 256, Messages = [new Message { Role = RoleType.User, Content = "只回复两个字:通了" }] }); Console.WriteLine(resp.Message);

如果这里能打印出"通了",说明 Key、Base URL、Model ID 三件套都对。这一步没过,后面工具写得再漂亮也跑不起来。拿到 Key 的入口在 TaoToken 控制台的 API Keys 页面,接入细节可以对照官方接入文档,里面有各语言的最小示例。

3. 可复制配置:4 个工具的 JSON Schema 与注册骨架

工具定义的本质,是给模型写一份"API 文档"。模型看不到你的 C# 代码,它只能读你传过去的Name、Description、InputSchema。所以 Description 写得好不好,直接决定模型会不会用、用得对不对。

先定义工具注册的骨架。我用一个ToolRegistry把定义和执行分开,定义给模型看,执行在本地跑:

public sealed class ToolRegistry { private readonly Dictionary<string, Func<JsonElement, Task<string>>> _executors = new(); public List<Tool> Definitions { get; } = new(); public void Register(string name, string description, string schemaJson, Func<JsonElement, Task<string>> executor) { Definitions.Add(new Tool { Name = name, Description = description, InputSchema = JsonSerializer.Deserialize<InputSchema>(schemaJson)! }); _executors[name] = executor; } public Task<string> ExecuteAsync(string name, JsonElement args) => _executors.TryGetValue(name, out var fn) ? fn(args) : Task.FromResult($"Unknown tool: {name}"); }

四个工具的 Schema 我直接写成字符串常量,方便你复制。bash是最重要的一个,它是模型和外部世界交互的总通道:

const string BashSchema = """ { "type": "object", "properties": { "command": { "type": "string", "description": "要执行的 shell 命令,例如 ls、grep、dotnet build" } }, "required": ["command"] } """;

read_file我加了一个limit参数,这是实战里非常关键的设计。大文件全读进来会瞬间撑爆上下文,模型只需要看前 N 行就能判断结构:

const string ReadFileSchema = """ { "type": "object", "properties": { "path": { "type": "string", "description": "文件的相对路径" }, "limit": { "type": "integer", "description": "最大读取行数,默认全部" } }, "required": ["path"] } """;

write_file的 Description 里我特意强调"会自动创建父目录",这样模型就不需要先mkdir -p,少一轮交互:

const string WriteFileSchema = """ { "type": "object", "properties": { "path": { "type": "string", "description": "目标文件相对路径" }, "content": { "type": "string", "description": "完整文件内容" } }, "required": ["path", "content"] } """;

edit_file用精确文本替换,而不是行号替换。行号在模型改完一次之后就全乱了,精确文本匹配更稳:

const string EditFileSchema = """ { "type": "object", "properties": { "path": { "type": "string" }, "old_text": { "type": "string", "description": "要被替换的精确原文" }, "new_text": { "type": "string", "description": "替换后的新文本" } }, "required": ["path", "old_text", "new_text"] } """;

注册的时候把执行函数一起挂上:

var registry = new ToolRegistry(); registry.Register("bash", "运行 shell 命令,用于 ls、grep、git、dotnet 等。", BashSchema, RunBashAsync); registry.Register("read_file", "读取文件内容,返回 UTF-8 文本。", ReadFileSchema, RunReadAsync); registry.Register("write_file", "写入文件,自动创建父目录。", WriteFileSchema, RunWriteAsync); registry.Register("edit_file", "精确文本替换,用于局部编辑。", EditFileSchema, RunEditAsync);

这里有个容易忽略的点:InputSchema的字段名要和Anthropic.SDK里的类型对齐。如果你用的版本里属性叫Properties而不是Properties的字典结构,编译会报错。遇到这种情况,直接看包的源码或者用JsonSerializer手动构造,别硬猜。

4. 验证请求:dotnet run 跑通工具调用链路

工具注册完,接下来是 Agent 主循环。这是整个骨架的心脏,逻辑其实只有五步:调模型 → 收集工具调用 → 判断是否结束 → 执行工具 → 把结果塞回对话。用 C# 写出来大概是这样:

async Task AgentLoopAsync(List<Message> messages) { while (true) { var response = await client.Messages.GetClaudeMessageAsync(new MessageParameters { Model = tao.ModelId, System = systemPrompt, Tools = registry.Definitions, MaxTokens = tao.MaxTokens, Messages = messages }); 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); } if (response.StopReason != StopReason.ToolUse) { messages.Add(response.AsRequestMessage()); return; } var results = new List<ContentBlock>(); foreach (var tc in toolCalls) { Console.WriteLine($"> {tc.Name}: {tc.Input}"); var output = await registry.ExecuteAsync(tc.Name, tc.Input); results.Add(ContentBlock.CreateToolResult(tc.Id, output)); } messages.Add(response.AsRequestMessage()); messages.Add(new Message { Role = RoleType.User, Content = results }); } }

注意工具结果是以user角色回传的,这是 Anthropic Messages API 的设计:tool_result作为用户消息的一部分,维持 user/assistant 交替的节奏。别自作主张改成 assistant,模型会懵。

现在跑一个真实任务验证。启动程序:

dotnet run

输入:

创建一个计算斐波那契数列的 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 程序运行成功。

这条链路跑通,说明四件事都对:模型正确选择了write_file而不是bash;Schema 参数被正确解析;工具执行结果被正确回传;模型根据结果决定下一步。如果模型一直用bash加echo写文件,说明你的write_fileDescription 不够有吸引力,把"自动创建父目录""完整文件内容"这些优势写清楚。

再测一个编辑场景,验证edit_file:

把 Fibonacci.cs 里的函数名改成 Fib

模型应该会先read_file看内容,再edit_file做精确替换。如果它直接write_file覆盖整个文件,也能work,但会浪费 token。这就是为什么edit_file的 Description 要强调"局部编辑"。

5. 本篇常见错排查:401、local proxy failed 与 reading choices

工具链跑起来之后,报错基本集中在通道层和解析层。我把几个高频错误和对应处理列出来,你对照着看。

401 Unauthorized:九成是 Key 或 Base URL 的问题。先确认appsettings.json里的ApiKey是 TaoToken 签发的,没有多余空格;再确认BaseUrl是https://taotoken.net/api,末尾不要多加/v1之类的路径,不同 SDK 拼接规则不一样,多一层就 404 或 401。如果 Key 是从别处复制来的,注意有没有把sk-前缀漏掉。

local proxy failed / connection refused:这个报错通常出现在你本地配了 HTTP 代理,但代理没起来。检查环境变量HTTP_PROXY、HTTPS_PROXY是否指向了一个不存在的端口。C# 的HttpClient默认会读这些变量。临时清掉再跑:

unset HTTP_PROXY HTTPS_PROXY dotnet run

reading 'choices' of undefined:这个报错说明你用的 SDK 或代码在按 OpenAI 的响应格式解析,但实际返回的是 Anthropic 格式(或者反过来)。Anthropic 的响应里没有choices字段,内容在content数组里。如果你混用了两套 SDK,检查一下MessageParameters和响应解析是不是同一个包。用Anthropic.SDK就全程用它,别中途换成 OpenAI 的客户端。

OAuth / authentication_error:如果你在配置里同时填了 API Key 和某种 OAuth token,可能会冲突。TaoToken 走的是 API Key 认证,把 OAuth 相关的字段清空,只留ApiKey。

工具调用返回 Unknown tool:模型传的工具名和你注册的名字大小写不一致。Anthropic 对工具名大小写敏感,read_file和Read_File是两个东西。注册时统一用小写下划线,Schema 里的Name也保持一致。

输出被截断:如果你看到工具结果只返回了一半,检查MaxOutputBytes配置。我在RunBashAsync里做了 50000 字节的截断,防止一个find /把上下文撑爆。这个值可以调,但别设太大。

排查顺序建议从外到内:先确认dotnet run能打印出模型回复(通道通),再确认工具被正确调用(Schema 对),最后确认工具执行结果正确(本地逻辑对)。三段分开测,比一次性 debug 快得多。

6. 把 Key 收口到一处,让工具链真正可维护

走到这里,你已经有了一个能读、能写、能改、能跑命令的 C# Agent 骨架。4 个工具,核心循环不到 40 行,但覆盖了编程任务里 90% 的动作。真正让它从"玩具"变成"能长期用"的,是配置层的收口——所有模型调用都经过 TaoToken 这一个入口,Key 只有一把,Base URL 只有一个,换模型只改ModelId。

我自己的习惯是把appsettings.json拆成appsettings.json和appsettings.Development.json,开发环境的 Key 不提交到 git,用环境变量覆盖:

export TaoToken__ApiKey="sk-你的密钥"

.NET 的配置系统会自动把TaoToken__ApiKey映射到TaoToken:ApiKey,双下划线是层级分隔符。这样本地调试和 CI 可以用不同的 Key,代码一行不用改。

下一步可以做的方向:给bash加一个命令白名单,只允许dotnet、git、ls、grep这些安全命令;给edit_file加一个"替换前先确认 old_text 唯一"的检查,避免误替换多处;再往上就是 v2 的 TodoManager,让模型把多步任务显式列出来,防止中途跑偏。这些都不难,难的是先把 v1 这条链路跑稳。

如果你还没拿到 Key,去 TaoToken 控制台建一个,然后在接入文档里对照 C# 示例把 Base URL 填对。通道通了,剩下的就是工具设计的手感问题——多跑几个真实任务,你会慢慢知道 Description 该怎么写模型才听话。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询