跳转到内容
本站为社区维护的非官方中文翻译,与 Cloudflare, Inc. 无关联。本站不提供账户登录,请勿在任何非官方站点输入您的 Cloudflare 账户信息。官方文档:developers.cloudflare.com

构建 AI 代码解释器

最后更新 查看 MarkdownAgent 设置

在本教程中,你将构建一个通过运行代码来回答问题的 Worker。Workers AI 模型编写 JavaScript,Dynamic Worker 在无网络访问的情况下运行它,模型再将结果用于回答。

完成后,Worker 会回答关于质数的问题,并返回模型运行的代码:

{
    "answer": "There are 9,592 prime numbers below 100,000.",
    "runs": [
        {
            "code": "function isPrime(n){if(n<2)return false; if(n%2===0) return n===2; for(let i=3;i*i<=n;i+=2){if(n%i===0) return false;} return true;} let count=0; for(let i=2;i<100000;i++){ if(isPrime(i)) count++; } return count;",
            "output": { "result": 9592, "logs": [] }
        }
    ]
}

你将学习如何:

  • 在没有网络访问且带有 CPU 时间限制的 Dynamic Worker 中运行模型生成的 JavaScript。
  • 向模型提供运行代码并返回结果的工具。
  • 将代码错误发回模型,以便它修复代码。

前提条件

  1. 注册 Cloudflare 账户 ↗。
  2. 安装 Node.js ↗。

Node.js 版本管理器

使用 Vite+ (vp env) ↗ 安装和管理 Node.js 版本。

有关支持的 Node.js 版本,请参阅 Wrangler 系统要求。

1. 创建项目

  1. 创建一个 Worker 项目:

    npm create cloudflare@latest -- sandbox-code-interpreter --category=hello-world --type=hello-world --lang=ts --no-deploy --no-git --no-agents
  2. 进入项目目录:

    cd sandbox-code-interpreter
  3. 安装 Agents SDK、AI SDK ↗ 和 Zod ↗。Worker 使用 agents/models/ai-sdk 中的 createAI 调用 Workers AI:

    npm i agents ai @ai-sdk/provider zod
  4. 替换 wrangler.jsonc,添加 Workers AI 绑定和 Worker Loader 绑定:

    {
        "$schema": "node_modules/wrangler/config-schema.json",
        "name": "sandbox-code-interpreter",
        "main": "src/index.ts",
        // Set this to today's date
        "compatibility_date": "2026-11-14",
        "observability": {
            "enabled": true,
        },
        "upload_source_maps": true,
        "ai": {
            "binding": "AI",
        },
        "worker_loaders": [
            {
                "binding": "LOADER",
            },
        ],
    }
    "$schema" = "node_modules/wrangler/config-schema.json"
    name = "sandbox-code-interpreter"
    main = "src/index.ts"
    # Set this to today's date
    compatibility_date = "2026-11-14"
    upload_source_maps = true
    
    [observability]
    enabled = true
    
    [ai]
    binding = "AI"
    
    [[worker_loaders]]
    binding = "LOADER"

    AI 调用 Workers AI 模型。LOADER 在运行时创建 Dynamic Workers。

  5. 为绑定生成类型:

    npx wrangler types

2. 在 sandbox 中运行代码

创建 src/sandbox.ts。runJavaScript() 函数将代码加载到新的 Dynamic Worker,并返回其结果和日志。如果代码未在五秒内完成,它会抛出错误:

src/sandbox.jsjs
const timeoutMs = 5_000;

export async function runJavaScript(env, code) {
	const sandbox = env.LOADER.load({
		compatibilityDate: "2026-11-14",
		mainModule: "code.js",
		modules: {
			"code.js": `
                import { WorkerEntrypoint } from "cloudflare:workers";

                function format(value) {
                    return typeof value === "string" ? value : JSON.stringify(value);
                }

                export class Code extends WorkerEntrypoint {
                    async run() {
                        const logs = [];
                        const console = {
                            log: (...values) => logs.push(values.map(format).join(" ")),
                        };
                        const result = await (async () => {
                            ${code}
                        })();
                        return { result, logs };
                    }
                }
            `,
		},
		globalOutbound: null,
		limits: { cpuMs: 50 },
	});

	let timer = null;
	const timeout = new Promise((_, reject) => {
		timer = setTimeout(() => {
			reject(new Error(`The code did not finish within ${timeoutMs} ms`));
		}, timeoutMs);
	});

	try {
		return await Promise.race([sandbox.getEntrypoint("Code").run(), timeout]);
	} finally {
		clearTimeout(timer);
	}
}
src/sandbox.tsts
import type { WorkerEntrypoint } from "cloudflare:workers";

export type Output = {
    result?: unknown;
    logs: string[];
};

type CodeEntrypoint = WorkerEntrypoint & {
    run(): Promise<Output>;
};

const timeoutMs = 5_000;

export async function runJavaScript(env: Env, code: string): Promise<Output> {
    const sandbox = env.LOADER.load({
        compatibilityDate: "2026-11-14",
        mainModule: "code.js",
        modules: {
            "code.js": `
                import { WorkerEntrypoint } from "cloudflare:workers";

                function format(value) {
                    return typeof value === "string" ? value : JSON.stringify(value);
                }

                export class Code extends WorkerEntrypoint {
                    async run() {
                        const logs = [];
                        const console = {
                            log: (...values) => logs.push(values.map(format).join(" ")),
                        };
                        const result = await (async () => {
                            ${code}
                        })();
                        return { result, logs };
                    }
                }
            `,
        },
        globalOutbound: null,
        limits: { cpuMs: 50 },
    });

    let timer: number | null = null;
    const timeout = new Promise<never>((_, reject) => {
        timer = setTimeout(() => {
            reject(new Error(`The code did not finish within ${timeoutMs} ms`));
        }, timeoutMs);
    });

    try {
        return await Promise.race([
            sandbox.getEntrypoint<CodeEntrypoint>("Code").run(),
            timeout,
        ]);
    } finally {
        clearTimeout(timer);
    }
}

Dynamic Worker 会限制生成代码能做的事:

  • globalOutbound: null 会使 fetch() 和 connect() 抛出错误,因此代码没有网络访问。
  • Dynamic Worker 收不到 env,因此代码无法调用 Workers AI 或你的 Worker 能访问的任何其他资源。
  • limits: { cpuMs: 50 } 会停止使用超过 50 毫秒 CPU 时间的代码。请参阅 自定义资源限制。
  • 如果代码在不使用 CPU 的情况下等待(例如长时间的 setTimeout()),五秒超时会阻止你的 Worker 继续等待。计时器运行在你的 Worker 中,因此生成的代码无法更改它。
  • 每次调用 load() 都会创建新的 Dynamic Worker,因此一次运行中的值在下一次运行中不可用。

生成的代码会成为异步函数的函数体,因此它可以使用 await,并且必须 return 其结果。本地 console 对象会收集 console.log() 输出。代码仍然可以更改 Dynamic Worker 内部的任何内容,包括其包装器,但无法访问 Dynamic Worker 未收到的任何内容。

3. 让模型运行代码

替换 src/index.ts。Worker 向模型提供 runJavaScript 工具,并返回模型的答案以及模型运行的每段代码:

src/index.jsjs
import { generateText, isStepCount, tool } from "ai";
import { createAI } from "agents/models/ai-sdk";
import { z } from "zod";
import { runJavaScript } from "./sandbox";

const model = "@cf/openai/gpt-oss-120b";

const Question = z.object({ question: z.string().min(1) });

const instructions = `Answer the question.
Always call the runJavaScript tool before you answer. Do not calculate, count, or sort in your head.
Write the body of an async JavaScript function and return the value you need.
If the tool returns an error, fix the code and run it again.
Answer in one or two plain-text sentences.`;

export default {
	async fetch(request, env) {
		if (request.method !== "POST") {
			return new Response("Send a POST request", { status: 405 });
		}

		const body = Question.safeParse(await request.json().catch(() => null));

		if (!body.success) {
			return Response.json(
				{ error: "Send a JSON body with a question" },
				{ status: 400 },
			);
		}

		const ai = createAI({ binding: env.AI });
		const runs = [];

		try {
			const result = await generateText({
				model: ai(model),
				instructions,
				prompt: body.data.question,
				tools: {
					runJavaScript: tool({
						description:
							"Run JavaScript in a sandbox with no network access. The code is the body of an async function. Return a JSON-serializable value. console.log output is captured.",
						inputSchema: z.object({
							code: z.string().describe("Body of an async JavaScript function"),
						}),
						execute: async ({ code }) => {
							try {
								const output = await runJavaScript(env, code);
								runs.push({ code, output });
								return output;
							} catch (error) {
								const message =
									error instanceof Error ? error.message : String(error);
								runs.push({ code, error: message });
								return { error: message };
							}
						},
					}),
				},
				// Stop after five model steps, so a model that keeps failing cannot
				// run code indefinitely
				stopWhen: isStepCount(5),
			});

			return Response.json({ answer: result.text, runs });
		} catch (error) {
			console.error("Model request failed", error);
			return Response.json(
				{ error: "The model request failed. Try again later.", runs },
				{ status: 502 },
			);
		}
	},
};
src/index.tsts
import { generateText, isStepCount, tool } from "ai";
import { createAI } from "agents/models/ai-sdk";
import { z } from "zod";
import { runJavaScript, type Output } from "./sandbox";

const model = "@cf/openai/gpt-oss-120b";

const Question = z.object({ question: z.string().min(1) });

const instructions = `Answer the question.
Always call the runJavaScript tool before you answer. Do not calculate, count, or sort in your head.
Write the body of an async JavaScript function and return the value you need.
If the tool returns an error, fix the code and run it again.
Answer in one or two plain-text sentences.`;

type Run = { code: string } & ({ output: Output } | { error: string });

export default {
    async fetch(request: Request, env: Env): Promise<Response> {
        if (request.method !== "POST") {
            return new Response("Send a POST request", { status: 405 });
        }

        const body = Question.safeParse(await request.json().catch(() => null));

        if (!body.success) {
            return Response.json(
                { error: "Send a JSON body with a question" },
                { status: 400 },
            );
        }

        const ai = createAI({ binding: env.AI });
        const runs: Run[] = [];

        try {
            const result = await generateText({
                model: ai(model),
                instructions,
                prompt: body.data.question,
                tools: {
                    runJavaScript: tool({
                        description:
                            "Run JavaScript in a sandbox with no network access. The code is the body of an async function. Return a JSON-serializable value. console.log output is captured.",
                        inputSchema: z.object({
                            code: z.string().describe("Body of an async JavaScript function"),
                        }),
                        execute: async ({ code }) => {
                            try {
                                const output = await runJavaScript(env, code);
                                runs.push({ code, output });
                                return output;
                            } catch (error) {
                                const message =
                                    error instanceof Error ? error.message : String(error);
                                runs.push({ code, error: message });
                                return { error: message };
                            }
                        },
                    }),
                },
                // Stop after five model steps, so a model that keeps failing cannot
                // run code indefinitely
                stopWhen: isStepCount(5),
            });

            return Response.json({ answer: result.text, runs });
        } catch (error) {
            console.error("Model request failed", error);
            return Response.json(
                { error: "The model request failed. Try again later.", runs },
                { status: 502 },
            );
        }
    },
} satisfies ExportedHandler<Env>;

generateText() 会将问题发送给模型。当模型调用 runJavaScript 时,AI SDK 会运行 execute() 并将返回值发回模型。然后模型可以运行更多代码或作答。

如果代码抛出错误、超过 CPU 时间限制或超时,或调用 fetch(),execute() 会返回错误消息而不是让请求失败。模型会读取错误并可以修复其代码。

Worker 会在调用模型之前用 Zod 验证请求正文。如果 Workers AI 请求失败(例如因为 速率限制),Worker 会记录错误,并以 502 以及已经运行的代码进行响应。wrangler.jsonc 中的 observability 会将这些日志保留在 Workers Logs 中。

4. 本地测试

  1. 启动本地开发 server:

    npx wrangler dev

    在 wrangler dev 期间,Dynamic Worker 在本地运行。Workers AI 请求始终在 Cloudflare 上运行,并计入你的 Workers AI 用量。

  2. 向 Wrangler 打印的 URL POST 一个问题。默认是 http://localhost:8787:

    curl http://localhost:8787 --json '{"question":"How many prime numbers are there below 100,000?"}'

    响应包含答案以及模型运行的代码:

    {
        "answer": "There are 9,592 prime numbers below 100,000.",
        "runs": [
            {
                "code": "function isPrime(n){if(n<2)return false; if(n%2===0) return n===2; for(let i=3;i*i<=n;i+=2){if(n%i===0) return false;} return true;} let count=0; for(let i=2;i<100000;i++){ if(isPrime(i)) count++; } return count;",
                "output": { "result": 9592, "logs": [] }
            }
        ]
    }

    模型每次会写出不同的代码和措辞。result 是 9592。

  3. 提出需要排序的问题:

    curl http://localhost:8787 --json '{"question":"Sort these words by length, then alphabetically: pear, fig, banana, kiwi, apple, date."}'

    答案会列出 fig, date, kiwi, pear, apple, banana。

5. 部署

  1. 部署你的 Worker:

    npx wrangler deploy
  2. 向 Wrangler 打印的 workers.dev URL POST 一个问题:

    curl https://sandbox-code-interpreter.<YOUR_SUBDOMAIN>.workers.dev --json '{"question":"How many prime numbers are there below 100,000?"}'

    响应形状与本地响应相同。

后续步骤

  • 让生成的代码调用 Worker 提供的方法。请参阅 Bindings。
  • 让模型从生成的代码中调用带类型的工具。请参阅 Code Mode。
  • 运行 shell 命令或其他 Linux 工具。请参阅 运行 Linux 命令。
  • 选择不同的模型。请参阅 Workers AI 模型。

这篇文档对您有帮助吗?