首页
/ AI SDK Code Mode 使用指南:让模型在 QuickJS 沙箱中编写代码调用工具

AI SDK Code Mode 使用指南:让模型在 QuickJS 沙箱中编写代码调用工具

2026-09-11 17:56:37作者:廉皓灿Ida

@ai-sdk/code-mode 是 AI SDK(Vercel AI SDK)生态中的一个实验性工具包:它允许模型直接编写 JavaScript/TypeScript 代码来编排你提供的 AI SDK 工具,代码在隔离的 QuickJS 沙箱中运行,并返回一个 JSON 可序列化的结果。本文将以 packages/code-mode/README.md 为核心,结合包内源码(src/run-code-mode.tssrc/code-mode-tool.tssrc/types.ts 等)与测试用例(src/core.test.ts),从安装、基础用法、执行策略、审批、中断续接到安全模型,完整讲解 code mode 的实战姿势与底层原理。

Code Mode 是什么:何时该用它

在常规的 AI SDK 工具调用流程里,模型每轮只输出一次"调用哪个工具、传什么参数"的结构化请求,工具调用的编排逻辑(先调哪个、结果如何变换、是否并行)完全由宿主代码负责。当模型需要连续调用多个工具、对工具结果做中间变换、或者希望并发执行相互独立的调用时,这种"一问一答"的模式就会变得低效且啰嗦。

Code mode 把编排能力直接交给模型:模型产出一段 JavaScript/TypeScript 源码,由沙箱执行。与"模型自由执行任意代码"的 Agent 方案不同,code mode 有两条硬性约束:

  • 只暴露你提供的工具:沙箱内的全局 tools 对象只包含你传给 code mode 的那几个工具,模型写不出工具列表之外的能力;
  • 结果必须是 JSON 可序列化值:沙箱执行完后,返回值会经过 JSON 序列化与反序列化再交还给宿主。

因此 code mode 适合"需要调用多个工具、变换它们的返回结果、或并发运行它们"的场景。例如先并行拉取库存与需求数据,再在沙箱内比较、计算并返回结论——这正是 README 中库存/需求对比示例的用途。

安装与环境要求

pnpm add ai @ai-sdk/code-mode

@ai-sdk/code-mode 是服务端包,要求 Node.js 22.13 或更高版本package.jsonengines 字段明确声明 "node": ">=22.13.0")。它的运行时依赖只有一个 run 包(版本 ^2.0.0),peer 依赖为同工作区的 ai。包内关键词(keywords)包括 ai-sdkagentscode-modequickjssandboxtools,其自述为 "QuickJS-backed code mode tool for AI SDK",即底层沙箱正是 QuickJS。

注意:所有 code mode API 目前均为实验性命名(带有 experimental_ 前缀),API 形态可能在后续版本中调整。

快速开始:在 generateText 中使用 code mode

下面是从 README 完整继承的官方示例,稍作拆解以便看清每个部件的作用。

import {
  DIRECT_TOOL_CALL,
  experimental_codeModeTool as codeModeTool,
} from '@ai-sdk/code-mode';
import { generateText, isStepCount, tool } from 'ai';
import { z } from 'zod';

// 宿主工具 1:查询库存
const getInventory = tool({
  description: 'Get available inventory for a product.',
  inputSchema: z.object({ productId: z.string() }),
  outputSchema: z.object({
    productId: z.string(),
    availableUnits: z.number(),
  }),
  execute: async ({ productId }) => ({
    productId,
    availableUnits: 42,
  }),
});

// 宿主工具 2:查询需求
const getDemand = tool({
  description: 'Get requested units for a product.',
  inputSchema: z.object({ productId: z.string() }),
  outputSchema: z.object({
    productId: z.string(),
    requestedUnits: z.number(),
  }),
  execute: async ({ productId }) => ({
    productId,
    requestedUnits: 31,
  }),
});

const tools = {
  code_mode: codeModeTool({
    executionPolicy: {
      timeoutMs: 30_000,
    },
  }),
  getInventory,
  getDemand,
} as const;

const result = await generateText({
  model,
  tools,
  experimental_toolCallers: {
    getInventory: ['code_mode', DIRECT_TOOL_CALL],
    getDemand: ['code_mode'],
  },
  stopWhen: isStepCount(10),
  prompt: 'Compare inventory and demand for product sku_123.',
});

示例中的几个关键机制:

  • codeModeTool(即 experimental_codeModeTool):创建的是一个"工具调用者"(tool caller),它本身不直接持有宿主工具列表,而是由外层 generateText 调用中的 experimental_toolCallers 配置动态绑定。从源码看,src/code-mode-tool.ts 中它基于 experimental_toolCaller 实现,bind 回调在运行时把 tools 绑定进 createCodeModeTool(tools, options)
  • experimental_toolCallers 映射:它声明了每个宿主工具可以被谁调用。getInventory: <a href="https://link.gitcode.com/i/01dd0f2e615494385ff3984c3ece9813" target="_blank">'code_mode', DIRECT_TOOL_CALL] 表示模型既可以通过 code mode 写代码调用 getInventory,也可以直接调用 getInventorygetDemand: ['code_mode'] 则表示 getDemand 只能经由 code mode 间接调用。DIRECT_TOOL_CALL 是常量字符串 'AI_SDK_DIRECT_TOOL_CALL'(见 [src/direct-tool-call.ts),之所以用字符串而非 Symbol,是为了保证可序列化。
  • stopWhen: isStepCount(10):限制最多 10 步循环,防止模型反复生成代码导致无限循环。

模型随后可能生成如下代码并交给沙箱执行(README 原例):

const [inventory, demand] = await Promise.all([
  tools.getInventory({ productId: 'sku_123' }),
  tools.getDemand({ productId: 'sku_123' }),
]);

return {
  sufficient: inventory.availableUnits >= demand.requestedUnits,
  remaining: inventory.availableUnits - demand.requestedUnits,
};

注意沙箱内工具的调用方式是异步的 tools.name(input),必须 await;对相互独立的调用建议用 Promise.all 并发执行。这段代码最终会返回 { sufficient, remaining } 这样的 JSON 可序列化对象。

直接执行:experimental_runCodeMode

如果不想经过模型生成环节,只想在沙箱里直接跑一段代码并调用宿主工具,可以使用 experimental_runCodeMode(README 原例):

import { experimental_runCodeMode as runCodeMode } from '@ai-sdk/code-mode';

const result = await runCodeMode({
  js: 'return await tools.getInventory({ productId: "sku_123" });',
  tools: { getInventory },
});

从源码看,src/run-code-mode.ts 是 code mode 的核心执行函数,codeModeToolexecute 最终也会走到这里。它接收四类输入(定义见 src/types.tsRunCodeModeInput):

字段 说明
js 要执行的 JavaScript 或 TypeScript 源码
tools 暴露给沙箱的宿主工具集合(CodeModeToolSet
toolExecutionOptions 透传给嵌套工具调用的 AI SDK 执行元数据(toolCallIdmessagesabortSignalcontext 等)
options code mode 选项(executionPolicycontinuationSecurityapproval

runCodeMode 的执行路径大致是:解析执行策略 → 校验源码大小 → 准备续接状态(continuation)→ 把用户源码包装进沙箱运行环境 → 创建宿主桥接函数 → 交给 run 包的 runner 执行 → 处理结果或中断。

沙箱内的源码包装

用户源码并不是原样执行的。createCodeModeSourcesrc/run-code-mode.ts)会把源码包装成如下形态:

const __codeModeBindings={...};       // 工具名 → 宿主桥接函数引用
const tools=new Proxy(Object.create(null),{...}); // 沙箱内全局 tools 代理
const __codeModeResult=await(async()=>{
  <用户源码>
})();
if(__codeModeResult===undefined)return undefined;
return JSON.parse(JSON.stringify(__codeModeResult));

由此可以得到几个可验证的沙箱行为:

  • 支持顶层 await 和顶层 return:源码被包进异步 IIFE;
  • 结果强制 JSON 序列化:返回值会经 JSON.parse(JSON.stringify(...)) 往返,非 JSON 可序列化内容(如函数、循环引用)会在边界被规整或报错;
  • tools 是一个 Proxy:对不存在的工具名会走 __codeMode.missing 路径,从而在宿主侧抛出"Unknown tool"错误,而不是静默返回 undefined;
  • 每次调用都是全新的全局作用域:测试 src/core.test.ts 验证了在一个调用里写入 globalThis.sharedValue,下一次调用读不到它。

TypeScript 支持:类型擦除

沙箱支持"type-stripped TypeScript"——即剥掉类型标注后执行。测试 src/core.test.ts 覆盖了三种情况:简单的 const value: number = 7 注解、interface Item {...} 声明、以及 satisfies 语法。因此模型可以放心写出带类型标注的代码,不必担心语法错误。

沙箱能力边界

自动生成的工具描述(详见下文"自动生成的工具描述"一节)会明确告诉模型:JSON.parse / JSON.stringify 可用,fetch 不可用。也就是说沙箱内没有网络能力,一切外部副作用都必须通过宿主工具完成——这正是隔离设计的关键:模型写的代码没有直接执行副作用的能力,任何对外操作都要经过你提供的、可控的宿主工具

执行策略(executionPolicy):精细控制沙箱资源

codeModeTool / createCodeModeTool / runCodeMode 都接受 options.executionPolicyCodeModeExecutionPolicy 的完整字段、默认值与含义如下(默认值定义见 src/types.ts 的 JSDoc,实际解析在 src/run-code-mode.tsresolveExecutionPolicy):

字段 默认值 含义
timeoutMs 30_000 单次沙箱调用的超时(毫秒),超时抛 CodeModeTimeoutError
memoryLimitBytes 64 * 1024 * 1024(64 MB) 沙箱内存上限
maxStackSizeBytes 2 * 1024 * 1024(2 MB) 栈大小上限
maxResultBytes 1024 * 1024(1 MB) 返回值序列化后的最大字节数
maxConsoleOutputBytes 64 * 1024(64 KB) 控制台输出(console)累计字节上限
maxSourceBytes 256 * 1024(256 KB) 用户源码字节上限,超限抛 CodeModeSourceTooLargeError
maxToolInputBytes 1024 * 1024(1 MB) 单个工具输入参数序列化字节上限
maxToolOutputBytes 4 * 1024 * 1024(4 MB) 单个工具返回结果序列化字节上限
maxBridgeRequests 256 单次沙箱调用可发起的宿主桥接请求总数上限
maxInFlightBridgeRequests 32 同时在途(in-flight)的宿主桥接请求数上限,防止并发失控

例如 README 示例里只配置了超时:

codeModeTool({
  executionPolicy: {
    timeoutMs: 30_000,
  },
})

值得注意的是这些限制在传给底层 run runner 时会被"放大":toRunLimitssrc/run-code-mode.ts)会通过 expandedSerializationLimit 把结果/参数/输出字节限制乘以 2 再加 1024(至少 4096),并考虑包装代码带来的源码体积开销——因为序列化本身也有开销。当底层 runner 因超限抛错时,错误信息中的 limits.* 路径会被翻译回 executionPolicy.* 的命名(见 translateLimitPath),方便你定位是哪个配置项触发的。

自动生成的工具描述:模型写代码的"说明书"

code mode 之所以能让模型"照着写代码",是因为工具描述是根据宿主工具的 schema 自动生成的。createCodeModeTool 在创建时调用 buildCodeModeToolDescription(tools)src/tool-prompt.ts)生成一段包含以下内容的提示文本:

  • 一个 declare const tools: { ... } 形式的 TypeScript 类型声明块,把每个宿主工具的输入/输出 schema 翻译成 TS 类型(支持 $refenumoneOf/anyOf/allOf、嵌套对象、数组、Record<string, unknown> 等,最大递归深度 8 层,见 schemaToTypeInner);
  • 一段工具调用示例:单个工具时给出 const result = await tools.xxx({...}); return ...;;多个工具时给出 Promise.all([...]) 并发调用并聚合成返回对象的模板;
  • 一系列约束声明:把完整程序放在 js 里、顶层 await/return 可用、只允许 tools.name(input) 异步调用、JSON.parse/JSON.stringify 可用、fetch 不可用
  • 若没有宿主工具,则明确告知模型"No host tools. Do not call tools.*."。

从源码推断,示例输入并非随机:它会优先取工具声明的 inputExamples,否则基于 schema 生成样例值(sampleFromSchemaInner,支持 defaultexamplesformat 等)。这意味着给宿主工具写清晰的 description、inputSchema、outputSchema,直接影响模型生成代码的质量——schema 越精确,生成的类型声明和调用示例就越准确。

宿主工具的调用链与工具级防护

沙箱代码调用 tools.xxx(input) 时,会通过宿主桥接函数 invokeHostToolsrc/tool-invocation.ts)走一条完整链路:

  1. 中止检查:外层 abortSignal 已中止则立即抛出;
  2. 存在性检查:工具不存在抛 CodeModeToolError('Unknown tool: ...'),无 execute 抛"does not have execute()";
  3. 输入校验:按工具 inputSchema 校验(validateToolInput),校验失败抛 CodeModeToolError,错误详情里带 toolNamecause
  4. 审批检查:见下一节;
  5. 执行:调用工具 execute,支持同步、Promise 以及 AsyncIterable(流式输出取最后一个值,见 executeHostTool);输出序列化后回传沙箱。

调用期间的 toolCallId 形如 ${outerToolCallId}:tool-${requestIndex},与外层 AI SDK 调用形成可追踪的嵌套关系;同时工具的 context / experimental_contextmessages 等执行上下文会被透传(CodeModeToolExecutionOptions,见 src/types.ts)。

工具审批:callback 与 interrupt 两种模式

对于敏感工具,AI SDK 工具本身支持 needsApproval。code mode 把这一机制带进了沙箱调用链(requiresApprovalsrc/tool-invocation.ts)。通过 options.approvalsrc/types.ts)配置审批行为:

codeModeTool({
  approval: {
    mode: 'callback',            // 默认值
    onApprovalRequired: async (request) => {
      // request: { toolName, input, toolCallId }
      return 'approved';         // 或 { approved: false, reason: '...' }
    },
  },
})

approval.mode 取值与行为:

模式 行为
'callback'(默认) 调用 onApprovalRequired({ toolName, input, toolCallId }),回调返回 'approved' / 'denied'{ approved: boolean; reason?: string }ApprovalDecision)。未配置回调时抛 CodeModeToolApprovalRequiredError;被拒绝时抛 CodeModeToolApprovalDeniedError(均见 src/errors.ts
'interrupt' 不调用回调,改为返回一个类型为 code-mode-interrupt 的审批中断对象,把决定权交还给宿主流程

在 interrupt 模式下,配套的辅助函数可以帮助你把审批嵌入 AI SDK 的消息循环(见 src/approval-continuation.ts):

  • experimental_isCodeModeApprovalInterrupt:判断一个值是否是审批中断(payload kind'ai-sdk-code-mode/tool-approval',见 src/approval.ts);
  • experimental_toCodeModeApprovalMessages:把审批中断转换为 ModelMessage[](含 tool-calltool-approval-request 两个 part),可追加到对话历史中交给上层模型/UI;
  • experimental_getCodeModeApprovalResponse:从消息中反向提取用户的 tool-approval-response(含 approvalIdapprovedreason);
  • experimental_continueCodeModeApproval:携带审批响应继续执行被中断的沙箱调用;approvalId 不匹配会抛 CodeModeProtocolError

中断与续接:沙箱外的人工确认

除了审批中断,宿主工具还可以主动发起任意自定义中断(context.interrupt(payload),payload 需是含 kind 字段的对象,见 src/types.tsCodeModeInterruptPayload)。这时沙箱执行会挂起,返回一个 CodeModeInterrupttype: 'code-mode-interrupt'),并携带经过签名的续接凭证(continuation)。

配套 API(见 src/interrupt-continuation.ts):

import {
  experimental_getCodeModeInterrupt,
  experimental_continueCodeModeInterrupt,
  experimental_unwrapCodeModeResult,
} from '@ai-sdk/code-mode';

// 从 generateText / runCodeMode 的结果中提取中断
const interrupt = experimental_getCodeModeInterrupt(result);

// 分类结果:已完成 or 被中断
const { status, output, interrupt } = experimental_unwrapCodeModeResult(result);

// 带着用户决议继续执行
const finalResult = await experimental_continueCodeModeInterrupt({
  interrupt,
  resolution: { approved: true },   // 由你的业务逻辑给出
  tools,
});

experimental_continueCodeModeInterrupt 内部会把源码、工具集合、continuation 凭证与本次决议重新交给 runCodeMode 恢复执行;getCodeModeInterrupt 还能递归地从 toolResults / content 数组以及 {type:'json'|'text'} 包裹中提取中断。每次续接只能按序消费一个 pending 中断(prepareContinuation 会校验 interruptId 与签名账本是否匹配)。

续接凭证的签名安全

continuation 不是裸的状态对象,而是经过 HMAC-SHA256 签名的凭证(CodeModeContinuationAuth,见 src/types.tssrc/continuation-capability.ts)。默认签名密钥是进程启动时 randomBytes(32) 生成的随机密钥,续接有效期默认 1 小时maxAgeMs 默认 60 * 60 * 1000)。校验逻辑包括:信封结构、过期时间、签发时间不早于当前时间 60 秒以上、常量时间比较(timingSafeEqual)签名。这意味着:

  • 续接状态不能被客户端篡改:源码、工具名、中断清单都在签名覆盖范围内;
  • 多实例部署时需要显式配置共享密钥,否则不同实例间无法互相续接(使用 experimental_setCodeModeContinuationSigningKey 设置全局默认,或在 options.continuationSecurity 里传入 signingKeymaxAgeMs);
  • 无状态续接:沙箱暂停时不占用 worker,凭证可持久化到数据库后跨请求恢复。

错误模型:稳定可机器读取的错误码

code mode 的所有错误都继承自 CodeModeErrorsrc/errors.ts),每个错误带稳定的 code 字符串和可选的 details 结构化诊断信息。下表汇总了所有错误类型:

错误类 code 触发条件
CodeModeTimeoutError CODE_MODE_TIMEOUT 沙箱执行超过 executionPolicy.timeoutMs
CodeModeAbortedError CODE_MODE_ABORTED 外层 AI SDK abort signal 中止
CodeModeConcurrencyError CODE_MODE_CONCURRENCY_LIMIT 达到进程级 worker 上限(setMaxWorkers
CodeModeSourceTooLargeError CODE_MODE_SOURCE_TOO_LARGE 源码超过 maxSourceBytes
CodeModeBridgeLimitError CODE_MODE_BRIDGE_LIMIT 超过桥接请求数限制
CodeModeDetachedBridgeRequestError CODE_MODE_DETACHED_BRIDGE_REQUEST 沙箱代码发起宿主桥接工作后未 await 就返回(游离请求)
CodeModeProtocolError CODE_MODE_PROTOCOL_ERROR 主线程与 worker 间协议消息非法/不匹配,或续接凭证校验失败
CodeModeToolError CODE_MODE_TOOL_ERROR 嵌套宿主工具调用失败(如未知工具、输入校验失败)
CodeModeToolApprovalRequiredError CODE_MODE_TOOL_APPROVAL_REQUIRED 需要审批但未配置回调
CodeModeToolApprovalDeniedError CODE_MODE_TOOL_APPROVAL_DENIED 审批被拒绝(含 reason)

另外,底层 runner 抛出的错误在传播时会被翻译为上述类型(toCodeModeRuntimeError),并且错误堆栈会被改写:把 run.js:行:列 映射回 code-mode.js:行:列(源码行偏移 1 行,见 translateSourceStack),让堆栈指向你的用户源码位置而非包装代码,便于调试模型生成的代码。

并发控制:experimental_setMaxWorkers

QuickJS 沙箱运行在 worker 上,进程内并发执行是有限额的。@ai-sdk/code-moderun 包重新导出了 experimental_setMaxWorkers(见 src/index.ts),用于设置进程全局的 worker 上限;达到上限时新的调用会抛 CodeModeConcurrencyError。测试代码里也出现了 setMaxWorkers(32) 的用法(src/core.test.ts)。在并发压力大的服务里,建议按可用 CPU/内存调大该值,同时留意 maxInFlightBridgeRequests 对单次调用内并发的约束。

测试验证:core.test.ts 揭示的沙箱行为

包的单元测试(src/core.test.ts)可以当作行为契约来读,除前面提到的点外还包括:

  • 运行 JS 并返回 JSON 值return { answer: 40 + 2 }{ answer: 42 }
  • 无返回值返回 undefined:脚本没有 return 时结果为 undefined
  • 类型擦除:类型注解、interfacesatisfies 均被剥离;
  • JSON 工具可用:沙箱内 JSON.parse/JSON.stringify 正常工作;
  • 全局作用域隔离:每次调用全新全局环境;
  • 高频并发Promise.all 同时发起 20 次独立调用也能正常完成。

包内还有 e2e 测试(src/e2e/code-mode-haiku.e2e.test.ts,基于 Claude Haiku 走真实模型链路)以及针对选项解析、序列化、审批、中断续接、工具调用的专项测试(src/options.test.tssrc/approval-continuation.test.tssrc/run-compatibility.test.ts 等),感兴趣的读者可以直接阅读这些测试文件深入了解边界行为。

安全模型与实践建议

综合以上源码事实,code mode 的安全模型可以概括为"最小暴露 + 边界序列化 + 凭证签名"三层:

  1. 最小暴露:沙箱代码只能调用你显式提供的宿主工具;没有 fetch、没有文件系统、没有进程访问,一切副作用都收敛在宿主工具里。tools Proxy 对未知工具名直接报错,杜绝动态逃逸;
  2. 边界序列化:所有进出沙箱的数据(工具输入、工具输出、最终结果、console 输出)都有字节上限,并强制 JSON 往返,避免对象引用/原型链等跨边界污染;
  3. 凭证签名:中断/续接状态经 HMAC-SHA256 签名并带过期时间,多实例环境必须配置共享签名密钥,续接凭证应视为不透明数据原样持久化,不要手工读写其字段(src/types.tsCodeModeContinuation 注释明确如此要求)。

实战建议:

  • 给宿主工具写精确的 inputSchema/outputSchema/description,它们会被翻译成沙箱内的类型声明与调用示例,直接决定模型生成代码的准确率;
  • 对执行敏感操作的工具配置 needsApproval,并用 approval.mode: 'interrupt' 把审批交给 UI/人工流程;
  • 根据业务容忍度收紧 executionPolicy(尤其 timeoutMsmaxBridgeRequests),并在 stopWhen 上限制循环步数;
  • 服务端多实例部署时务必调用 experimental_setCodeModeContinuationSigningKey 设置稳定的共享密钥,并让密钥与业务密钥同等级别管理。

从"模型生成代码执行"到"审批、中断、续接、并发、错误码",@ai-sdk/code-mode 为 TypeScript 生态的 AI 应用提供了一个兼顾灵活性与可控性的工具编排范式:灵活的部分(编排逻辑)交给模型在隔离沙箱内自由书写,可控的部分(副作用、资源、审批、安全)牢牢留在宿主手中。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
34
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.16 K
2.78 K
docsdocs
暂无描述
Markdown
904
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
936
1.86 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
862
1.36 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
535
606
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.38 K
1.47 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
4.02 K
1.03 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
549
400
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.07 K
538