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 Updateappsettings.json CopyToOutputDirectoryPreserveNewest/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).GetTaoTokenOptions()!; var agentOpts config.GetSection(Agent).GetAgentOptions()!; var client new AnthropicClient(new APIAuthentication(tao.ApiKey)) { // 关键把请求指向 TaoToken 兼容端点 };注意Anthropic.SDK不同版本对自定义 Base URL 的支持方式不一样。如果你的版本没有直接暴露 BaseUrl 属性就在HttpClient层做一次BaseAddress替换或者用HttpClientHandler拦截。核心是让所有 Messages 请求都走https://taotoken.net/apiKey 用 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 Dictionarystring, FuncJsonElement, Taskstring _executors new(); public ListTool Definitions { get; } new(); public void Register(string name, string description, string schemaJson, FuncJsonElement, Taskstring executor) { Definitions.Add(new Tool { Name name, Description description, InputSchema JsonSerializer.DeserializeInputSchema(schemaJson)! }); _executors[name] executor; } public Taskstring 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(ListMessage 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 ListToolUseBlock(); 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 ListContentBlock(); 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而不是bashSchema 参数被正确解析工具执行结果被正确回传模型根据结果决定下一步。如果模型一直用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 runreading 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__ApiKeysk-你的密钥.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 该怎么写模型才听话。
阅读完成 · 觉得有帮助?