从Agent Loop到PlanMode:一套能跑生产的AI Agent工程骨架
类比一下来记忆,整篇文章所讲的知识的体系:
Harness是操作系统——管所有资源的顶层容器;
LLM是CPU——吃进指令吐出结果;
Agent Loop 是内核主调度循环——不停地取任务、调度、等I/O返回、再取下一个;
Two-Stage ReAct是操作系统的模拟执行(dry-run)模式——先试运行看计划,确认了再真正执行;
四个工具是文件系统的几种基本操作——read、write、edit、bash,对应rwx权限模型;
AGENTS.md/Skills/PlanMode是三级存储体系——BIOS、硬盘、内存+磁盘持久化。

一、Agent Loop:内核主调度循环
所有Agent的本质就是一个while(!done)。
while not terminated:
1. 组装上下文(system prompt+历史+工具声明+本次观测)
2. 把上下文丢给LLM
3. 解析LLM返回:
3.1 文本段 → 追加进上下文历史
3.2 有tool_calls → 逐个执行,结果写回历史,continue
3.3 无tool_calls → 终止循环,返回文本
就像操作系统内核的主调度循环:
从就绪队列取进程 → 分配 CPU 时间片 → 等 I/O 中断返回 → 再调度下一个。
Agent Loop是:
不停地组装上下文 → 丢给 LLM → 等返回 → 执行工具(I/O)→ 写回历史 → 再循环。
是整个系统的心脏。
跑稳这个循环要想清楚三件事:
- 结束条件:无tool_call、命中max_turns、用户取消、校验器判完成。没有明确的终止条件,loop会一直转,token烧到破产。
- 验证方法:别信模型自己说做完,而是通过跑测试、diff检查、断言脚本。
- 持久化状态:上下文历史、PLAN.md/TODO.md、工具调用日志。上下文压缩会丢吞掉早期信息,要写在磁盘上。
适合上Agent Loop的场景:
- 高频(每周至少跑1次),一次性任务没必要别上Agent。
- 几乎无需人工介入。
- 失败可以重跑。
二、Two-Stage ReAct:先Dry-run,再真正执行
直接把工具声明丢给LLM,它会非常冲动地去调用工具,而不是先思考该怎么做。
Two-Stage ReAct把“想”和“做”拆成两次独立的LLM调用(让LLM克制一下欲望),物理上隔离:
Stage 1(无工具prompt):只给任务+上下文,不暴露任何工具
→ LLM输出计划/推理链/假设(dry-run)
Stage 2(带工具prompt):把Stage1的输出拼回去,再暴露工具声明
→ LLM按计划调read/bash/edit...
就像操作系统的dry-run模式——apt-get install -s、kubectl apply --dry-run、make -n。
第一次调用是模拟执行,不碰任何系统状态,只输出打算做什么;
第二次才是真正执行,修改状态。

为什么即使模型已经具备“原生思考”能力,这一层依然有工程价值?
- 介入点:Stage1和Stage2之间可以拦下来——改计划、人工审批、合规审计。原生思考是黑盒,拦不住。
- 可观测性:Two-Stage让计划显式成文本,能diff、能回滚、能复盘。原生thinking块看得到但很难审计。
- 多模型兼容:任何程序都能dry-run,不需要程序本身支持什么特殊能力。换一个便宜的小模型也能跑,不必依赖具备原生思考能力的大模型。
什么时候开启思考(不是每轮都开,贵):
- 新任务目标刚下发时(先 dry-run 出计划)
- 工具返回非预期结果(执行出错了,重新dry-run想想要不要换方案)
- 涉及删除、推送、付费、写生产库等高危动作前(大操作先试运行)
三、工具:文件系统的几种基本操作
只给Harness四个基础工具:read、write、edit、bash。
这正好对应文件系统/权限模型里的几种基本操作:
|
工具 |
文件系统操作 |
作用 |
|---|---|---|
|
|
读文件(open+read) |
无副作用,只看不动 |
|
|
写文件(覆盖,像 |
整文件覆盖 |
|
|
修改文件内容(像 |
只改该改的那几行 |
|
|
执行程序(execute) |
跑任何命令,一条顶十个专用工具 |
为什么有write还要edit:write是整文件覆盖,文件一大,token消耗极其巨大,既慢又贵。而且大模型生成长文本时极易截断,或者不小心引入新的语法错误。edit像sed -i,只动需要改的那几行,diff 可控、可评审、可回滚。
给太多工具等于chmod 777,乱开权限:不是能力更强,而是模型更容易冲动调用不该用的工具,出错面反而更大。而且容易限制聪明模型的主动性(聪明模型本来可以做更好的操作,但看到你已经提供一个现成的某种操作后,根据你给的描述以为它更合适,就用了,但模型并不知道你是怎么实现的,可能并不是模型真正想要的操作)。四个基础操作加上bash的万能执行能力,已经能覆盖绝大多数任务。
edit的唯一性校验(必做):模糊匹配若命中超过1处,工具绝对不能盲目替换——就像sed匹配到多行时如果不指定行号就全局替换,会把不该改的行一起改掉。必须抛出错误,要求大模型提供更多的上下行代码以精确定位。这是防止Agent改代码时"顺手改错另一个相似片段"的措施。
def apply_edit(text, old, new):
cnt = text.count(old)
if cnt == 0:
raise ToolError("old_text not found")
if cnt > 1:
raise ToolError("ambiguous match (>1); provide more surrounding context")
return text.replace(old, new, 1)
四、Harness执行效率的提升:只读并发,涉写串行
模型一轮推理可能吐出多个tool_call,如果harness都一个一个串行执行,效率就低了,但harness也不应该无脑串行——要有合适的调度策略:一般如果都是只读工具,就可以并行。

func dispatch(calls []ToolCall) {
var readonly, writable []ToolCall
for _, c := range calls {
if isReadOnly(c) {
readonly = append(readonly, c)
} else {
writable = append(writable, c)
}
}
if len(writeable)==0:
// 只读批:并发 goroutine
fanOut(readonly)
else:
// 涉写批:严格顺序执行
for _, c := range calls {
exec(c)
}
}
这个策略以极低的复杂度,在绝大多数场景下同时保证了性能与正确性。
五、三级记忆:AGENTS.md/Skills/PlanMode
1. AGENTS.md
解决的是“当前项目是什么样”的问题。可以类比于操作系统BIOS信息——启动即加载,告诉系统这台机器什么配置、什么目录结构、有什么禁区。Agent启动时读一次,就知道自己在哪个项目里干活。
注:claudecode目前还是固执地读CLAUDE.md,它的作用跟AGENTS.md是一样的。
2. Skills
解决的是“特定任务该怎么做”的问题。可以类比于硬盘上按需加载的二进制程序,命中对应场景才加载进上下文(CPU)。AGENTS.md告诉机器是什么配置,Skills告诉这个任务该调哪个“程序”。
3. PlanMode
- PLAN.md:宏观路由表,战略方向。保证Agent在跨越几十轮对话的长任务中不跑偏。
- TODO.md:调度队列,每完成一项划掉一项。
上下文压缩就像内存回收会丢数据——早期对话历史被compact掉就没了。所以得定期flush到磁盘:把进展写进PLAN.md/TODO.md,防止掉电(压缩)丢失。Agent醒来重新读磁盘,就知道该接着干啥。
ThinkingPhase(慢思考)是 CPU 的流水线级纠错:哪怕TODO.md里已经写了新任务,如果没有每一轮的慢思考约束,模型仍可能在选择具体实现路径时走捷径——跳过边界条件验证、不加权衡地选第一个能跑的方案。慢思考是每一步的即时纠偏,PlanMode是磁盘上的状态兜底,两者互补。
六、串成一句话
Harness(操作系统)
├── LLM(CPU算力核心)
│ └── Agent Loop(内核主调度循环)
│ └── Two-Stage ReAct(dry-run模拟执行 → 真正执行)
├── 工具集(文件系统基本操作:read/write/edit/bash)
└── 三级记忆(BIOS/硬盘/内存+磁盘持久化)
跑得稳的Agent不是模型最猛的那个,是:Loop有终止条件、工具有边界约束、读写操作有并行串行优化、长任务有磁盘持久化兜底的那一个。

附:可直接参考的Loop伪代码
def agent_loop(task, max_turns=30):
ctx = bootstrap(task) #AGENTS.md+Skills发现+历史
for turn in range(max_turns):
if need_plan(ctx): #PlanMode介入
ctx = two_stage_plan(ctx)
resp = llm(ctx) #Stage2(带工具)
ctx.append(resp.text)
if not resp.tool_calls:
return resp.text #终止条件
batch = group_by_readonly(resp.tool_calls)
for r in concurrent_run(batch.readonly):
ctx.append(r.result)
for w in sequential_run(batch.writable):
if w.name == "edit" and match_count(w) != 1:
ctx.append(edit_ambiguity_error(w))
break
ctx.append(w.result)
raise Timeout("max_turns reached")
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)