Prime Agent 拆解:把工具面砍到只剩一个,让上下文变成变量
拆 PrimeIntellect-ai/prime-agent(commit a18809e)。它只给模型一个工具叫 ipython,读文件、改代码、派子 agent、改自己的提示词全在 Python 里做。核心赌注是:与其把工具结果塞进消息历史再总结,不如让它留在内核变量里,上下文只放索引。
大部分 coding agent 的形状是一样的:给模型十几个工具(读文件、写文件、grep、bash、web 搜索……),每轮把这些工具的 schema 发过去,模型挑一个调用,结果原样回到消息历史里。上下文快满了就总结一遍,接着跑。
Prime Agent 把这套东西整个换掉了。它只给模型一个工具,名字叫 ipython,参数就一个字符串。
| 要做的事 | 在 Prime Agent 里怎么写 |
|---|---|
| 读文件 | open().read() |
| 改代码 | await edit(...) |
| 派一个子 agent | await rlm("...") |
| 改自己的长期记忆 | rlm.harness.create_memory(...) |
全都是 Python 表达式,在一个跨轮次活着的 IPython 内核里执行。
阅读前提
本文基于 commit
a18809e(2026-08-07 16:23 -0700),文中行号对应该 commit。我没有在本地执行这个仓库的代码。 所有结论来自读源码、读
docs/和读它自己的单元测试断言,不来自跑起来的输出。涉及运行时行为的判断都是按代码逻辑推导的,不是实测。
先说判断:它真正下注的是什么
README 把自己总结成两个抽象:RLM(Recursive Language Model)和 Continual Harness。这两个词单看都挺唬人,拆开之后各自只解决一件很具体的事。
- RLM 解决「工具结果放哪儿」。
- Continual Harness 解决「agent 学到的东西存哪儿」。
这两件事对应长跑任务里最贵的两笔账:上下文窗口,和跨会话的经验。前者每轮都在烧钱,后者每次新开会话都在重来。
我读完的判断是:RLM 这一层自洽且激进,值得认真看;Continual Harness 想法对,但完成度明显低一截。 具体差在哪,后面单独说。
还有一个前提不该跳过。README 和 docs/architecture.md:49 都写了同一句话:
Prime Agent executes model-generated Python and project commands with your user permissions. Its worker and kernel processes improve lifecycle isolation and recovery; they are not a security sandbox.
拆进程是为了别互相拖死,不是为了别乱动你的机器。这句话在讲进程拓扑时还会再出现一次。
赌注一:同一份日志,两种存法
场景很具体:agent 要排查构建失败,build.log 有 80k tokens。

左边那条路的问题不是「上下文会满」,而是模型对这件事没有选择权。
它发出 read_file 的那一刻,还没看到内容,内容就已经在上下文里了。哪怕只需要其中三行,剩下的 79k 也得占着位置,此后每一轮都要重发。
右边这条路把决定权交回给模型。 log = open("build.log").read() 执行完,80k 字符在内核的内存里;进上下文的只有这一行代码,加上模型自己选择 print 出来的部分。下一轮想换个角度,re.findall(r"ERROR.*", log) 再切一刀,不用重读文件。
这条纪律被直接写进了系统提示词(prompts/rlm.ts:23):
Use Python for reading, searching, and editing files — it gives you reusable variables you can slice, filter, and act on without re-reading. Always assign read/search results to named variables so you can revisit them later.
但没有任何自动卸载机制
这一点容易被宣传语盖过去,我专门去代码里确认过。
没有 bind_prompt(),也没有「输出超过 N 就自动存成变量」。tools/ipython.ts、kernel/index.ts、kernel/state-snapshot.ts 三个文件我从头读到尾,没找到这类机制。
唯一的隐式绑定是 IPython 自带的输出历史。宿主发执行请求时带了 store_history: true(kernel/index.ts:855),所以 _、Out[n] 这些东西存在。但宿主代码从没提过它们,快照时还特意排除掉。
也就是说,这个机制完全靠模型自觉。 提示词让它把结果绑到变量上,它就绑;它要是写成 print(open("build.log").read()),80k 一样进上下文,只不过会被 DEFAULT_MAX_OUTPUT_CHARS = 65536(kernel/index.ts:31)砍断,末尾补一行 [... output truncated at 65536 chars ...]。
所以「RLM 从不总结上下文」成立的前提,是模型足够会写检索代码。这是个真实的能力门槛,不是免费午餐。Prime Intellect 自己训模型,这个前提对他们比对别人更容易成立。
赌注二:能力的单位不再是一个工具
上面那套做法有个直接推论:既然一切都在 Python 里发生,就不需要第二个工具了。

这不是我的概括,是源码里的字面写法。
// core/tools/index.ts:46-47
export type ToolName = "ipython";
export const allToolNames: Set<ToolName> = new Set(["ipython"]);
// core/system-prompt.ts:65
const tools = selectedTools ?? ["ipython"];
更能说明态度的是 CLI 参数校验那段:
// cli/args.ts:63-64
const REMOVED_BUILTIN_TOOL_NAMES = new Set(["read", "write", "grep", "find", "ls"]);
const BUILTIN_TOOL_NAMES = ["ipython"];
这几个工具名不是「没实现」,是被删掉了并且专门留了报错提示。显式传 --tool read,它会告诉你这个内建工具已经不存在了。仓库里 bash.ts、edit.ts 的工厂函数还在,但没有注册进 allToolNames。
ipython 工具的完整 schema 就一个字段(core/tools/ipython.ts:143-148):
const ipythonSchema = Type.Object({
code: Type.String({ description: "Python scratchpad code or `%%bash` shell cells ..." }),
});
跑 shell 也不是另一个工具,是 IPython 的 %%bash 魔法命令。docs/rlm.md:51 划了语义边界:每个 %%bash cell 是一个一次性子 shell,但 Python 状态和 %cd 会留在内核里。
MCP 接进来也不加工具
docs/mcp-integrations.md 写得很直白:
Consistent with Prime Agent’s single-tool design, MCP integrations are not exposed as new agent tools.
Linear 和 Notion 以 Python 模块的形式进命名空间(skills/linear/、skills/notion/),未知属性会被转成一次异步 MCP 调用(mcp_base.py:283-303)。接十个 MCP server,模型看到的工具数还是 1。
真正的好处是组合,不是省 schema
十几个工具的 harness 里,想把 grep 的结果喂给一个 HTTP 请求,中间结果必须原样经过一次上下文。在这里就是一句 for m in re.findall(...): await fetch(m),一个 cell 里跑完,进上下文的只有主动 print 的那部分。
代价也很清楚:能力的单位从「一条工具定义」变成了「一个 Python 包加一份 SKILL.md」。 门槛更高,但也意味着模型可以在运行时自己造能力——仓库里的 skill-creator 就是干这个的,它把新能力打包成可编辑安装的 Python 包塞进内核的 venv。
await rlm(...) 不返回答案
递归子 agent 是 RLM 名字里的 R。这里有个反直觉的地方,我认为是全项目最容易读错的 API。

docs/rlm.md:55-62 的原话:
The call returns immediately after task admission with a child handle; it never waits for or returns the child’s answer.
句柄里只有四个字段:rlm_child_id、name、session_dir、model(agent-session.ts:9948-9953)。宿主把启动和执行明确拆开了,注释写得很直接(agent-session.ts:9732):
// Runtime startup and the task run are deliberately detached. The public spawn
// resolves at admission, while this task owns live tracking, usage, retention,
// cancellation, and late-startup cleanup.
结果通过两条路回来: 子 agent 显式调 await agent_message.send(..., receiver_role="parent"),或者把结果写进自己的 session_dir 让父 agent 去读。
要是子 agent 跑完一句话都没回,宿主会补一条 completed_without_reply 通知(agent-session.ts:9838-9847),不至于让父 agent 干等。
这个 API 形状意味着父 agent 派完活应该就地结束这一轮,而不是原地写循环轮询。这跟大多数人对 await 的直觉是反的,也是这里最容易踩的坑。
几个值得记的细节:
- 参数只有两个:
name和model。传别的会报错而不是被忽略(agent-session.ts:9603-9610)。docs/rlm-runtime.md:151-156明确写了 Unknown options fail instead of being ignored。 model指定了但不可用就直接失败,不静默降级到父模型。它会先做一次鉴权预检,过滤掉 stale 和 expired 的凭据。- 默认递归深度是 1(
_resolveRlmMaxDepth,agent-session.ts:1573-1592)。根会话能生孩子,孩子默认不能再生。环境变量里也有RLM_MAX_DEPTH,但代码注释说得很清楚:kernel 的环境变量是启动时快照,可能是旧的,以 TypeScript 侧的检查为准。 - 子 agent 是真正隔离的:新的
SessionManager、新的会话文件、自己的 IPython 内核、自己的sub-xxxxxxxx/目录。共享的只有 provider、模型注册表和重试配置。 - 用量往上归并:子 agent 花的 token 异步记到发起它的那条父 assistant 消息上(
appendChildUsageAttribution),账单总额是对的,但不会撑大父模型自己的上下文读数。
一个漂亮的工程细节:响应走 control 通道
内核向宿主发反向请求(rlm.run 是其中一种)用的是 Jupyter comm,target 叫 host.request。宿主的响应必须走 control 通道,不能走 shell 通道(kernel/index.ts:1272-1279):
const channel = this.control ?? this.shell;
原因在 docs/rlm-runtime.md:113-123:shell 通道正被那个还没执行完的 cell 占着。响应要是也走 shell,就变成「cell 等响应、内核等 cell 结束才处理响应」的死锁。Python 侧对应地把 comm handler 装在了 control 上(rlm/__init__.py:63-64)。
还有一处小心思:comm.open(data={**(payload or {}), "type": request_type})。type 放在最后展开,这样 payload 里如果有个同名的 type 键,也劫持不了路由。
让 agent 改自己的提示词,但只准改一层
这是第二个抽象。prompts/rlm.ts:31 给了一句精确的术语划分:
continual harness names the persisted prompt, memory, skill, and subagent layer; RLM names the runtime, IPython kernel, and native call interface exposed to the model.

「基础提示词不可变」靠的是结构,不是校验器
这是我一开始最怀疑的一句话。看完 system-prompt.ts:116-166 的拼装顺序才明白。
它成立不是因为有个校验器拦着,而是因为 harness 的条目根本没有任何路径能进到 buildRlmPrompt 里去。基础提示词是编译进二进制的字符串,harness 内容作为一个独立的 # Continual Harness State 段落追加在它后面。代码注释也点了这件事:这一段是 Appended AFTER the trained buildRlmPrompt prefix。
校验层面确实还有一道:prompt 类型的条目里,id 为 base_system_prompt 的编辑会被明确挡掉。但那更像双保险。
trained prefix 这个词值得留意。 Prime Intellect 自己训模型,基础提示词是训练时输入分布的一部分。让 agent 随便改它,等于在推理时把模型推出训练分布。这个不可变约束有具体动机,不只是洁癖。
进提示词的是索引,不是全文
这是我读这一块最大的收获,也是最容易被文档一句话带过去的细节。
# Continual Harness State 这一段不是把条目全文贴进去:
// refinement/refinement.ts:26-28
const DEFAULT_OVERVIEW_ENTRY_LIMIT = 6;
const DEFAULT_OVERVIEW_REFINEMENT_LIMIT = 5;
const DEFAULT_OVERVIEW_CONTENT_LIMIT = 180;
每一类最多注入 6 条,每条内容截断到 180 字符,多出来的显示成 +N more。提示词里自己也说了,这些是 compact summaries, not full descriptions… routing/context hints——它是一份目录,告诉模型有这么个东西存在,要看细节自己去 rlm.harness 里读。
这个设计本身是对的,跟前面「上下文里放索引不放原文」是同一个思路。但它有个直接后果:条目写超过 6 条之后,后面的在提示词里基本等于隐身。而我在代码里没找到任何淘汰、打分或者按相关性排序的机制,条目只增不减。长期跑下来,早期写的东西会自然沉底。
/refine 一次做什么
触发有三条路:人手敲 /refine、模型自己 await refine.run()、自动触发。自动那条的默认参数在 settings-manager.ts:883-896:每 25 个 assistant 轮次检查一次,压缩之后也会检查一次,冷却 20 分钟。子 agent 里完全禁用。
自动路径比手动多一道审核闸:单独起一次 LLM 调用,只回答「这段轨迹值不值得写进去」。它的系统提示词(refinement.ts:175)明确要求驳回一次性的噪音和没证据的猜测,宁可返回空。
这一层的存在说明作者清楚:让模型自己决定记什么,最大的失败模式不是记漏,是记一堆废话。
规划阶段喂进去三样东西:
- 轨迹的最后 80k 字符(
refinement.ts:892) - 当前 harness 概览
- 最近 20 条改动历史(
refinement.ts:553)
输出被限制成 JSON 编辑列表,而且刻意关掉了 thinking。这是个有意思的选择,说明他们把这一步当成结构化抽取,不当成推理。
然后逐条校验、原子写盘、重建系统提示词。某一条编辑挂了只记错误,不影响同批其他条目。
回滚这一块设计得很好
每条 edit 写盘时都存了这个条目改动前后的完整快照。/refine rollback <id> 就是把这批快照倒着回放:删掉的重建、改过的还原、新建的删除。
全程零 LLM 调用,所以它便宜、确定、可重复。回滚本身也被记成一次新的 refinement,所以回滚也能再回滚。
「让模型改状态、但回退是纯确定性的」,这个组合比 refine 本身更值得抄。
但这一层的完成度明显不如 RLM
两个具体的粗糙处:
没有任何界面能浏览 harness 条目,也没有命令能列出可回滚的 id。 /refine rollback <id> 需要一个 id,但得自己去翻 harness_state.json 找。这不像设计取舍,更像还没做。
条目没有生命周期。 只增不减,配合「每类只注入 6 条」,效果是早期条目静默失效。这个组合在短会话里没问题,在它主打的长跑 agent 场景里恰恰最要命。
为什么关掉终端 agent 还在跑
前面讲的都是模型看到的那一面。下面这层是它能长跑的原因。

docs/architecture.md:43-49 把职责划分写得很清楚,这里只挑几条实现细节。
客户端是纯视图。 docs/agent-connection.md:142-148 用架构约束强制了这件事:InteractiveMode 不允许依赖 AgentSessionRuntime、AgentSession、SessionManager 或者守护进程的 socket 路径,它只能握一个 AgentConnection。所以同一个 TUI 既能驱动远端 worker,也能驱动进程内会话。
detach 是一条协议命令,不是信号。 daemon-mode.ts:3653-3661 的实现就是把这个客户端从事件扇出集合里摘掉,压根不碰会话、队列和 worker。终端被杀、SSH 断了走的是同一条路(socket close),效果一样。worker 本身是 detached: true 起的,stdio 里没有终端,所以也没有 SIGHUP 这条路径。
唯一会被自动清理的是客户端拥有的 worker。 --print、--json、rpc 这几种一次性调用起的 worker 有 30 秒断连宽限期(OWNED_WORKER_DISCONNECT_GRACE_MS = 30_000),超时才停。交互式常驻会话不走这条路。
worker 崩了只影响一棵会话树。 重试延迟是 250ms、1s、5s,三次失败标记这个根为 failed(daemon-supervisor.ts:136)。supervisor 自己从不 import provider SDK、不执行工具、不扫 transcript(那是另一个 catalog 子进程的活),所以能一直保持小而稳。
supervisor 挂了 worker 也还在。 ~/.prime/agent/daemon-workers/<key>/ 下面存着 worker descriptor 和恢复日志,新起的 supervisor 拿着里面的 token 重新连上去认领。
这里有两个防御细节:pid 配 processStartId 防 PID 复用;stopRequestedAt 是「在杀掉之前先写进文件的意图」,防止替补 supervisor 把你故意停掉的东西又拉起来。
transcript 每条都是同步 appendFileSync。 吞吐让位给持久性,好处是这个文件在任何时刻都是一个合法的 JSONL 前缀。它还是懒创建的:第一条 assistant 消息之前什么都不写盘,所以打开又关掉的会话不留垃圾文件。
读文档会被坑到的两个点
docs/daemon.md标题写着 Public Daemon Protocol v4,但daemon-protocol.ts:52里是DAEMON_PROTOCOL_VERSION = 7。文档滞后了,以代码为准。文档里的拓扑图既没画 catalog 子进程,也没画 Linux 上的 Python forkserver。
长跑要的那些零件
这部分不展开讲,数字比形容词有用。先给核对过的默认值:
| 机制 | 关键默认值 | 源码位置 |
|---|---|---|
| 压缩触发 | contextTokens > contextWindow - 16384,保留最近 20000 tokens | compaction.ts:128-132 |
| 快照上限 | 每变量单独 dill,单个失败不毁整体 | state-snapshot.ts:56-138 |
| goal 预算 | 只算 input + output,不算缓存 token | goals.ts:96-98 |
| 心跳 | 默认 5 分钟一次,递归间隔最小 10 秒 | — |
| 自主模式 | 3 次续跑 / 12 轮 / 80000 tokens / 30 分钟 | autonomous.ts:48-55 |
| agent 消息 | 单条 ≤ 16384 字符,每会话 ≤ 20 条待处理 | agent-messages.ts:24 |
| 空闲驱逐 | 90 分钟降级为 passive | session-action-store.ts:337-367 |
下面四件事值得单独说。
压缩不碰内核,但要重新播报
压缩的切点永远不会落在 toolResult 上,否则会切出一个没有结果的工具调用。总结用固定的六段式:Goal / Constraints & Preferences / Progress / Key Decisions / Next Steps / Critical Context。
压缩跟 RLM 的配合是这里最巧的一处:压缩完全不碰内核。 它只重写 transcript,Python 变量一个都不掉。
但模型会忘掉这些变量叫什么——定义它们的那些 cell 被总结掉了。所以压缩的最后一步是去内核里列一遍活着的名字,塞一条消息回去(agent-session.ts:6887-6935):
<ipython_state>
Your IPython kernel persisted through compaction; all variables, imports, and helpers
you defined remain available. These names are still defined: log, errs, src_text, ...
</ipython_state>
给总结用的提示词里也专门有一句(compaction.ts:498-499):内核还活着,定义这些名字的 cell 不会出现在上面,把值得记的名字写进总结里,别去重新定义一遍。
跨会话是另一套机制:dill 逐变量打包成 kernel-state.dill,一个对象序列化失败只丢那一个名字。rlm 和 asyncio 在永不快照的名单里,因为它们每次启动都会被重新注入——恢复顺序也是先恢复再 bootstrap,让活的句柄盖掉恢复出来的旧壳。
goal 跟着分支走
goal 存在会话 JSONL 里的一条 custom 条目,读的时候从后往前找最近一条,所以它天然跟着分支和回溯走。
续跑提示词里有一句我很喜欢的话:完成前要对着目标逐条核对,不要拿意图、部分进展、对早前工作的记忆、或者一个看起来说得通的最终答案当作完成的证据。目标文本本身被当成不可信数据处理,明确告诉模型这是任务,不是更高优先级的指令。
自主模式:指纹没变就不重跑
质量闸就是一串 shell 命令。这里有个很聪明的设计:重跑一个之前失败的闸之前,它先给工作区做指纹——git status --porcelain 加 git diff --binary HEAD 加未跟踪文件的 SHA-256。
指纹没变就不重跑,直接告诉模型:
The autonomous gate was not rerun because the workspace has not changed since this failure. Edit source files, tests, or a blocker artifact before attempting to finish again.
这一手直接掐死了「反复跑测试指望它自己变绿」这种行为。
agent 之间发消息:只能找核心家庭
寻址范围被限制成 parent、sibling、child,没有祖父母也没有堂兄弟。From 字段由守护进程根据发送方身份填,发送方伪造不了。
代码里有一处很实在的注释:投递不能 await。排队的消息只有在目标那轮推进时才会送达,而发送方正卡在自己那轮里——互相发消息的两个繁忙会话会直接死锁。
心跳和定时任务共用同一套 AgentCronJobStore。投递模式分 steer(插进当前这轮)和 follow_up(排队等下一轮)。定时语法支持 in 30m、every 10m、at <ISO 时间> 和标准 cron 表达式。有个反馈环值得注意:带活跃心跳的会话不会被空闲驱逐。
子 agent 的驻留状态有三档:
- running:在跑。
- retained:跑完了,但会话对象还留在内存,还能收消息。
- passive:被逐出内存,只剩磁盘上的 registry 行。
降级成 passive 要同时满足五个条件:空闲 90 分钟、没有附着客户端、没有心跳、没有定时任务、没有活着的后代。
要用的时候再从 registry 行水化回来,但必须有个还在内存里的根父会话——孤儿子 agent 是复活不了的。
边界在哪
它不是沙箱。 这一句在 README、architecture.md:49 和 docs/rlm.md:143 各写了一遍。内核以你本人的系统权限执行模型生成的 Python。拆成 worker 和 kernel 两个进程是为了故障和生命周期,跟安全无关。跑不可信仓库请自己套一层外部隔离。
模型必须真的会写代码。 没有自动卸载,整个上下文策略压在模型愿不愿意、会不会把结果绑到变量上再自己写检索逻辑。模型弱一点,这套设计的收益就打折,而且失败模式很难看——它会在一个自己看不见的变量上瞎猜。
harness 那层还缺东西。 没有浏览界面、没有列 ID 的命令、条目没有淘汰策略。
文档和代码有漂移。 协议版本 v4 与 v7 是一处。docs/long-running-agents.md 里写的 agent_message.send(..., mode="auto") 是另一处——发布的 Python skill 里根本没有 mode 参数,投递侧硬编码成 "steer"。读文档时要留个心眼。
内核是单线程串行的。 工具声明了 executionMode: "sequential",一批里不能并发跑两个 ipython 调用。一个 cell 卡住,这条会话就卡住了。UI 上给了两个选项:等它跑完保住状态,或者杀掉内核重启。这是个真实的可用性权衡。
代码规模不小。 agent-session.ts 一个文件将近 400 KB,daemon-supervisor.ts 181 KB,daemon-mode.ts 242 KB。这几个文件我和调研用的子 agent 都只能分片读,不能说全都看过。
换到自己的活里,能拿走什么
我平时做客户端性能和 GUI Agent 的自动化测试,长跑任务和大块非结构化数据这两件事都躲不掉。四条我准备直接搬:
第一,把大块数据留在执行环境里,上下文只放句柄。 这条最通用,跟用不用 IPython 无关。GUI Agent 跑一轮会产出一堆截图、view hierarchy、日志,全塞进上下文一轮就废了。留在一个进程的变量里,只把「第几帧、控件树多大、有几个 ERROR」这种索引信息给模型,让它自己写代码去取需要的那一段。前提是这个执行环境得跨轮次活着——这是实现成本的大头,也是 Prime Agent 花力气最多的地方。
第二,压缩之后要把「你还有什么」重新报一遍。
<ipython_state> 那一手成本极低但很关键:状态还在,只是模型不知道它还在。任何「上下文之外还有状态」的设计都得配一个这样的重新播报,否则模型会重新做一遍已经做过的事。
第三,让模型写状态,但回退必须是确定性的。
/refine 的写入靠 LLM,回滚靠 before/after 快照,零模型调用。这个组合可以直接抄到任何「agent 自己维护配置、规则或用例」的场景里。让模型改,但每次改都留一条能确定性倒回去的路。
第四,重跑之前先看工作区指纹。 自主模式那招 git 指纹去重,改成 GUI 测试就是:重跑一个失败用例之前,先看被测应用的版本、用例文件、环境配置有没有变;没变就别重跑,把这句话原样告诉 agent。这能省掉大量「重试三次希望它自己好」的无效轮次。
至于要不要直接用 Prime Agent 本身。它的默认工具集只有一个 ipython,换到它上面不是换个工具,是换一整套工作方式,而且这套方式对模型的代码能力有明确要求。
我的判断是:单工具加持久 REPL 这个思路值得认真评估,尤其是数据密集型的 agent 任务。但直接换过去之前,先拿自己在用的模型试一下它到底会不会主动把结果绑到变量上——这一条不成立,后面全都不成立。
它最有价值的地方,是把上下文管理从 harness 的一个后处理步骤(总结压缩),变成了模型可以直接编程操作的东西。这个转变比它砍掉多少个工具更值得看。
本文基于 PrimeIntellect-ai/prime-agent commit a18809e(2026-08-07)的源码与 docs/ 分析。未在本地执行该仓库代码,涉及运行时行为的判断均来自代码逻辑推导,不构成实测结论。文中行号对应该 commit,后续提交可能已经改动。