第 3 步:安全阀
只加一样东西:循环上限。加完 Agent Loop 就完整了。
问题
第 2 步的循环没有终止条件:
ts
for (let step = 1; ; step++) { // 永远不会自己停模型返回 answer 时会 return,但如果它永远不返回 answer 呢?程序就卡死了。
这不是假想。真实 LLM 会反复调同一个工具,会因为提示词不清楚而绕圈, 也会因为工具一直报错而不断重试。
加上限
ts
const MAX_STEPS = 10;
function runSafe(task: string): string {
const messages: Message[] = [{ role: "user", content: task }];
// ↓ 差别 1:循环有了上限
for (let step = 1; step <= MAX_STEPS; step++) {
const output = model(messages);
if (output.type === "answer") return output.content;
const result = tools[output.name]?.() ?? "unknown tool";
messages.push({ role: "tool", name: output.name, result });
}
// 差别 2:走完上限还没答案 → 主动失败
throw new Error(`模型在 ${MAX_STEPS} 步内没有完成`);
}和第 2 步只差这两处。其余代码(模型、工具、消息类型)直接 import 复用:
ts
import { model, tools, type Message, type ModelOutput } from "./2-loop.js";上限设多少
这是个权衡:
| 值 | 后果 |
|---|---|
| 太小(3) | 复杂任务做不完 |
| 太大(1000) | 模型出错时白烧资源和 token |
| 10 | 学习够用 |
| 20~50 | 真实框架的常见区间 |
运行
bash
npx tsx learn/3-safe.ts文件里演示了两种模型。正常的:
text
=== 正常模型 ===
用户: 现在几点?天气怎么样?
[step 1] getTime → 2026/8/15 17:31:44
[step 2] getWeather → 晴天 28°C
[step 3] 给出答案
答案: 现在是 2026/8/15 17:31:44,天气 晴天 28°C坏掉的(永远要工具,从不回答):
text
=== 坏模型:被安全阀拦住 ===
用户: 永远不回答
[step 1] getTime → ...
...
[step 10] getTime → ...
拦住了: 模型在 10 步内没有完成完整的 Agent Loop
三步走完,你得到了这个:
text
用户任务
↓
┌──────────────────────────────────────┐
│ for (step = 1; step <= MAX; step++) │ ← 第 3 步:安全阀
│ │
│ model(messages) │ ← 第 1 步:模型是函数
│ ↓ │
│ type === "answer" ? → return │ ← 第 1 步:两种输出
│ ↓ 否则 │
│ 执行工具 │ ← 第 1 步:工具
│ ↓ │
│ messages.push(结果) ───────────────┼─ ← 第 2 步:结果回传
│ ↓ │
└───── 继续下一轮 ─────────────────────┘
↓ 超过上限
throw Error真实框架多了什么
循环本身不会再变。生产级框架在此之上加的是:
- 参数校验 —— 模型给的参数类型不对、缺字段怎么办
- 路径沙箱 —— 模型要读文件时,怎么防止它读到工作区外面
- 错误回传 —— 工具执行失败时,把错误信息也写进 messages,让模型自己纠正
- trace 日志 —— 每一步都记下来,事后能完整重放
- 接入真实 LLM —— 把
model()换成 API 调用,工具定义转成 JSON Schema
本项目根目录的 mini_harness.ts 就是加上这些之后的完整版,可以对照着看。