agno CodeMode 实战:用持久化 Python 内核构建编程型 Agent(附 01_basics 测试日志全解析)
导读
本篇技术指南围绕 agno 开源仓库中 cookbook/code/01_basics 目录下的 basic.py 与 with_shell.py 两个最小可运行示例,及其对应的 TEST_LOG.md 测试记录展开。你将掌握 agno CodeMode 工具集的完整用法:如何让 Agent 在一个随会话存活的 IPython 内核中编写并执行 Python 与 %%bash 单元格,如何理解内核变量跨轮次持久化、快照恢复与安全边界,以及如何对照真实测试结果验证 Agent 的行为。读完本文,你可以在自己的 agno 项目中用 20 行代码复现一个"零工具面"的编程型 Agent。
一、背景:为什么需要一个"可编程环境"而不是一堆工具
传统的工具型 Agent 思路是给模型一张宽泛的工具 schema:web_search、read_file、execute_python……工具越多,模型的选择开销越大,输出上下文也越容易被中间结果撑爆。agno CodeMode 提供了另一种思路——给模型一个"活着的解释器"作为唯一环境。
从 cookbook/code/01_basics/basic.py 的模块注释可以看到它的设计目标:
"Give the agent one programmable environment instead of a wide tool schema. The model writes Python; the code runs in an IPython kernel that lives as long as the session, so variables, imports, and helper functions survive across turns."
核心收益体现在两个层面:
- 状态持久化:中间计算结果以变量形式保存在内核中,不进入对话 transcript,只在最后输出结论。例如 basic.py 中前 200 个斐波那契数完全以内核变量的形式存在,对话里只出现"67 even; 42 digits"这样的结论。
- 零额外工具面:模型只需要
execute(以及可选的restart)两个函数即可完成任意计算任务。
二、关联文档解读:01_basics 的测试记录
TEST_LOG.md 是 01_basics 目录下的官方测试日志,记录于 2026-08-21,验证了该目录两个示例在真实环境中的运行结果。测试基线如下:
- 模型:
gpt-5.5(通过 agno 的OpenAIResponses模型类调用) - 内核依赖:ipykernel 7.3.0、jupyter_client 8.9.1、dill 0.4.1
- 运行环境:仓库 worktree 自带的 Python(agno 来自当前分支,
from agno.tools.code import CodeMode)
日志还特别说明了一个无害的告警:每次运行 stderr 都会打印一行 [IPKernelApp] WARNING | Kernel is running over TCP without encryption...。这是 ipykernel 7 在 kernel 启动时产生的标准提示,对 CodeMode 运行的仅回环(loopback-only)内核不构成实际风险,也不属于该分支引入的变更。了解这一点,可以在调试时避免误判。
三、示例一:basic.py——用内核变量完成斐波那契计算
3.1 代码解析
cookbook/code/01_basics/basic.py 是"最小的可工作 CodeMode Agent":
from agno.agent import Agent
from agno.models.openai import OpenAIResponses
from agno.tools.code import CodeMode
code = CodeMode()
agent = Agent(
model=OpenAIResponses(id="gpt-5.5"),
tools=[code],
instructions="Use the code environment to compute answers. Print summaries, not raw data.",
markdown=True,
)
if __name__ == "__main__":
try:
agent.print_response(
"Build a list of the first 200 Fibonacci numbers in the code environment, "
"keep it in a variable, and tell me only how many of them are even and "
"how many digits the largest one has.",
session_id="code-mode-basic",
)
finally:
code.shutdown()
几个关键点:
CodeMode()无参数构造:默认开启execute与restart两个工具、允许%%bash单元格、启用快照(见下文参数表)。instructions要求"只打印摘要,不打印原始数据":这是让中间数据留在内核、只让结论进入对话的关键 prompt 设计。session_id显式指定:CodeMode 按session_id一对一维护内核与快照,固定 session_id 可以让同一内核跨多次运行复用。finally: code.shutdown():显式释放内核。源码中 code_mode.py 的shutdown会先快照再杀掉内核;即便开发者忘记调用,对象销毁时weakref.finalize注册的_cleanup_kernels也会兜底清理,但显式调用仍是推荐做法。
3.2 测试结果与验证
TEST_LOG 记录的运行结果为:一次 execute 调用即完成任务,单元格打印 even_count=67, largest_digits=42,Agent 回答 "67 even; 42 digits"。日志确认了正确性:该单元格构建的是从零开始的斐波那契序列(zero-first sequence),其最大项 173402521172797813159685037284371942044301 恰好是 42 位数字。进程退出码为 0,无 traceback。
四、示例二:with_shell.py——%%bash 单元格的规则
4.1 代码解析
cookbook/code/01_basics/with_shell.py 演示 IPython 单元格魔法(cell magic)随内核免费可用,模型可以直接使用 %%bash 进行 shell 编排:
code = CodeMode()
agent = Agent(
model=OpenAIResponses(id="gpt-5.5"),
tools=[code],
instructions=[
"Use the code environment for shell work.",
"Report only what you learned, not raw command output.",
],
markdown=True,
)
if __name__ == "__main__":
try:
agent.print_response(
"Using a %%bash cell, count how many Python files are in the current "
"directory tree, then report the count and the Python version the "
"environment is running.",
session_id="code-mode-shell",
)
finally:
code.shutdown()
4.2 关于 %%bash 的三个关键规则
结合源码与 README,%%bash 的使用必须遵守以下规则:
%%bash必须是单元格的第一行。CodeMode 生成的内核指令明确要求 "%%bash must be the first line of its cell - no comment, import, or statement before it"。源码在 code_mode.py 中甚至对"位置错误的%%bash"会追加一条 hint:%%bash must be the first line of its cell - move any comment, import, or statement into a separate cell。- 每个
%%bash单元格是一次性子 shell(throw-away subshell):cd、export和 shell 变量不会延续到下一个单元格。依赖 shell 状态的步骤必须放在同一个单元格里,或者改用内核级操作。 %cd与os.environ[...]是内核级的,会持久生效:它们作用于之后的每一个%%bash单元格。这一设计见 README.md 的说明。
4.3 测试结果与验证
TEST_LOG 记录了该示例最有说服力的一次行为:模型在第一个单元格直接调用了裸 python,而测试机器上不存在 python 命令;Agent 自主重新发起,改用 python3 后成功。最终回答为:4,577 个 Python 文件、Python 3.14.6。日志验证方式如下,可以视为复现该示例的判据:
find . -name '*.py' -type f | wc -l # 应输出 4577
python3 -V # 应输出 Python 3.14.6
进程同样退出码为 0,无 traceback。
4.4 allow_shell=False 的降级策略
README 与脚本注释都强调:传入 allow_shell=False 会移除 %%bash 魔法,任何 %%bash 单元格都会被拒绝。源码中 _rejects_shell 在 code_mode.py 实现判断,被拒绝时返回 Error: %%bash cells are disabled (allow_shell=False)。对应单元测试 test_code.py 验证了这一点。但务必注意:这只是"减少误触发的保险丝"(a footgun reducer),不是安全边界——CodeMode 从来不是沙箱。
五、源码纵深:CodeMode 的架构与核心参数
5.1 架构总览
CodeMode 是 agno 的一个 Toolkit 子类,实现在 code_mode.py。模块 agno/tools/code 由以下部分组成:
- code_mode.py——CodeMode 本体:模型面工具、生命周期、快照调度
- kernel.py——
KernelSession:一个 session 对应一个 IPython 内核子进程,负责单元格执行、超时、忙碌策略 - snapshot.py——
SnapshotManager:基于 dill 的会话状态持久化 - bridge.py——
ToolBridge:把注入的工具桥接进内核,作为可await的句柄 - errors.py 与 types.py——错误类型与
CellResult数据结构
从 kernel.py 的模块注释可以看到其底层设计:每个 KernelSession 拥有一个 IPython 内核子进程,通过显式的 KernelSpec 启动,从不查询系统安装的 kernelspec,解释器精确等于所请求的 python;所有 ZMQ 通道、锁与定时器运行在单一后台事件循环(LoopRunner)上,使同步与异步工具面共享同一客户端和同一把按会话的 asyncio.Lock。
5.2 完整参数表(来自构造函数)
CodeMode.__init__ 在 code_mode.py 中定义了以下可配置项,均为可选:
| 参数 | 默认值 | 作用说明 |
|---|---|---|
tools |
None |
注入到内核中的工具序列(Toolkit / Callable / Function),以内核内可 await 句柄形式提供 |
fs |
None |
FileSystem 实例,用于存放快照;不传则关闭快照持久化 |
snapshot |
True |
是否启用会话快照(dill 序列化,恢复即执行代码) |
snapshot_debounce |
1.5 |
快照防抖间隔(秒) |
max_variable_bytes |
2_000_000 |
单个变量超过该字节数则不入快照,需重建 |
max_snapshot_bytes |
64_000_000 |
会话总状态超过该字节数则不入快照 |
max_output_chars |
65_536 |
单单元格输出字符上限(流式截断) |
max_result_bytes |
1_000_000 |
工具结果负载上限,超出抛 ResultTooLarge |
allow_restart |
True |
是否向模型暴露 restart 工具(内核损坏/卡死时重建) |
allow_shell |
True |
是否允许 %%bash 单元格;False 时直接拒绝 |
on_busy_kernel |
"wait" |
内核忙时策略:"wait" 等待(busy_wait 秒)或 "restart" 强制重启后重跑 |
busy_wait |
5.0 |
忙碌轮询等待秒数 |
idle_ttl |
1800 |
空闲淘汰时间(秒),超时内核被回收 |
timeout |
300 |
每个单元格的执行超时;None 表示不设限(有挂死风险,生产环境务必保留超时) |
python |
None |
内核解释器路径,显式指定则完全可控 |
cwd |
None |
内核工作目录 |
env |
None |
内核环境变量字典 |
startup_code |
None |
内核启动时执行的代码 |
max_images_per_cell |
8 |
单单元格最多返回的图片数 |
max_image_bytes |
5_000_000 |
单张图片大小上限 |
max_kernels |
None |
进程内同时保活的内核上限,超出按 LRU 淘汰(先快照再淘汰) |
依赖项方面,模块在 init.py 中强制检查 dill、ipykernel、jupyter_client,缺失时会提示安装命令:pip install 'agno[code]'。
5.3 模型面工具与开发者面工具
CodeMode 对外暴露两类接口(均有同步/异步成对实现,且被 test_code.py 的测试强制保证"每个公开方法都有 async 孪生"):
- 模型面:
execute(run_context, code)——执行一个单元格并返回 stdout、stderr、末表达式 repr 与 traceback;restart(run_context)——重建内核,清空全部状态。 - 开发者面:
run(session_id, code)返回原始CellResult;variables(session_id)返回"变量名 → 类型名"映射(跳过下划线前缀与 IPython 内部名);value(session_id, name)通过 dill 往返取出单个变量;shutdown(session_id=None)快照后杀内核(None表示全部)。
值得注意的安全细节:_session_key 在 code_mode.py 中强制 session_id 只来自框架注入的 RunContext,"模型永远无法指定参数去触达另一个会话的内核"。
5.4 快照与多用户隔离
- 快照:启用
fs且snapshot=True时,SnapshotManager会周期性把内核命名空间 dill 序列化保存。恢复(restore)也是执行代码,因此快照存储继承持有它的数据库的信任级别——这也是源码在模块 docstring 中强调的信任模型。恢复发生在 bootstrap 之前(code_mode.py),保证新鲜的动态句柄覆盖过期的 pickle 句柄。 - 用户隔离:会话在内存与快照清单中记录归属用户(owner),后续其他用户的 run 访问同一 session_id 时会被拒绝并返回
OWNER_REFUSAL(code_mode.py)。这是多租户场景下防止 A 用户读到 B 用户内核变量的关键机制。 - 会话共享语义:内核与快照仅按
session_id键控。团队模式下 leader 与成员共享同一 session_id 时,会共享同一个内核命名空间;两个 CodeMode 实例快照进同一 FileSystem 且使用相同 session_id 时,会互相恢复对方的变量。若不希望共享,应给每个 CodeMode 独立的 FileSystem 命名空间。
六、安全边界:明确这不是沙箱
这是使用 CodeMode 前必须读三遍的结论。源码在 code_mode.py 的模块 docstring 中开宗明义:
"CodeMode executes arbitrary Python and shell with the permissions of the process running the agent. It is not a sandbox and does not pretend to be one: use it with a trusted operator or inside an isolated container."
同时在运行时,每次调用 execute/restart 都会经 _warn() 输出一次警告:CodeMode 以当前进程权限执行任意 Python 与 shell,不是沙箱,需要人工监督或在隔离容器中运行(code_mode.py)。allow_shell=False 只能降低误用概率,不能提供隔离。
七、运行方式与复现建议
按 README.md 的说明,从仓库根目录直接运行即可:
python cookbook/code/01_basics/basic.py
python cookbook/code/01_basics/with_shell.py
复现 TEST_LOG 记录的判定标准:
basic.py应输出 "67 even; 42 digits",且只发生一次execute调用;with_shell.py应输出 4,577 个 Python 文件与当前解释器版本号;- 两条脚本均以退出码 0 结束,无 traceback;
- stderr 上出现
[IPKernelApp] WARNING | Kernel is running over TCP without encryption...属正常现象,不影响功能。
八、总结
从 TEST_LOG.md 的两次真实运行可以看到,CodeMode 把一个"可编程环境"完整地交给了模型:basic.py 用内核变量承载 200 个斐波那契数,对话中只出现结论;with_shell.py 让模型自主完成 python → python3 的纠错后统计出 4,577 个 Python 文件。二者都没有引入任何除 execute 之外的模型面工具。结合 code_mode.py 源码可知,其能力边界由内核会话、dill 快照、忙碌/超时策略与明确的非沙箱警告共同定义。对于需要"计算密集型 + 中间状态不污染上下文 + 跨轮次复用"的 Agent 场景,这是 agno 中一个开箱即用的高性价比选择。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00