Effect AI SDK 修复实录:Anthropic 客户端执行工具(Memory / Text Editor / Computer Use / Bash)线上不可用的三项根因与修复
2026/9/14 22:18:46 网站建设 项目流程

Effect AI SDK 修复实录:Anthropic 客户端执行工具(Memory / Text Editor / Computer Use / Bash)线上不可用的三项根因与修复

【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code

本篇文章以@effect/ai-anthropic的一则 patch 级 changeset(.repos/effect-smol/.changeset/pre/fix-anthropic-memory-tool.md)为主线,完整还原 Anthropic 提供商工具在传输(wire)层面不可用的三个根因及其修复方案。读完本文,你将理解 Effect AI SDK 中"provider 定义工具"(provider-defined tool)在请求/响应两端的名称映射机制、命令载荷 Schema 的字段要求,以及Schema.optionalSchema.optionalKey在 Anthropic 编解码器中的关键差异,并能在自己的工具开发中直接复用这些修复经验。

一、问题背景:为什么"客户端执行工具"在线上不可用

该 changeset 针对@effect/ai-anthropic包发布了一个patch级修复,改动点概括如下:

Fix client-executed provider tools (Memory, Text Editor, Computer Use, Bash) which were unusable on the wire.

所谓"客户端执行工具"(client-executed provider tools),是指由 Anthropic 服务端预定义、但由客户端代码实际执行(requiresHandler: true)的工具,包括:

工具提供方 wire 名称SDK 自定义名称用途
MemorymemoryAnthropicMemory跨会话的持久化文件操作(创建、查看、编辑、重命名、删除)
Text Editortext_editorAnthropicTextEditor文件内容读写与目录列举
Computer UsecomputerAnthropicComputerUse鼠标/键盘等屏幕操作
BashbashAnthropicBash沙箱内执行 shell 命令

以 Memory 工具为例,其在 AnthropicTool.ts 中的定义如下:

export const Memory_20250818 = Tool.providerDefined({ id: "anthropic.memory_20250818", customName: "AnthropicMemory", providerName: "memory", requiresHandler: true, parameters: Memory_20250818_Commands, success: Schema.String })

注意customName"AnthropicMemory")与providerName"memory")并不相同。在 Effect AI SDK 中,自定义名称用于解决"同一套 toolkit 中包含来自多个 provider 的同名工具"的命名冲突(例如多个 provider 都有web_search),因此providerDefined工具会同时维护两个名字。这恰恰是本轮修复第一项问题的导火索。

二、根因一:wire 名称与自定义名称映射断裂,导致ToolNotFoundError

2.1 问题现象

当 Anthropic 在响应中返回一个客户端执行工具的调用时,其tool_use内容块里的name字段是服务端 wire 名称(如"memory"),而不是 SDK 层 toolkit 所注册的自定义名称(如"AnthropicMemory")。

在修复之前,makeResponse(以及流式场景对应的makeStreamResponse)在处理tool_use时会直接拿 wire 名称去 toolkit 中查找工具,结果自然找不到——因为 toolkit 的键是自定义名称——最终抛出ToolNotFoundError,导致 Memory、Text Editor、Computer Use、Bash 这四个客户端执行工具在真实链路上全部不可用。

2.2 修复方案:引入toolNameMapper双向映射

修复后的makeResponsetool_use分支中,先通过toolNameMapper.getCustomName(part.name)把 wire 名称反解回自定义名称,再以该名称执行参数转换与回调分发,见 AnthropicLanguageModel.ts:

case "tool_use": { // ... // Map the provider wire name (e.g. "memory") back to the tool's // custom name (e.g. "AnthropicMemory") that the toolkit is keyed by const toolName = toolNameMapper.getCustomName(part.name) const params = yield* transformToolCallParams(options.tools, toolName, part.input) parts.push({ type: "tool-call", id: part.id, name: toolName, params, // ... }) break }

toolNameMapper是在请求入口处基于用户传入的工具列表构造的(AnthropicLanguageModel.ts):

const toolNameMapper = new Tool.NameMapper(options.tools) const request = yield* makeRequest({ config, options, toolNameMapper }) return yield* makeResponse({ options, rawResponse, response, toolNameMapper })

2.3NameMapper的底层实现

NameMapper定义在核心库 Tool.ts,内部维护两张互逆的映射表:

export class NameMapper<Tools extends ReadonlyArray<Any>> { readonly #customToProvider: Map<string, string> = new Map() readonly #providerToCustom: Map<string, string> = new Map() constructor(tools: Tools) { for (const tool of tools) { if (isProviderDefined(tool)) { this.#customToProvider.set(tool.name, tool.providerName) this.#providerToCustom.set(tool.providerName, tool.name) } } } getCustomName(providerName: string): string { return this.#providerToCustom.get(providerName) ?? providerName } getProviderName(customName: string): string { return this.#customToProvider.get(customName) ?? customName } }
  • getCustomName(providerName):wire 名称 → 自定义名称(响应方向,makeResponse使用);
  • getProviderName(customName):自定义名称 → wire 名称(请求方向,makeRequest在组装工具定义与消息时使用,见 AnthropicLanguageModel.ts 与 L1137);
  • 两个 getter 均为"查不到就原样返回"的兜底策略,保证非 provider 定义工具的兼容性。

getCustomName的映射在流式路径同样生效:makeStreamResponse中所有tool_useserver_tool_useweb_fetchweb_searchcode_execution等分支均通过toolNameMapper.getCustomName(...)归一化工具名(AnthropicLanguageModel.ts、L2247 等),这正是 changeset 中"and the streaming equivalents"所指的改动。

2.4 修复后的错误路径仍然存在

值得注意的是,映射修复后ToolNotFoundError并未从代码库中消失,而是退回到真正的"异常兜底"职责:在 transformToolCallParams 中,若映射后仍未在用户工具列表中找到对应工具,仍会抛出带availableTools清单的ToolNotFoundError;随后使用Schema.decodeEffecttool_use载荷做参数校验,校验失败则包装为ToolParameterValidationError。这说明修复目标是"可用的工具不再被误判为未找到",而不是吞掉真实的错误。

三、根因二:MemoryCreateCommand缺失file_text,创建文件时正文被丢弃

3.1 问题现象

Memory 工具的create命令用于在模型的内存空间创建新文件。修复前,MemoryCreateCommand的 Schema 中file_text被声明为可选

file_text: Schema.optional(Schema.NullOr(Schema.String)) // 修复前

效果是:当模型想"创建文件并写入正文"时,payload 中的file_text会被当作可选字段处理,实际发送给客户端执行器的create命令丢掉了文件正文,导致创建出的文件内容为空。

3.2 修复方案:file_text升级为必填字段

修复后,MemoryCreateCommand 将file_text改为必填的Schema.String

export const MemoryCreateCommand = Schema.Struct({ command: Schema.Literal("create"), /** * The path to the file that should be created. */ path: Schema.String, /** * The content to write to the file. */ file_text: Schema.String })

commandSchema.Literal("create")作为判别字段,与deleteinsertrenamestr_replaceview等命令通过Schema.Union组合成Memory_20250818_Commands(AnthropicTool.ts)。字段从可选变为必填后,create命令的载荷在构造、传输与解码三个环节都能保证file_text存在,文件正文不再丢失。

这一改动同时印证了 Anthropic 服务端对该命令的协议要求:create语义上"创建即写内容",正文属于命令的组成部分而非可选项。

四、根因三:Schema.optionalSchema.optionalKey的编解码差异——"Unsupported AST Undefined"

4.1 问题现象

changeset 明确记录了一行异常信息:

which the Anthropic codec rejected with "Unsupported AST Undefined"

客户端执行工具中有一批可选参数,包括:

工具可选参数类型
Memory / Text Editorview_range[start, end]行号元组(1 起始,-1表示读到文件末尾)
Computer Usecoordinate[x, y]像素坐标(缺省时使用当前鼠标位置)
Bashrestartboolean

修复前它们使用Schema.optional(...)声明,例如 Bash 工具(AnthropicTool.ts):

parameters: Schema.Struct({ command: Schema.String, restart: Schema.optional(Schema.Boolean) // 修复前:Anthropic codec 拒绝 })

当 SDK 把工具定义编码为 Anthropic 的 JSON Schema(wire format)时,Schema.optional会生成包含"Undefined"的 AST,Anthropic 编解码器无法理解该 AST 节点,直接报出"Unsupported AST Undefined",使得带可选参数的工具整体无法注册/使用。

4.2 修复方案:统一改用Schema.optionalKey

修复后,所有受影响的可选参数统一改用Schema.optionalKey

  • Memory 的MemoryViewCommand(AnthropicTool.ts):
export const MemoryViewCommand = Schema.Struct({ command: Schema.Literal("view"), path: Schema.String, view_range: Schema.optionalKey(ViewRange) })
  • Text Editor 的TextEditorViewCommand(AnthropicTool.ts):
view_range: Schema.optionalKey(ViewRange)
  • Bash 的restart(AnthropicTool.ts):
restart: Schema.optionalKey(Schema.Boolean)
  • Computer Use 各动作中的coordinate(如左键点击 AnthropicTool.ts、双击 L832、拖拽 L958、滚轮 L1102 等)也全部为Schema.optionalKey(Coordinate)

两者的差异在于:Schema.optional允许字段值本身为undefined(在 AST 中引入Undefined节点),而Schema.optionalKey表示"键可缺省",缺省时整个字段从对象中移除,从而在编码为 Anthropic JSON Schema 时不产生UndefinedAST,规避了编解码器的拒绝行为。这是使用外部 LLM provider 时非常典型的"SDK 内部 Schema 语义"与"服务端协议"之间的适配问题。

五、修复的完整落地与验证

本轮修复的三项改动共同作用于四个工具,最终由@effect/ai-anthropicpatch版本发布(对应 issue #2615)。相关的回归保障体现在:

  • 单测覆盖:AnthropicLanguageModel.test.ts 与 AnthropicClient.test.ts 覆盖了makeResponsetool_use的解析、toolNameMapper映射以及参数编解码路径;
  • 同源修复:同样的NameMapper机制在 OpenAiLanguageModel.ts 与 OpenAiLanguageModel.ts (openai-compat) 中被一致使用,说明"wire 名称 ↔ 自定义名称"双向映射是 Effect AI SDK 各 provider 适配器的通用设计。

在 t3code 仓库中,effect-smol 作为受控第三方仓库被归档在.repos/effect-smol下,根工作区通过 patches/effect@4.0.0-rc.112.patch 对 Effect 生态进行补丁管理。因此,当你升级依赖时,可以重点关注@effect/ai-anthropic的 patch 版本变更记录(CHANGELOG.md),确认是否包含本 changeset 描述的修复。

六、对工具开发者的实践启示

  1. provider 定义工具的命名是"双轨制"customName是 SDK/toolkit 层的键,providerName是服务端 wire 名称。凡是处理工具调用回包的地方,都应通过NameMapper(或等价的getCustomName映射)归一化名称,切勿直接假定 wire 名称与本地工具名一致。
  2. 命令类工具的载荷字段"宁可必填"create类命令一旦语义上要求携带正文,就应当把正文字段声明为必填,避免可选字段在编码/传输环节被静默丢弃。
  3. 面向外部编解码器的可选字段优先用optionalKey:当 Schema 会被编码为第三方(如 Anthropic)的 JSON Schema 时,Schema.optional产生的UndefinedAST 可能不被外部编解码器接受;Schema.optionalKey通过"键缺省"表达可选性,兼容性更好。这也是本次"Unsupported AST Undefined"报错给出的最直接经验。
  4. changeset 是问题定位的浓缩档案:一条 patch changeset 往往包含问题现象、根因、修复位置与关联 issue,是理解开源 SDK 演进脉络的高性价比入口——本次三个根因恰好对应"名称映射、字段必填、Schema 语义"三类高频 bug 类别。

【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询