拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

agentsdk-go:用 Go 复刻 Claude Code 架构的 Agent 开发框架与 MCP 接入实践

agentsdk-go:用 Go 复刻 Claude Code 架构的 Agent 开发框架与 MCP 接入实践

1. 为什么 Go 开发者需要 agentsdk-go 这类 Claude Code 架构框架

如果你写过 Go,又想自己搭一个能跑工具、能多轮编排的 Agent,大概率会遇到一个尴尬局面:Python 那边 LangGraph、Claude Agent SDK 生态成熟,但 Go 这边要么是薄薄一层 HTTP 封装,要么干脆让你自己从零拼装。agentsdk-go 想解决的正是这个断层——它用 Go 复刻了 Claude Code 的整套 Agent 架构,把 Hooks、MCP、Sandbox、Skills、Subagents、Commands、Plugins 这七块能力都搬了过来,核心 Agent 主循环只有 189 行,整个生产代码约 20,300 行,六大核心模块测试覆盖率在 90% 到 93% 之间。

先说清楚它是什么。agentsdk-go 是一个面向生产环境的 Agent 开发框架,不是简单的 LLM 调用封装。它的定位是给 CLI 工具、CI/CD 流水线、企业内部平台提供完整的 Agent 工程能力。适合谁?适合那些已经熟悉 Go、想自建 Agent 框架、又不想被 Python 运行时和进程模型拖累的开发者。它能做什么?你可以用它注册自定义工具、接入 MCP server、写中间件拦截请求生命周期、跑多轮任务编排,最后通过一个统一的 API 通道把模型侧对接起来。

我试过用 Python 方案做并发 Agent 调度,每个任务起一个完整实例,内存线性增长,CI 里跑几十个任务机器就开始喘。agentsdk-go 的单进程模型在这里优势明显:所有 Agent 共享一个运行时,会话历史用 LRU 管理,Skills、Plugins、MCP 工具按需初始化,没有进程冷启动开销。这也是它相比 Claude Agent SDK 的核心差异——后者基于命令行实例实现,每次调用都要在本地拉起一个完整实例,CPU 和内存占用在高并发下很难接受。

架构上它分成核心层和特性层。核心层六个模块:pkg/agent 负责执行循环,pkg/middleware 提供六层拦截点,pkg/model 做模型适配,pkg/tool 管工具注册和执行,pkg/message 基于 LRU 管理会话历史,pkg/api 暴露统一接口。特性层七个模块覆盖 Hooks、MCP、Sandbox、Skills、Subagents、Commands、Plugins。中间件拦截点从 before_agent 一路到 after_agent,中间穿插 before_model、after_model、before_tool、after_tool,你可以在任意节点注入日志、审计、参数校验、结果过滤。

配置系统直接兼容 Claude Code 的.claude/目录结构,settings.json、skills、commands、agents、plugins 都能复用。配置优先级从托管策略到运行时覆盖再到本地文件,最后落到内置默认值。这意味着你团队里已有的 Claude Code 配置资产不用重写,直接迁移过来就能用。

对 Go 开发者来说,这套框架的价值在于透明和可控。LangGraph 的状态图抽象偏重,调试时很难追踪 Agent 到底走到哪一步;agentsdk-go 把主循环摊开给你看,每个环节都能拦截、观测、调试。你要做的不是接受一个黑盒,而是拿着这套骨架往里填自己的业务逻辑。接下来我会从项目初始化开始,一步步带你跑通 MCP 工具调用链路,并完成一次完整的连通性自检。

2. 用 TaoToken 统一 Key/API 通道完成模型侧前置准备

在动手写 Agent 之前,模型侧的对接得先理顺。agentsdk-go 默认走 Anthropic 协议,你需要一个能稳定调用的 API 通道。这里我用 TaoToken 作为统一入口,它的好处是 Key 和 Base URL 一套配置就能覆盖模型对话、编码计划、控制台和 API Keys 管理,省得在多个平台之间来回切换。

先拿到你的 API Key。打开 https://taotoken.net/api-keys ,登录后创建一个新的 Key,复制保存好。这个 Key 后面会写进环境变量,不要硬编码到代码里。如果你还没注册,可以先从 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 进官网了解整体能力。

TaoToken 的 API 基地址是 https://taotoken.net/api ,注意这个地址不带任何查询参数,直接作为 Base URL 使用。agentsdk-go 的 AnthropicProvider 需要你指定模型名和 API 端点,模型名建议用claude-sonnet-4-5-20250929这类明确的版本标识,避免用模糊别名导致行为不一致。

环境变量配置建议这样写:

export ANTHROPIC_API_KEY="你的TaoToken Key" export ANTHROPIC_BASE_URL="https://taotoken.net/api"

如果你用的是 Claude Code 或 Cline 这类工具,配置方式略有不同。以 Claude Code 为例,它读取的是ANTHROPIC_BASE_URL和ANTHROPIC_AUTH_TOKEN,你需要把 Key 写到 auth token 里。Cline 的 MCP 配置则是在 settings 里指定 base URL 和 api key。不管哪种方式,核心三件套都是 Base URL、Key、Model ID,缺一不可。

这里有个容易踩的坑:很多人把 Base URL 写成带/v1后缀的地址,结果请求 404。TaoToken 的 API 地址就是https://taotoken.net/api,SDK 内部会自己拼接路径,你不需要额外加版本号。另一个坑是 Key 权限,创建时确认勾选了模型调用权限,否则会返回 401。

配置完成后,建议先用一个最小请求验证通道是否通。你可以用 curl 快速测一下:

curl -X POST https://taotoken.net/api/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-sonnet-4-5-20250929", "max_tokens": 64, "messages": [{"role": "user", "content": "ping"}] }'

如果返回里有content字段和正常的文本输出,说明通道没问题。如果返回 401,检查 Key 是否正确复制、有没有多余空格;如果返回 404,检查 Base URL 是不是写成了带/v1的形式。这一步过了,再进 agentsdk-go 的代码环节,能省掉很多排查时间。

对于长期跑编码任务或 Agent 编排的场景,可以考虑用 Coding Plan,它在持续调用和批量任务上有更合适的配额策略。入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。如果你只是想先验证模型对话效果,可以直接用模型对话页面 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 快速试几条 prompt。

模型侧准备好之后,接下来就是把它接进 agentsdk-go 的运行时。记住一点:agentsdk-go 的 AnthropicProvider 会读取环境变量里的 Base URL,所以你不需要在代码里写死地址,保持配置和代码分离,换环境时只改环境变量就行。

3. 可复制的 agentsdk-go 项目初始化与 MCP server 注册配置

这一节给你可以直接复制粘贴的配置和代码。先建项目目录,初始化 Go module:

mkdir agentsdk-demo && cd agentsdk-demo go mod init github.com/yourname/agentsdk-demo go get github.com/cexll/agentsdk-go

前置要求是 Go 1.24.0 或更高版本,低于这个版本编译会报错。装完之后,在项目根目录建.claude/配置目录,结构如下:

.claude/ ├── settings.json ├── settings.local.json ├── skills/ ├── commands/ ├── agents/ └── plugins/

settings.json是项目级配置,settings.local.json是本地覆盖,建议加进.gitignore。配置优先级从高到低是:托管策略、运行时覆盖、settings.local.json、settings.json、内置默认值。一个可用的 settings.json 示例:

{ "permissions": { "allow": ["Bash(ls:*)", "Bash(pwd:*)"], "deny": ["Read(.env)", "Read(secrets/**)"] }, "env": { "MY_VAR": "value" }, "sandbox": { "enabled": false } }

这段配置做了三件事:允许 ls 和 pwd 这类只读命令,禁止读取 .env 和 secrets 目录,关闭沙箱。生产环境建议把 sandbox 打开,后面会讲。

接下来写主程序。这是最小可运行示例,来自 examples/01-basic 的结构:

package main import ( "context" "fmt" "log" "github.com/cexll/agentsdk-go/pkg/api" "github.com/cexll/agentsdk-go/pkg/middleware" modelpkg "github.com/cexll/agentsdk-go/pkg/model" ) func main() { provider := &modelpkg.AnthropicProvider{ ModelName: "claude-sonnet-4-5-20250929", } traceMW := middleware.NewTraceMiddleware(".trace") rt, err := api.New(context.Background(), api.Options{ ModelFactory: provider, Middleware: []middleware.Middleware{traceMW}, }) if err != nil { log.Fatalf("build runtime: %v", err) } defer rt.Close() resp, err := rt.Run(context.Background(), api.Request{ Prompt: "你好", }) if err != nil { log.Fatalf("run: %v", err) } if resp.Result != nil { fmt.Println(resp.Result.Output) } }

关键点四个:模型初始化用 AnthropicProvider,中间件注入 TraceMiddleware 把执行日志写到.trace目录,api.New 创建完整运行时,rt.Run 发起同步调用。运行前确认环境变量已设置:

export ANTHROPIC_API_KEY="你的TaoToken Key" export ANTHROPIC_BASE_URL="https://taotoken.net/api" go run .

现在加 MCP server 注册。MCP 是 Model Context Protocol,用来桥接外部工具,支持 stdio 和 SSE 两种传输方式。注册一个 time-server 的配置:

opts := api.Options{ ModelFactory: provider, MCPServers: []mcp.ServerConfig{ { Name: "time-server", Command: "uvx", Args: []string{"mcp-server-time"}, }, }, }

这段配置告诉运行时:启动一个名为 time-server 的 MCP 服务,用uvx mcp-server-time命令拉起。stdio 模式下,agentsdk-go 会管理子进程的生命周期,你不需要手动启停。如果你用的是 SSE 模式,配置里改成 URL 字段指向远端地址即可。

工具注册规则要记清楚:EnabledBuiltinTools为 nil 时启用所有内置工具,为空切片时禁用所有内置工具,指定具体名称时只启用列出的工具,大小写不敏感。自定义工具通过CustomTools追加。一个同时启用内置工具和自定义工具的配置:

opts := api.Options{ ProjectRoot: ".", ModelFactory: provider, EnabledBuiltinTools: []string{"bash", "file_read"}, CustomTools: []tool.Tool{&EchoTool{}}, MCPServers: []mcp.ServerConfig{ {Name: "time-server", Command: "uvx", Args: []string{"mcp-server-time"}}, }, }

如果你用 Cline 的 MCP 配置,格式类似但字段名不同,核心还是 Base URL、Key、Model ID 三件套。Claude Code 的 settings 里则是通过mcpServers字段声明。不管哪个工具,注册逻辑都是先声明 server 名称和启动命令,再由运行时负责连接和工具发现。

配置写完后,建议先跑一次go build确认没有编译错误。常见问题是 mcp 包路径没导入,或者 tool 接口方法签名不匹配。下一节我会带你验证一次完整的工具调用,看 MCP 链路是否真的通了。

4. 验证一次完整 MCP 工具调用与多轮任务编排

配置写完不代表链路通了,得实际跑一次工具调用才能确认。这一节我带你从请求发起到工具执行再到结果返回,完整走一遍,并给出成功结果的判断标准。

先定义一个自定义工具,用来验证工具注册和执行链路。参考 examples/05-custom-tools 的写法:

type EchoTool struct{} func (t *EchoTool) Name() string { return "echo" } func (t *EchoTool) Description() string { return "返回提供的文本" } func (t *EchoTool) Schema() *tool.JSONSchema { return &tool.JSONSchema{ Type: "object", Properties: map[string]any{ "text": map[string]any{ "type": "string", "description": "要返回的文本", }, }, Required: []string{"text"}, } } func (t *EchoTool) Execute(ctx context.Context, params map[string]any) (*tool.ToolResult, error) { return &tool.ToolResult{ Output: fmt.Sprint(params["text"]), }, nil }

这个工具接收一个 text 参数,原样返回。注册进运行时:

rt, err := api.New(ctx, api.Options{ ProjectRoot: ".", ModelFactory: provider, EnabledBuiltinTools: []string{"bash", "file_read"}, CustomTools: []tool.Tool{&EchoTool{}}, MCPServers: []mcp.ServerConfig{ {Name: "time-server", Command: "uvx", Args: []string{"mcp-server-time"}}, }, })

现在发起一个会触发工具调用的请求。用流式 API 能实时看到工具执行事件:

events := rt.RunStream(ctx, api.Request{ Prompt: "现在几点了?请用 time-server 查询当前时间,然后用 echo 工具把结果复述一遍。", SessionID: "mcp-verify", }) for event := range events { switch event.Type { case "content_block_delta": fmt.Print(event.Delta.Text) case "tool_execution_start": fmt.Printf("\n[工具执行] %s\n", event.ToolName) case "tool_execution_stop": fmt.Printf("[工具结果] %s\n", event.Output) } }

成功的结果长这样:你会先看到[工具执行] time-server或对应的 MCP 工具名,然后是[工具结果]带着时间字符串,接着[工具执行] echo,最后模型把结果组织成自然语言输出。如果只看到文本输出没有工具执行事件,说明模型没有触发工具调用,检查 prompt 是否明确要求使用工具,以及工具是否真的注册成功。

多轮任务编排靠 SessionID 维持上下文。同一个 SessionID 下的多次请求会共享会话历史,LRU 机制会自动管理内存。你可以这样串一个多轮任务:

sessionID := "multi-step-task" // 第一轮:分析 events1 := rt.RunStream(ctx, api.Request{ Prompt: "列出当前目录下的 Go 文件。", SessionID: sessionID, }) for e := range events1 { /* 处理事件 */ } // 第二轮:基于上一轮结果继续 events2 := rt.RunStream(ctx, api.Request{ Prompt: "把刚才列出的文件按行数排序。", SessionID: sessionID, }) for e := range events2 { /* 处理事件 */ }

第二轮能引用第一轮的结果,因为会话历史被保留了。如果你发现第二轮模型"失忆",检查 SessionID 是否一致,以及 message 模块的 LRU 容量是否太小导致历史被淘汰。

中间件在这里也能帮上忙。加一个 BeforeTool 拦截,打印每次工具调用的参数:

loggingMW := middleware.Middleware{ BeforeTool: func(ctx context.Context, req *middleware.ToolRequest) (*middleware.ToolRequest, error) { log.Printf("[TOOL] %s with params: %v", req.Name, req.Parameters) return req, nil }, }

把它加进Middleware切片,每次工具执行前都会打日志。实测下来,这个日志对排查"工具为什么没被调用"特别有用——如果日志里根本没有 TOOL 行,说明模型侧没发起工具调用,问题在 prompt 或工具 schema;如果有 TOOL 行但参数不对,问题在 schema 定义。

验证 MCP 链路时,注意 stdio 模式下子进程的启动需要uvx在 PATH 里。如果报exec: "uvx": executable file not found,先装 uv 工具链。SSE 模式则要确认远端地址可达。工具调用成功后,整个链路就算通了,接下来可以进生产配置。

5. agentsdk-go 常见报错排查对照

这一节把实际会遇到的报错和对应解法列出来,都是我在调试过程中踩过的。

401 Unauthorized。最常见的原因是 Key 没设置或设置错了。检查ANTHROPIC_API_KEY环境变量是否导出,值有没有多余空格或换行。如果你用的是 TaoToken 的 Key,确认创建时勾选了模型调用权限。另一个隐蔽原因是 Base URL 和 Key 不匹配——比如 Key 是 A 平台的,Base URL 却指向 B 平台。确认ANTHROPIC_BASE_URL是https://taotoken.net/api,不带/v1后缀。

local proxy failed / connection refused。这个报错通常出现在你配置了本地代理但代理没启动,或者 Base URL 指向了本地地址。agentsdk-go 本身不要求代理,直接把 Base URL 设成 TaoToken 的 API 地址即可。如果你在 CI 环境里跑,检查网络策略是否允许出站到taotoken.net。还有一种情况是 DNS 解析失败,用curl -v https://taotoken.net/api确认能通。

reading choices: unexpected end of JSON input。这个报错说明响应体不是合法 JSON,常见于 Base URL 写错导致返回了 HTML 错误页。检查 URL 路径是否正确,以及请求头里content-type和anthropic-version是否带上。如果你用的是自定义 HTTP 客户端,确认没有在中间层改写响应体。

OAuth / authentication_error。如果你之前用过 Claude Code 的 OAuth 登录,环境里可能残留了ANTHROPIC_AUTH_TOKEN,它会覆盖ANTHROPIC_API_KEY。检查环境变量,把不用的清掉。agentsdk-go 的 AnthropicProvider 优先读 API Key,但环境变量冲突时行为可能不符合预期。

MCP server 启动失败。报错通常是exec: "uvx": executable file not found或子进程立即退出。先确认uvx在 PATH 里,用which uvx检查。如果命令存在但启动就退出,手动跑一次uvx mcp-server-time看输出,可能是依赖缺失。stdio 模式下,agentsdk-go 会捕获子进程的 stderr,日志里能看到具体原因。

工具没被调用。模型返回了文本但没有 tool_execution 事件。检查三点:工具是否真的注册进CustomTools或EnabledBuiltinTools;工具的 Schema 是否合法,Required 字段和 Properties 是否对应;prompt 是否明确要求使用工具。有时候模型会"偷懒"直接回答,你可以在 prompt 里加"必须使用 XX 工具"来强制触发。

配置不生效。改了 settings.json 但行为没变。检查配置优先级:settings.local.json 会覆盖 settings.json,运行时传入的 Options 会覆盖文件配置。如果你在代码里硬编码了某个参数,文件里的配置就被忽略了。建议把可变配置都放文件里,代码只读不写。

编译报错 undefined: mcp.ServerConfig。包路径没导入。确认 import 了github.com/cexll/agentsdk-go/pkg/mcp,并且 go.mod 里的版本是最新的。如果用的是旧版本,某些类型可能不存在,go get -u github.com/cexll/agentsdk-go升级一下。

排查顺序建议从外到内:先确认网络和 Key 能通(curl 测),再确认 SDK 能初始化(api.New 不报错),再确认模型能响应(简单 prompt),最后确认工具能调用(带工具的 prompt)。每一步都过了,链路就是通的。如果卡在某一步,把对应层的日志打开,TraceMiddleware 会把执行细节写到.trace目录,翻一下就能定位。

6. 把 agentsdk-go 接进你的工作流

跑通验证之后,接下来是怎么把它用起来。agentsdk-go 提供了五层渐进式示例,从 01-basic 到 05-custom-tools,你可以按顺序过一遍。01 是最小单次请求,02 是交互式 REPL 带会话历史,03 是 REST + SSE 服务器监听 8080,04 是完整管道包含中间件、Hooks、MCP、Sandbox、Skills、Subagents,05 是选择性内置工具加自定义工具注册。建议从 03 开始改,因为它已经是一个可用的 HTTP 服务,你只需要替换业务逻辑。

生产环境记得打开 Sandbox。配置里可以限制 CPU 使用率、内存、磁盘和网络访问:

Sandbox: &sandbox.Options{ Enabled: true, Root: "/app/workspace", AllowedHosts: []string{"api.example.com"}, CPULimit: 50.0, MemLimit: 512, DiskLimit: 1024, },

这样即使 Agent 执行了不可预期的命令,影响范围也被限制在沙箱内。Skills 和 Subagents 适合把复杂任务拆开,比如一个 analyzer subagent 专门做代码分析,一个 reviewer subagent 专门做审查,通过主 Agent 调度。

模型侧如果长期跑编码任务,用 Coding Plan 的配额更划算,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。需要管理多个 Key 或查看调用量,去控制台 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,里面有各语言的示例。如果你用 Claude Code 做日常编码,它的 Anthropic 兼容配置可以参考 https://taotoken.net/claudecode-anthropic?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。

最后给一个实用技巧:把 TraceMiddleware 的输出目录加到.gitignore,但保留最近几次的 trace 文件用于排查。每次改完配置或工具注册,先跑一次带工具的 prompt,确认 tool_execution 事件正常出现,再进正式任务。这样能把配置问题和业务问题分开,排查效率高很多。

返回列表