news 2026/9/16 21:16:55

anthropic-sdk-go Tool Runner 详解:nhost 仓库中工具定义、自动对话循环与流式执行全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
anthropic-sdk-go Tool Runner 详解:nhost 仓库中工具定义、自动对话循环与流式执行全解析

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之下(BetaToolRunnerBetaMessageNewParams等类型前缀均带Beta),与client.Messages的稳定 Messages API 并行存在;
  • vendored 副本范围:当前仓库 vendor 目录保留了该 SDK 的 Runner 实现(betatoolrunner.go)、核心模型文件与本文档tools.md,但 SDK 上游仓库中tools.md末尾指向的examples/tool-runnerexamples/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,requireddescriptionenum等约束全部来自标签:

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()RunToCompletionMaxIterations: 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自动接管「模型发工具调用 → 本地执行 → 结果回填 → 再问模型」的循环。文档描述的每个迭代为:

  1. 将当前消息发给 Claude;
  2. 若 Claude 回复中包含工具调用,则并行执行这些工具;
  3. 把工具结果追加进对话;
  4. 重复,直到 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,因此模型、MaxTokensSystemMessages等 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)揭示了精确的轮次语义:

  1. 上限检查:若MaxIterations > 0iterationCount >= MaxIterations,标记completed并返回lastMessage(不返回 nil),即「达限即停、保留最后一条模型回复」;
  2. 先执行后请求:先检查lastMessage中的tool_use块并执行(executeTools),把工具结果作为一条 user 消息追加到Params.Messages;若没有工具调用,则标记完成并返回最后消息;
  3. 发起 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) })

源码中该检查出现在NextMessageNextStreaming的入口处,达限时把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 返回 errorError: <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 在开跑前检查派生 contextgctx是否已取消;任一工具使组失败或 ctx 取消时,其余工具随之中止;
  • 结果顺序稳定:结果写入预分配的results[i](下标即工具调用顺序),保证tool_result块与tool_use块一一对应,再打包成一条 user 消息NewBetaUserMessage(results...)追加进对话。

并发模型上有一条重要约束写在类型注释中:BetaToolRunnerBetaToolRunnerStreaming不是并发安全的,所有方法必须从单个 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 = 2defaultMaxTokens = 8192两个常量约束重试与输出长度。

从源码结构看,nhost 目前并未直接调用BetaToolRunner系列 API,而是直接基于 Messages API 组织 agent 循环——这反而印证了本文的价值:betatoolrunner.go展示的循环骨架(先执行上一轮工具、再请求模型、达限即停、错误回传)正是自行实现 agent 循环时需要覆盖的全部行为。若 nhost 需要接入「模型自主多轮调用工具」的场景,toolrunner包提供的RunToCompletion/All/ 流式三档抽象是最直接的现成方案;对应 SDK 能力在 CHANGELOG 中标记为 client 层面的BetaToolRunner新特性。

小结

tools.md描述的 Tool Helpers 把「工具定义、本地执行、对话循环、流式输出、错误自愈」收敛为两个入口:toolrunner.New*构造BetaToolclient.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),仅供参考

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

机器人控制器为何全面转向PCIe总线架构

1. 为什么机器人控制器正在悄悄换掉传统总线&#xff0c;全面拥抱PCIe&#xff1f;最近三年&#xff0c;我参与过七款工业级机器人控制器的硬件架构评审&#xff0c;从协作机械臂到AGV调度主控&#xff0c;再到高精度SCARA运动控制器&#xff0c;一个明显趋势是&#xff1a;PCI…

作者头像 李华
网站建设 2026/9/16 21:14:56

Win10局域网远程桌面连接全指南:开启配置与故障排查

在局域网里用远程桌面连另一台Win10电脑&#xff0c;这件事说起来简单&#xff0c;真做起来能碰到不少幺蛾子。早几年我在公司做桌面运维&#xff0c;一个楼层的电脑问题来回跑是常态&#xff0c;后来把常用机器的远程桌面全部开好&#xff0c;坐在工位上就能处理绝大多数问题。…

作者头像 李华
网站建设 2026/9/16 21:14:34

gods-eye-view:空间坐标系重构的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 21:12:54

无限debugger卡死?用Hook和文件替换轻松绕过

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 21:12:47

Flutter代码混淆实战:从R8配置到iOS字符串加密的安全加固指南

1. Flutter-Notebook为什么要做代码混淆&#xff1a;威胁模型与收益1.1 从一段真实的逆向经历说起先讲一个我亲历的案例。去年朋友做了一个Flutter开发的小工具App&#xff0c;因为没做任何加固和混淆&#xff0c;发布后不到两个月就被人在某个论坛上拆了个底朝天。对方用jadx打…

作者头像 李华