Loop Engineering:把 AI Agent 从对话助手变成可运行的工程系统

2026-08-04 46 预计阅读时间: 1 分钟
来源: my.oschina.net AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

多数 AI 应用仍停留在“人提问、模型回答、人再修正”的交互方式里。Loop Engineering 改变的是责任边界:开发者不再逐轮指导模型,而是设计一套能够代替自己持续指导模型的系统,包括目标、工具、反馈、预算、停止条件与审计记录。

这并不是某个新的 Agent 框架。它更像一种工程方法:把一次性的提示词,升级为一个可验证、可恢复、可约束的执行循环。

真正需要设计的不是 Prompt,而是 Loop

一个最小 Agent 循环通常包含四个动作:

  1. 读取当前任务和运行状态。
  2. 让模型选择下一步动作。
  3. 执行动作并收集结果。
  4. 将结果反馈给模型,直到任务完成或触发停止条件。

可以把它抽象成下面的状态转换:

目标 + 当前状态
       ↓
   模型决策
       ↓
工具调用 → 结构化结果 → 状态更新
       ↑                    ↓
       └──── 未完成则继续 ──┘

这里最重要的变化是:模型不直接拥有系统控制权。模型只能提出结构化意图,循环控制器负责验证参数、调用工具、记录结果,并判断是否允许继续。

因此,Loop Engineering 的核心工作通常不是反复润色一句提示词,而是回答这些工程问题:

  • 模型能执行哪些动作,哪些动作必须禁止?
  • 每个动作返回什么结构,失败是否可以重试?
  • 什么证据可以证明任务已经完成?
  • 最多允许多少轮、多少 Token、多少费用和多少运行时间?
  • 进程中断后,能否从最近一次状态继续?
  • 人类应该在哪些风险节点介入审批?

一个可以运行的受控 Agent 循环

下面是一个最小实践示例。它使用确定性的 decide 函数模拟模型,因此不需要 API Key,可以直接运行。接入真实 LLM 时,只需替换该函数,并要求模型返回相同的 JSON 结构。

将代码保存为 agent_loop.py,然后在任意 Python 项目目录执行 python agent_loop.py。示例只开放“检查 README”和“运行单元测试”两个工具,并把每一步写入 agent-events.jsonl

from __future__ import annotations

import json
import subprocess
import sys
from pathlib import Path
from typing import Any

WORKSPACE = Path.cwd().resolve()
MAX_STEPS = 6
LOG_FILE = WORKSPACE / "agent-events.jsonl"


def check_file(path: str) -> dict[str, Any]:
    target = (WORKSPACE / path).resolve()
    if WORKSPACE not in target.parents and target != WORKSPACE:
        return {"ok": False, "error": "path escapes workspace"}
    if not target.is_file():
        return {"ok": False, "error": f"missing file: {path}"}
    return {"ok": True, "path": path, "size": target.stat().st_size}


def run_tests() -> dict[str, Any]:
    command = [sys.executable, "-m", "unittest", "discover"]
    try:
        result = subprocess.run(
            command,
            cwd=WORKSPACE,
            capture_output=True,
            text=True,
            timeout=30,
        )
    except subprocess.TimeoutExpired:
        return {"ok": False, "error": "tests timed out"}

    output = (result.stdout + result.stderr)[-4000:]
    return {
        "ok": result.returncode == 0,
        "returncode": result.returncode,
        "output": output,
    }


TOOLS = {
    "check_file": lambda args: check_file(args["path"]),
    "run_tests": lambda args: run_tests(),
}


def decide(history: list[dict[str, Any]]) -> dict[str, Any]:
    """确定性模型替身;生产环境可替换为返回 JSON 的 LLM 调用。"""
    if not history:
        return {
            "action": "tool",
            "tool": "check_file",
            "args": {"path": "README.md"},
            "reason": "确认项目说明文件存在",
        }
    if len(history) == 1:
        return {
            "action": "tool",
            "tool": "run_tests",
            "args": {},
            "reason": "收集可验证的测试结果",
        }

    tests = history[-1]["result"]
    return {
        "action": "finish",
        "status": "completed" if tests.get("ok") else "needs_human",
        "summary": "测试通过" if tests.get("ok") else "测试失败,需要人工检查",
    }


def append_event(event: dict[str, Any]) -> None:
    with LOG_FILE.open("a", encoding="utf-8") as file:
        file.write(json.dumps(event, ensure_ascii=False) + "\n")


def main() -> None:
    history: list[dict[str, Any]] = []

    for step in range(1, MAX_STEPS + 1):
        decision = decide(history)
        append_event({"step": step, "type": "decision", "data": decision})

        if decision.get("action") == "finish":
            print(json.dumps(decision, ensure_ascii=False, indent=2))
            return

        tool_name = decision.get("tool")
        if tool_name not in TOOLS:
            raise RuntimeError(f"tool is not allowed: {tool_name}")

        result = TOOLS[tool_name](decision.get("args", {}))
        observation = {"decision": decision, "result": result}
        history.append(observation)
        append_event({"step": step, "type": "observation", "data": result})

    print(json.dumps({
        "action": "finish",
        "status": "budget_exhausted",
        "summary": f"超过最大步数 {MAX_STEPS}",
    }, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()

这个示例刻意把“决策”和“执行”分开。真实模型可以决定调用 check_file,但无法自行读取工作区之外的文件,也无法把任意字符串交给 Shell。即使模型输出了不存在的工具名,控制器也会拒绝执行。

接入 LLM 时,可以这样约束决策输出:

你是项目检查 Agent。根据目标、历史动作和工具结果选择下一步。

只允许输出 JSON,不要输出解释文字。
可用动作:
1. {"action":"tool","tool":"check_file","args":{"path":"..."},"reason":"..."}
2. {"action":"tool","tool":"run_tests","args":{},"reason":"..."}
3. {"action":"finish","status":"completed|needs_human","summary":"..."}

规则:
- 不得虚构工具执行结果。
- 完成状态必须有测试结果作为证据。
- 同一个失败动作最多重试一次。
- 信息不足或风险不可判断时返回 needs_human。

在生产系统中,还应使用 JSON Schema 或 SDK 的结构化输出能力校验响应,而不是依赖字符串格式恰好正确。

让循环可靠的五个控制面

1. 状态必须显式保存

只把历史放在模型上下文中,会同时遇到上下文长度、费用和故障恢复问题。任务目标、动作结果、重试次数、资源消耗和审批状态应保存在数据库或事件日志中。模型上下文只是状态的一个投影,不应成为唯一事实来源。

2. 工具应当小而窄

与其开放一个可以执行任意命令的 shell,更稳妥的做法是提供 run_testsread_issuecreate_patchrequest_review 这类边界清晰的工具。工具参数应做类型校验、路径限制、超时控制和输出截断。

3. 完成需要证据

模型说“已经完成”不等于任务真的完成。代码 Agent 可以要求测试退出码、静态检查结果和变更摘要;数据 Agent 可以要求行数、校验和与异常记录;运维 Agent 可以要求健康检查和回滚点。

4. 预算是循环的一部分

循环必须具备硬停止条件,例如最大步数、最大持续时间、最大 Token、最大费用和连续失败次数。没有预算的自主运行,本质上是无法预测成本和影响范围的后台任务。

5. 高风险动作需要审批

读取文档与删除数据不应拥有相同权限。可以按风险把工具分为自动执行、执行前确认、禁止执行三类。涉及生产发布、资金、权限、外部通信和不可逆写入时,应把人工审批设计为状态机中的正式节点。

从小闭环开始,而不是追求全自主

采用 Loop Engineering 时,可以先选择一个结果容易验证、工具数量有限、失败成本较低的任务,例如检查仓库健康状态、整理工单信息或生成待审核补丁。

上线前至少检查以下事项:

  • 目标和完成条件能否被机器验证?
  • 模型输出是否经过结构化校验?
  • 工具是否使用最小权限并限制参数?
  • 是否设置步数、时间、费用和重试预算?
  • 每一步是否留下可检索的审计记录?
  • 中断后是否能够恢复或安全重跑?
  • 高风险动作是否进入人工审批?

Loop Engineering 的价值不在于让模型无限运行,而在于把模型放进一个边界明确的反馈系统。真正可以代表开发者工作的,不是一段更长的 Prompt,而是一套能够观察、决策、执行、验证,并在不确定时停下来的工程循环。


相关推荐