CodeActAgent(CodeAct)
CodeActAgent 是 LlmAgent 的同类方案,它通过编写和运行代码来执行操作,而不是一次只发出一个工具调用。每一轮中,模型生成一个脚本;工具以可调用函数的形式提供,脚本可以对这些函数进行组合;脚本则通过返回带标签的值来传达其结果。
这就是 CodeAct 模式:模型不是 call tool A → observe → call tool B,而是在一个脚本中编写 b(a(x)),因此多步骤工作可以在单轮中完成。该模式由 adk-agent 上的 codeact 功能启用。
何时使用
- 每轮需要串联或组合多个工具的任务(数据处理、批量操作、胶合逻辑)。
- 针对代码生成进行后训练的模型。
- 可使用真实解释器(例如 Python)作为操作执行基础的工作流。
对于原生工具调用,优先使用 LlmAgent。对于沙箱化的文件/ shell 编码工具框架,请参阅 编码代理。
循环的工作方式
每一轮:
- 模型发出一个围栏代码块(脚本)。
- 脚本在 [
CodeRuntime] 上运行;工具调用会传递给主机,由主机执行工具,然后将结果带回脚本并恢复其运行。 - 脚本返回一个带标签的
ScriptOutput:observation— 反馈给模型;循环继续。error— 作为消息反馈;循环继续。final_result— 返回给调用方;循环结束。transfer_to_agent— 将控制权交给另一个代理;循环结束。
该框架与语言无关:CodeRuntime trait 是逐步解释器的衔接点,并通过自由格式提示向模型报告自身的语言/环境。预期的生产适配器会封装 Monty,这是一个原生 Rust 的 Python 解释器。
持久性:暂停与恢复
CodeActAgent 在不同调用之间是无状态的——持久状态存储在会话中,与 LlmAgent 完全相同。以下两种情况会暂停运行:
- 尚未做出决定的确认门控工具(HITL),以及
- 结果在带外到达的长时间运行工具。
暂停时,正在运行的解释器延续状态会被序列化为
CodeActCheckpoint 并写入会话状态;下一次 run() 会将其读回
并恢复运行——确认决定通过 RunConfig::tool_confirmation_decisions 到达,而长时间运行工具的结果会在下一条消息中作为
FunctionResponse 到达。内联工具调用会使用预写(SAVE-BEFORE)和 SAVE-AFTER 检查点进行包围:一旦 SAVE-AFTER
检查点持久化,恢复时就会使用存储的结果,绝不会重新运行工具。如果工具产生副作用后、其 SAVE-AFTER
检查点写入前的狭窄时间窗口内发生崩溃,恢复时将重新运行该工具,因此非幂等工具应对此进行防护(与 LlmAgent 相同的至少一次
边界)。
这要求运行时能够对暂停的调用进行快照。无法做到这一点的运行时会以内联方式运行长时间运行工具,并拒绝确认暂停。
构建 CodeActAgent
use adk_agent::codeact::CodeActAgent;
use std::sync::Arc;
// `model` implements `adk_core::Llm`; `runtime` implements `CodeRuntime`.
let agent = CodeActAgent::builder()
.name("analyst")
.model(model)
.runtime(runtime)
.instruction("Prefer concise, composable steps.")
.tool(Arc::new(load_csv_tool))
.output_key("report")
.build()?;
model 和 runtime 是必需的;其他所有内容都有默认值。
与 LlmAgent 的一致性
构建器镜像了 LlmAgentBuilder:
- 模型:
generate_content_config,以及temperature/top_p/top_k/max_output_tokens简写形式。 - 指令:
instruction/instruction_provider、global_instruction/global_instruction_provider,通过{state.key}模板注入;以及技能(skills功能)。 - 历史记录:
include_contents。 - 工具:静态
tool和按调用生成的toolset;tool_timeout、default_retry_budget/tool_retry_budget、circuit_breaker_threshold,以及on_tool_error回退机制。 - 授权:
ToolConfirmationPolicy(require_tool_confirmation/require_tool_confirmation_for_all)。 - 转移:
sub_agent和disallow_transfer_to_parent/disallow_transfer_to_peers。 - 输出:
output_key、output_schema/output_type,以及带有 修正重试循环(output_max_retries)。 - 回调:
before_callback/after_callback、before_model_callback/after_model_callback,以及before_tool_callback/after_tool_callback/after_tool_callback_full。 工具执行后的回调可以通过CallbackContext::tool_outcome()检查结构化的执行元数据。 - 受功能开关控制:输入/输出防护措施(
guardrails)以及EnhancedPlugin流水线(enhanced-plugins)。
每次工具调用都会获得一个全新的 ToolContext,其中携带解释器调用 ID,
并将工件、记忆、共享状态、用户作用域和机密信息委托给实时调用——因此工具在
CodeActAgent 或 LlmAgent 下的行为完全一致。
有意的差异
- 代码执行沙箱由
CodeRuntime负责,而不是通过附加组件实现。 - 工具调度按设计顺序执行(单个延续在一次调用边界处进行快照),因此不存在并行
tool_execution_strategy。 - 没有
skip_summarization构建器选项——模型通过final_result自行结束循环——不过,工具若在其操作上设置skip_summarization,仍会结束运行。
示例
一个可运行、无依赖的端到端演示——由一个自包含的 CodeRuntime 和
一个确定性模型组成——位于
examples/codeact_agent:
cargo run --manifest-path examples/codeact_agent/Cargo.toml
实现 CodeRuntime
CodeRuntime 会解析并逐步执行脚本,一次呈现一个外部调用:
pub trait CodeRuntime: Send + Sync {
fn start(&self, script: &str, script_name: &str) -> Result<RunStep, RuntimeError>;
fn resume(&self, snapshot: &[u8], with: ResumeWith) -> Result<RunStep, RuntimeError>;
fn capabilities(&self) -> RuntimeCapabilities { /* default */ }
fn render_tools(&self, tools: &[Arc<dyn Tool>]) -> String { /* default */ }
}
RunStep是一组结构体变体——Call { call, stdout }、Complete { value, stdout }和Raised { message, stdout }。使用RunStep::call/RunStep::complete/RunStep::raised辅助函数构造它们,并通过.with_stdout(..)附加捕获的输出。RunStep::Call恰好呈现一个待处理的调用;使用值或错误恢复该调用,或者dump()其继续部分以暂停执行。运行时附加的stdout会重新呈现给模型并持久化到检查点中,因此在暂停/恢复后仍然保留。PendingCall会按照解释器生成参数的方式报告参数——分别报告positional_args()和keyword_args()。不要自行将位置参数映射到名称:驱动程序会通过adk_agent::codeact::bind_call_args在中心位置将它们绑定到工具参数,因此运行时在调用边界无需工具模式,而render_tools可以只是工具切片的纯函数。- 脚本错误与宿主错误。 模型可以通过编写不同代码来修复的任何问题——语法/解析错误、未捕获的异常、资源限制导致的取消——都是
RunStep::Raised(原样反馈给模型的不透明字符串)。RuntimeError专用于真正的宿主故障(快照的序列化/反序列化、解释器内部错误),并会中止运行。 RuntimeCapabilities::supports_suspension必须是true,才能支持 HITL 和长时间运行的延迟处理;prompt会向模型描述语言/环境。
请参阅 examples/codeact_agent/src/runtime.rs,其中提供了支持暂停/恢复的完整、最小实现。
通过 Monty 使用 Python
预期用于生产环境的适配器是
adk-codeact-monty,
这是一个由 Pydantic Monty 支持的 CodeRuntime。它
让模型通过编写 Python来执行操作,在进程内运行,无需容器或
子进程,并将暂停的运行快照保存为字节——这正是挂起/恢复
所需的功能。Monty 解释器通过 adk-code 的 embedded-python
内核使用,该内核将 monty crate 集中固定在同一处;需要 rustc 1.95+。
[dependencies]
adk-agent = { version = "2.1.0", features = ["codeact"] }
adk-codeact-monty = "2.1.0"
或者通过 umbrella crate(重新导出为 adk_rust::codeact_monty):
[dependencies]
adk-rust = { version = "2.1.0", features = ["minimal", "codeact-monty"] }
use adk_codeact_monty::MontyRuntime;
// Conservative default resource limits (per-advance time + memory caps) make
// `new()` safe for untrusted, LLM-generated code.
let runtime = Arc::new(MontyRuntime::new());
// Tighten or relax with the builder; `unlimited()` removes the caps for
// trusted scripts only.
let runtime = Arc::new(
MontyRuntime::builder()
.max_duration(std::time::Duration::from_secs(2))
.max_memory(64 * 1024 * 1024)
.build(),
);
操作系统访问
脚本尝试执行的操作系统效果——文件系统读写、
os.getenv/os.environ,以及 date.today()/datetime.now()——均根据
由主机控制的策略就地处理。它们不是工具,也不会暂停
代理循环。默认情况下,运行时处于完全沙箱化状态(无文件系统访问权限、
环境为空,但启用主机时钟)。使用构建器授予特定访问权限:
use adk_codeact_monty::{MontyRuntime, PathAccess};
let runtime = Arc::new(
MontyRuntime::builder()
// Mount host directories at virtual paths; Monty enforces the boundary
// (canonicalization + symlink-escape detection) so a script can never
// escape a mount. Reads/writes outside every mount raise PermissionError.
.allow_path("/data", "/srv/agent/data", PathAccess::ReadOnly)
.allow_path("/out", "/srv/agent/out", PathAccess::ReadWrite)
// Expose an explicit environment map to os.getenv / os.environ. Empty by
// default — the host process environment is never exposed implicitly.
.environ_var("PROJECT", "acme")
// date.today() / datetime.now() read the host clock (enabled by default).
.system_clock(true)
.build(),
);
网络和子进程访问没有 Monty 操作系统调用接口,无论策略如何设置都不可用。 授予的访问权限会在系统提示中告知模型,因此模型知道可以读取或写入哪些路径, 以及存在哪些环境变量。
Monty 仅实现了 pathlib.Path 的一个子集,因此挂载路径时,
提示会列出确切支持的方法(任何其他方法都会引发 AttributeError):
- 读取/查询(任意挂载):
exists()、is_file()、is_dir()、is_symlink()、read_text()、read_bytes()、stat()、iterdir()、resolve()、absolute()、open("r")。 - 写入(仅限读写挂载):
write_text()、write_bytes()、append_text()、append_bytes()、mkdir()、unlink()、rmdir()、rename()、open("w")/open("a")。 - 纯路径操作(无 I/O):
/运算符以及joinpath()、is_absolute()、with_name()、with_stem()、with_suffix()、as_posix(), 以及.name、.parent、.stem、.suffix、.suffixes、.parts属性。
工具通过单个内置函数调用,即 call_tool("name", {"arg": value, ...})——这是调用工具的唯一方式;它们绝不会以裸可调用对象的形式处于作用域中。工具名称是字符串字面量,每个参数都是一个字典中的字符串键条目,因此真实名称会包含在序列化的延续中(挂起/恢复时无需依赖宿主端名称表即可保留),工具及其每个参数都可以使用任意名称(不必是有效的 Python 标识符,例如 "fetch-cart"、Python 关键字,甚至可以是 "call_tool"),驱动程序会严格按名称绑定字典条目——不会进行位置推断。每个工具都会在提示中以一行 call_tool("name", {...}) 用法的形式出现,并附带其参数和描述。除这一种形式之外的任何调用方式——裸 fetch_cart(...)、关键字参数、非字典参数或非字符串键——都会被拒绝并返回纠正性错误,而不是静默分发,因此模型只需学习一种调用形式。
可运行的
examples/codeact_monty_agent
会在完全离线的情况下,使用真实的 Python 驱动 CodeActAgent。