从 Agent Loop 重构成 Agent Runtime
写到第 14 天,可以把前面几篇收一下了。
前面写了 100 行 Agent Loop。
随后加了 max steps。
再往后加了 token budget。
接着处理了 retry。
也讲了 idempotency。
最后讲到 checkpoint。
如果你把这些代码都塞进一个 while 里,恭喜,你会得到一个能跑、但很快开始发臭的 Agent。
这不是代码洁癖。
Agent Loop 一旦进入生产环境,问题会从'怎么让模型调用工具'变成'谁对这次行动负责'。Loop 只是控制流。Runtime 才是执行系统。
100 行 Loop 的甜蜜期很短
最小 Agent Loop 大概长这样:
while (step < maxSteps) {
const response = await model.complete(messages, tools);
if (response.final) return response.text;
for (const call of response.toolCalls) {
const result = await tools[call.name](call.input);
messages.push(toToolMessage(result));
}
}
它适合教学,也适合 demo。
但你上线之后,很快会补东西:
这个工具不存在怎么办?
参数 JSON 坏了怎么办?
read_file 读到 5MB 日志怎么办?
shell 命令要不要用户确认?
工具超时要不要重试?
重试会不会重复下单?
模型连续 5 次调用同一个工具怎么办?
任务跑到一半进程挂了怎么办?
用户取消后状态写到哪里?
下次怎么复盘这次失败?
每补一个问题,你就在 Loop 里加一层 if。
最后那个 while 会变成一团东西:模型调用、工具校验、权限判断、预算扣减、trace 写入、checkpoint 保存、错误归因,全都挤在一起。任何人改一行,都可能把另一个责任撞坏。
这就是从 Loop 走向 Runtime 的拐点。
重构不是为了抽象,是为了隔离责任
我不太喜欢一上来就讲'架构分层'。太容易写成 PPT。
更朴素的判断是:如果两个问题的失败处理完全不同,它们就不该挤在同一个函数里。
模型超时和工具越权,不是一类问题。
上下文超限和文件写入失败,也不是一类问题。
checkpoint 保存失败和用户拒绝权限,更不是一类问题。
所以第一版 Runtime 至少应该拆出这些角色:
RunState
ContextBuilder
ModelProvider
ToolRegistry
PermissionGate
ToolExecutor
MessageStore
CheckpointStore
TraceSink
BudgetPolicy
LoopController
这些名字看起来普通,但它们把责任边界切开了。

