跳转至

工具代码编排

codeexecutor/codeact 实现的是 CodeAct-style pattern:模型生成的代码只能经由受限工具网关编排 trpc tools。作为框架能力,对外更建议叫 工具代码编排。未知工具会被拒绝,工具输入与输出都在 Go 侧按声明的 JSON Schema 校验。它不替代稳定业务流程中的 Go 代码。

LLM -> execute_tool_code -> Runtime -> guest call_tool(name, JSON args)
                                  -> Go Gateway -> trpc Tool

Gateway 是能力边界,负责 allowlist、schema 校验和真实 Go tool 调用。Runtime 是传输/执行边界:LocalRunner 直接运行 Python stdio guest;SandboxRunner 在每次 调用新建的 OS sandbox workspace 中运行同一套 callback 协议。远端服务或 microVM 平台应直接实现 codeact.Runtime,并把 guest 产生的 ToolCall 路由给传入的 ToolCallHandler

安全边界

LocalRunner 仅用于开发或已经隔离的容器/VM;它不是安全 sandbox。它使用共享的 local Python runtime,为本地生成代码执行提供 defense-in-depth 防护,包括限制源码大小、 使用最小进程环境、默认使用空的临时工作目录、将 bootstrap 脚本放在私有目录、尽力终止 guest 进程(Unix-like 系统下会清理进程组),以及通过 codeact.NewLocalRunner(codeact.LocalRunnerConfig{Timeout: ...}) 配置的可选全流程 timeout。

CodeAct 会保留通用 Python 语法和 builtins,包括 import 与异常处理。它与 Dynamic Workflow 不同,不应用 AST 或 builtin allowlist。二者共享的是进程启动和生命周期 加固,而不是相同的语言策略。

相较之前的 LocalRunner,强化后的默认行为不再继承宿主环境,默认使用空的临时工作 目录,并会拒绝超过 64 KiB 的生成源码,除非调用方显式调整限制。这些是有意的行为 变化,但不会构成安全 sandbox 边界。

需要本地 OS 隔离时,可以在原来传入 LocalRunner 的位置改用:

runtime := codeact.NewSandboxRunner()
orchestrator, err := toolcode.NewTool(runtime, managedTools)

使用上面不带 option 的构造方式时,每次执行都会使用一次性 workspace 和 clean environment。它使用 codeexecutor/sandbox 的 managed backend,默认限制网络; 如果 sandbox 初始化失败,会直接报错,不会 fallback 到 LocalRunner。Linux 需要 bubblewrap,macOS 使用 /usr/bin/sandbox-exec,Windows 尚未实现 managed sandbox。

SandboxRunner.Timeout 会为完整执行设置 deadline,并把取消信号传导给 guest 和宿主 Tool 调用。Go context 采用协作式取消,Tool handler 必须在 context 结束后及时返回。 零值不会额外添加 deadline,而是依赖调用方 context;生产环境必须为调用方 context 设置 deadline,或者显式配置该 timeout。两者同时存在时,以更早到期者为准。CPU、 内存和进程数配额仍应由外层容器、microVM 或远端 runtime 提供。

SandboxRunner.Python 为空时,sandbox 会从自己的 clean PATH 解析 python3。任何 非空值(包括显式的 "python3")都会先通过宿主 PATH 解析并转换成绝对路径。如果 解释器不在 backend 默认开放的 runtime 路径中,还需要通过 sandbox.WorkspaceWriteProfile().WithReadPaths(...) 扩展 managed permission profile,并将其传给 sandbox.WithPermissionProfile(...)

OS sandbox 仍然运行在宿主机上,并会开放启动解释器所需的平台与 runtime 路径;具体 只读可见范围因 backend 而异。它不等价于 microVM 级租户边界。如果 guest 不能看到 任何宿主文件,或者需要更强的 CPU、内存和租户隔离,应使用容器、microVM 或远端 Runtime

为什么不让生成代码直接调用 HTTP API?

工具代码编排把动态控制流和系统能力访问分开:生成代码适合循环、分支和数据转换,业务 API 访问仍应是宿主侧工具。这样认证、授权、重试与幂等策略、API 版本适配、审计和限流仍由应用代码掌控,而不是落在模型生成的代码中。

确实需要 HTTP 能力时,应把具体业务操作做成宿主 adapter tool;如果需要一组较宽但明确受限的 HTTP 能力,可暴露受约束的 http_request 类工具,由宿主控制允许的域名和方法、凭证以及响应大小限制。不能把 LocalRunner 的通用 Python 环境当作安全控制;生产 runtime 必须自行落实网络和进程策略。

需求 推荐实现
稳定、确定性的业务流程 Go 应用代码
在已批准工具间做动态分支或循环 execute_tool_code
受限的外部 HTTP 集成 宿主 adapter 或受约束 HTTP tool
不可信代码或无边界外部访问 隔离 runtime 加明确的应用策略

工具 A/B 的小型结构化数据经 JSON 传递;中间结果留在 guest 代码中,只有最终 value 和捕获的 stdout 会作为外层工具结果返回。应返回紧凑的聚合结果、标识符或 artifact/workspace 引用,而不是把大块原始数据交回模型。大数据应返回 artifact/workspace 引用,由 guest 在挂载 workspace 中处理。业务语义不匹配必须用显式转换代码或宿主 adapter tool 处理,CodeAct 不会猜测字段或单位含义。

要运行 Agent 的多工具编排示例(产品搜索 → 库存筛选 → 创建报价),设置 OPENAI_API_KEY(以及可选的 OPENAI_BASE_URL)后执行:

cd examples && go run ./codeact -model gpt-5

该示例只把 execute_tool_code 暴露给模型;search_catalogget_inventorycreate_quote 仅能由 guest Python 经 allowlist 调用。它不设置 GenerationConfig, 模型配置完全由 openai.New 和环境变量处理。

需要给 Agent 暴露工具时,调用 tool/toolcode.NewTool(runtime, managedTools),将返回的 execute_tool_code 注册给 Agent。只有 managedTools 能被 guest 中的 await call_tool(...) 调用。

第一版中,managed tool 是同步的直接宿主能力调用:内置 Python guest 任一时刻只有一个调用在执行,await 是 guest API 的调用形式,但 asyncio.gather(...) 不会产生并行的宿主工具调用。宿主工具失败会在 Python 中表现为 RuntimeError,生成代码可用 try/except 处理。它不会重放 Agent 的 callback、retry 或内层 tracing 生命周期,也不能在执行中暂停以等待交互式审批。不要把需要“审批后恢复”流程的工具加入 managedTools;授权应在业务工具自身或应用定义的 adapter tool 中实现。

面向模型的工具说明

默认的 execute_tool_code 声明会告诉模型:能在一次调用完成流程时优先一次完成;只能使用 await call_tool(name, **json_arguments);按顺序调用工具;并只返回完成任务所需的紧凑 JSON 兼容值。它还会动态列出每个 managed tool 的名称、描述、输入 JSON Schema 和输出 JSON Schema。中间的 managed tool 结果留在 guest 代码中,只有 execute_tool_code 最终的 value 和捕获的 stdout 会作为外层工具结果返回;不要打印或返回模型不需要的大型原始结果。这里的说明只是模型引导,不能替代前述 runtime 安全边界。

managed tool 的描述应写成业务契约:说明操作含义、前置条件、单位、枚举含义和结果语义。除非模型必须依此选择操作,否则不要塞入 header、SDK 等传输细节。两个工具之间若需要领域特定的语义映射,应使用宿主 adapter tool,而不要让通用字段转换去猜测含义。

toolcode.WithDescription 会替换默认描述。使用自定义描述时,应保留“只能经由 call_tool 访问能力”和 JSON 入参与结果约束。

与其他执行工具的边界

execute_tool_code 不是 workspace_execworkspace_write_stdinworkspace_kill_session 的替代品:

  • workspace_exec 系列用于在共享 workspace 里运行 shell command、脚本、 CLI 或长任务,输入输出主要是 terminal 文本、文件、artifact、exit code。
  • execute_tool_code 用于让生成的 Python 胶水代码通过 call_tool(...) 编排一组显式 allowlisted 的普通 Go tools,适合分支、循环、JSON 转换和 多工具结果聚合。
  • tool/codeexecexecute_code 用于普通代码执行、计算、数据分析或逻辑 验证;它不会把 Python 调用桥接回 Go tools。

因此默认工具名刻意不叫 execute_codetool/codeexec 已经把 execute_code 用于普通代码执行。如果一个应用只暴露工具编排版本,可以用 toolcode.WithName("execute_code") 覆盖;但不要在同一个 Agent 上注册两个 同名工具。

managed tools 选择

managedTools 是应用显式配置的独立 capability registry,不会从 Agent 已注册的 tools 自动推导。应优先放业务工具、数据工具和宿主侧 adapter tool。通常应排除:

  • workspace_exec / workspace_write_stdin / workspace_kill_session
  • workspace_save_artifact
  • execute_code / execute_tool_code
  • skill_run / skill_exec / skill_write_stdin / skill_poll_session / skill_kill_session
  • transfer_to_agent / await_user_reply

对于同一个业务操作,通常应只选择一种模型侧入口:要么作为 Agent 的直接工具暴露,要么仅加入 execute_tool_code 的 managed registry。应用当然可以有意同时注册两者,但这会让模型对同一个操作看到两条执行路径,弱化何时应该使用代码编排的引导。无论如何,managed registry 仍是 guest 代码的实际能力边界。

这些工具通常不应加入 registry:执行类工具会形成递归或异质执行链; transfer_to_agentawait_user_reply 会修改外层 Invocation 的控制状态; AgentTool / DynamicAgentTool 会启动子 Agent 并带来独立的历史、流式事件和 成本边界。框架不按名称或类型替应用判断;应用应只把明确需要编排的普通能力工具 传给 tool/toolcode.NewTool