掘金近期关于 Loop Engineering 的文章把焦点从"写一条完美提示词"转向"设计一套能判断下一步的循环"。真正落地时,循环不能只是 while 语句,还需要明确状态、预算、工具权限、停止条件和证据。本文给出一个最小可运行的检查点模型,帮助团队把 Agent 从一次性对话改造成可恢复、可审计的工程流程。
1. 循环的核心不是重复调用模型
一个可控循环至少包含五个阶段:读取任务、规划一步、执行工具、验证结果、决定继续或停止。任何阶段都可能失败,所以每一轮都应该留下可序列化的状态:
ts
type Step =
| { kind: "plan"; text: string }
| { kind: "tool"; name: string; args: Record<string, unknown> }
| { kind: "verify"; command: string; passed: boolean; output: string };
type Checkpoint = {
runId: string;
revision: number;
goal: string;
steps: Step[];
budgetUsed: number;
status: "running" | "waiting-human" | "done" | "failed";
};
只保存最终答案是不够的。出了错,团队需要知道 Agent 在哪一轮拿到了什么证据、执行了什么命令,以及为什么继续。
2. 用有限状态机约束循环
不要让模型直接决定任意状态跳转。把允许的迁移写成白名单:
ts
const transitions: Record<Checkpoint["status"], Checkpoint["status"][]> = {
running: ["running", "waiting-human", "done", "failed"],
"waiting-human": ["running", "failed"],
done: [],
failed: [],
};
function move(from: Checkpoint["status"], to: Checkpoint["status"]) {
if (!transitions[from].includes(to)) {
throw new Error(`illegal transition: ${from} -> ${to}`);
}
}
例如删除文件、修改数据库、发布版本等高风险动作,必须先进入 waiting-human,由人工批准后才能回到 running。这比在提示词里写"请谨慎操作"可靠得多。
3. 每一轮只做一个可验证动作
循环容易失控,常见原因是一次让模型生成大量代码、运行多个命令,再用一句"看起来没问题"收尾。更稳妥的做法是把一轮限制为一个动作:
- 读取一个明确的输入或错误日志。
- 生成一个小修改计划。
- 执行一个工具调用。
- 运行一个确定性验证。
- 写入检查点,再决定下一轮。
ts
async function runRound(cp: Checkpoint, model: Model, tools: Tools) {
const plan = await model.plan({ goal: cp.goal, history: cp.steps.slice(-6) });
cp.steps.push({ kind: "plan", text: plan.text });
if (plan.risk === "high") {
cp.status = "waiting-human";
return cp;
}
const result = await tools.execute(plan.tool);
cp.steps.push({ kind: "tool", name: plan.tool.name, args: plan.tool.args });
const check = await tools.verify(plan.verifyCommand);
cp.steps.push({ kind: "verify", command: plan.verifyCommand, passed: check.ok, output: check.output });
cp.revision += 1;
return cp;
}
history.slice(-6) 不是固定最佳值,只是为了防止上下文无限增长。生产环境应按 token 预算和任务阶段动态选择历史。
4. 停止条件必须独立于模型自述
"已经完成"不能作为验收证据。停止条件应该由代码检查:
- 测试命令退出码为 0。
- 类型检查和 lint 没有新增错误。
- 变更文件在允许目录内。
- 关键接口返回符合 schema。
- 预算未超限,且没有未处理的高风险动作。
ts
function canFinish(cp: Checkpoint, changedFiles: string[], checks: boolean[]) {
const allowed = changedFiles.every(f => f.startsWith("src/") || f.startsWith("tests/"));
return cp.status === "running"
&& allowed
&& checks.every(Boolean)
&& cp.budgetUsed < 100;
}
如果验证失败,下一轮应携带失败输出和差异摘要,而不是把整个仓库重新塞给模型。这样既节省预算,也降低模型反复改动无关文件的概率。
5. 接入层也要能替换
循环系统通常需要在不同模型之间切换。可以把提供方抽象成最小接口:
ts
interface Model {
plan(input: { goal: string; history: Step[] }): Promise<{
text: string;
risk: "low" | "high";
tool: { name: string; args: Record<string, unknown> };
verifyCommand: string;
}>;
}
如果团队评估统一的多模型接入点,HaerAPI 公开首页自述可通过一个密钥接入 Claude、GPT、Gemini 等模型,并展示订阅转 API、会话保持和按量计费。它可以放在实验环境的提供方适配器后面,但不能据此推断所有模型、协议或计费细节都满足你的任务。
上线前应独立核验:模型列表和版本、流式/工具调用兼容性、限额与错误码、日志和数据处理、会话保持语义、服务条款及退出方案。保留直连实现和配置开关,避免把循环状态绑定到单一入口。
6. 让循环可回放、可审计
每个检查点建议包含:输入摘要哈希、模型标识、提示模板版本、工具参数、命令输出摘要、人工批准人和时间。敏感内容可以脱敏,但不要删除决定性证据。
回放时只重放纯函数和验证步骤;真实写操作默认使用模拟工具。这样可以复现"为什么进入下一轮",又不会在调试时再次修改生产资源。
总结
Loop Engineering 的价值不在于让 Agent 无限运行,而在于让每一轮都小、可验证、可暂停。有限状态机约束权限,检查点保存证据,独立验收决定停止,提供方适配器保证可替换。做到这些,循环才是工程系统,而不是更长的提示词。