VolcengineVolcengine ADK
智能体

运行时

运行时决定智能体「内层循环」如何执行——也就是每一轮怎样调用模型、解析意图、调用工具。默认运行时基于 Google ADK 的内置执行流程,开箱即用。你也可以切换到其他执行后端,或在执行流程上插入统一的处理逻辑(如鉴权、日志、重试),而无需改动智能体本身。

基本使用

默认即为 ADK 运行时,无需任何额外配置:

agent.py
import asyncio
from veadk import Agent, Runner

agent = Agent(name="assistant")  # 默认使用 ADK 运行时
print(asyncio.run(Runner(agent=agent).run("你好")))

切换执行后端

通过 runtime 选择内层循环的执行后端:

agent.py
agent = Agent(name="assistant", runtime="codex")
取值说明
adk(默认)使用 Google ADK 内置的执行流程,适用于绝大多数场景。
codex使用 OpenAI Codex SDK 驱动内层循环,在沙箱中执行命令与文件操作。
piagent使用本地 Pi coding agent 二进制(通过其 RPC 模式)驱动内层循环。二进制会在首次运行时自动下载,也可用 PIAGENT_BINARY 指定已有路径。

定位:codex / piagent 是「沙箱执行运行时」,不是 ADK 执行流程的等价替代品。 它们把整个内层循环交给外部 agent harness,因此 ADK 请求/响应处理器(planner、code_executor、example store、知识库注入)、逐次模型调用的回调与 span 不会在这层流程里执行。VeADK 会把 sub_agents 桥接成 transfer_to_agent 工具,但这些运行时仍更适合「模型需要在受控沙箱里跑命令、改文件」的任务,而不是所有 ADK 能力的透明后端替换。具体差异见下方支持矩阵

使用 Codex runtime 前安装可选依赖:

pip install "veadk-python[codex]"

什么时候该用 codex 运行时

上面的定位说明可以压成一条判断标准:codex 运行时只有一个真正的优势——模型可以写下一个文件、把它跑起来、读到报错、改掉、再跑一遍,整个循环发生在一个 OS 沙箱里,而你不需要事先把每一步都定义成工具。 除此之外的每一条轴上,它都比 runtime="adk" 更贵。

适合:

  • 步骤无法事先枚举成工具——临时数据分析、日志排障、脏数据清洗、按需改代码。这类任务的下一步取决于上一步的输出,而不是取决于你写智能体时的设想。
  • 工作对象是一个文件系统——中间产物留在 workspace 里,同一 Session 的下一轮仍然看得见(见 Workspace 生命周期)。
  • 第一次做错是正常的,且重试很便宜——报错信息本身就是模型的下一份输入。在 ADK 运行时里,一次失败的工具调用只是一条错误字符串;在这里,它是一段可以被读、被诊断、被修好的 traceback。

不适合:

  • 一次固定的工具调用 + 一段格式化回答。 这正是 ADK 运行时的主场:更快、更便宜,而且不受下面任何一条限制。
  • 需要 output_schemaplannercode_executorgenerate_content_config 的智能体。 这些配置在非 adk 运行时下会直接报错,而不是降级——完整清单见支持矩阵
  • 对延迟敏感的链路。 每一次 invocation 都会重建一个临时 CODEX_HOME、拉起一个 Codex 子进程、开一个 ephemeral thread;这笔固定开销每回合都要付。
  • run_live 非 adk 运行时没有 live/bidi 实现,见下文

成本,如实说清楚:

  • 每次 invocation 一个 Codex 子进程。 runtime 每回合都会重建 CODEX_HOME 并进入 AsyncCodex(...),thread 以 ephemeral=True 创建。
  • 每回合都会把整段 ADK 会话历史重新序列化进 prompt。 历史被渲染成一个 <conversation_history> JSON 块,附在当前消息前面;codex 没有增量续接机制,所以 prompt token 随对话轮数增长。这也是 include_contents="none" 被拒绝(而不是被忽略)的原因。
  • ADK 工具的结果会穿过模型上下文。 见下方 Workspace 是数据面——这是本运行时最容易踩的一个坑。

一句话版本:能把任务写成一串确定的工具调用,就用 runtime="adk";只有当你只能描述目标、路径必须试出来时,才用 runtime="codex"

仓库里四个 codex 示例的分工:

示例讲什么
codex_data_analysis本运行时是干什么用的。 Codex 写分析脚本 → 运行 → 在脏数据上撞到真实报错 → 自己改好 → 重跑 → 出报告。自迭代循环。
codex_ops_assistant本运行时是干什么用的。 ADK 工具把日志、指标与发布记录落进 workspace,Codex 写一次性脚本把三者关联起来、定位根因,全程在断网沙箱里。
codex_with_skill_and_mcp怎么接线。 本地 skill 和 MCP 工具在这个运行时下分别走哪条路。
codex_runtime_on_agentkit怎么部署。 把一个 runtime="codex" 智能体发布到火山引擎 AgentKit。

Codex 安全配置

Codex 默认使用独立 Session workspace、workspace_write 沙箱、禁用网络,并拒绝一切需要提权的操作:

agent.py
from veadk import Agent
from veadk.runtime.codex import CodexRuntimeConfig

agent = Agent(
    name="assistant",
    runtime="codex",
    codex_runtime_config=CodexRuntimeConfig(
        sandbox="workspace_write",
        approval_mode="deny_all",  # 默认值;auto_review 等同于全自动批准,见下
        network_access=True,
        # 若需在已有工程内工作,显式指定目录;默认使用 Session 隔离目录。
        workspace_root="/workspace/codex",
    ),
)

approval_mode="auto_review" 不是「人工复核」,而是「全自动批准」。 Codex SDK 内置的审批处理器对每一个 requestApproval 通知都回答 accept,而 AsyncCodex 没有提供替换该处理器的接口。因此 auto_review 会自动批准每一次沙箱提权和文件修改,既不询问人工,也不经过 ADK。只有默认的 deny_all 才真正把 Codex 约束在沙箱内。在多租户或不可信输入场景中,请勿使用 auto_review

关于沙箱与网络的组合,有一个容易误判的点:network_access 只会写入 Codex config.toml[sandbox_workspace_write] 段,只有 workspace_write 沙箱会读取它

sandboxnetwork_access 是否生效
workspace_write生效。
read_only不生效(设为 True 会记录一条警告)。
full_access不生效——danger-full-access 本身就意味着完全的网络与文件系统访问。

因此 CodexRuntimeConfig(sandbox="full_access", network_access=False)直接抛出 ValueError:这个组合读起来像「禁用网络」,实际却给了完全访问权限。需要 full_access 时必须显式写上 network_access=True 以确认风险。

full_accessreuse_workspace=True 会放宽不同调用之间的文件系统边界,只应在受信环境中开启。

安全基线组合

把上面几项拼起来,就是在不可信输入下跑 codex 的默认配方。四个设置各挡一个方向,缺一不可:

agent.py
from google.adk.agents import RunConfig
from veadk import Agent, Runner
from veadk.runtime.codex import CodexRuntimeConfig

agent = Agent(
    name="analyst",
    runtime="codex",
    tools=[load_orders, publish_report],  # 唯一的进出通道
    codex_runtime_config=CodexRuntimeConfig(
        sandbox="workspace_write",  # 只能写 workspace
        network_access=False,       # 沙箱内没有出网通道
        approval_mode="deny_all",   # 默认值;拒绝一切提权请求
    ),
)

await Runner(agent=agent).run(
    "...",
    run_config=RunConfig(max_llm_calls=40),  # 单次 invocation 的成本上限
)

它们之所以能组合成一套,是因为关掉网络之后,你自己挂上去的那几个 ADK 工具就是唯一的出站通道:模型可以在沙箱里对敏感数据任意计算、任意重试,但要把任何东西送出去,只能经过一个你审计过、有签名、有日志的函数。换句话说,工具列表就是你的数据出口策略——这条边界要成立,前提是每个工具自己也不提供任意外发能力。

配套的三条,不在这里重复展开:

  • approval_mode="auto_review" 会拆掉这套配方的地基,理由见上方的红色警告
  • sandboxnetwork_access 的组合语义见上表:只有 workspace_write 会读 network_access
  • VEADK_CODEX_* 环境变量的优先级高于这里的 Python 配置,见 Codex 环境变量。部署环境里一个 VEADK_CODEX_SANDBOX=full_access 就能推翻上面整段代码。

RunConfig(max_llm_calls=...) 不设时,VeADK 的 Runner 会用环境变量 MODEL_AGENT_MAX_LLM_CALLS(默认 100)兜底。在这个运行时下建议显式写一个更小的值:模型自己决定跑多少轮脚本,只有这个配额是硬上限。

Workspace 生命周期

默认情况下,workspace 由 app_name / user_id / session_id / agent 名四元组定位:同一 Session 的多次调用共享同一目录,回合结束后不会删除,因此上一轮写下的文件下一轮仍然可见。进程退出时整棵目录树被清理;进程存活期间,空闲超过 6 小时的 Session workspace 会在下一次创建 workspace 时被顺带回收,以限制长期运行的服务进程的磁盘增长。

工具不需要自己推导这个路径:不做任何配置,current_workspace() 就会返回当前调用它的这一轮的 workspace,见下方 Workspace 是数据面

显式传入 workspace_root 时,这块目录属于你自己:runtime 既不会删除它,也不会对它执行上述空闲回收——它仍会在其下为每个 Session 建独立子目录,但这些子目录需要你自行清理。reuse_workspace=True 只有在同时设置了 workspace_root 时才有意义,它让所有 Session 直接共用该目录本身,而不再做 Session 隔离。

Workspace 是数据面

这是用好 codex 运行时最重要、也最容易被忽略的一条:ADK 工具的参数和返回值是控制面,workspace 才是数据面。

原因在工具的执行路径上。ADK/MCP 工具不由 Codex 自己调用,而是由 runtime 的 Responses shim 执行:shim 拿到工具返回值后 json.dumps 成字符串,作为一条 function_call_output 塞回模型的 input 数组。而且因为 Codex 每完成一次原生工具调用就会重建整个 input,shim 还必须在本回合后续的每一次后端请求里,把这对 function_call / function_call_output 重放一遍。于是:

一个返回 5 万行日志的工具,会把这 5 万行灌进模型上下文,并在同一回合里重复灌若干次。这不是慢一点的问题,是这一回合直接废掉。

正确的做法是让工具交出一个路径,而不是一份数据:把数据写进 workspace(Codex 的 cwd 就是它),只返回位置和少量元信息。数据落在磁盘上,模型用 shell 去读、去切片、去统计,上下文里只留下一行。

tools.py
from pathlib import Path

from veadk.runtime.codex import current_workspace


def load_orders(day: str) -> dict:
    """把某一天的订单写成 CSV,放进你的工作目录。

    只返回一张回执,不返回订单本身——请用你自己的代码去读返回路径上的 CSV。
    """
    workspace = current_workspace()   # 本轮的工作目录,或者 None
    if workspace is None:             # 不在 Codex 轮次里:返回模型能读懂的错误
        return {"status": "error", "message": "no codex workspace on this call"}
    rows = warehouse.query(day)
    name = f"orders-{day}.csv"
    write_csv(Path(workspace) / name, rows)
    # 回执,不是数据:告诉模型文件在哪、有多大形状。
    return {"path": name, "rows": len(rows), "columns": list(rows[0])}

这里的 docstring 是真正干活的部分:模型读到的就是它,所以必须写清楚「返回的是回执」。 否则模型会直接让工具把数据交出来,你又回到了原点。

反方向同理:Codex 在 workspace 里写好报告之后,「发布」工具应该收下它写的那个文件的路径,由工具自己读盘再送往外部系统——而不是让模型把整篇报告当作工具参数复述一遍(那同样要过一遍上下文,而且内容可能在复述中走样)。

工具怎么知道 workspace 在哪:

  • current_workspace()——首选做法,也是多租户唯一可行的做法。 from veadk.runtime.codex import current_workspace,返回当前调用该工具的这一轮 workspace 的绝对路径。它不需要任何配置,workspace_rootreuse_workspace不设时同样有效。这个值是在每次工具调用前后绑定的,而不是从调用方的环境上下文里读,所以同一进程里并发的多个回合各自看到自己的目录。调用栈上没有 Codex 回合时它返回 None,而不是抛异常——同一个工具对象也会被别的 runtime、被 AgentTool、被单元测试执行——因此请判断 None 并返回你自己的 {"status": "error", ...} 让模型去处理,而不是抛异常,也不要退回到某个你自己选定的目录。
  • 固定路径——单租户,并且你想在跑完之后翻这个目录。 设置 workspace_root 并配合 reuse_workspace=True,此时 workspace 就是 workspace_root 本身,工具和 CodexRuntimeConfig 可以共用同一个常量,而且进程退出后目录仍在磁盘上。代价是所有 Session 共用它,见 Workspace 生命周期

无论哪种做法,来自模型的路径都是不可信输入:打开之前请把它解析回 workspace,并拒绝一切越界的路径(..、绝对路径、指向外部的符号链接)。

Codex 执行参数

参数默认值说明
reasoning_effort"medium"推理强度,可选 minimal/low/medium/high/xhigh。越高越慢、消耗 token 越多。
personality"pragmatic"Codex 自带的回复风格,可选 none/friendly/pragmatic
max_tool_iterations32整个 Codex turn 内 shim 可执行的 ADK/MCP 工具轮次上限(1–256)。见下方行为变更说明。
tool_timeout_seconds120.0单个 ADK/MCP 工具调用的超时秒数;设为 None 表示不超时。
reuse_workspaceFalse仅在同时设置了 workspace_root 时生效,而且只值得在单租户场景下开启,见上方 Workspace 生命周期。否则两个字段都别设,让工具调用 current_workspace(),见 Workspace 是数据面

行为变更:max_tool_iterations 的含义已改变,默认值从 8 提高到 32 它现在约束的是整个 Codex turn,而不再是单次后端请求。Codex 每完成一次原生工具调用就会发起一次新的后端请求,所以旧的「每请求」计数实际允许的执行次数是「请求轮数 × budget」。默认值同时调高,是为了让「先跑若干轮原生工具、之后才调用 ADK 工具」的回合不被提前截断。如果你此前靠 8 这个值来兜底控制成本,请重新评估——现在更合适的成本上限是 RunConfig(max_llm_calls=...)(见下)。

指令是怎么下发给 Codex 的

Codex 自带一份约 20KB、针对自身工具链(apply_patchupdate_plan、shell、AGENTS.md)调优过的系统提示词。VeADK 不会通过 base_instructions 覆盖它——Codex 在该字段被设置时会整体替换内置模板,这些指导会被全部删掉(Codex 官方文档也强烈不建议这么做)。

因此,Agent 的身份块与 instruction 走的是 Codex 原生的 developer_instructions 通道:Codex 把它渲染成一条独立的 developer 消息,与 AGENTS.md、skills、环境上下文并列,是纯追加而非替换。

副作用是 personality 现在真的会生效:它渲染在 Codex 内置的系统提示词模板里,而以前只要设置了 base_instructions,整个模板连同 personality 一起被替换掉,该字段形同虚设。

同一条通道上还会追加两条更正——Codex 被保留下来的系统提示词描述的工具链,这座桥并不能完整提供。runtime 会在每一轮的 developer instructions 后面追加一段简短的工具可用性说明

  • 本次运行没有 apply_patch:shim 只转发 function 类型的工具,而 Codex 的文件编辑工具不是,所以创建和修改文件要用 exec_command(例如 cat > file <<'EOF' heredoc)。
  • request_user_input 确实被通告了,但没有人能回答它:一次 ADK 调用没有交互通道,调用它只会让这一轮什么都没做就结束。说明里会要求模型用手上已有的信息作判断,并在最终回复里讲清楚缺了什么。

这两点你不需要再在自己 Agent 的 instruction 里手写一遍。

Codex 环境变量

以下四个环境变量覆盖 Python 中的 CodexRuntimeConfig,而不是作为它的默认值。也就是说,即使代码里写死了 sandbox="workspace_write",部署环境里的 VEADK_CODEX_SANDBOX=full_access 依然会生效。这一优先级与大多数配置项相反,在容器/函数计算等由平台注入环境变量的场景中尤其需要注意。

环境变量覆盖的配置项说明
VEADK_CODEX_SANDBOXsandbox取值同 sandbox 字段。
VEADK_CODEX_APPROVAL_MODEapproval_mode取值同 approval_mode 字段。
VEADK_CODEX_WORKSPACE_ROOTworkspace_root工作目录绝对路径。
VEADK_CODEX_NETWORK_ACCESSnetwork_access1/true/yes/on 视为开启,其余视为关闭。

另有四个用于 Responses→Chat 转换层(shim)的环境变量。它们不参与上面的覆盖规则,只是 shim 自身的调参:

环境变量默认值说明
CODEX_SHIM_NUM_RETRIES2后端请求遇到 429/5xx/超时等瞬时错误时的重试次数。
CODEX_SHIM_TIMEOUT0(不超时)单次后端请求的超时秒数。
CODEX_SHIM_START_TIMEOUT10等待 shim 的本地 HTTP 服务起来的秒数;超时即判定启动失败并明确报错,而不是让 Codex 一直连不上。
CODEX_SHIM_CACHE_MAX8进程内缓存的 shim 实例上限(按后端地址+凭证区分),用于限制多租户进程里的服务与端口数量。

Codex 可观测性

Codex 原生生命周期和 ADK Function/MCP 工具调用都会转换为 ADK Event。运行日志使用稳定的 codex_* 事件名,并包含 invocation_idcall_idtoolstatusduration_ms 等可归因字段。日志不会记录工具参数、工具结果、API Token、凭证或后端地址;Token Usage 通过 codex_event_type=token_usage 事件及对应日志提供。

此外:

  • call_llm span:ADK 的 call_llm span 本由它自己的 LLM flow 打开,而该 flow 已被 codex 替换,因此 runtime 会自行打开一个同名 span,并在回合结束时写入 prompt、响应与 token 用量。VeADK 的整条遥测链路——内存 exporter 的 Session 索引、Trace 上报、Portal 指标、以及基于 Trace 的评测——因此都能看到 codex 回合。注意粒度:每回合一个 span,而不是每次内层模型调用一个。
  • usage_metadata:整轮的 token 用量汇总后,只挂在每回合一个的合并后终态 Event 上(而不是逐条事件累加),避免下游做求和统计时重复计数。
  • RunConfig(max_llm_calls=...) 会被强制执行:shim 在每一次真实的后端模型调用前扣减配额。超限时 Codex 侧收到一个 429 llm_calls_limit(而不是它会重试的 500——重试会把本回合的工具副作用重跑一遍),回合结束后 runtime 再向调用方重新抛出 LlmCallsLimitExceededError,而不是返回 Codex 勉强拼出的半截答案。

支持矩阵

runtime="codex"runtime="piagent" 会替换整个 ADK LLM 流程,因此 Agent 上相当一部分配置不会生效。VeADK 会在智能体构造时以及每次调用前检查这些配置:产生错误结果的配置直接报错(ValueError只是被忽略的配置记录一次警告

报错:直接拒绝运行

配置原因
model=...运行时从 model_name 解析模型,完全忽略 model 对象,其 api_base、请求头和 fallback 都会丢失。请改用 model_name
generate_content_config=...只有 system_instruction 会被转发,temperaturemax_output_tokensthinking_config 等都会被丢弃。
output_schema=...schema 既不会下发给后端,也不会进入提示词,模型从未被要求按 schema 输出;state[output_key] 要么是未校验的回复,要么直接缺失。
planner=... / code_executor=...二者以 ADK 请求/响应处理器的形式运行,外部运行时不执行这一层。
include_contents="none"外部运行时始终发送完整会话历史,该设置会被静默忽略并泄漏历史轮次。
enable_supervisor=True监督流程挂在 ADK LLM flow 上,而该流程已被替换,不会有任何监督生效。

runtime="codex"runtime="piagent" 都支持 sub_agents:VeADK 会注入一个 transfer_to_agent 工具供外部 agent 调用。把一个 runtime="codex" 智能体放进 SequentialAgent / ParallelAgentsub_agents 中也支持——这种场景由父级负责调度。

警告:被忽略但仍可运行

配置行为
model_name=[主, 备...]只使用第一个模型,fallback 链不生效。
model_provider(非 openai运行时始终以 OpenAI 兼容协议访问 model_api_base
model_extra_config仅对 piagent 成立。codex 会转发它extra_headersextra_body(含 VeADK 默认的请求加密与 prompt 缓存配置)由 shim 原样带到后端请求上,与 adk 路径一致。
enable_responses / enable_responses_cache不使用 Ark Responses API,previous_response_id 续接与响应缓存均不生效。
example_storeExampleTool.process_llm_request 注入,外部运行时不调用该钩子,few-shot 示例不会进入 prompt。
knowledgebase知识库被静默禁用。 同样由 LoadKnowledgebaseTool.process_llm_request 注入使用说明,外部运行时不调用,模型不知道知识库的存在,检索不会被触发。
skills_mode外部运行时在桥接工具时会跳过 VeADK 的 SkillsToolset,指令里宣称的 execute_skills/skills_tool 并未注册。
enable_skills_checklist依赖 ADK 的 before-tool 回调与 skills 工具集,两者都不生效。
after_model_callback语义不同:每回合在合并后的最终文本上触发一次,而不是每次模型调用触发一次。
tracers粒度不同:codex 会补一个每回合一个call_llm span(含 prompt、响应、整轮 token 用量),但内层模型循环跑在外部 harness 里,仍然没有逐次模型调用的 span。
codex_runtime_config(配合 runtime="piagent"只对 codex 运行时有效,piagent 完全忽略。
RunConfig(max_llm_calls=...)仅对 piagent 成立——它的模型循环在 harness 内部自行计数,不受该配额约束。codex 已支持:见上方可观测性

不支持 run_live

adk 运行时没有 live/bidi 实现。通过 ADK get_fast_api_app 暴露的 /run_liveveadk web 使用)对这类智能体会抛出 NotImplementedError,而不是静默回退到 ADK 流程——否则会用完全不同的模型循环和工具集执行。live 场景请使用 runtime="adk",其余场景使用 runner.run_async

请求处理

在不侵入业务逻辑的前提下,可为每次执行插入统一的横切处理——鉴权、日志、重试、性能监控等。把处理器传入 run_processor 即可,例如接入身份认证做登录态校验:

agent.py
from veadk import Agent
from veadk.integrations.ve_identity import AuthRequestProcessor

agent = Agent(name="assistant", run_processor=AuthRequestProcessor())

不设置时使用默认的空处理器,不改变任何行为。

本页导航