anthropic-sdk-go Tool Runner 详解:nhost 仓库中工具定义、自动对话循环与流式执行全解析
【免费下载链接】nhostThe Open Source Firebase Alternative with GraphQL.项目地址: https://gitcode.com/GitHub_Trending/nh/nhost
在 nhost 仓库中,vendor/github.com/anthropics/anthropic-sdk-go目录下随附了 Anthropic Go SDK v1.26.0 的 vendored 副本,其中tools.md是该 SDK「工具助手(Tool Helpers)」功能的官方文档。本文以该文档为核心,完整覆盖三种工具定义方式、BetaToolRunner自动对话循环、流式运行与运行时参数控制,并结合 vendored 源码 betatoolrunner.go 逐层还原循环机制、并行工具执行与错误回传的实现细节,帮助读者在 Go 应用中构建可自我修复、可并行、可流式的 Claude 工具调用系统。
文档定位与适用前提
tools.md所属的 SDK 版本可通过 go.mod 确认:仓库依赖github.com/anthropics/anthropic-sdk-go v1.26.0。该版本的能力边界需注意以下前提:
- Go 版本:README 声明最低要求 Go 1.22+。而从源码结构看,betatoolrunner.go 导入了
iter标准库包(All()/AllStreaming()返回iter.Seq2迭代器),iter包自 Go 1.23 起提供,因此工具 Runner 相关特性实际要求 Go 1.23+; - Beta 命名空间:Runner 挂在
client.Beta.Messages之下(BetaToolRunner、BetaMessageNewParams等类型前缀均带Beta),与client.Messages的稳定 Messages API 并行存在; - vendored 副本范围:当前仓库 vendor 目录保留了该 SDK 的 Runner 实现(
betatoolrunner.go)、核心模型文件与本文档tools.md,但 SDK 上游仓库中tools.md末尾指向的examples/tool-runner、examples/tool-runner-streaming示例目录并未包含在 vendor 快照内,完整可运行示例需查阅 SDK 上游仓库。
核心抽象:BetaTool 接口
Runner 能「自动执行工具」的前提是:每个工具不只是 API 参数,而是同时携带定义与执行逻辑。vendored 源码中BetaTool是一个接口(betatoolrunner.go#L14-L23):
type BetaTool interface { // Name returns the tool's name Name() string // Description returns the tool's description Description() string // InputSchema returns the JSON schema for the tool's input InputSchema() BetaToolInputSchemaParam // Execute runs the tool with raw JSON input and returns the result Execute(ctx context.Context, input json.RawMessage) (BetaToolResultBlockParamContentUnion, error) }从源码结构看,Runner 在初始化时会把每个BetaTool转成 API 所需的BetaToolParam(只取 Name/Description/InputSchema)并写入请求参数,同时以工具名为 key 建立toolMap供后续执行分发(betatoolrunner.go#L47-L72)。也就是说:定义与处理函数分离,定义上送模型,处理函数留在本地——这正是toolrunner包三种构造函数的目标。
定义工具:三种构造方式与原始 JSON 输入
文档给出了三种创建工具的方式,推荐程度递减:NewBetaToolFromJSONSchema(自动从结构体生成 schema)、NewBetaToolFromBytes(直接提供 JSON schema 字节)、NewBetaTool(显式传入BetaToolInputSchemaParam)。三者的泛型参数会从 handler 函数签名自动推断,无需手动指定类型。
方式一:从结构体自动生成 Schema(推荐)
NewBetaToolFromJSONSchema依据结构体字段上的jsonschema标签自动生成输入 schema,required、description、enum等约束全部来自标签:
type GetWeatherInput struct { City string `json:"city" jsonschema:"required,description=The city name"` Units string `json:"units,omitempty" jsonschema:"enum=celsius,enum=fahrenheit,description=Temperature units"` } weatherTool, err := toolrunner.NewBetaToolFromJSONSchema( "get_weather", "Get current weather for a city", func(ctx context.Context, input GetWeatherInput) (anthropic.BetaToolResultBlockParamContentUnion, error) { return anthropic.BetaToolResultBlockParamContentUnion{ OfText: &anthropic.BetaTextBlockParam{ Text: fmt.Sprintf("Weather in %s: 72°F, sunny", input.City), }, }, nil }, )注意标签语义:json:"city"决定字段在 JSON 中的键名,jsonschema:"required"表示必填,enum=celsius,enum=fahrenheit把取值约束为枚举,omitempty表示该字段可缺省。README 的「Tool helpers」小节给出了同一模式的完整可运行程序(含anthropic.NewClient()、RunToCompletion与MaxIterations: 5的组合),可作为本节的落地参考。
方式二:使用 JSON 字节
当 schema 由外部系统(如数据库、配置文件)维护,或需要与多语言定义保持一致时,用NewBetaToolFromBytes直接提供 schema 字节:
type GetWeatherInput struct { City string `json:"city"` } weatherTool, err := toolrunner.NewBetaToolFromBytes( "get_weather", "Get current weather for a city", []byte(`{ "type": "object", "properties": { "city": {"type": "string", "description": "The city name"} }, "required": ["city"] }`), func(ctx context.Context, input GetWeatherInput) (anthropic.BetaToolResultBlockParamContentUnion, error) { // Your handler here }, )方式三:显式 Schema,完全控制
NewBetaTool接受BetaToolInputSchemaParam,以 Go map 直接描述 properties,适合需要程序化拼装 schema 的场景:
weatherTool := toolrunner.NewBetaTool( "get_weather", "Get current weather for a city", anthropic.BetaToolInputSchemaParam{ Properties: map[string]any{ "city": map[string]any{ "type": "string", "description": "The city name", }, }, }, handler, )原始 JSON 输入
如果不想让 SDK 替你反序列化,把 handler 的输入类型声明为json.RawMessage或[]byte即可自行解析:
rawTool, err := toolrunner.NewBetaToolFromBytes( "process_data", "Process raw JSON data", schemaBytes, func(ctx context.Context, input json.RawMessage) (anthropic.BetaToolResultBlockParamContentUnion, error) { // Parse the JSON yourself var data map[string]any json.Unmarshal(input, &data) // ... }, )这与BetaTool接口的Execute(ctx, input json.RawMessage)签名呼应:无论哪种构造方式,底层都以原始 JSON 字节交给 handler 封装层完成解码。
BetaToolRunner:自动对话循环
BetaToolRunner自动接管「模型发工具调用 → 本地执行 → 结果回填 → 再问模型」的循环。文档描述的每个迭代为:
- 将当前消息发给 Claude;
- 若 Claude 回复中包含工具调用,则并行执行这些工具;
- 把工具结果追加进对话;
- 重复,直到 Claude 产出最终响应(不再有工具调用)。
基本用法:RunToCompletion
tools := []anthropic.BetaTool{weatherTool} runner := client.Beta.Messages.NewToolRunner(tools, anthropic.BetaToolRunnerParams{ BetaMessageNewParams: anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeSonnet4_20250514, MaxTokens: 1024, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("What's the weather in Tokyo?")), }, }, }) // Run to completion:跑完整段对话直到完成 message, err := runner.RunToCompletion(context.Background())BetaToolRunnerParams内嵌BetaMessageNewParams,因此模型、MaxTokens、System、Messages等 Messages API 参数原样可用,另加一个 Runner 专属字段MaxIterations(betatoolrunner.go#L26-L31)。
遍历消息:All()
All()返回iter.Seq2[*BetaMessage, error]迭代器,每一轮 assistant 消息都会 yield 出来,便于实时渲染工具调用过程:
for message, err := range runner.All(ctx) { if err != nil { log.Fatal(err) } for _, block := range message.Content { switch b := block.AsAny().(type) { case anthropic.BetaTextBlock: fmt.Println("[assistant]:", b.Text) case anthropic.BetaToolUseBlock: fmt.Printf("[tool call]: %s(%v)\n", b.Name, b.Input) } } }block.AsAny()是 SDK 响应联合类型的变体判别方式,按内容块实际类型(文本块 / 工具使用块)分别处理。
逐轮控制:NextMessage()
需要在中途插入用户干预、日志或暂停逻辑时,用NextMessage()一次只推进一轮:
for { message, err := runner.NextMessage(ctx) if err != nil { log.Fatal(err) } if message == nil { break // Conversation complete } // Process the message... }源码级循环机制
NextMessage的实现(betatoolrunner.go#L226-L267)揭示了精确的轮次语义:
- 上限检查:若
MaxIterations > 0且iterationCount >= MaxIterations,标记completed并返回lastMessage(不返回 nil),即「达限即停、保留最后一条模型回复」; - 先执行后请求:先检查
lastMessage中的tool_use块并执行(executeTools),把工具结果作为一条 user 消息追加到Params.Messages;若没有工具调用,则标记完成并返回最后消息; - 发起 API 调用:
iterationCount++后调用messageService.New,并把 assistant 回复的message.ToParam()追加进历史。
RunToCompletion本身就是一个简单循环,反复调用NextMessage直到拿到nil消息(betatoolrunner.go#L273-L283),最终返回最后一条 assistant 消息。All()则在每次NextMessage返回nil且无错误时结束迭代(betatoolrunner.go#L296-L312)。
流式执行:BetaToolRunnerStreaming
流式版本通过NewToolRunnerStreaming()创建,内部同样是逐轮执行工具,但每轮以NewStreaming发起 SSE 流式请求,并在本地用Accumulate累积出完整消息,供下一轮使用。
AllStreaming:外层轮次、内层事件
runner := client.Beta.Messages.NewToolRunnerStreaming(tools, anthropic.BetaToolRunnerParams{ BetaMessageNewParams: anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeSonnet4_20250514, MaxTokens: 1024, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("What's the weather in Tokyo?")), }, }, }) for eventsIterator := range runner.AllStreaming(ctx) { for event, err := range eventsIterator { if err != nil { log.Fatal(err) } switch e := event.AsAny().(type) { case anthropic.BetaRawContentBlockDeltaEvent: switch delta := e.Delta.AsAny().(type) { case anthropic.BetaTextDelta: fmt.Print(delta.Text) } } } }从源码看(betatoolrunner.go#L426-L435),AllStreaming是「迭代器的迭代器」:外层在!r.completed时每轮 yield 一个内层事件序列,内层事件即BetaRawMessageStreamEventUnion。
NextStreaming:逐轮流式
for !runner.IsCompleted() { for event, err := range runner.NextStreaming(ctx) { // Handle streaming events... } }实现细节值得注意(betatoolrunner.go#L345-L407):每一轮流式请求内部先defer stream.Close(),再对事件流逐条finalMessage.Accumulate(event);流结束后的stream.Err()若非空,会作为迭代器错误 yield 出去并中止对话。因此内层迭代器必须被完全消费——文档明确说明这一点,否则累积出的消息不完整,下一轮工具执行会基于残缺状态。
配置与运行时控制
MaxIterations:防止失控循环
MaxIterations限制 API 调用次数。设为0(默认值)表示不限制,Runner 会一直运行到模型停止使用工具:
runner := client.Beta.Messages.NewToolRunner(tools, anthropic.BetaToolRunnerParams{ // ... MaxIterations: 10, // Stop after 10 API calls (0 = no limit) })源码中该检查出现在NextMessage与NextStreaming的入口处,达限时把completed置真,IsCompleted()随即返回 true(betatoolrunner.go#L232-L235、betatoolrunner.go#L352-L355)。
会话中途修改参数
Params是导出字段,可以直接改,改动在下一轮请求生效:
// Update maximum tokens runner.Params.MaxTokens = 2048 // Update maximum iterations runner.Params.MaxIterations = 10 // Update system prompt runner.Params.System = []anthropic.BetaTextBlockParam{ {Text: "You are a helpful assistant."}, } // Add messages to the conversation (direct field access) runner.Params.Messages = append(runner.Params.Messages, anthropic.NewBetaUserMessage( anthropic.NewBetaTextBlock("Now check the weather in London too"), )) // Or use the convenience method runner.AppendMessages(anthropic.NewBetaUserMessage( anthropic.NewBetaTextBlock("Now check the weather in London too"), ))AppendMessages等价于对Params.Messages做 append(betatoolrunner.go#L83-L85)。由于newBetaToolRunnerBase构造时就对初始Messages做了拷贝(betatoolrunner.go#L64),runner 内部历史与调用方传入的切片相互独立,中途追加不会影响外部变量。
检查运行状态
// Get most recent assistant message lastMsg := runner.LastMessage() // Get full conversation history (returns a copy) messages := runner.Messages() // Check iteration count count := runner.IterationCount() // Check if completed if runner.IsCompleted() { // ... }对应源码:Messages()返回历史的副本,可安全修改而不影响 runner 状态(betatoolrunner.go#L89-L93);IterationCount()返回已发起的 API 调用次数。此外还有一个文档未列但源码存在的方法Err(),用于在使用All()/AllStreaming()遍历结束后取出最后一次迭代中发生的错误(betatoolrunner.go#L107-L112)。
错误处理:工具错误回传给模型而非崩溃
工具执行出错时,Runner 不会向上传播 Go error,而是把错误文本包装为「带is_error: true标记」的工具结果发回 Claude,让模型自行恢复或换一种方式重试:
func handler(ctx context.Context, input MyInput) (anthropic.BetaToolResultBlockParamContentUnion, error) { if input.City == "" { return anthropic.BetaToolResultBlockParamContentUnion{}, errors.New("city is required") } // ... }错误转发生成在executeToolUse中(betatoolrunner.go#L165-L195),覆盖三类情况:
| 失败情形 | 回传给模型的错误文本 |
|---|---|
| 工具名未注册 | Error: Tool '<name>' not found |
| 输入 JSON 序列化失败 | Error: Failed to marshal tool input: <err> |
| handler 返回 error | Error: <err> |
三者在源码中统一经由newBetaToolResultErrorBlockParam构造(betatoolrunner.go#L160-L162)。而真正会导致迭代中断的 Go error 只有上下文取消与 API 请求失败两类(executeTools返回ctx.Err()、NextMessage包装failed to get next message)。
并行工具执行:errgroup 实现细节
当 Claude 在一条消息里请求多个工具调用时,Runner 使用golang.org/x/sync/errgroup并行执行(betatoolrunner.go#L119-L158),带来三个特性:
- 并发执行:多个工具调用各自在 goroutine 中运行,互不阻塞,整体时延取决于最慢的工具;
- 正确的取消传播:每个 goroutine 在开跑前检查派生 context
gctx是否已取消;任一工具使组失败或 ctx 取消时,其余工具随之中止; - 结果顺序稳定:结果写入预分配的
results[i](下标即工具调用顺序),保证tool_result块与tool_use块一一对应,再打包成一条 user 消息NewBetaUserMessage(results...)追加进对话。
并发模型上有一条重要约束写在类型注释中:BetaToolRunner与BetaToolRunnerStreaming均不是并发安全的,所有方法必须从单个 goroutine 调用;但当一轮内触发多个工具时,handler 会被并发调用,因此 handler 自身必须线程安全(betatoolrunner.go#L197-L207)。
与 nhost 仓库的关联:Anthropic Provider 集成
nhost 仓库实际消费该 SDK 的位置是 AI 服务的 agents 模块。services/ai/agents/provider/anthropic.go 基于anthropic-sdk-go实现了anthropicMessagesprovider:它通过newAnthropicMessagesConfiguration支持自定义baseURL与额外请求头(即兼容自建 Anthropic Messages 兼容端点),并定义了anthropicMessagesMaxRetries = 2、defaultMaxTokens = 8192两个常量约束重试与输出长度。
从源码结构看,nhost 目前并未直接调用BetaToolRunner系列 API,而是直接基于 Messages API 组织 agent 循环——这反而印证了本文的价值:betatoolrunner.go展示的循环骨架(先执行上一轮工具、再请求模型、达限即停、错误回传)正是自行实现 agent 循环时需要覆盖的全部行为。若 nhost 需要接入「模型自主多轮调用工具」的场景,toolrunner包提供的RunToCompletion/All/ 流式三档抽象是最直接的现成方案;对应 SDK 能力在 CHANGELOG 中标记为 client 层面的BetaToolRunner新特性。
小结
tools.md描述的 Tool Helpers 把「工具定义、本地执行、对话循环、流式输出、错误自愈」收敛为两个入口:toolrunner.New*构造BetaTool,client.Beta.Messages.NewToolRunner(Streaming)获得BetaToolRunner(或BetaToolRunnerStreaming)。结合 vendored 源码可以确认其工程要点:MaxIterations的「达限即停并保留最后回复」语义、Params导出字段支持的中途干预、工具错误转is_error结果的自恢复设计,以及 errgroup 支撑的保序并行执行。这些机制在 Go 1.23+ 环境下可直接复用,也为在 nhost 这类自研 agent 框架中引入标准工具循环提供了可验证的实现参照。
【免费下载链接】nhostThe Open Source Firebase Alternative with GraphQL.项目地址: https://gitcode.com/GitHub_Trending/nh/nhost
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考