返回文章列表

Agent 到底在循环什么:从手写有界 Agent Loop 看模型、工具与状态

从游戏变更验证 Agent 的真实工作流出发,解释 Agent Loop 中模型、状态、工具、观察结果与终止条件分别承担什么职责,以及为什么真正可控的 Agent 必须有边界。

在做 Game Change Verification Agent 之前,我对 Agent 的理解比较容易停留在一句话:

LLM 不只是回答问题,还可以调用工具。

例如模型说:

我要查询天气

系统执行天气工具,再把结果返回给模型。

模型继续思考。

这当然已经包含 Agent 的核心特征,但真正开始写一个可以反复运行、修改外部系统、根据结果继续行动的 Agent 后,我发现:

“会调用工具”只是 Agent Loop 中的一部分。

还需要回答:

模型现在知道什么?
下一步由谁决定?
工具执行以后结果保存在哪里?
什么时候继续循环?
什么时候必须停下来?
模型能直接修改什么?
模型输出错误怎么办?
循环最多跑多少次?
什么时候必须交给人?

这些问题决定了一个 Agent 是:

能够被工程系统控制的执行流程

还是:

一个可能不断调用模型和工具的 while true

Game Change Verification Agent 的主要场景是:

策划用自然语言描述一次弹幕 Boss 战修改,系统生成候选配置,在隔离的 Unity / UE5 环境中运行修改前后版本,采集 Telemetry,根据运行证据判断修改是否达标;如果没有达标,可以在有限动作空间中修复并重新测试。

整个过程不是让模型直接操作 Unity,更不是让模型随便修改游戏代码。

它是一条手写的、有预算、有状态、有确定性验证、有人工边界的 Agent Loop。

这篇文章从这条真实工作流出发,整理我对 Agent Loop 的理解。

项目地址:theHerta27/game-change-verification-agent

如果只有一次 LLM 调用,还算不算 Agent

假设策划说:

把第二阶段改成更密集的双向螺旋弹幕,
但是峰值存活子弹不能超过 350,
低分位 FPS 不能低于 55,
玩家固定轨迹下最多受击 3 次。

最简单的大模型程序可以这样:

Requirement
    ↓
LLM
    ↓
JSON Config

例如模型直接返回:

{
  "phase_2": {
    "pattern": "spiral",
    "bullets_per_wave": 30
  }
}

这更像:

一次结构化生成。

模型输入一次,输出一次,流程结束。

但如果候选配置拿去真实运行以后发现:

峰值存活子弹 = 430
FPS = 49

系统怎么办?

如果它只是:

生成一次
→ 结束

就没有后续行为。

真正的 Agent 场景更接近:

理解目标
  ↓
提出行动
  ↓
执行
  ↓
观察结果
  ↓
根据结果决定下一步
  ↓
再次行动

也就是:

Observe
→ Decide
→ Act
→ Observe
→ Decide
→ Act
...

循环真正出现以后,才会开始遇到 Agent 工程里最重要的问题:

这个循环由谁控制?

Game Change Agent 实际上有两个不同的 Agent

项目的弹幕验证主流程中,没有让一个模型既负责理解需求,又负责最后裁判一切。

而是明确区分:

Requirement Agent

和:

Quality Review Agent

两个角色。

Requirement Agent

负责:

自然语言需求
    ↓
结构化目标
+
完整候选配置

它的输出类型是严格定义的:

class BulletHellStructuredGoal(StrictAgentModel):
    target_phase_id: str
    requested_pattern: PatternType
    increase_pressure: bool
    preserve_visual_style: bool
    constraints: SafetyConstraints
    source_text: str

最终还必须同时输出:

class RequirementAgentOutput(StrictAgentModel):
    structured_goal: BulletHellStructuredGoal
    candidate_config: BulletHellContract

也就是说模型不能只说:

“我觉得可以增加一些子弹。”

它必须给出系统可以继续处理的结构化结果。

Quality Review Agent

它不再负责生成完整配置。

它看到的是:

原始需求
结构化目标
Config Diff
真实运行 Evaluation
修复历史

然后只能输出:

class QualityReviewOutput(StrictAgentModel):
    decision: Literal[
        "accept",
        "repair",
        "human_review",
    ]

    repair_action: RepairAction | None

    reason: str

    evidence_refs: list[str]

它回答的是:

根据真实运行证据,下一步应该接受、修复,还是交给人?

所以两个 Agent 的职责不同:

Requirement Agent
→ 把自然语言目标变成候选方案

Quality Review Agent
→ 根据运行证据提出下一步决策

它们之间不是互相聊天。

而是由外部 Workflow 串起来。

真正控制 Agent Loop 的不是模型

这是这个项目里我觉得最重要的一点。

循环核心在 Python Workflow 中,大致结构是:

for iteration in range(1, max_runs + 1):
    validate(candidate)

    telemetry = run_engine(candidate)

    evaluation = evaluate(telemetry)

    review = quality_review_agent.review(
        requirement,
        structured_goal,
        diff,
        evaluation,
        repair_history,
    )

    if review == "accept":
        break

    if review == "human_review":
        break

    candidate = apply_repair(
        candidate,
        action,
        evaluation,
    )

所以整个流程并不是:

LLM:
“我想再运行一次 Unity。”

系统:
“好的。”

LLM:
“我想改这个数字。”

系统:
“好的。”

LLM:
“再执行一次。”

系统:
“好的。”

真正决定:

什么时候可以运行
什么时候调用模型
什么时候执行修复
什么时候结束循环

的是确定性的 Workflow 代码。

模型只是被放在其中几个明确节点上。

因此我现在更愿意把 Agent 系统理解成:

LLM
不是整个 Agent

LLM
只是 Agent 中负责部分决策的组件

外面还必须存在:

State
Workflow
Tools
Validation
Budget
Termination
Human Boundary

一个 Agent Loop 到底由什么组成

用这个项目来拆,可以把 Agent Loop 简化成五个部分。

State
  ↓
Model / Policy
  ↓
Action
  ↓
Tool / Environment
  ↓
Observation
  ↓
重新进入 State

对应到项目:

当前 Candidate + 历史证据
        ↓
Quality Review Agent
        ↓
Repair Action
        ↓
确定性 Repair Tool + Unity
        ↓
Telemetry / Evaluation
        ↓
更新 Candidate 和 History

然后再循环。

所以循环的真正核心不是:

模型重复调用

而是:

系统状态经过一次行动以后发生变化,再把新的状态交给下一轮决策。

State 为什么这么重要

如果 Agent 每轮都只看到一句:

“刚才失败了,再试试。”

它其实并不知道之前发生了什么。

Game Change Workflow 会保存很多明确状态。

例如:

requirement.json
structured_goal.json
baseline_config.json
candidate_config.json
candidate_history.json
config_diff.json
validation_results.json
comparison_report.json
repair_history.json
quality_reviews.json
agent_runs.json
workflow_manifest.json

其中:

candidate_config

表示当前正在验证的候选。

repair_history

表示以前已经做过哪些修复。

comparison_report

记录当前候选与 Baseline 的运行比较。

workflow_manifest

保存:

当前状态
运行次数
模型调用次数
当前 iteration
授权信息
最终人工决策

所以 Agent 的“记忆”并不一定意味着:

给模型一个无限增长的聊天记录

很多时候更可靠的方式是:

把真正需要的状态显式存储成结构化数据。

下一轮只把需要的部分重新提供给模型。

为什么不能只依赖 LLM 的上下文窗口保存状态

如果整个 Workflow 都依赖:

messages = [
  第一轮所有内容,
  第二轮所有内容,
  第三轮所有内容,
  ...
]

会产生几个问题。

第一:

状态越来越长

Token 成本不断增加。

第二:

真正重要的数据
混在自然语言历史里

程序很难准确知道当前事实是什么。

第三:

如果进程重启:

内存里的消息历史

可能直接丢失。

而这个项目把关键事实写成:

{
  "current_iteration": 2,
  "status": "repairing",
  "budget": {
    "max_unity_runs": 3,
    "unity_runs_used": 2,
    "max_model_calls": 4,
    "model_calls_used": 3
  }
}

程序可以明确读取:

现在已经跑了两轮。

而不是让模型自己从几千 Token 对话中推断:

我好像之前运行过两次。

所以:

Conversation History 和 Workflow State 并不是一回事。

Structured Output 为什么是 Agent 工程的基础能力

Requirement Agent 的 Prompt 明确要求:

Return exactly one JSON object

而且最终还要经过:

RequirementAgentOutput.model_validate(
    json.loads(raw_output)
)

如果模型返回:

我认为第二阶段改成 spiral 比较合适。
下面是我的建议……

哪怕语言上完全正确,也不能进入后续 Workflow。

因为系统需要的是:

{
  "structured_goal": {...},
  "candidate_config": {...}
}

Quality Review Agent 同样必须返回:

{
  "decision": "repair",
  "repair_action": "REDUCE_BULLETS_PER_WAVE",
  "reason": "...",
  "evidence_refs": [...]
}

而不是:

“我觉得可以稍微减少一点子弹。”

这让我更容易理解 Structured Output 在 Agent 中的价值:

自然语言是给人理解的,结构化输出才方便程序继续执行。

如果模型的下一步结果要被代码消费,它最好尽可能进入一个明确 Contract。

extra="forbid" 在防什么

项目里的 Agent Schema 使用:

class StrictAgentModel(BaseModel):
    model_config = ConfigDict(
        extra="forbid"
    )

也就是说模型如果偷偷多返回:

{
  "decision": "repair",
  "repair_action": "...",
  "run_shell_command": "..."
}

这种 Schema 之外的字段:

不会被静默接受

而是验证失败。

这个细节很小,但它表达的是:

Agent 输出能力应该来自显式 Contract,而不是模型临时想到什么字段就接受什么字段。

在普通聊天里:

多说一句

通常没什么问题。

在自动执行系统里:

多出一个字段

可能意味着程序开始解释一个本来没有授权的动作。

Agent 给出的候选为什么不能直接运行

Requirement Agent 得到 Candidate 后,Workflow 不会马上:

启动 Unity

而是先执行四层确定性检查。

源码中明确写的是:

validate_bullet_hell_proposal(
    baseline=baseline,
    candidate=candidate,
    structured_goal=goal,
)

主要检查:

Schema
Reference
Rule Engine
Safety Gate

Schema

首先验证:

字段有没有缺
类型对不对
数值范围是否合法
额外字段是不是出现了

例如:

bullets_per_wave: int = Field(
    ge=1,
    le=64,
)

所以模型不能输出:

bullets_per_wave = 100000

然后期待引擎帮它试试看。

Reference

检查模型引用的对象是否真的存在。

例如:

target_phase_id = phase_2

必须能在 Candidate 里找到。

Pattern 也必须属于系统支持的:

ring
aimed_fan
spiral
petal

否则模型可能提出一个听起来合理但系统根本没有实现的能力。

Rule Engine

即使单个字段都在合法范围内,组合起来仍然可能产生危险结果。

例如:

每波子弹数
×
发射频率
×
生命周期

可能让同时存活子弹远超安全边界。

所以程序还会根据配置估计:

estimated_peak_alive_bullets

并与:

max_alive_bullets

比较。

Safety Gate

还会限制模型能修改哪些内容。

例如阶段 ID 不允许:

新增
删除
重命名

模型的 Candidate 也不能随意越过允许的配置范围去修改其他东西。

这让我觉得:

LLM 的输出通过 Schema,只能证明“格式合法”,不能证明“行为安全”。

Structured Output 只是第一层。

为什么 Agent 不能直接决定修复数值

这是这个项目里我特别喜欢的一条边界。

Quality Review Agent 可以说:

REDUCE_BULLETS_PER_WAVE

但它不能直接说:

把 bullets_per_wave 从 32 改成 17。

Prompt 里明确要求:

Do not change configuration values and do not calculate replacement numbers.

也就是说模型只能选择:

修复意图。

真实数值由确定性函数计算:

def apply_repair(
    contract,
    action,
    evaluation,
):

例如峰值存活子弹超标时:

actual = max(
    float(check.get("actual") or 1),
    1,
)

limit = candidate[
    "runtime_targets"
][
    "max_alive_bullets"
]

factor = min(
    0.85,
    max(
        0.5,
        limit / actual * 0.9,
    ),
)

pattern["bullets_per_wave"] = max(
    1,
    round(
        pattern["bullets_per_wave"]
        * factor
    ),
)

也就是说:

LLM
→ “应该降低单波子弹数”

确定性程序
→ “根据实际超标幅度计算应该降低多少”

这把:

策略判断

和:

精确数值执行

分开了。

为什么这种划分比“让 LLM 自己算”更稳

假设实际 Telemetry:

peak_alive_bullets = 430

目标:

<= 350

模型当然也可以被要求:

计算一个新的 bullets_per_wave。

但模型并不是为严格数值计算设计的。

不同调用还可能得到:

18
21
20
24

不同建议。

而确定性函数面对同样输入:

永远得到同样输出

因此项目的基本原则是:

LLM
负责模糊理解和策略选择

Deterministic Tool
负责精确计算和受约束修改

这也是我现在理解 Agent 工具设计时很重要的一点:

不是能让模型做的事情都应该让模型做。

能可靠写成程序的部分,通常更适合留给程序。

Quality Review Agent 甚至不能随便选择 Repair Action

项目允许的 Repair Action 是一个固定集合:

RepairAction = Literal[
    "REDUCE_BULLETS_PER_WAVE",
    "INCREASE_WAVE_INTERVAL",
    "REDUCE_BULLET_SPEED",
    "REDUCE_BULLET_LIFETIME",
    "REDUCE_PATTERN_LAYERS",
    "PRESERVE_VISUAL_STYLE",
    "REQUEST_HUMAN",
    "STOP",
]

但即使模型返回的是这个集合里的合法值,也不代表一定会执行。

因为后面还有:

Policy Gate

确定性程序会根据 Evaluation 自己计算:

expected_action = choose_repair_action(
    evaluation
)

例如:

peak_alive_bullets failed

系统期望:

REDUCE_BULLETS_PER_WAVE

如果 Agent 却建议:

REDUCE_BULLET_SPEED

即使这也是合法 Action,Policy Gate 仍然会认为:

不匹配

最终:

转人工复核

而不是执行模型建议。

为什么 Structured Output 通过了还要 Policy Gate

因为这里有两个不同层面的正确性。

第一层:

Schema Correctness

回答:

模型输出格式是不是合法?

例如:

{
  "decision": "repair",
  "repair_action": "REDUCE_BULLET_SPEED"
}

完全符合 Schema。

第二层:

Decision Correctness

回答:

在当前证据下,这个动作是不是被系统策略允许?

如果真实失败项是:

peak_alive_bullets

确定性策略认为应该:

REDUCE_BULLETS_PER_WAVE

模型却选:

REDUCE_BULLET_SPEED

那:

格式合法
≠
决策可执行

所以 Agent 的输出必须再经过 Policy Gate。

这让我觉得 Agent 工程里很重要的一条原则是:

能 Parse,不等于能 Execute。

Tool Calling 在这个项目里其实是“受控的”

很多 Agent Framework 中,模型会直接返回类似:

tool_call:
    name = run_unity
    args = ...

然后框架自动执行。

这个项目的 Bullet Hell 主流程目前并不是这种设计。

模型不会直接获得:

run_unity()

或者:

apply_config()

这种无限工具权限。

而是 Workflow 自己决定:

现在该不该运行 Engine
现在是否允许 Apply Repair
下一步调用哪个确定性函数

例如:

Quality Review Agent
        ↓
repair_action
        ↓
Policy Gate
        ↓
apply_repair()

所以更准确地说:

模型提出 Action,Workflow 根据状态与 Policy 决定是否调用 Tool。

而不是:

模型想调用什么就调用什么。

这一点对于理解后面的 MCP 和 Tool Calling 会很重要。

Engine 本身也是一种 Tool

从 Agent 的角度看,Unity / UE5 可以看成一个非常重的外部工具。

输入:

candidate config
seed
run mode

执行:

真实游戏 Runtime

输出:

Telemetry
Screenshots
Logs

整个关系其实就是:

Agent Workflow
    ↓
EngineRunner
    ↓
Unity / UE5
    ↓
Observation

项目定义统一:

EngineRunner

接口,使 Workflow 不需要知道底层:

Unity 怎么启动
UE5 怎么启动
命令行参数长什么样

它只关心:

给定一个 Candidate
能不能运行
返回什么 Evidence

这正是 Tool Abstraction 的意义:

Agent 层关心能力,不应该和具体执行实现紧耦合。

Observation 为什么不能只是一句“运行成功”

运行 Candidate 后,系统不会只得到:

success

而是 Telemetry:

total_bullets_spawned
peak_alive_bullets
player_hits
player_survival_seconds
phase_results
average_fps
low_percentile_fps
minimum_fps
exception_log_count

然后再由:

evaluate_bullet_hell_telemetry(
    candidate,
    telemetry,
)

转成确定性 Evaluation。

因此下一轮 Quality Review Agent 看到的不是:

“游戏感觉有点卡。”

而是类似:

peak_alive_bullets
actual = 430
limit  = 350
passed = false

low_percentile_fps
actual = 49
limit  = 55
passed = false

这其实就是 Agent Loop 中:

Observation。

没有 Observation:

Action
→ Action
→ Action

只能叫连续执行。

只有外部环境执行后产生结果,并重新进入下一轮决策:

Action
→ Environment
→ Observation
→ Decision

才真正形成闭环。

为什么 Baseline 和 Candidate 要用同一个 Seed

如果 Baseline:

random_seed = A

Candidate:

random_seed = B

那么两次运行的变化可能来自:

配置变化
+
随机行为变化

就很难判断指标差异到底是谁造成的。

所以工作流运行:

self._run_automated(
    baseline,
    ...,
    20260727,
)

Candidate 同样:

self._run_automated(
    candidate,
    ...,
    20260727,
)

也就是:

同一 Player
同一 Seed
同一固定移动轨迹
同一运行时长

再比较修改前后。

这让我觉得一个 Agent 有工具还不够。

如果 Tool 输出本身高度随机:

Observation

就不稳定。

Agent 下一轮决策也会跟着漂移。

所以对可以控制的环境:

尽量让 Observation 可重复,也是一种 Agent 工程能力。

Loop 为什么一定要有 Termination Condition

最危险的 Agent Loop 写法大概是:

while True:
    result = agent.run(state)

    if result.needs_repair:
        state = repair(state)
        continue

如果 Agent 永远觉得:

还可以再优化一点

它就可以无限运行。

于是:

LLM 调用次数
Engine 运行次数
Token
时间
费用

都没有上限。

Game Change Workflow 从一开始就把预算写进:

manifest["budget"] = {
    "max_unity_runs": 3,
    "max_model_calls": 4,
    ...
}

其中:

Requirement Agent
最多消耗 1 次模型调用

Quality Review Agent
最多跟随 3 轮 Candidate

所以整个主要流程最多:

4 次 Model Call
3 次 Candidate Engine Run

这就是:

Bounded Agent Loop。

为什么预算不仅是控制费用

模型调用预算看起来首先是在控制:

API Cost

但它还有更重要的意义。

它定义:

Agent 最多拥有多少次自主修复机会。

例如:

Iteration 1
→ failed
→ repair

Iteration 2
→ failed
→ repair

Iteration 3
→ still failed

系统不会:

继续 Iteration 4
继续 Iteration 5
直到模型满意

而是进入:

budget_exhausted

也就是说:

预算本身就是安全边界的一部分。

项目还有“停滞”终止条件

即使还没有完全用完三轮,如果修复没有实际改善,也不应该无意义地继续。

Workflow 会计算 Evaluation Score。

如果连续修复:

没有改善

stagnant_rounds 会增加。

当:

stagnant_rounds >= 2

同样结束自动修复:

budget_exhausted

这避免了:

Agent
→ Repair A
→ 没改善

Agent
→ Repair B
→ 还是没改善

Agent
→ 继续换动作碰运气

所以终止条件不仅可以是:

次数到了

还可以是:

继续执行已经没有有效进展

Human-in-the-loop 不只是最后点一下“确认”

这个 Workflow 里至少有两个明显的人工边界。

第一个:执行引擎之前

Requirement Agent 生成 Candidate,并通过静态校验以后,状态是:

awaiting_authorization

用户必须显式调用:

authorize

才能变成:

authorized

授权信息还会记录:

{
  "actor": "...",
  "scope": "candidate_json_only",
  "max_unity_runs": 3
}

也就是说:

模型生成了合法 Candidate,并不等于它自动获得运行外部环境的权限。

这是:

行动前授权。

第二个:最终接受结果

自动工作流最多只能进入:

evidence_ready

最终仍需要人做:

accept
revise
rollback

而且即使选择:

accept

当前实现也明确:

baseline_overwritten = false

不会因为人点击接受就自动覆盖正式仓库 Baseline。

这是:

结果后决策。

所以 HITL 不是简单的:

流程最后弹一个确认框

而是应该根据风险,把人工节点放在真正重要的权限边界上。

Agent 自己也可以主动要求 Human Review

Quality Review Agent 的输出允许:

human_review

而 Policy Gate 规定:

人工复核在任何阶段都是允许的保守决策。

如果证据:

缺失
冲突
超出允许修复动作

Agent 可以:

REQUEST_HUMAN

而不是必须硬猜下一步。

另外,如果 Agent 建议和确定性 Policy 不一致:

Agent:
REDUCE_BULLET_SPEED

Policy:
应该 REDUCE_BULLETS_PER_WAVE

系统也不会替模型找理由。

而是自动:

human_review

这让我觉得一个好的 Agent 系统应该允许:

“我不知道,交给人”也是一个合法动作。

如果 Agent 被迫任何时候都必须:

choose an action

反而容易把不确定性变成错误执行。

Agent Loop 不是越 Autonomous 越好

很多 Agent Demo 会强调:

完全自主
自动完成
无需人工

但这个游戏验证项目反而刻意限制:

模型不能直接修改正式 Baseline
模型不能直接运行任意程序
模型不能自己生成任意 Repair Tool
模型不能自行增加运行预算
模型不能绕过 Validation
模型不能在 Evidence 不足时强行 Accept

甚至:

第一版不允许 Agent 修改 C#

模型只在一个非常明确的空间里行动。

这其实让我重新理解了:

Agent Autonomy。

自主性不是:

给模型最多权限。

更准确地说是:

在一个定义清楚的边界里,允许它自己选择部分行动。

边界越清楚,系统反而越容易真的自动运行。

Agent 和 Workflow 到底有什么区别

如果把这套系统完全写死:

如果 peak bullets 超标
→ 减 bullets

如果 FPS 低
→ 增加 interval

整个过程甚至不需要 LLM。

它就是:

Workflow。

而加入 Agent 的地方在于:

自然语言需求理解
复杂上下文综合判断
基于证据选择下一步意图

这些不容易全部提前写成固定 if/else 的部分。

所以我的理解是:

Workflow
→ 定义允许怎样流转

Agent
→ 在某些节点处理难以完全规则化的决策

在这个项目中,两者是组合关系。

并不是:

用了 Agent
→ 就不需要 Workflow

恰恰相反:

真正需要执行外部动作的 Agent,通常更需要明确 Workflow。

为什么我没有直接用 LangGraph 编排这条主流程

这个项目的 Bullet Hell 主流程目前是:

手写 Python Workflow

并没有为了“像 Agent 项目”而强行加入 LangGraph。

因为当前状态转换其实非常明确:

awaiting_authorization
→ authorized
→ running_baseline
→ running_candidate
→ repairing
→ evidence_ready / blocked / budget_exhausted

修复循环最大也只有:

3 次

所以直接用普通 Python:

for
if
state
function call

已经能很清楚地表达控制关系。

这也让我觉得:

Agent Framework 是一种工程工具,不是 Agent 成立的必要条件。

真正需要先搞清楚的还是:

State 是什么?
Node 做什么?
Edge 怎么走?
谁决定下一条 Edge?
如何保存状态?
怎样退出?

这些关系搞清楚以后,再换成 StateGraph 之类的框架,主要是在改变:

编排表达方式

而不是突然让普通程序“变成 Agent”。

为什么这个项目也没有直接用 MCP

当前 Bullet Hell 主流程同样没有接 MCP。

EngineRunner、Validator、Repair Function 都是:

项目内部 Python 接口

而不是 MCP Server 暴露出来的 Tool。

这并不妨碍它构成 Agent Workflow。

因为:

MCP 解决的是工具和上下文怎样以标准协议暴露给 AI 应用。

Agent Loop 解决的是:

模型、状态、动作、工具结果怎样形成一个决策闭环。

两者是不同层次的问题。

关于:

Tool Calling
EngineRunner
MCP

三者到底是什么关系,我会单独写下一篇。

Error 为什么也必须成为状态的一部分

模型可能返回:

非法 JSON

或者:

JSON 合法
但不符合 Pydantic Schema

Requirement Agent 会抛出:

BulletHellAgentError

Workflow 会保存:

error_type
error_message
raw_model_output
provider
model
stage

进入:

badcases.json

Quality Review Agent 同样如此。

这意味着一次 Agent Failure 不是:

print(error)
然后结束

而是变成可检查 Evidence。

因为如果一个 Agent 系统以后要迭代 Prompt 或模型,一个很重要的问题就是:

它到底在哪些输入上失败?

没有保存:

Raw Output
Stage
Error Type

就很难真正分析。

Trace 为什么不能只记录聊天内容

项目中的:

agent_runs.json

会记录每一次 Agent:

agent_name
prompt_name
provider
model
latency_ms
usage
token_estimate
status
model_call
input_artifacts
output_artifacts
iteration

Workflow Manifest 还记录时间线:

Requirement structured
Feasibility Gate
Candidate engine run
Quality Review Agent
Bounded repair policy
Final human decision

因此 Trace 不只是:

User 说了什么
Assistant 回了什么

还应该回答:

哪一个 Agent 被调用?
用了哪个 Prompt?
读了哪些 Artifact?
输出到哪里?
第几轮?
耗时多少?
最终走向哪个状态?

这对于调试 Agent 比保存纯聊天记录更有意义。

一个完整循环实际发生了什么

假设需求是:

第二阶段改成更密集的螺旋弹幕,但不能突破性能和受击边界。

第 0 步:Requirement Agent

输入:

Requirement
Baseline Config

输出:

Structured Goal
Candidate Config

第 1 步:Deterministic Validation

检查:

Schema
Reference
Rules
Safety

如果失败:

blocked

不会运行引擎。

第 2 步:Human Authorization

Candidate 合法后:

awaiting_authorization

人明确授权:

最多三次隔离测试

第 3 步:Baseline

固定:

Seed
Trajectory
Duration
Engine

运行原始版本。

得到:

Baseline Telemetry

第 4 步:Candidate

运行 Candidate。

得到:

Candidate Telemetry

程序比较:

Baseline
vs
Candidate

并计算硬指标。

第 5 步:Quality Review Agent

输入:

Requirement
Structured Goal
Diff
Evaluation
Repair History

输出:

accept
repair
human_review

第 6 步:Policy Gate

确定性程序再检查 Agent Decision。

如果通过:

执行

如果不一致:

human_review

第 7 步:Repair Tool

如果需要:

Repair Intent
    ↓
Deterministic Calculator
    ↓
New Candidate

第 8 步:Loop

新 Candidate:

重新 Validate
重新运行 Engine
重新 Evaluate
重新 Review

最多三轮。

这才是这个项目里所谓的:

Agent Loop。

面试时我会怎样回答

如果面试官问:

你说你手写了 Agent Loop,它到底是什么?

我会回答:

我这个项目不是让 LLM 在 while true 里随便调用工具。主流程由 Python Workflow 显式控制。Requirement Agent 先把自然语言需求变成 Structured Goal 和完整 Candidate,Candidate 必须先过 Schema、引用、规则和安全四层确定性校验,再经过人工授权才能进入隔离引擎。

Engine 跑完以后会产生 Telemetry,程序先做确定性 Evaluation,再把原始需求、Diff、Evaluation 和 Repair History 交给 Quality Review Agent,让它只选择 accept、repair 或 human_review,以及一个受限 Repair Action。

Agent 的建议还不能直接执行,要再过 Policy Gate;真正的数值修改由确定性 Repair Function 计算。修改以后重新跑 Engine,形成 Observation → Decision → Action → Observation 的闭环。

整个循环最多 3 次 Candidate Run、4 次 Model Call,而且连续没有改善会提前停止,所以它是一个有预算和终止条件的 bounded Agent Loop。

如果继续问:

为什么不直接让模型修改数值?

我会回答:

因为 LLM 比较适合处理自然语言理解和策略判断,不适合承担需要稳定重复的精确计算。我的设计是让 Quality Review Agent 只选“降低单波子弹数”这种 Repair Intent,具体降低多少由确定性程序根据 Telemetry 和目标阈值计算,所以相同输入得到相同修改。

如果继续问:

Agent 和普通 Workflow 有什么区别?

我会回答:

Workflow 定义允许怎样流转,比如必须先 Validate、再授权、再运行、最多三轮;Agent 只放在难以完全规则化的节点,比如从自然语言形成 Candidate,以及综合需求和运行证据选择修复意图。我的理解不是用 Agent 替代 Workflow,而是在确定性 Workflow 里嵌入有限的模型决策。

如果继续问:

为什么叫有界 Agent?

我会回答:

因为模型的能力、动作空间、运行次数和权限都有边界。模型不能直接运行 Unity、不能任意改配置、不能自己计算修复数值、不能增加预算,也不能绕过 Policy Gate。最多三次候选运行、四次模型调用,证据不足或决策冲突时直接转人工,而不是继续自主尝试。

我从这个问题中理解到的事情

以前我比较容易把 Agent 理解成:

LLM
+
Tools

后来我觉得这只是一个非常粗略的公式。

一个真正能执行工作的 Agent 系统至少还要回答:

State 在哪里?

下一步 Decision 的输入是什么?

Action 有哪些?

哪些 Action 模型有权提出?

哪些 Tool 真正可以执行?

Tool Result 怎样变成 Observation?

Observation 怎样进入下一轮?

模型输出怎样验证?

什么时候需要人工?

循环什么时候结束?

失败以后留下什么 Trace?

在这个项目里,这些关系最终变成:

Structured State
→ 保存当前 Candidate、Evidence 和 History

Requirement Agent
→ 把模糊自然语言转成结构化候选

Deterministic Validation
→ 模型输出不能直接获得执行权

Human Authorization
→ 外部执行之前必须明确授权

EngineRunner
→ 在真实环境执行 Candidate

Telemetry
→ 把真实运行转成 Observation

Quality Review Agent
→ 根据证据选择有限的下一步意图

Policy Gate
→ 判断模型建议是否允许执行

Deterministic Repair
→ 模型决定方向,程序计算精确数值

Budget
→ 限制模型调用和引擎运行次数

Termination
→ 成功、人工复核、预算耗尽或停滞都可以结束循环

Trace
→ 每一轮输入、输出、状态和 Evidence 都可回放

所以我现在更愿意把 Agent 定义成:

一个让模型能够基于当前状态和外部观察选择行动,并通过工具影响环境,再把结果带回下一轮决策的受控执行系统。

而其中真正困难的部分不是:

怎么调用一次 LLM

也不是:

怎么让模型调用一个函数

而是:

如何决定模型可以控制什么,又必须把什么控制权留在确定性程序和人手里。

这也是我手写这条 Agent Loop 以后,觉得比“用了哪个 Agent Framework”更重要的基础问题。

延伸阅读

  • OpenAI:Structured Outputs
  • OpenAI:Function Calling / Tool Calling
  • LangGraph:StateGraph Concepts
  • Anthropic:Building Effective Agents