从Prompt到工程化:Loop Engineering如何构建可靠AI智能体系统
1. 从“点”到“面”:为什么我们需要 Loop Engineering?
如果你最近在折腾 AI 编程助手,比如 Claude Code 或者 OpenAI Codex,那你肯定对“prompt agent”这个概念不陌生。简单说,就是你写一段指令(prompt),让 AI 去执行一个特定的、相对复杂的任务,比如“重构这段代码”或者“为这个函数写单元测试”。这个过程,我们姑且称之为“点对点”的交互:你发起一个请求,AI 返回一个结果,然后你基于结果再调整、再请求,如此循环。
但不知道你有没有这种感觉:当任务稍微复杂一点,需要多个步骤,或者需要结合不同工具(比如读文件、调 API、运行测试)时,这种“你问我答”的模式就变得非常低效且脆弱。你得像一个蹩脚的“人肉调度器”,不断地给 AI 喂新的上下文,纠正它的错误,告诉它下一步该干嘛。更头疼的是,一旦中间某一步出错,整个流程就卡住了,你得手动介入,从头梳理。这根本不是“智能体”(Agent)该有的样子,充其量是个“高级复读机”。
这就是“Loop Engineering”要解决的问题。它的核心思想,用一句话概括就是:你不再去直接“提示”(prompt)单个的智能体,而是去设计和构建一个能够自动、持续、可靠地“提示”和管理这些智能体的系统。这个系统,就是“Harness”(套件/缰绳)。你可以把它想象成一个智能的“自动化流水线”或者“调度中枢”。你定义好任务的目标、规则、可用工具和成功标准,然后启动这个“Harness”,它就会自动分解任务,调用合适的 AI 模型(如 Claude Code),监控执行过程,处理异常,并循环迭代直到达成目标或满足退出条件。
为什么这很重要?因为 AI 模型的能力再强,它也是一个“黑盒”,其行为具有不确定性和上下文依赖性。直接与黑盒进行点对点的、手动的交互,无法形成稳定、可复用的生产力。而 Loop Engineering 通过引入一个外部的、确定性的“控制系统”(Harness),将不确定的 AI 行为纳入到一个确定的、可观测、可调试的工程框架内。这标志着我们从“玩转一个 AI 工具”的阶段,进化到了“用工程化方法规模化部署 AI 能力”的阶段。对于开发者而言,这意味着你可以把 AI 真正当作一个“软件组件”来集成和调用,而不是一个需要你时时刻刻盯着、哄着的“魔法黑箱”。
2. 核心组件拆解:Harness 与 Agent 的共生关系
要理解 Loop Engineering,必须厘清两个核心概念:Harness和Agent。很多人容易混淆,甚至觉得 Harness 是来取代 Agent 的,这完全错了。它们的关系是共生与互补,就像汽车的方向盘(Harness)和发动机(Agent)。
2.1 Agent:执行具体任务的“专家”
Agent,在这里特指基于大语言模型(LLM)的“智能体”,比如 Claude Code 或 Codex。它的核心能力是理解自然语言指令,并在特定领域(如编程)内进行推理、规划和执行。它就像一个拥有深厚专业知识和强大推理能力的专家,但你得用它能听懂的语言(prompt)准确地告诉它要做什么。
然而,Agent 有几个天生的“缺陷”:
- 无状态性:典型的对话式 Agent 是“健忘的”。每次交互的上下文窗口有限,长程任务中很容易丢失之前的决策逻辑和中间状态。
- 工具调用依赖外部调度:Agent 知道“需要读取文件”,但它自己不会去调用操作系统的文件 API。它需要外部环境提供“工具”(Tools),并告诉外部环境“请帮我执行读文件这个工具”。
- 缺乏长期规划和自我纠正机制:面对一个多步骤的复杂任务(如“修复这个仓库里所有的编译错误”),单个 Agent 调用很难做出全局规划,并在执行中遇到错误时,自动调整策略。
- 输出不确定性:同样的输入,可能产生不同的输出,质量不稳定。
2.2 Harness:管理 Agent 生命周期的“基础设施”
Harness,直译是“马具”、“安全带”,在工程上引申为“控制系统”或“基础设施层”。在 Loop Engineering 的语境下,Harness 就是包裹在 Agent 核心推理逻辑之外的那一层确定性系统。它不负责代替 Agent 思考,而是负责管理 Agent 的“生命周期”和“工作流”。
Harness 的核心职责包括:
- 状态管理:维护任务的全局状态、历史记录、中间结果。它是任务的“记忆体”,确保 Agent 在每一步都能获得完整、准确的上下文。
- 工作流编排:将宏观任务分解为一系列可执行的原子步骤(Step),并定义步骤之间的依赖关系和执行顺序。这解决了 Agent 不擅长长期规划的问题。
- 工具集成与调用:为 Agent 提供一套标准化、安全的工具集(如文件读写、终端执行、网络请求)。当 Agent 决定使用某个工具时,Harness 负责安全地执行该工具调用,并将结果格式化后返回给 Agent。
- 循环控制与错误处理:这是“Loop”的精髓。Harness 监控每一步的执行结果,判断成功与否。如果失败,它可以依据预定策略(如重试、换一种方法、回滚、请求人工干预)来处理,并驱动流程进入下一个循环迭代。
- 观测与评估:记录整个执行过程的日志,评估最终输出是否符合预设的质量标准(通过验证器或评估函数)。这为调试和优化提供了数据基础。
用一个比喻:Agent 是公司里才华横溢但天马行空的创意设计师,而 Harness 是经验丰富的项目经理。设计师负责提出具体的创意方案(生成代码),项目经理负责厘清需求(任务分解)、协调资源(提供工具)、跟踪进度(状态管理)、处理客户反馈(错误处理与迭代),并确保项目按时按质交付(循环控制与评估)。没有设计师,项目没有灵魂;没有项目经理,项目会陷入混乱,无法规模化。
3. 实战:构建你的第一个 Harness 系统
理论说再多,不如动手搭一个。这里,我将以“自动为一个 Python 项目编写单元测试”这个常见任务为例,带你走一遍构建一个简易 Harness 系统的核心流程。我们将使用 Claude Code 作为核心 Agent,并围绕它设计 Harness 逻辑。请注意,以下是一个概念性的、框架级的实现,旨在阐明原理,你可以用任何熟悉的编程语言(Python、JavaScript 等)和框架来实现它。
3.1 定义任务与成功标准
首先,我们必须明确 Harness 系统的输入和输出。
- 输入:一个 Python 项目的根目录路径。
- 输出:为该项目中所有(或指定)的 Python 模块生成对应的单元测试文件,并且这些测试能够通过
pytest运行。 - 成功标准:
- 生成的测试文件结构清晰,位于正确的
tests/目录下。 - 测试覆盖了核心函数的主要分支和边界条件。
- 所有生成的测试执行通过(
pytest退出码为 0)。
- 生成的测试文件结构清晰,位于正确的
3.2 设计工作流与状态
接下来,我们需要将宏观任务分解为 Harness 可以执行的原子步骤,并设计状态对象来跟踪进度。
工作流分解:
- 项目分析:扫描目标目录,识别出所有
.py文件(排除测试文件本身),并解析其模块结构、导入关系和函数/类定义。 - 测试生成:针对每一个需要测试的模块或函数,调用 Claude Code,根据其代码上下文,生成合理的单元测试代码。
- 测试写入:将生成的测试代码写入到
tests/目录下对应的文件中。 - 测试执行:运行
pytest命令,执行所有新生成的测试。 - 结果验证:检查
pytest的执行结果。如果全部通过,任务成功;如果有失败,进入修复循环。
状态对象设计:我们需要一个全局状态来记录每一步的进展和结果。
class ProjectState: def __init__(self, project_path): self.project_path = project_path self.target_modules = [] # 待测试的模块列表 self.generated_tests = {} # key: 模块名, value: 生成的测试代码 self.test_results = {} # key: 测试文件路径, value: (通过与否, 错误信息) self.current_step = “init” # 当前执行步骤 self.retry_count = 0 # 重试计数器3.3 实现核心 Harness 逻辑
Harness 的核心是一个循环控制器,它根据当前状态和预定义的工作流,决定下一步做什么。
class TestGenerationHarness: def __init__(self, llm_client): # llm_client 是 Claude Code 的客户端 self.llm = llm_client self.state = None def run(self, project_path): self.state = ProjectState(project_path) self._loop() def _loop(self): while not self._is_task_complete(): next_step = self._decide_next_step() if next_step == “analyze_project”: self._analyze_project() elif next_step == “generate_for_module”: self._generate_for_current_module() elif next_step == “write_tests”: self._write_tests() elif next_step == “execute_tests”: self._execute_tests() elif next_step == “handle_failure”: self._handle_failure() elif next_step == “success”: print(“任务成功完成!”) break else: raise ValueError(f“未知步骤: {next_step}”) # 每次循环后,可以持久化状态,便于调试和恢复 self._save_state_snapshot() def _decide_next_step(self): # 基于当前 state 的简单决策逻辑 if not self.state.target_modules: return “analyze_project” elif len(self.state.generated_tests) < len(self.state.target_modules): return “generate_for_module” elif self.state.current_step == “generate_complete” and not self.state.test_results: return “write_tests” elif self.state.current_step == “write_complete”: return “execute_tests” elif self.state.test_results and any(not result[0] for result in self.state.test_results.values()): return “handle_failure” elif self.state.test_results and all(result[0] for result in self.state.test_results.values()): return “success” return “analyze_project” # 默认 def _analyze_project(self): # 实现项目分析逻辑,填充 state.target_modules # 例如,使用 ast 模块解析 .py 文件 print(f“分析项目: {self.state.project_path}”) # ... 具体分析代码 ... self.state.current_step = “analyze_complete” def _generate_for_current_module(self): # 取出一个尚未处理的模块 target_module = [m for m in self.state.target_modules if m not in self.state.generated_tests][0] print(f“为模块 {target_module} 生成测试...”) # 1. 构建给 Claude Code 的 Prompt module_code = self._read_module_code(target_module) prompt = f””” 你是一个资深的 Python 测试工程师。请为以下 Python 模块编写完整的单元测试。 要求: 1. 使用 pytest 风格。 2. 覆盖模块中所有公开的函数和类的主要功能。 3. 包含合理的边界条件测试和错误处理测试。 4. 测试代码应独立,不依赖外部网络或特殊环境。 模块代码: ```python {module_code} ``` 请只输出测试代码,不需要任何解释。 ””” # 2. 调用 Claude Code Agent try: response = self.llm.generate(prompt) test_code = self._extract_code_from_response(response) self.state.generated_tests[target_module] = test_code print(f“模块 {target_module} 测试生成成功。”) except Exception as e: print(f“为模块 {target_module} 生成测试时出错: {e}”) # 可以将出错模块记录,稍后重试或跳过 self.state.generated_tests[target_module] = None # 检查是否所有模块都处理完了 if len(self.state.generated_tests) == len(self.state.target_modules): self.state.current_step = “generate_complete” def _write_tests(self): # 将生成的测试代码写入 tests/ 目录 print(“开始写入测试文件...”) for module, test_code in self.state.generated_tests.items(): if test_code: test_file_path = self._determine_test_path(module) self._write_to_file(test_file_path, test_code) self.state.current_step = “write_complete” def _execute_tests(self): print(“执行单元测试...”) # 调用 subprocess 运行 pytest import subprocess result = subprocess.run([“pytest”, “tests/”], capture_output=True, text=True) # 解析结果,填充 state.test_results # 这里简化处理,实际需要解析 pytest 的输出 if result.returncode == 0: print(“所有测试通过!”) for test_file in self._get_all_test_files(): self.state.test_results[test_file] = (True, “”) else: print(“部分测试失败。”) # 需要更精细的解析,将失败信息对应到具体文件 self.state.test_results[“整体”] = (False, result.stderr) self.state.current_step = “execute_complete” def _handle_failure(self): # 错误处理策略 print(“进入错误处理循环...”) self.state.retry_count += 1 if self.state.retry_count > 3: print(“重试次数过多,任务失败。需要人工介入。”) # 可以在这里触发通知,或者将状态标记为需人工处理 raise RuntimeError(“自动修复失败”) else: # 策略1:针对失败的测试,让 AI 分析错误并重新生成 failed_tests_info = self.state.test_results.get(“整体”)[1] # 获取错误信息 repair_prompt = f””” 以下单元测试执行失败,错误信息如下: {failed_tests_info} 请分析失败原因,并提供修复后的测试代码。请只输出修复后的完整测试代码。 ””” try: repair_response = self.llm.generate(repair_prompt) repaired_code = self._extract_code_from_response(repair_response) # 用修复后的代码覆盖原测试文件 self._write_to_file(“tests/test_repaired.py”, repaired_code) # 重置状态,准备重新执行测试 self.state.test_results.clear() self.state.current_step = “write_complete” # 跳回写入步骤,重新执行 print(“已尝试修复测试,准备重新执行。”) except Exception as e: print(f“修复尝试失败: {e}”) # 可以尝试其他策略,如忽略某些测试、回滚等 def _is_task_complete(self): # 判断任务是否完成(成功或最终失败) return self.state.current_step == “success” or self.state.retry_count > 3这个简易的 Harness 已经具备了 Loop Engineering 的核心要素:状态管理、工作流编排(_decide_next_step)、工具调用(文件读写、执行 pytest)、循环控制(_loop和_handle_failure)。Agent(Claude Code)被封装在_generate_for_current_module和_handle_failure中,它只负责最擅长的“代码生成”部分,而何时调用、用什么参数调用、调用失败怎么办,都由 Harness 决定。
4. 进阶:Harness 设计的关键模式与最佳实践
构建一个玩具系统容易,但要设计一个健壮、可扩展、能用于生产环境的 Harness,就需要考虑更多工程细节。下面分享几个关键模式和我在实践中的心得。
4.1 状态管理的艺术:持久化与快照
Harness 驱动的循环可能是漫长的,中间可能因为各种原因中断(网络错误、系统重启)。一个健壮的 Harness 必须支持状态持久化和从快照恢复。
- 怎么做:在每一个步骤执行完成后,将整个
ProjectState对象序列化(如用 JSON 或 Pickle)保存到磁盘或数据库中。在 Harness 启动时,检查是否存在之前的快照,如果存在,则加载并从中断的步骤继续执行。 - 注意事项:序列化时要注意,有些对象(如 LLM 客户端连接、文件句柄)可能无法直接序列化,需要将其设为临时对象,在恢复时重新初始化。状态版本化也是一个好习惯,当你的 Harness 逻辑升级后,可能需要对旧快照进行迁移。
4.2 工具集的设计:安全与抽象
为 Agent 提供工具(Tools)是 Harness 的核心功能之一。工具设计的好坏直接决定了 Agent 能力的边界和系统的安全性。
- 安全性是第一位的:绝对不要让 Agent 拥有直接执行任意 Shell 命令或访问敏感文件的权限。应该提供经过严格过滤和参数化的工具接口。例如,提供一个
run_specific_command工具,它只允许运行预定义白名单内的命令(如git pull,pytest),并对参数进行消毒(sanitize)。 - 良好的抽象:工具接口应对 Agent 友好。例如,与其让 Agent 直接拼接复杂的
curl命令,不如提供一个http_request工具,它接受method、url、headers、body等结构化参数。这降低了 Agent 的 prompt 编写难度,也提高了可靠性。 - 工具的描述(Description):每个工具都需要一个清晰、详细的自然语言描述,这个描述会被拼接到给 Agent 的 system prompt 中,帮助 Agent 理解何时以及如何使用这个工具。描述应包含功能、输入参数格式和示例。
4.3 循环策略:超越简单的重试
错误处理循环(_handle_failure)是体现 Harness 智能的关键。简单的重试往往无效,因为同样的输入会产生同样的错误输出。你需要更精细的策略:
- 分级策略:
- Level 1 - 瞬时错误重试:对于网络超时、API 限流等错误,可以立即重试几次。
- Level 2 - 逻辑错误修复:对于测试失败、代码编译错误等,可以收集错误信息,构造一个“修复提示”(repair prompt)再次调用 Agent。这个新 prompt 应包含原始任务、已生成的代码、以及具体的错误信息。
- Level 3 - 策略切换:如果多次修复无效,可以尝试切换任务分解方式。例如,生成单元测试失败,可以退而求其次,先只生成集成测试,或者换一个更简单的测试框架。
- Level 4 - 人工干预:设置一个阈值(如重试 N 次后,或特定类型的错误),将任务挂起,并通过通知系统(如 Slack、邮件)请求人工介入。
- 超时与看门狗:为每个步骤甚至整个循环设置超时。如果一个步骤卡住,看门狗(watchdog)机制可以强制中断并触发错误处理流程,防止系统僵死。
4.4 可观测性与评估
没有度量,就无法改进。Harness 系统必须内置强大的日志和指标收集功能。
- 记录什么:
- 操作日志:每个步骤的开始、结束、输入、输出。
- Agent 交互日志:发送给 Agent 的完整 prompt 和接收到的完整 response。这是调试 AI 行为最宝贵的资料。
- 性能指标:每个步骤的耗时、Token 消耗量、工具调用次数、循环迭代次数。
- 结果评估:最终输出是否通过验证器(Validator)的检查。验证器可以是规则(如代码风格检查)、另一个 AI 评估(如判断代码质量),也可以是实际运行的结果(如测试通过率)。
- 如何用:这些数据可以用来分析瓶颈(哪个步骤最耗时?)、优化成本(哪些 prompt 的 Token 消耗可以优化?)、改进策略(哪种错误处理策略成功率最高?),并最终迭代你的 Harness 设计。
5. 避坑指南:从原型到生产的关键挑战
我自己在将 Loop Engineering 理念落地时,踩过不少坑。这里总结几个最常见的挑战和应对思路,希望能帮你少走弯路。
5.1 幻觉与不一致性:Agent 的“自由发挥”
这是最大的挑战之一。你设计的工作流是 A->B->C,但 Agent 可能在 B 步骤生成的内容,完全不符合 C 步骤的输入预期,或者干脆“幻觉”出一些不存在的 API 和逻辑。
- 对策:
- 强类型状态约束:在状态对象中,尽可能使用强类型和枚举来定义字段。例如,
current_step字段应该是枚举类型,而不是自由字符串,防止 Agent 或错误逻辑将其设置为非法值。 - 输出格式强制:在给 Agent 的 prompt 中,明确要求其输出必须遵循特定的结构化格式(如 JSON、YAML),甚至提供 JSON Schema。在 Harness 端,对 Agent 的返回进行严格的格式解析和校验,解析失败则立即进入错误处理循环,而不是带着脏数据继续往下走。
- 上下文压缩与精炼:随着循环进行,上下文会越来越长。不要无脑地把所有历史都塞给 Agent。Harness 应该负责总结和精炼上下文,只传递与当前步骤最相关的信息,减少干扰和幻觉风险。
- 强类型状态约束:在状态对象中,尽可能使用强类型和枚举来定义字段。例如,
5.2 长任务与上下文窗口限制
复杂的工程任务可能需要几十甚至上百个步骤,远超任何 LLM 的上下文窗口。
- 对策:
- 分层与递归:将大 Harness 分解为多个小 Harness。一个顶层的“协调者 Harness”负责宏观规划,然后将子任务(如“为 X 服务生成 API 层”)委托给一个专门的“子 Harness”去执行。子任务完成后,将摘要结果返回给协调者。这类似于人类项目经理将工作分派给不同团队。
- 外部记忆体:不要依赖 LLM 的上下文作为唯一记忆。将所有关键信息(决策依据、中间结果、工具输出)持久化到 Harness 的状态或外部数据库(如向量数据库)中。当需要历史信息时,由 Harness 负责进行相关性检索,只提取必要的片段注入 prompt。
5.3 成本与延迟控制
每一次调用 Claude Code 或类似的商业 API 都需要花钱和时间。一个设计不佳的 Harness 可能导致循环陷入无意义的死循环,产生天价账单。
- 对策:
- 设置预算和熔断:在 Harness 初始化时,就设定本次任务的最大 Token 消耗预算或最大 API 调用次数。在循环中实时累计消耗,接近阈值时优雅地停止或降级(例如,切换到更便宜的模型或更简单的策略)。
- 缓存:对于确定性较高的子任务(如分析项目结构),其结果可以缓存起来。如果 Harness 第二次处理同一个项目,可以直接读取缓存,跳过昂贵的 AI 分析步骤。
- 异步与并行:如果任务中的多个子步骤之间没有强依赖关系,Harness 应该能够并行调度它们。例如,为多个独立的模块生成测试,可以同时发起多个 AI 调用,大幅减少总耗时。
5.4 测试与调试的复杂性
一个包含非确定性 AI 组件的循环系统,其调试难度远高于传统软件。问题可能出在 Prompt 设计、工具接口、状态逻辑、AI 模型本身,或者它们之间复杂的交互。
- 对策:
- 录制与回放:像上面提到的,完整记录每一次 AI 交互和状态变更。当出现问题时,你可以像看“黑匣子”录音一样,回放整个执行过程,精准定位问题源头。
- 单元测试 Harness 逻辑:将 AI 调用部分 Mock 掉。为你的
_decide_next_step、_handle_failure等纯逻辑函数编写单元测试,用预设的、确定性的状态来验证你的控制流是否正确。 - 集成测试沙盒:建立一个与生产环境隔离的沙盒,用一套固定的、已知的输入任务来运行整个 Harness。对比每次运行的最终输出和中间日志,监控系统的稳定性和输出的质量漂移。
Loop Engineering 不是一个具体的工具,而是一种构建可靠 AI 应用的方法论和思维模式。它要求我们从“魔法师”(不断念咒语/prompt)转变为“工程师”(设计自动化系统)。这条路刚开始走可能会觉得繁琐,需要设计状态机、写很多胶水代码。但一旦你的 Harness 系统搭建起来,你会发现你获得了一种全新的能力:将模糊、复杂的自然语言需求,通过确定性的工程框架,转化为稳定、可重复的高质量输出。这不仅仅是效率的提升,更是工作范式的根本转变。
