ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

LLM Agent循环工程的关键:以运行时控制器视角评估模型

LLM Agent循环工程的关键:以运行时控制器视角评估模型 这两年在做 LLM Agent 相关工程时我经常遇到一个很典型的“割裂”现象某个模型单轮写代码、分析 bug 的表现都不错一旦把它放进“读日志 → 改代码 → 跑测试 → 看报错 → 再修改”的真实闭环里效果就变得很不稳定。有时候它会反复执行同一个错误操作有时候又连测试都没跑就宣告任务完成。这并不是某个模型平台的个例而是 Agent 类应用共同面临的深层问题模型在循环中承担的角色和我们平时评价的“文本生成能力”并不是同一件事。模型不是只负责给一个答案而是要像一个运行时控制器一样持续观察状态、决定下一步动作、判断何时结束。LoopArena 基准发布之后很多做 AI 编程和 Agent 评测的开发者开始讨论一个关键词把模型当作“循环工程的运行时控制器”来评估。本文会从工程视角拆解这个概念说明为什么这种评估比传统评测更难并给出一个可运行的最小循环评估骨架帮助大家理解控制器在循环里到底做了什么、该记录哪些信息、又该如何避免失控。1. 背景从一个普遍痛点说起1.1 会写代码的模型不一定会“干活”如果只做静态代码补全模型只要根据注释和上文生成一段代码就行正确性相对容易衡量。但在真实开发任务的 Agent 场景里模型面对的不是一个空文件而是一个包含历史修改、测试结果、编译错误、临时文件状态的项目。举个常见例子def divide(a, b): return a / b如果任务单轮问答问“这段代码有什么问题”模型通常能准确回答“b 等于 0 时会抛异常”。但如果我们让 Agent 自己读文件、修改源码、执行测试用例情况就完全不同。模型可能只给建议不产生实际修改修改逻辑正确却没有触发测试测试失败后反复用同一种错误方案重试或者在某次测试误通过后没有做回归验证就提前结束。这里真正难的不是“写代码”而是“持续控制”。这也是为什么很多团队在自建 Agent 评测时总觉得单看“最终跑没跑通”不够但又说不清还缺什么指标。1.2 从静态评测到循环工程评测早期的代码生成评测例如常见的函数级、类级生成任务更多是判断“模型是否一次生成出正确答案”。随着 Agent 普及评测方式逐渐向真实场景靠拢模型需要在多轮迭代中完成一个工程任务并自己决定何时修改、何时验证、何时收手。这类任务可以统称为循环工程核心特征是存在一个外部状态或环境例如文件系统、代码仓库、运行进程模型可以通过工具或动作修改这个状态修改结果会以日志、测试结果、异常信息等形式反馈给模型模型需要基于反馈继续操作直到任务完成或资源耗尽。LoopArena 正是围绕“循环工程”来做模型评估的。与传统单轮问答评测不同这类基准更关注模型在多轮外部反馈下的控制能力也就是“运行时控制器”能力。1.3 怎么理解“运行时控制器”这一角色控制器这个词最早来自计算机系统。无论是操作系统调度进程还是微服务里的流量控制控制器都要完成三件事感知当前状态计算下一步动作执行并观察结果。LLM 作为 Agent 时也完全符合这个结构。它不再只是文本生成器而是循环中枢观察读取当前代码、测试输出、报错堆栈决策决定是继续改代码、补测试、查文档还是结束任务动作调用工具、修改文件、执行命令复盘根据反馈判断自己的操作是否有效并选择下一步。也就是说模型是否“会干活”取决于它在这个闭环控制中的整体表现而不只是某个中间步骤的对错。LoopArena 这类基准的价值就是把这种整体表现变成可以量化对比的任务集。2. 运行时控制器到底在控制什么2.1 控制器的观察对象在一个 Agent 循环中控制器每次做决定前都应该获得足够的状态信息。以代码工程任务为例最小状态集合包括状态信息示例任务目标修复浮点数除以零时的崩溃文件内容当前目标文件中最新源码最近动作上一次调用的是哪个工具动作结果测试是否通过、报错信息是什么资源消耗已执行步数、token 消耗、剩余时间这里很容易出现一个问题模型没有及时更新状态。例如它修改了文件但下一次决定仍然基于旧版本或者测试已经失败它还坚持认为之前的修改是对的。评价一个控制器是否合格首先要看它有没有把“最新 observation”纳入下一轮推理。2.2 四类核心决策若把一个模型控制器放进代码工程循环中它的决策大致可以拆成四类第一类理解任务并选择动作。任务是修 bug还是加功能需要读哪个文件这相当于传统程序的初始化分支。第二类调度工具调用。是修改文件、运行测试还是查看日志工具选择直接影响状态变化。比如有些任务必须先跑测试看失败原因而模型却跳过测试直接改代码这往往会放大错误。第三类解读反馈并调整策略。测试失败后模型要能区分“实现思路错了”和“只是有小细节没对齐”。如果连续两次相同失败控制器应该改变方式而不是机械重试。第四类决定终止条件。这其实是最容易被忽视、也最难评估的控制点。模型必须判断当前状态是否满足任务要求是立即结束还是继续验证。过早结束会造成假阳性迟迟不结束则浪费资源。2.3 与传统流程控制有什么不同传统程序的控制逻辑写在代码里每个分支都是开发者预先定义的。例如while test_failed: run_fix()这套流程是确定的开发者可以通过加日志、打断点来观察每一步。但在 LLM Agent 里“下一步动作”和“何时退出”都由模型根据上下文动态判断不同次运行可能得到完全不同的路径。因此评估体系也需要跟着改变。我们不能只检查“最后一次输出是否正确”而要检查整个循环内模型的动作序列、反馈处理、终止行为。LoopArena 这一类基准做的事情就是把这些动态行为标准化为可重复的任务把模型的“控制轨迹”作为主要观测对象。3. 将模型当作控制器时评测难点在哪3.1 “终点正确”掩盖了“过程失控”假设一个修复任务最终跑通了测试看起来结果是成功的。但如果路径是这样的第 1 步读文件 第 2 步直接修改文件 → 测试失败 第 3 步再修改 → 测试失败 第 4 步跳过测试直接重复“第 2 步”的修改 第 5 步盲目删除文件 第 6 步测试通过但问题并没有真正修复这种轨迹显然不具备工程可靠性。可如果评测只看最终 pass它就会被记为一次成功。合格的控制器评估必须保留完整轨迹包含动作类型、依赖参数、测试结果、是否出现无效操作等才能识别这类“过程失控”。3.2 终止策略最难测很多 Agent 跑偏往往不是不知道“下一步要做什么”而是不知道“什么时候该停”。模型可能因为误判测试输出而过早结束也可能在已经完成任务后继续反复测试直到步数耗尽。从 LoopArena 这类基准的视角看终止策略又是一个可量化的控制指标。可以这样理解同一模型面对简单任务时如果平均需要 10 步才结束说明它的“继续执行阈值”过高如果面对明显失败仍提前收工则说明“结束条件”设计得太宽松。评测数据集里必须同时包含需要快速结束的简单任务、需要多轮折腾的困难任务以及存在误导信号的陷阱任务否则很难测出控制器的真实终止能力。3.3 循环反馈存在依赖和噪声工程任务的每个动作结果往往依赖于前序动作。比如新增了一个工具函数后续编译才会通过某个测试用例本身不稳定第一次跑失败、第二次跑可能成功。这种依赖和噪声对控制器评估提出了极高要求评测环境必须可重置最好支持 git 快照或临时文件副本每次运行需要使用相同初始状态避免变量污染对不稳定结果要设计重试机制而不是直接判模型失败错误堆栈和测试输出需要结构化存储方便事后分析。这也是为什么很多团队在自建 Agent 评测时会选择用测试任务而不是真实生产仓库来做初始验证因为真实仓库不可控因素太多。4. 最小演示编写一个可观测的循环评估骨架上面说了很多概念下面直接用 Python 实现一个最简单的“控制器 运行器”骨架。它不依赖任何外部模型 API也不需要安装额外依赖用标准库就能跑通。这里的目的不是复刻 LoopArena而是让你直观看到控制器在循环里到底是怎么做决策的、反馈是如何进入下一步的、不同控制器会得到怎样不同的评估结果。4.1 文件路径与核心思路我建议将代码保存为mini_loop_arena.py放在一个空目录中运行。设计上把代码拆成两部分DemoRunner负责任务初始化、循环调度、状态保存、结果收集BaseController代表模型控制器实际场景中它由 LLM 调用实现这里先用规则控制器演示。为了简化这里的patch动作直接使用完整文件内容覆盖旧文件。真实多文件工程中会用 diff 补丁或apply_patch工具但循环评估结构是一样的。4.2 定义任务与运行器 mini_loop_arena.py 最小循环评估骨架 核心思想 - Runner 维护当前代码状态与完整历史轨迹。 - Controller 负责在每一步根据历史和执行结果决定下一个 action。 - 评测结束后我们可以从 result 中读取 success、steps 和完整轨迹。 from dataclasses import dataclass from typing import Callable, Dict, List, Optional dataclass class Task: task_id: str instruction: str initial_code: str expected_fragment: str def passes(self, code: str) - bool: 用“关键片段是否出现”来判断任务是否通过。 真实场景中这里应该替换成单元测试、编译命令或端到端验证。 return self.expected_fragment in code class BaseController: 控制器接口。 在真实 Agent 中decide 内部会调用 LLM并把 history、instruction、 current_code 拼进 prompt最后解析成 JSON action。 def decide(self, step: int, history: List[dict], instruction: str, current_code: str) - dict: raise NotImplementedError class DemoRunner: def __init__(self, controller: BaseController, max_steps: int 8): self.controller controller self.max_steps max_steps self.code self.history: List[dict] [] def run(self, task: Task) - dict: self.code task.initial_code self.history [] for step_id in range(1, self.max_steps 1): action self.controller.decide( stepstep_id, historyself.history, instructiontask.instruction, current_codeself.code, ) record { step: step_id, action: action, } if action[type] patch: # 为简化演示patch 的 content 视为整份文件的最新内容 self.code action[content] record[observation] {ok: True, message: patched} elif action[type] test: passed task.passes(self.code) record[observation] { ok: True, passed: passed, message: test passed if passed else test failed } elif action[type] finish: passed_when_finish task.passes(self.code) record[observation] { ok: True, passed: passed_when_finish, message: controller declared finish } self.history.append(record) return self._result(finished, step_id, passed_when_finish) else: record[observation] { ok: False, message: funknown action type: {action.get(type)} } self.history.append(record) # 如果测试已经通过可以提前结束循环等价于成功终止 if record[observation].get(passed): return self._result(finished, step_id, True) return self._result(max_steps, self.max_steps, False) def _result(self, status: str, steps: int, passed: bool) - dict: return { status: status, passed: passed, steps: steps, history: self.history, }这里有几个工程细节值得注意history中同时保存了模型决策action和外部反馈observation这两者缺一不可。如果不保存 action就无法事后分析模型在哪里做出了错误选择如果不保存 observation就无法还原模型“看到了什么才做出下一步决策”。max_steps是硬上限。真实场景中即使模型已经接近正确方案只要步数超限也应该按失败处理。这是控制器的资源预算约束。finish时仍然要检查task.passes(self.code)。也就是说“我结束了”还不能算成功必须以最终状态是否通过任务为准。这样能识别过早结束。4.3 设计几个简单任务和示例控制器下面定义几个非常简单但有代表性的任务。这里用“代码片段中是否出现某段文字”代替真实测试只是为了让你把注意力集中在控制循环上。TASKS [ Task( task_idfix_default_config, instruction修复配置加载函数当配置文件缺失时value 应该使用 DEFAULT_VALUE, initial_codevalue None, expected_fragmentvalue DEFAULT_VALUE, ), Task( task_idfix_answer_value, instruction补全计算函数程序运行后 answer 应该等于 42, initial_codeanswer ???, expected_fragmentanswer 42, ), ]现在实现三种控制器模拟真实模型可能出现的不同“控制风格”。第一种预设了正确补丁的控制器。这种控制器表现极好但只擅长它内部预先知道的任务类似“看过标准答案的模型”在评测中用来验证循环骨架是否正常。class PresetPatchController(BaseController): 如果读取到任务是修 DEFAULT_VALUE就写入正确内容。 它只会修自己预设过的任务其他任务会直接 finish模拟过早结束。 def __init__(self, patch_content: str): self.patch_content patch_content def decide(self, step: int, history: List[dict], instruction: str, current_code: str) - dict: if step 1: return {type: patch, content: self.patch_content} if step 2: return {type: test} # 无论测试是否通过第 3 步都强制结束 return {type: finish, answer: I think it is done.}第二种只会原地打转的控制器。它每个奇数步都提交一次 patch但 patch 内容没有带来任何有效进展偶数步又去跑测试。这类控制器很像现实中的“反复重试同一个错误方案”的模型最终会耗尽步数。class RunawayController(BaseController): 模拟一个原地打转的控制器不停提交无意义 patch再不停 test。 def decide(self, step: int, history: List[dict], instruction: str, current_code: str) - dict: if step % 2 1: return {type: patch, content: current_code} return {type: test}第三种把 LLM 调用抽象成控制器。在真实项目中你只需要把上述“规则判断”换成一次模型调用。下面代码是思路示例实际 API 参数需要根据所用模型 SDK 调整class LLMController(BaseController): 把 LLM 接入控制器的示例思路。 这里不绑定具体模型供应商只演示如何把循环状态转成一条可发送的消息。 def __init__(self, model_client, build_messages: Callable): self.model_client model_client self.build_messages build_messages def decide(self, step: int, history: List[dict], instruction: str, current_code: str) - dict: messages self.build_messages( stepstep, historyhistory, instructioninstruction, current_codecurrent_code, ) # 伪代码实际需要替换成模型客户端调用 # response self.model_client.chat.completions.create( # modelyour-model, # messagesmessages, # temperature0, # ) # action_text response.choices[0].message.content # return json.loads(action_text) raise NotImplementedError(请根据实际模型 SDK 实现该控制器)4.4 评估函数与主流程我们还需要一个统一的评估函数让多个控制器在同一组任务上跑一遍并输出统计结果。def evaluate(controller_name: str, controller: BaseController, tasks: List[Task]) - dict: passed_count 0 steps_list [] details [] for task in tasks: result DemoRunner(controllercontroller).run(task) steps_list.append(result[steps]) passed result[passed] passed_count 1 if passed else 0 details.append({ task_id: task.task_id, status: result[status], passed: passed, steps: result[steps], }) avg_steps sum(steps_list) / len(steps_list) if steps_list else 0 print(f\n evaluate: {controller_name} ) for detail in details: print(detail) print(fpass{passed_count}/{len(tasks)} avg_steps{avg_steps:.2f}) return { controller: controller_name, pass: f{passed_count}/{len(tasks)}, avg_steps: avg_steps, details: details, } if __name__ __main__: evaluate( controller_namepreset_default_config, controllerPresetPatchController(patch_contentvalue DEFAULT_VALUE), tasksTASKS, ) evaluate( controller_namepreset_answer_42, controllerPresetPatchController(patch_contentanswer 42), tasksTASKS, ) evaluate( controller_namerunaway_controller, controllerRunawayController(), tasksTASKS, )4.5 运行与预期结果在命令行运行python mini_loop_arena.py预期输出大概如下 evaluate: preset_default_config {task_id: fix_default_config, status: finished, passed: True, steps: 2} {task_id: fix_answer_value, status: finished, passed: False, steps: 3} pass1/2 avg_steps2.50 evaluate: preset_answer_42 {task_id: fix_default_config, status: finished, passed: False, steps: 3} {task_id: fix_answer_value, status: finished, passed: True, steps: 2} pass1/2 avg_steps2.50 evaluate: runaway_controller {task_id: fix_default_config, status: max_steps, passed: False, steps: 8} {task_id: fix_answer_value, status: max_steps, passed: False, steps: 8} pass0/2 avg_steps8.00从这个结果能直接看出几种不同控制器失败模式preset_default_config在任务一上成功但在任务二上属于“过早结束”第 3 步主动声明完成实际上并没有通过测试preset_answer_42正好相反runaway_controller则一直打转直到max_steps8被强制终止。如果你的目标是评测一个真实模型控制器只需要保证模型输出的action是类似结构化 JSON就可以把这个骨架扩展成批量评测任务。比如某个动作是{type: patch, content: ...}、{type: test}、{type: finish}。5. 从“能跑通”到“评估得好”一套实用指标上面的演示只有三个状态值status、passed、steps。这远不足以支撑复杂工程的模型选型决策。下面梳理一套更完整的指标也正好对应模型作为运行时控制器的评估维度。5.1 主要效果指标指标含义为什么重要任务成功率通过测试的任务数 / 总任务数最核心的上限指标平均步数每个任务消耗的循环轮数反映控制效率提前结束错误率主动 finish 但未通过的次数 / 总任务数识别终止策略过于激进步数耗尽率达到 max_steps 的任务比例识别无法收敛的控制器有效修改率使状态朝正确方向变化的 patch 占比反映反馈利用能力重复失败率对相同错误模式重复尝试的比例反映策略调整能力只有这些指标组合在一起才能回答“这个模型适合作 Agent 控制器吗”。例如一个模型任务成功率高但平均步数是其他模型的 3 倍那在真实业务中可能并不划算。5.2 保留结构化的轨迹日志评估不只为了得到一个分数更为了发现问题。所以每个循环步骤都应记录结构化日志而不是只有一段自然语言输出。推荐保留如下字段{ run_id: 2025-05-20_001, task_id: fix_default_config, model: example-llm-7b, step: 1, action: { type: patch, content_preview: value DEFAULT_VALUE }, observation: { ok: true, message: patched, passed: null }, timestamp_ms: 1234567890 }记录content_preview而不是完整文件内容是为了控制日志体量。真实项目中可以在有需要时将完整补丁单独落盘逻辑日志只保留关键摘要。5.3 评估边界条件在真实测评集里任务设计应该刻意加入边界情况否则“控制器能力”很容易被平均值掩盖。我建议按以下类型构造小任务集一步就能完成的任务检查控制器是否会无意义地多绕路需要 3 到 5 步才能完成的修复任务检查长期规划能力存在误导性错误日志的任务检查控制器是否能忽略干扰、抓住根因已经正确但测试环境不稳定的任务检查控制器的重试和终止策略需要主动放弃的任务当任务无法完成时控制器是否会给用户明确反馈而不是无限重试。LoopArena 这类基准背后的思路也是通过控制任务难度、反馈复杂度、动作空间大小让模型暴露在不同控制压力下。你能从评测结果中看到的不再只是一个“正确率”而是模型在循环中的行为画像。6. 常见问题与排查思路在把这类循环评估骨架接入自己的 Agent 项目时有几个高频问题值得提前排查。问题现象常见原因解决思路模型反复执行同一个错误操作缺少对历史失败的归纳模型没有看到“相同动作已失败”的摘要在 prompt 中显式携带最近失败摘要并限制相同动作的重复次数模型没跑测试就宣布完成控制器把“代码看起来正确”当成“任务已完成”将 finish 动作定义为必须先通过验证未验证的 finish 直接判失败任务一直不停循环直到 max_steps没有定义收敛条件参数化搜索空间过大设置步数上限、时间上限、token 上限三份预算模型连续多次输出非法 JSON 动作prompt 没有给够动作格式示例或模型不支持严格 JSON 输出使用函数调用/结构化输出能力解析失败时让模型基于错误信息重试一次同一任务多次运行结果差异大模型存在采样随机性环境初始状态也没有彻底重置固定 temperature0 或使用确定性采样每次运行前重置临时目录评测里测试通过但在
返回列表