LangChainJS结构化工具复杂Zod类型生成不兼容JSON Schema求助
问题定义
实现了一个基于StructuredTool的Notion子页面创建工具,代码如下:
export class CreateChildNotionPageTool extends StructuredTool { name = "createChildNotionPageTool"; description = ` Creates new notion page inside parent page from provided input. Accepts children as content of the page. `; schema = createChildNotionPageToolSchema; private api = new NotionAPIWrapper(); protected async _call({ page, children, }: CreatePagePayload): Promise<unknown> { return this.api.createPage({ ...page, children }); } }
工具的Schema由多个跨文件的Zod结构组合而成:
const createChildNotionPageToolSchema = z.object({ page: CreateChildPageSchema, children: z.array(NotionBlockSchema).optional(), }); type CreatePagePayload = z.infer<typeof createChildNotionPageToolSchema>;
将工具绑定到ChatOpenAI调用时,触发错误:Invalid schema for function 'createNotionPageTool'. Please ensure it is a valid JSON Schema.
问题根源
LangChainJS底层使用zod-to-json-schema生成工具的JSON Schema,但默认会保留$ref引用,而OpenAI无法解析包含跨文件$ref的Schema。手动转换为无引用的扁平Schema可正常运行,但生成的代码长达4000行,维护成本极高。
解决方案
1. 重写单个工具的Schema生成逻辑
直接在工具类中重写getJSONSchema方法,手动调用zod-to-json-schema并指定$refStrategy: none:
import { StructuredTool } from "langchain/tools"; import { zodToJsonSchema } from "zod-to-json-schema"; export class CreateChildNotionPageTool extends StructuredTool { // 原有属性和方法不变 getJSONSchema() { return zodToJsonSchema(this.schema, { $refStrategy: "none", }); } }
优点:针对性修改,代码侵入性低;缺点:每个工具都需要单独重写,适合少量工具场景。
2. 手动构建工具并传入预扁平化Schema
提前生成扁平化的JSON Schema,直接传入Tool类替代StructuredTool:
import { ChatOpenAI } from "@langchain/openai"; import { Tool } from "langchain/tools"; import { zodToJsonSchema } from "zod-to-json-schema"; // 预生成无引用的JSON Schema const flatSchema = zodToJsonSchema(createChildNotionPageToolSchema, { $refStrategy: "none", }); // 手动创建工具 const createChildNotionPageTool = new Tool({ name: "createChildNotionPageTool", description: "Creates new notion page inside parent page from provided input. Accepts children as content of the page.", schema: flatSchema, async func(input) { const api = new NotionAPIWrapper(); return api.createPage({ ...input.page, children: input.children }); }, }); // 绑定工具到LLM const llm = new ChatOpenAI({ model: "gpt-3.5-turbo" }).bindTools([createChildNotionPageTool]);
优点:跳过StructuredTool的默认处理逻辑,灵活度高;缺点:需要手动维护工具的输入类型,适合临时或简单工具场景。
3. 封装通用扁平化Schema工具类
创建一个继承自StructuredTool的通用类,统一处理Schema扁平化逻辑,所有工具都可复用:
import { StructuredTool, StructuredToolOptions } from "langchain/tools"; import { ZodSchema } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; // 通用扁平化工具类 export class FlatStructuredTool<RunInput extends ZodSchema> extends StructuredTool<RunInput> { constructor(options: Omit<StructuredToolOptions<RunInput>, "getJSONSchema">) { super(options); } getJSONSchema() { return zodToJsonSchema(this.schema, { $refStrategy: "none", }); } } // 业务工具继承通用类 export class CreateChildNotionPageTool extends FlatStructuredTool<typeof createChildNotionPageToolSchema> { name = "createChildNotionPageTool"; description = ` Creates new notion page inside parent page from provided input. Accepts children as content of the page. `; schema = createChildNotionPageToolSchema; private api = new NotionAPIWrapper(); protected async _call({ page, children }: CreatePagePayload): Promise<unknown> { return this.api.createPage({ ...page, children }); } }
优点:一次封装,所有工具复用,维护成本低;缺点:需要额外封装通用类,适合多工具场景。
前置依赖
确保安装zod-to-json-schema依赖:
npm install zod-to-json-schema
内容的提问来源于stack exchange,提问作者Pavlo Sobchuk

