loop-context 实战:为 AI Agent 循环加上上下文管理与熔断器(loop-engineering 状态记忆原语)
【免费下载链接】loop-engineeringPractical patterns, starters & CLI tools for loop engineering with AI coding agents. Design systems that prompt and orchestrate agents (inspired by Addy Osmani and Boris Cherny). Includes loop-audit, loop-init, loop-cost.项目地址: https://gitcode.com/gh_mirrors/lo/loop-engineering
导读
loop-context是 loop-engineering 项目中的「有状态内存管理器」(Stateful Memory Manager),专门解决长跑 AI 编码循环的两大经典故障——上下文膨胀/腐化(Context rot)与停滞空转(Stagnant loop)。本文围绕 tools/loop-context/README.md 展开,结合其 Quickstart 接入方式与源码实现,带你掌握:如何用--check熔断器让无人值守的 L2+ 循环在烧光 Token 之前升级给人类,如何用--prune/--inject/--summary保持上下文窗口干净,以及如何从loop-cost自动解析 Token 预算而不是手写猜测。读完你可以在自己的循环控制脚本中直接落地这套机制。
一、为什么长跑循环会失败:两类经典故障
长期运行的 Agent 循环通常在两个方面失守,这正是 loop-engineering 文档反复警告的:
- 上下文溢出与腐化——对话历史和错误日志不断累积,直到模型丢失最初的目标,或淹没在过期的堆栈跟踪里。
- 停滞 / 无进展循环——Agent 一遍又一遍重试同一个失败动作,悄悄烧掉整个 Token 预算。
loop-context就坐在 Agent 与其持久内存(STATE.md、运行日志)之间,在每次迭代之前做三件事:
- Summarize(总结)——汇总已经尝试过什么、失败了什么;
- Prune(修剪)——裁剪冗长的堆栈跟踪、折叠重复错误、丢弃最近窗口之外的陈旧尝试;
- Inject(注入)——只把必要信息注入下一次 Prompt。
而**熔断器(circuit breaker)**同时盯着迭代次数、Token 消耗、停滞(同一错误连续出现 N 次)和无进展(连续失败过多)四种信号——一旦命中,就升级给人类处理,而不是在无望的循环里空转。
关键设计是:整个过程完全确定、零依赖——总结和修剪不需要调用任何 LLM,因此便宜到可以在每次迭代都跑。这一点可以从 context-manager.ts 的模块注释直接印证:「All logic here is deterministic and dependency-free — no LLM call is required to summarize or prune」。
二、安装与运行
@cobusgreyling/loop-context已发布到 npm(package.json 中版本为 1.5.0,Node >= 18,MIT 协议,见 tools/loop-context/package.json),无需克隆仓库即可使用:
# 查看完整帮助(含全部操作与选项说明) npx @cobusgreyling/loop-context --help # 从本仓库源码构建并测试 cd tools/loop-context && npm install && npm test仓库内的npm test会先执行 TypeScript 构建(npm run build)再运行node --test test/*.test.mjs,覆盖熔断器判定、修剪折叠、摘要分组、CLI 退出码与预算解析等全部行为(见 tools/loop-context/package.json)。
三、运行账本(Ledger):循环的内存模型
工具读取一个运行账本——记录循环目标与每次尝试的 JSON 文件:
{ "goal": "Get the failing migration test to pass", "attempts": [ { "iteration": 1, "action": "run migration", "outcome": "failure", "error": "Error: connect ECONNREFUSED 127.0.0.1:5432", "tokensUsed": 1500 }, { "iteration": 2, "action": "run migration again", "outcome": "failure", "error": "Error: connect ECONNREFUSED 127.0.0.1:5432", "tokensUsed": 1400 } ] }账本结构定义在 context-manager.ts:goal是循环的原始目标(Agent 绝不能丢失的锚点),attempts是按时间先后排序的尝试列表。
每条尝试(Attempt)的字段:
| 字段 | 类型 | 说明 |
|---|---|---|
iteration | number | 循环内从 1 开始计数的迭代号 |
timestamp | string(可选) | 尝试的 ISO 时间戳 |
action | string | 本轮 Agent 尝试的动作(简短描述) |
outcome | success \| failure \| noop | 尝试结果 |
error | string(可选) | 失败时的原始错误信息或堆栈跟踪 |
tokensUsed | number(可选) | 本轮消耗的 Token 数 |
repeated | number | 由修剪器在折叠连续相同失败时写入的重复次数 |
outcome是success | failure | noop;error与tokensUsed均为可选。启动循环时初始化一次即可:{ "goal": "Get CI green on main", "attempts": [] }。
CLI 对账本有严格校验:缺少goal字符串或attempts数组会直接报Invalid ledger: expected { goal: string, attempts: Attempt[] }.(见 cli.ts)。
四、五个操作:check / prune / inject / summary / status
# 熔断器——退出码 0 = 继续,2 = 升级(接入循环的控制流) loop-context --check --ledger run.json # 为下一次 Prompt 生成紧凑的上下文块 loop-context --inject --ledger run.json # 对整轮运行做事实性汇总 loop-context --summary --ledger run.json --json # 输出修剪后的账本(保留最近窗口、截断跟踪、折叠重复) loop-context --prune --ledger run.json # 从 stdin 管道读取 cat run.json | loop-context --check各操作含义(与 cli.ts 中帮助文本一致):
| 操作 | 作用 | 输出 |
|---|---|---|
--check | 运行熔断器判定 | 退出码0继续 /2升级,可加--json输出结构化决策 |
--prune | 生成修剪后的账本 | JSON(只保留window条最近尝试,堆栈截断、重复折叠) |
--inject | 生成注入下一次 Prompt 的上下文块 | Markdown 文本 |
--summary | 事实性汇总 | 文本或--json结构化汇总 |
--status | 人类可读总览(summary + 熔断判定) | 文本 |
默认操作是--status;账本来源缺省为 stdin,也可用-f, --ledger <file>指定文件。
五、熔断器:何时熔断、如何判定
5.1 六类触发条件
--check是接入循环控制流的核心。它按「最具体、最易修复」优先的顺序判定(见 context-manager.ts 的注释:先查停滞、再查无进展,之后才是绝对上限——这样当多个条件同时成立时,报告的原因最有可操作性):
| 触发条件 | 默认阈值 | 含义 |
|---|---|---|
stagnation(停滞) | 同一错误连续出现3次 | 相同错误签名在尾部重复 |
frustration(挫败循环) | 语义相近动作连续3次 | Agent 反复尝试高度相似的动作但始终失败 |
no-progress(无进展) | 连续失败5次 | 中间没有任何成功的连续失败 |
token-budget(Token 预算) | 无(不启用) | 累计 Token 达到上限 |
daily-budget(日预算) | 无(不启用) | 跨多次运行的当日累计花费达到loop-cost建议的每日上限 |
max-iterations(迭代上限) | 10次 | 硬性迭代次数上限 |
判定顺序说明:停滞与挫败循环检查的是「尾部失败段」(最后一次非失败之后的所有失败,见trailingFailureRun);一次成功会重置尾部失败段——测试breaker stagnation resets after a success验证了这一点:失败-失败-成功-失败 的序列尾部只剩 1 次失败,不会误触熔断(见 context-manager.test.mjs)。
5.2 「同一个错误」是怎么识别的:错误签名 + Jaccard 相似度
这是整个熔断器的技术核心。两个细节值得一提:
- 错误签名归一化(errorSignature):把原始错误/堆栈折叠成稳定签名,即使行号、地址、时间戳、端口、临时路径等易变细节不同也能识别为「同一个错误」。它依次做:取首个非空行、把 ISO 时间戳替换为
<ts>、十六进制地址替换为<addr>、路径折叠为 basename、去掉行:列后缀、剩余数字替换为#(见 context-manager.ts)。测试验证了/home/u/app/foo.js:12:5与/tmp/build/foo.js:88:9会得到相同签名,而127.0.0.1:5432与127.0.0.1:5433也会被折叠为同一类(见 context-manager.test.mjs)。 - 字符三元组 Jaccard 相似度(calculateSimilarity):用小写化的字符三元组集合计算 0.0~1.0 的相似度,对措辞细微变化非常鲁棒。
--similarity-threshold(默认0.85)同时作用于熔断判定与修剪折叠;测试用0.5阈值验证了「connection timeout on port 8080 / timeout on port 8080 / socket timeout on port 8080」这样措辞略有差异的错误同样会被判定为停滞(见 context-manager.test.mjs)。
5.3 熔断决策的结构化输出
--check --json会输出完整的BreakerDecision:shouldContinue、escalate、trigger(六类触发之一或ok)、reason(人类可读说明)、iterations、tokensUsed(结构定义见 context-manager.ts)。
退出码约定:0继续 ·2升级 ·1错误。例如相同的ECONNREFUSED连续出现 3 次时,--check输出ESCALATE [stagnation] — Same error repeated 3× in a row ...并以退出码 2 结束。
六、选项速查表
| Flag | 默认值 | 含义 |
|---|---|---|
--max-iterations <n> | 10 | 硬性迭代次数上限 |
--stagnation <n> | 3 | 同一错误连续出现 N 次则升级 |
--no-progress <n> | 5 | 连续失败 N 次则升级 |
--token-budget <n> | 无 | 累计 Token 达到上限则升级 |
--window <n> | 5 | 修剪时保留的最近尝试条数 |
--max-trace-lines <n> | 8 | 修剪时每个堆栈跟踪保留的行数 |
--similarity-threshold <f> | 0.85 | 0.0~1.0 浮点,用于聚类相似错误(同时作用于熔断与修剪) |
值得注意的实现细节:所有数值 Flag 都经过parsePositiveIntFlag/parsePositiveFloatFlag严格校验,拒绝 NaN、0、浮点数——注释明确说明这样做的目的是「坏参数不能静默关闭熔断器」(见 cli.ts)。对应的 CLI 测试覆盖了--stagnation nope、--no-progress 0、--max-iterations 1.5等非法输入均以退出码 1 拒绝(见 cli.test.mjs)。
七、上下文管理三件套的源码行为
7.1 修剪(--prune)
pruneLedger只保留最近window条尝试,做两件事:
- 堆栈截断:超过
max-trace-lines行(默认 8 行)的跟踪保留头部,末尾标注… (N more lines pruned)——pruneStackTrace的实现见 context-manager.ts。 - 重复折叠:连续相同(相似度 ≥ 阈值)的失败合并为一条,
repeated计数递增,iteration前进到最新一轮——测试验证 3 条相同失败会折叠为repeated: 3, iteration: 3的 1 条(见 context-manager.test.mjs)。
另一个保证:修剪绝不修改输入账本——pruneLedger返回新对象,测试pruneLedger does not mutate the input ledger明确断言了这一点(见 context-manager.test.mjs)。
7.2 汇总(--summary)
summarizeAttempts是不需要 LLM 的确定性事实汇总:总尝试数、成功/失败/noop 计数、累计 Token、按频次排序的去重错误分组(按签名相似度聚类)、已尝试过的动作列表。结构见 context-manager.ts。CLI 的人类可读输出形如:
Attempts: 4 (1 ok · 3 failed · 0 no-op) Tokens used: 1200 Distinct errors (most frequent first): (2×) Error: connect ECONNREFUSED 127.0.0.1:5432 Actions tried: - run migration - patch code7.3 注入(--inject)
buildContextInjection生成追加到下一次 Prompt 的紧凑 Markdown 块,只包含 Agent 推进所需的信息:目标、进度(迭代数/成败/token)、已尝试动作(标注 do NOT repeat)、失败模式分组、最近的(已修剪)错误、熔断器状态(OK 或> STOP — circuit breaker tripped (trigger))。生成逻辑见 context-manager.ts,测试断言了注入块包含 goal、动作列表、do NOT repeat与停滞时的STOP指令(见 context-manager.test.mjs)。
八、从 loop-cost 自动解析 Token 预算
手写--token-budget <n>等于靠猜。loop-cost已经为每个 pattern 和就绪级别(L1–L3)计算了贴近实际的单次运行估算(scenarios.realistic.tokensPerRun),所以正确的做法是从那里解析上限:
loop-context --check --ledger run.json --budget-from-pattern ci-sweeper --budget-level L2相关选项:
| Flag | 默认值 | 含义 |
|---|---|---|
--budget-from-pattern <id> | 无 | 在loop-cost注册表中查询的 pattern id |
--budget-level <L1\|L2\|L3> | L1 | 传给loop-cost的就绪级别 |
--budget-scenario <realistic\|action\|report\|caching> | realistic | 用作上限的loop-cost场景(caching要求 pattern 配置了stable_fraction,并会向loop-cost传--with-caching) |
--budget-cadence <spec> | pattern 默认 | 透传给loop-cost的节奏覆盖 |
--budget-conservative | 关闭 | 使用区间内较慢的节奏(loop-cost自身的 Flag) |
优先级规则:显式--token-budget <n>永远优先于--budget-from-pattern——派生值只在你没写数字时兜底。测试cli --token-budget wins over --budget-from-pattern验证了两者同时给出时以token-budget触发熔断(见 cli.test.mjs)。
实现层面,loop-context通过 spawn 子进程调用loop-cost的 CLI(先找 monorepo 兄弟包../loop-cost/dist/cli.js,再找已安装的@cobusgreyling/loop-cost依赖),解析其--json输出——这与loop-init调用loop-audit所用的解析方式相同,两个包在源码层面保持独立(见 budget-resolver.ts)。若 pattern id 未知,会直接暴露loop-cost自身的错误(如Unknown pattern: not-a-pattern,见 cli.test.mjs),而不是静默失败。
九、跨多次运行的每日预算跟踪
--budget-from-pattern只守护单次运行。但 pattern 还有跨越一天多次运行的每日上限(loop-cost的suggestedDailyCap),由--daily-budget-from-pattern负责跟踪:
loop-context --check --ledger run.json --daily-budget-from-pattern ci-sweeper \ --budget-level L2 --on-exceed ./scripts/on-budget-exceed.sh| Flag | 默认值 | 含义 |
|---|---|---|
--daily-budget-from-pattern <id> | 无 | 跟踪并限制该 pattern 的当日累计花费 |
--daily-state-dir <dir> | .loop-context | daily-spend.<pattern>.json的存放目录 |
--on-exceed <script> | 无 | 任何升级发生时,把BreakerDecision以 JSON 形式通过 stdin 管道给该脚本 |
工作原理(结合 daily-spend.ts 与 cli.ts):
- 每次
--check调用把账本最新一条尝试的tokensUsed累加到该 pattern 的状态文件({ "date": "...", "tokensUsedToday": N }); - 以UTC 日期为滚动边界,存储日期不是今天则归零重计(
todayUTC()见 daily-spend.ts); - 当日累计达到 pattern 的建议每日上限后,以
daily-budget触发升级——但仅当没有更早触发的单次运行信号(停滞、无进展、token-budget、max-iterations 优先;测试does not override an existing per-run trigger验证了这一点,见 cli.test.mjs); - 状态文件读写使用锁文件 + 轮询串行化(30 秒陈旧超时),防止两个重叠的循环进程读到同一份过期总数、后写覆盖前写、静默丢失每日熔断增量——与
loop-worktree的 manifest 互斥锁同型(见 daily-spend.ts)。
--on-exceed <script>在任何升级时都会触发(不限于daily-budget)——它是自动化loop-budget.md中「On budget exceed」清单(暂停调度器、记录事件、通知人类)的通用钩子。脚本的退出码不会被检查,也不影响--check自身的退出码;即便脚本不读 stdin 就退出,进程也不会崩溃(EPIPE/EOF 写失败被吞掉,见 cli.ts 及对应回归测试 cli.test.mjs)。
十、填充账本:接入你的循环控制脚本
循环控制脚本每轮迭代向run.json追加一个对象(或把同构 JSON 从 stdin 管道传入):
# 每轮 Agent 迭代之后——先追加本次尝试,再为下一轮把关 node -e " const fs = require('fs'); const ledger = JSON.parse(fs.readFileSync('run.json', 'utf8')); ledger.attempts.push({ iteration: ledger.attempts.length + 1, action: 'run migration tests', outcome: 'failure', error: process.argv[1], tokensUsed: Number(process.argv[2] || 0), }); fs.writeFileSync('run.json', JSON.stringify(ledger, null, 2)); " "\$ERROR_MSG" "\$TOKENS" loop-context --check --ledger run.json || { loop-context --inject --ledger run.json; exit 2; }循环内每轮迭代之前的标准写法:
# 在循环控制脚本内部、每次迭代之前: if ! loop-context --check --ledger run.json; then loop-context --inject --ledger run.json > escalation.md # 把干净的摘要交给人类 exit 0 # 停止而不是重试 fi--inject的输出可以直接作为升级材料:包含目标、已尝试动作、失败模式与最新(已修剪)错误,人类接手时有完整上下文,无需翻原始日志。
十一、库级 API:直接嵌入你的程序
loop-context同时导出库 API(exports入口为dist/context-manager.js,见 tools/loop-context/package.json):
import { checkCircuitBreaker, pruneLedger, summarizeAttempts, buildContextInjection, } from '@cobusgreyling/loop-context'; const decision = checkCircuitBreaker(ledger); if (decision.escalate) escalateToHuman(decision.reason); else runNextIteration(buildContextInjection(ledger));四个核心导出与 CLI 一一对应,全部是纯函数、无副作用、零依赖,便于单元测试与嵌入。
十二、在 Quickstart 中接线:L2+ 熔断器(关联文档的落地场景)
在 docs/QUICKSTART.md 的「3. Check cost before you schedule」之后,Quickstart 已经为 L2+ 循环预留了熔断器小节——这正是本次关联文档(scripts/issue-bodies/quickstart-loop-context.md)要落实的内容。核心链路如下:
- 何时需要:当循环开始无人值守地修代码时(L2 及以上,例如 CI Sweeper、PR Babysitter),就应接入熔断器,让它升级而非无限重试同一失败。
- 脚手架预置:
loop-init会为可修复(fix-capable)的 pattern 生成loop-ledger.json和一个loop-guardskill(见 tools/loop-init/README.md)。 - 每次重试前检查:
npx @cobusgreyling/loop context --check --ledger loop-ledger.json # 等价于独立包形式: npx @cobusgreyling/loop-context --check --ledger loop-ledger.json- 解读退出码:
0继续 ·2升级给人类。熔断在以下任一条件触发:达到最大迭代数、同一错误连续重复 N 次、连续失败过多、或 Token 预算触顶。
完整 API 与全部选项见 tools/loop-context/README.md。
与其它工具配合时的两条建议(Quickstart 中已给出):
- 当
loop-context --check退出码为2时,把对应的 worktree 标记为escalated再移交给人类(loop-worktree mark --run-id <id> --status escalated),两个工具保持独立——见 tools/loop-worktree/README.md。 - 预算触顶的替代路径:L3 自主循环在达到每日 Token 上限的 90% 且仍有高优先级事项时,可以通过
budget-negotiatorskill 请求一次扩展(每日最多一次,+20%或至多+50ktokens),而不是被硬性截停;人类必须在loop-budget.md中显式批准,Agent 严禁自提上限。
十三、它在 loop-engineering 原语体系中的位置
从架构看,loop-context对应的是 docs/primitives.md 中「五大原语 + 内存」里的Memory / State 原语的动态化实现:STATE.md静态存储状态;loop-context则在多次迭代之间动态管理它。成本侧由 tools/loop-cost 提供估算与注册表(数据来自 patterns/registry.yaml),tools/loop-cost/README.md 中也有与本文对应的「Feed the circuit breaker」示例。运行与安全语义可进一步参考 docs/operating-loops.md。
一句话总结这套模式的落地路径:静态状态写在STATE.md,动态状态放进run.json账本,每次迭代前用loop-context --check把关、用--inject喂上下文,触顶就退出码 2 升级给人类——这样无人值守的循环才能既不被上下文腐化拖垮,也不在失败里烧光预算。
【免费下载链接】loop-engineeringPractical patterns, starters & CLI tools for loop engineering with AI coding agents. Design systems that prompt and orchestrate agents (inspired by Addy Osmani and Boris Cherny). Includes loop-audit, loop-init, loop-cost.项目地址: https://gitcode.com/gh_mirrors/lo/loop-engineering
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考