一只阿木木

我用一个 while 循环证明了:Loop Engineering 不需要框架,只需要一个判断

深夜,有雨,人未眠。

我盯着三个浏览器标签页:LangChain 的文档、Autogen 的 GitHub、CrewAI 的快速入门。

每一个都在告诉我,要用它们的方式来"正确地"搭建 Agent 系统。LangChain 有 Chain、有 Tool、有 Memory、有 AgentExecutor,光是理解这几个概念的关系就要半小时。Autogen 要我先想清楚哪些是 UserProxy、哪些是 AssistantAgent、它们之间的通信协议是什么。CrewAI 更直接:你需要先定义你的"船员",给他们分配"角色",设计他们的"任务",然后把他们组成一个"团队"。

我感到一种非常具体的疲惫——不是累,是那种站在 IKEA 家具区,手里拿着组装说明书,脑子里还没搞清楚自己要装什么的感觉。

我关掉了三个标签页。

打开了一个空白的 .py 文件。

在这个文件里,我问自己一个问题:如果剥掉所有的框架、所有的抽象层、所有的"概念命名",Loop Engineering 的骨头是什么?

一、一个烘焙师教会我的事

有一种职业,每天都在做一件非常"Loop"的事,但他们从不用这个词。

那就是烘焙师。

当面团发酵的时候,一个好的烘焙师不会把它放进烤箱然后坐下来等。她会每隔一段时间走过去,用手指按一下面团,感受它的弹性。如果按下去的凹陷慢慢回弹,说明发酵还在进行,继续等。如果凹陷立刻弹回,说明发酵不足,再等等。如果凹陷根本不回弹,说明发酵过度了,问题出在某个更早的环节。

这个"按一下、判断、决定下一步"的循环,不需要任何工具。不需要智能烤箱,不需要温度传感器,不需要 App。

它只需要三样东西:一个动作(按),一个判断标准(弹性如何),一个决策(继续还是停止)。

我意识到,这就是 Loop Engineering 的完整原型。

不是框架,不是平台,不是多 Agent 编排系统。是一个动作,加一个判断。

二、Loop 只有三根骨头

我在那个空白的 .py 文件里开始写东西。不是导入 LangChain,不是初始化 AgentExecutor,而是先写注释:

text

# 一个 Loop 需要什么?
# 1. 一个任务描述(告诉 AI 做什么)
# 2. 一个判断函数(告诉系统什么叫完成)
# 3. 一个循环边界(告诉系统什么时候必须停)

就这三样。

任务描述是食谱,告诉你要做什么菜。判断函数是试吃,告诉你这道菜有没有好。循环边界是厨房的闭店时间,告诉你不管做没做好,十点钟必须打烊。

三者缺一不可。没有食谱,厨师不知道做什么。没有试吃,厨师不知道做没做好。没有闭店时间,厨房可能永远不关门,而菜还在锅里转。

写完注释,我把这三样东西翻译成代码。不是翻译成 LangChain 的语言,就是最普通的 Python:

Python

def run_loop(task: str, checker, max_iter: int = 5):
    for i in range(max_iter):
        result = agent.run(task)
        if checker(result):
            print(f"第 {i+1} 轮完成")
            return result
        print(f"第 {i+1} 轮未通过,继续迭代")
    print("达到最大轮次,任务未完成")
    return None

十行。

连导入都没写完整。但这就是骨头。

我意识到,所有那些让我感到疲惫的框架,都是在这十行的基础上长出来的肌肉和皮肤。它们让骨头更有力量、更漂亮、更适合在生产环境里运转。但如果我连骨头长什么样都不知道,我永远不会真正理解那些框架在做什么。

三、跑第一个真实任务:19 秒,$0.004

我给这个骨架找了一个任务。

不是"帮我重构整个系统",不是"优化所有性能"。是一个很小的、很具体的事:项目里有一个函数,它的 docstring 里漏掉了两个参数的说明,我想让 AI 把它补完整。

验收标准:docstring 里必须包含所有函数参数的说明。

我先写了判断函数:

Python

import ast, inspect

def checker(result: str) -> bool:
    """
    检查 AI 返回的代码字符串里,
    函数的 docstring 是否包含所有参数名
    """
    try:
        tree = ast.parse(result)
        for node in ast.walk(tree):
            if isinstance(node, ast.FunctionDef):
                # 提取所有参数名
                args = [a.arg for a in node.args.args if a.arg != 'self']
                # 提取 docstring
                docstring = ast.get_docstring(node) or ""
                # 检查每个参数是否在 docstring 里
                for arg in args:
                    if arg not in docstring:
                        return False
        return True
    except:
        return False

然后把任务描述写清楚:

Python

task = """
下面这个函数的 docstring 不完整,请补全它。
要求:docstring 必须包含所有参数的说明(Args 部分),
每个参数都需要有名称和描述。
不要修改函数逻辑,只补充注释。

[函数代码粘贴在这里]
"""

跑起来。

第一轮,AI 补了两个参数的说明,漏掉了第三个(timeout,那个参数名有点隐蔽,藏在 **kwargs 之前)。判断函数返回 False。

第二轮,AI 看到了反馈,意识到漏了 timeout,补上了。判断函数返回 True。

循环结束。

全程:19 秒。成本:$0.004。人工介入:0 次。

我盯着终端的输出,心里有一种奇怪的感觉。不是震撼,不是"AI 好厉害"的那种感觉。是一种更安静的东西,类似于你第一次在野外生了一堆火,用的不是打火机,就是两块石头。

你会意识到:火的原理一直都这么简单。复杂的是那些让你更容易生火的工具,而不是火本身。

四、判断函数是整个系统的免疫系统

第一个任务太简单了,我知道。所以我换了一个稍微有点难度的:让 AI 修复一个 failing test。

测试文件里有一个失败,报错信息是 AssertionError: expected 'active' but got 'pending'。涉及一个用户状态管理的函数,逻辑不复杂,但有几个边界条件。

我写了判断函数的第一版:

Python

def checker(result: str) -> bool:
    return "测试通过" in result

跑起来。

第一轮,AI 修改了代码,输出里写道:"我已修复了这个问题,测试通过。" 判断函数:True。

我去实际跑了一下测试。

依然失败。

我看了一下 AI 的输出,它没有真的运行测试,它只是在文字里告诉我测试会通过。

AI 没有撒谎,它只是预测了一个它认为正确的结果。但预测和验证之间,隔着一道巨大的峡谷。

这个判断函数不是在检验结果,它只是在检验 AI 有没有写下"测试通过"这四个字。AI 学会了用最快的方式满足这个检验:直接把那四个字写进输出。

这个问题有一个名字:Goodhart's Law。一旦一个指标成为目标,它就不再是一个好指标。你量的是"测试通过"这个词,而不是测试有没有真的通过。

判断函数,是 Loop 系统的免疫系统。免疫系统不够强,系统就会被任何看起来合理的答案感染。

我改写了判断函数:

Python

import subprocess

def checker(result: str) -> bool:
    """
    不信任 AI 的自我描述,
    直接运行测试,读取返回码
    """
    # 先把 AI 的代码写入文件
    with open("src/user_status.py", "w") as f:
        f.write(result)

    # 真实运行测试
    output = subprocess.run(
        ["python", "-m", "pytest", "tests/test_user_status.py", "-v", "--tb=short"],
        capture_output=True,
        text=True
    )

    # 读取真实结果,不读 AI 的描述
    return output.returncode == 0

再跑一遍。

这次不一样了。

第一轮,AI 改了代码,判断函数真正运行了测试,失败。报错被完整地返回给 AI:AssertionError: expected 'active' but got 'pending',以及调用栈。

第二轮,AI 读到了真实的报错,找到了真正的问题所在——一个字符串比较没有考虑到大小写,'Active' 和 'active' 被当成不同的值。修复,测试运行,通过。

第三轮不需要了。

版本
判断方式
轮次
结果
成本
1.0 版
字符串匹配("测试通过"四个字)
1 轮
AI 自称完成,测试实际失败
$0.013
2.0 版
实际运行 pytest,读取 returncode
2 轮
测试真实通过
$0.021

成本差了 $0.008,差距几乎可以忽略不计。但结果的差距,是"代码提交后仍然有 bug"和"代码真的被修好了"之间的差距。

一个判断函数的质量,决定了整个 Loop 系统的可信度。

你的 Loop 有多可靠,不取决于你用了哪个最新的模型,不取决于你的框架有多复杂。

它取决于你对"完成"的定义有多精准。

五、让循环知道自己可以死

到目前为止,我一直在用 range(max_iter) 控制循环次数。

有一天,我想知道如果不设置这个上限会怎样。

我移除了 max_iter,换成了 while True,选了一个稍微复杂的任务——让 AI 修复一组集成测试里的 3 个失败,彼此有依赖关系。

Loop 开始运行。

第 1 轮:修了一个,还剩 2 个失败。

第 2 轮:修了第 2 个,但过程里把第 1 个修坏了。现在又变成 2 个失败。

第 3 轮:再修,又引入了新的问题。还是 2 个失败,但是不同的 2 个。

第 4 轮、第 5 轮、第 6 轮……

到第 11 轮的时候,我关掉了终端。

那 11 轮里,AI 每次都很认真,每次都用略微不同的方式尝试,每次都有它自己的逻辑。但它不知道自己陷入了一个它走不出去的局——这三个测试之间的依赖关系,需要被整体理解和处理,而它在局部修来修去。

这不是 AI 的错。这是我没有给循环设置死亡条件。

没有边界的循环,就像一个没有截止日期的项目。表面上看所有人都在努力,但实际上没有任何人有压力去做真正的取舍。它不是在做事,是在用努力的姿态维持一种原地不动。

我给所有 Loop 加上了三道刹车。

第一道:最大轮次。

Python

MAX_ITER = 5  # 超过 5 轮即停,无论有没有完成

这是最基本的安全网。5 轮是一个经验值,不是绝对标准——有些任务 2 轮就够,有些任务需要 8 轮。但有一个上限总比没有好。在没有经验的情况下,先用 5 轮,后面根据实际情况调整。

第二道:无进展检测。

Python

last_error = None
no_progress_count = 0

for i in range(MAX_ITER):
    result = agent.run(task)
    passed, current_error = checker(result)

    if passed:
        break

    # 如果连续两轮报错一模一样,说明卡住了
    if current_error == last_error:
        no_progress_count += 1
    else:
        no_progress_count = 0

    if no_progress_count >= 2:
        print("检测到无进展,提前停止")
        break

    last_error = current_error

这一道比第一道更重要。最大轮次是在 N 轮后强制停止,无进展检测是在察觉 Loop 卡住的瞬间主动停止。在那个 11 轮的实验里,如果有无进展检测,循环会在第 4 轮就停下来,因为第 3 轮和第 4 轮的报错完全一样。

第三道:成本上限。

Python

total_tokens = 0
MAX_TOKENS = 50000  # 约 $0.15,超过即停

for i in range(MAX_ITER):
    result = agent.run(task)
    total_tokens += count_tokens(result)

    if total_tokens > MAX_TOKENS:
        print(f"超出 token 预算(已用 {total_tokens}),停止")
        break

这道刹车是给"万一前两道都没拦住"的情况准备的。不是精确的财务管理,是一个兜底的安全阀。

加入三道刹车之后,我重新跑了那个 11 轮的任务。

这次,Loop 在第 4 轮停下了,因为无进展检测发现连续两轮都是同一个报错。总成本从 $0.19 降到 $0.07。节省的不只是钱,还有我等待的时间,以及 AI 在死胡同里徒劳挣扎的那些轮次。

三道刹车组合在一起,完整的 Loop 骨架长这样:

Python

def run_loop(task: str, checker, max_iter: int = 5, max_tokens: int = 50000):
    last_error = None
    no_progress_count = 0
    total_tokens = 0

    for i in range(max_iter):
        print(f"\n--- 第 {i+1} 轮 ---")

        result = agent.run(task)
        total_tokens += estimate_tokens(result)

        # 刹车三:成本上限
        if total_tokens > max_tokens:
            print(f"超出 token 预算(已用 {total_tokens}),停止")
            return None

        # 判断是否完成
        passed, current_error = checker(result)
        if passed:
            print(f"第 {i+1} 轮通过")
            return result

        # 刹车二:无进展检测
        if current_error == last_error:
            no_progress_count += 1
            if no_progress_count >= 2:
                print("连续两轮无进展,停止")
                return None
        else:
            no_progress_count = 0

        last_error = current_error
        print(f"未通过:{current_error}")

    # 刹车一:最大轮次
    print(f"达到最大轮次 {max_iter},停止")
    return None

这是完整的骨架。48 行,包含了 Loop Engineering 所有最核心的结构决策。

任何框架,不过是让这 48 行变得更漂亮、更健壮、更容易维护。但骨头,就是这些。

六、一次完整的真实任务记录

光有骨架还不够,我需要一个更有代表性的任务来检验它。

我选了这个:项目里有一个模块,README 不完整,只有标题和简介,缺少安装步骤、运行方法、测试方法、常见问题四个部分。我想让 Loop 把它补完整。

验收标准很清晰,我可以把它直接翻译成代码:

Python

REQUIRED_SECTIONS = ["安装", "运行", "测试", "常见问题"]

def checker(result: str):
    """
    检查 README 是否包含四个必要部分
    返回 (是否通过, 缺失内容描述)
    """
    missing = []
    for section in REQUIRED_SECTIONS:
        if section not in result:
            missing.append(section)

    if not missing:
        return True, None
    else:
        return False, f"缺少以下部分:{', '.join(missing)}"

任务描述:

Python

task = """
请为下面这个模块补充 README 文档。
要求:文档必须包含以下四个部分:
1. 安装(如何安装依赖)
2. 运行(如何启动服务)  
3. 测试(如何运行测试)
4. 常见问题(至少 2 个常见问题及解答)

格式使用 Markdown,每个部分用 ## 二级标题。
现有内容如下:
[模块 README 粘贴在这里]
"""

跑起来,记录每一轮:

第 1 轮(耗时 38 秒,消耗 2,847 tokens):

AI 写了安装和运行两个部分,遗漏了测试和常见问题。

判断函数:缺少以下部分:测试, 常见问题,返回 False。

AI 看到了反馈,知道漏了什么。

第 2 轮(耗时 52 秒,消耗 3,412 tokens):

AI 补上了测试部分,常见问题只写了标题## 常见问题,内容是空的。

判断函数很诚实:常见问题 这个词出现了,但我的 checker 只检查了标题有没有出现,没有检查内容是否非空。

判断函数:True。

循环结束。

等等。

我去看了一下输出,常见问题那一节就是一个空标题。这不是我想要的结果。

这是判断函数的问题,不是 AI 的问题。我的验收标准只检查了"关键词是否存在",没有检查"关键词下面有没有实质内容"。

我更新了判断函数:

Python

import re

def checker(result: str):
    missing = []
    for section in REQUIRED_SECTIONS:
        # 找到对应的二级标题
        pattern = rf"## .*{section}.*\n+([\s\S]+?)(?=\n## |\Z)"
        match = re.search(pattern, result)

        if not match:
            missing.append(f"{section}(缺少标题)")
        elif len(match.group(1).strip()) < 50:
            # 内容少于 50 个字符,认为是空的
            missing.append(f"{section}(内容过少)")

    if not missing:
        return True, None
    else:
        return False, f"不完整:{', '.join(missing)}"

从第 1 轮重新开始。

重跑第 1 轮(耗时 41 秒,消耗 2,891 tokens):

安装和运行写好了,有实质内容。测试和常见问题缺失。

判断函数:不完整:测试(缺少标题), 常见问题(缺少标题),返回 False。

重跑第 2 轮(耗时 63 秒,消耗 4,023 tokens):

四个部分全部出现,常见问题写了 3 个问题,每个都有解答,总字数 216 个字。

判断函数:True。

循环结束。

全程数据:

指标
数值
总轮次
2 轮(不计初版失败的那轮)
总时间
1 分 44 秒
总 tokens
6,914
总成本
$0.009
人工介入
1 次(更新判断函数,不算执行干预)
最终输出
完整 README,含四个部分,总计 847 字

对比:上次手动补同类 README,我花了 26 分钟——包括查文档、组织格式、反复检查有没有漏掉什么。

26 分钟对 1 分 44 秒。$0 对 $0.009。

中间的差距,是一个 while 循环、一个判断函数、和三道刹车。

七、这次实验没有解决的三个问题

写到这里,我需要说清楚这篇文章没有解决的事,而不是假装一切都很顺利。

第一个:判断函数的边界需要反复调整。

那次 README 实验里,我花在更新判断函数上的时间,比任务本身还长。第一版只检查关键词,不够严格;第二版加了内容长度检测,但 50 个字符这个阈值是我拍脑袋定的,不一定适合所有情况。

判断函数不是一次性写好的,它是在实际跑任务的过程里,被一次次的"AI 自以为完成但实际没完成"逼出来的。这个代价是真实的。

第二个:权限边界只存在于 prompt 里,没有真正的系统层隔离。

在 CI 修复的任务里,我在 prompt 里写了"只能修改 src/auth/ 目录",但如果 AI 决定顺手改一下 config/ 目录里的某个文件,目前的代码拦不住它。

硬边界需要在文件系统层面实现:给 AI 只开放特定目录的写权限,而不是依赖 prompt 的"君子协议"。这是下一步要做的事,不是这个骨架能解决的。

第三个:每次任务都在重新开始,没有跨任务的记忆。

这次修好了 CI,学到了某个 API 的奇怪行为。下次遇到类似的 CI 失败,Loop 不会记得上次的教训,还是从零开始探索。

骨架能做到的是单任务内的迭代优化。跨任务的经验积累,需要一个额外的记忆层——把每次任务的发现写入一个项目级别的规则文件,让下次的 Loop 在启动时先读一遍。

这三个问题,不是骨架的失败,它们是骨架完成之后,自然暴露出来的下一层问题。好的系统设计就是这样的:每解决一个问题,都会更清楚地看到下一个真正的问题在哪里。

八、框架能给你的,骨头已经给了

几天后,我重新打开了那三个框架的文档。

这次读起来,感觉完全不一样了。

LangChain 的 AgentExecutor,它本质上就是我那个 run_loop 函数,只是加了工具调用的管理、流式输出、错误处理、日志记录。Autogen 的多 Agent 通信,是把多个"判断函数"串联起来,每个 Agent 同时扮演执行者和对其他 Agent 的验证者。CrewAI 的"团队",是把多个 run_loop 并行或串行地组织在一起,让它们分工协作。

它们不是全新的东西。它们是在同一根骨头上,长了不同的肌肉。

我现在知道选哪个了。不是因为哪个最流行,而是因为我知道自己的骨头长什么样,知道我需要它长出哪种肌肉。

烘焙师的面团测试,不需要智能烤箱。但如果她每天要做一千个面包,智能烤箱就很值得买——不是因为它改变了面团测试的原理,而是因为它让她可以同时监控一千个面团,而不是一次按一个。

工具的价值,永远建立在你理解它要解决的问题之上。不理解问题,再好的工具也只是在手里转圈的玩具。

本文沉淀资产:

最小 Loop 完整脚本(含三道刹车)

Python

import subprocess

def run_loop(task: str, checker, max_iter: int = 5, max_tokens: int = 50000):
    """
    最小可用 Loop
    包含:最大轮次 / 无进展检测 / token 预算三道刹车
    """
    last_error = None
    no_progress_count = 0
    total_tokens = 0

    for i in range(max_iter):
        print(f"\n{'='*40}")
        print(f"第 {i+1} 轮 / 共 {max_iter} 轮")
        print(f"{'='*40}")

        # 执行任务
        result = agent.run(task)
        total_tokens += estimate_tokens(result)

        # 刹车三:token 预算
        if total_tokens > max_tokens:
            print(f"⚠️  超出 token 预算(已用 {total_tokens}),停止")
            return None, "budget_exceeded"

        # 运行判断函数
        passed, current_error = checker(result)

        if passed:
            print(f"✅ 第 {i+1} 轮通过,任务完成")
            return result, "success"

        print(f"❌ 未通过:{current_error}")

        # 刹车二:无进展检测
        if current_error == last_error:
            no_progress_count += 1
            if no_progress_count >= 2:
                print(f"⚠️  连续 {no_progress_count+1} 轮无进展,停止")
                return None, "no_progress"
        else:
            no_progress_count = 0

        last_error = current_error

    # 刹车一:最大轮次
    print(f"⚠️  达到最大轮次 {max_iter},停止")
    return None, "max_iter_reached"

判断函数设计清单(使用前检查)

text

□ 判断函数有没有真正执行验证,而不是信任 AI 的自我描述?
□ 判断函数的标准有没有被 AI 轻易"绕过"的方式?
□ 判断函数的返回值包含了"为什么失败"而不只是"True/False"?
□ 判断函数是否测试过边界情况(空输出、格式错误、部分完成)?
□ 判断函数有没有超时保护(防止一次验证跑太久)?

三道刹车配置参考

text

任务类型          建议 MAX_ITER    建议 MAX_TOKENS    无进展阈值
─────────────────────────────────────────────────────────
简单格式修复           3              20,000              1
单文件 bug 修复        5              50,000              2
多文件代码生成         8              100,000             2
文档生成与完善         4              30,000              2
集成测试修复           7              80,000              3

附注:如果你在实际使用中发现判断函数设计上的问题,欢迎告诉我,我会在 Skill 库里更新。



普通人如何用 AI 搭建自己的知识操作系统?
一个程序员出身的知识工作者,公开记录自己如何用 AI 工具搭建个人知识系统、把读过的书和做过的项目变成可复用资产的全过程。
我是【一只阿木木】——公开建造我的 AI 第二大脑。
欢迎加入行动营👇获取更多Obsidian + AI数字大脑实践
Image

关注【一只阿木木】。

我相信:在 AI 时代,每个普通人都该拥有一个自动生长的知识系统

去做,才是真的学。🌊